443 lines
14 KiB
TypeScript
443 lines
14 KiB
TypeScript
// Базовый URL API
|
||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
|
||
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:8000';
|
||
|
||
// Типы для API
|
||
export interface Address {
|
||
address_line1: string;
|
||
address_line2?: string;
|
||
city: string;
|
||
postal_code: string;
|
||
country: string;
|
||
}
|
||
|
||
export interface ApiResponse<T> {
|
||
data: T;
|
||
success: boolean;
|
||
error?: string;
|
||
}
|
||
|
||
// Базовая функция для выполнения запросов
|
||
export async function fetchApi<T>(
|
||
endpoint: string,
|
||
options: RequestInit = {}
|
||
): Promise<ApiResponse<T>> {
|
||
try {
|
||
const url = `${API_URL}${endpoint}`;
|
||
console.log(`API запрос: ${options.method || 'GET'} ${url}`);
|
||
|
||
// Добавляем заголовки по умолчанию
|
||
const headers: Record<string, string> = {
|
||
...(options.headers as Record<string, string> || {}),
|
||
};
|
||
|
||
// Content-Type добавляем только если это не FormData
|
||
if (!(options.body instanceof FormData)) {
|
||
headers['Content-Type'] = 'application/json';
|
||
} else {
|
||
console.log('Отправка FormData, Content-Type не устанавливается');
|
||
}
|
||
|
||
// Получаем токен из localStorage (только на клиенте)
|
||
if (typeof window !== 'undefined') {
|
||
const token = localStorage.getItem('token');
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
}
|
||
|
||
// Логируем заголовки для отладки
|
||
console.log('Заголовки запроса:', headers);
|
||
|
||
const response = await fetch(url, {
|
||
...options,
|
||
headers,
|
||
});
|
||
|
||
// Логируем ответ
|
||
console.log(`Ответ API: ${response.status} ${response.statusText}`);
|
||
|
||
// Проверяем статус ответа на ошибки аутентификации
|
||
if (response.status === 401 || response.status === 403) {
|
||
console.error("Ошибка аутентификации:", response.status);
|
||
|
||
// Очищаем токен при ошибке аутентификации
|
||
if (typeof window !== 'undefined') {
|
||
localStorage.removeItem('token');
|
||
|
||
// Если это не запрос на вход, перенаправляем на страницу входа
|
||
if (!endpoint.includes('/auth/login')) {
|
||
console.log("Перенаправление на страницу входа из-за ошибки аутентификации");
|
||
window.location.href = '/admin/login';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Пытаемся распарсить ответ как JSON
|
||
let data;
|
||
const contentType = response.headers.get('content-type');
|
||
if (contentType && contentType.includes('application/json')) {
|
||
data = await response.json();
|
||
console.log('Полученные данные:', data);
|
||
|
||
// Если ответ содержит обертку data.product, извлекаем данные из нее
|
||
if (data && typeof data === 'object') {
|
||
if (data.product) {
|
||
data = data.product;
|
||
} else if (data.success && data.data) {
|
||
data = data.data;
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
data,
|
||
success: response.status === 200,
|
||
error: response.ok ? undefined : data?.error || 'Неизвестная ошибка',
|
||
};
|
||
} catch (error) {
|
||
console.error("API Error:", error);
|
||
return {
|
||
data: null as unknown as T,
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Неизвестная ошибка',
|
||
};
|
||
}
|
||
}
|
||
|
||
// Типы для админки
|
||
export interface DashboardStats {
|
||
ordersCount: number;
|
||
totalSales: number;
|
||
customersCount: number;
|
||
productsCount: number;
|
||
}
|
||
|
||
export interface Order {
|
||
id: number;
|
||
user_id: number;
|
||
user_name?: string;
|
||
user_email?: string;
|
||
status: string;
|
||
total: number;
|
||
created_at: string;
|
||
updated_at?: string;
|
||
items_count: number;
|
||
items?: OrderItem[];
|
||
shipping_address?: Address | string;
|
||
payment_method?: string;
|
||
tracking_number?: string;
|
||
notes?: string;
|
||
}
|
||
|
||
export interface OrderItem {
|
||
id: number;
|
||
order_id: number;
|
||
product_id: number;
|
||
variant_id?: number;
|
||
quantity: number;
|
||
price: number;
|
||
product: {
|
||
id: number;
|
||
name: string;
|
||
image?: string;
|
||
sku?: string;
|
||
};
|
||
variant_name?: string;
|
||
}
|
||
|
||
export interface Product {
|
||
id: number;
|
||
name: string;
|
||
slug: string;
|
||
description?: string;
|
||
price: number;
|
||
discount_price?: number | null;
|
||
care_instructions?: Record<string, string> | string | null;
|
||
is_active: boolean;
|
||
category_id?: number;
|
||
collection_id?: number;
|
||
stock?: number;
|
||
category?: {
|
||
id: number;
|
||
name: string;
|
||
};
|
||
collection?: {
|
||
id: number;
|
||
name: string;
|
||
};
|
||
images?: Array<string | { id: number; image_url: string; is_primary?: boolean; alt_text?: string }>;
|
||
variants?: Array<{
|
||
id: number;
|
||
size_id: number;
|
||
stock: number;
|
||
sku?: string;
|
||
is_active?: boolean;
|
||
size?: {
|
||
id: number;
|
||
name: string;
|
||
code: string;
|
||
};
|
||
}>;
|
||
created_at?: string;
|
||
updated_at?: string;
|
||
}
|
||
|
||
export interface ProductVariant {
|
||
id: number;
|
||
product_id: number;
|
||
size_id: number;
|
||
sku: string;
|
||
stock: number;
|
||
is_active?: boolean;
|
||
size?: {
|
||
id: number;
|
||
name: string;
|
||
code?: string;
|
||
};
|
||
}
|
||
|
||
export interface ProductsResponse {
|
||
items: Product[];
|
||
total: number;
|
||
total_pages: number;
|
||
page: number;
|
||
}
|
||
|
||
export interface Size {
|
||
id: number;
|
||
name: string;
|
||
code: string;
|
||
description?: string;
|
||
}
|
||
|
||
// Функции API для админки
|
||
export async function fetchDashboardStats(): Promise<ApiResponse<DashboardStats>> {
|
||
return fetchApi<DashboardStats>('/analytics/dashboard/stats');
|
||
}
|
||
|
||
export async function fetchRecentOrders(params: { limit?: number } = {}): Promise<ApiResponse<Order[]>> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params.limit) {
|
||
queryParams.append('limit', params.limit.toString());
|
||
}
|
||
|
||
return fetchApi<Order[]>(`/orders?${queryParams.toString()}`);
|
||
}
|
||
|
||
export async function fetchPopularProducts(params: { limit?: number } = {}): Promise<ApiResponse<Product[]>> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params.limit) {
|
||
queryParams.append('limit', params.limit.toString());
|
||
}
|
||
|
||
return fetchApi<Product[]>(`/catalog/products/popular?${queryParams.toString()}`);
|
||
}
|
||
|
||
// Функции API для работы с товарами
|
||
export async function fetchProducts(params: {
|
||
page?: number;
|
||
limit?: number;
|
||
search?: string;
|
||
category_id?: number;
|
||
} = {}): Promise<ApiResponse<Product[] | ProductsResponse>> {
|
||
const queryParams = new URLSearchParams();
|
||
|
||
if (params.page) {
|
||
queryParams.append('page', params.page.toString());
|
||
}
|
||
|
||
if (params.limit) {
|
||
queryParams.append('limit', params.limit.toString());
|
||
}
|
||
|
||
if (params.search) {
|
||
queryParams.append('search', params.search);
|
||
}
|
||
|
||
if (params.category_id) {
|
||
queryParams.append('category_id', params.category_id.toString());
|
||
}
|
||
|
||
return fetchApi<Product[] | ProductsResponse>(`/catalog/products?${queryParams.toString()}`);
|
||
}
|
||
|
||
// Функция для получения данных о продукте (по ID или slug)
|
||
export async function fetchProduct(idOrSlug: string | number): Promise<ApiResponse<Product>> {
|
||
let endpoint;
|
||
|
||
if (typeof idOrSlug === 'number' || !isNaN(Number(idOrSlug))) {
|
||
// Если передан числовой ID
|
||
endpoint = `/catalog/products/${idOrSlug}`;
|
||
} else {
|
||
// Если передан строковый slug
|
||
endpoint = `/catalog/products/slug/${idOrSlug}`;
|
||
}
|
||
|
||
return fetchApi<Product>(endpoint);
|
||
}
|
||
|
||
export async function createProduct(data: Partial<Product>): Promise<ApiResponse<Product>> {
|
||
return fetchApi<Product>('/catalog/products', {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
export async function updateProduct(id: number, data: Partial<Product>): Promise<ApiResponse<Product>> {
|
||
return fetchApi<Product>(`/catalog/products/${id}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
export async function deleteProduct(id: number): Promise<ApiResponse<void>> {
|
||
return fetchApi<void>(`/catalog/products/${id}`, {
|
||
method: 'DELETE',
|
||
});
|
||
}
|
||
|
||
// Функция для получения списка размеров
|
||
export async function fetchSizes(): Promise<ApiResponse<Size[]>> {
|
||
return fetchApi<Size[]>('/catalog/sizes');
|
||
}
|
||
|
||
// Утилиты для работы с URL изображений
|
||
export function getImageUrl(imagePath: string): string {
|
||
if (!imagePath) return '';
|
||
|
||
// Если путь уже содержит протокол (http/https) или абсолютный URL, возвращаем как есть
|
||
if (imagePath.startsWith('http://') || imagePath.startsWith('https://') || imagePath.startsWith('//')) {
|
||
return imagePath;
|
||
}
|
||
|
||
// Если путь начинается с '/', то это относительный путь от корня сервера
|
||
if (imagePath.startsWith('/')) {
|
||
return `${BASE_URL}${imagePath}`;
|
||
}
|
||
|
||
// В противном случае это частично путь, добавляем базовый URL
|
||
return `${BASE_URL}/${imagePath}`;
|
||
}
|
||
|
||
// Функция для загрузки изображения на сервер
|
||
export async function uploadProductImage(productId: number, file: File): Promise<ApiResponse<{id: number; image_url: string; is_primary: boolean}>> {
|
||
const formData = new FormData();
|
||
|
||
// Имя поля file должно совпадать с ожидаемым именем на сервере
|
||
formData.append('file', file);
|
||
formData.append('is_primary', 'false');
|
||
|
||
// Логируем содержимое FormData для отладки
|
||
console.log('Загрузка изображения:', {
|
||
productId,
|
||
fileName: file.name,
|
||
fileType: file.type,
|
||
fileSize: file.size,
|
||
formDataFields: ['file', 'is_primary']
|
||
});
|
||
|
||
return fetchApi<{id: number; image_url: string; is_primary: boolean}>(`/catalog/products/${productId}/images`, {
|
||
method: 'POST',
|
||
body: formData,
|
||
// Не устанавливаем Content-Type для FormData - fetchApi автоматически его пропустит
|
||
});
|
||
}
|
||
|
||
// Функция для установки главного изображения товара
|
||
export async function setProductImageAsPrimary(productId: number, imageId: number): Promise<ApiResponse<void>> {
|
||
return fetchApi<void>(`/catalog/products/${productId}/images/${imageId}/primary`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ is_primary: true })
|
||
});
|
||
}
|
||
|
||
// Функция для удаления изображения товара
|
||
export async function deleteProductImage(productId: number, imageId: number): Promise<ApiResponse<void>> {
|
||
return fetchApi<void>(`/catalog/products/${productId}/images/${imageId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
}
|
||
|
||
// Функции API для работы с заказами
|
||
export async function fetchOrders(params: {
|
||
skip?: number;
|
||
limit?: number;
|
||
status?: string;
|
||
dateFrom?: string;
|
||
dateTo?: string;
|
||
search?: string;
|
||
} = {}): Promise<ApiResponse<Order[]>> {
|
||
const queryParams = new URLSearchParams();
|
||
if (params.skip) queryParams.append('skip', params.skip.toString());
|
||
if (params.limit) queryParams.append('limit', params.limit.toString());
|
||
if (params.status) queryParams.append('status', params.status);
|
||
if (params.dateFrom) queryParams.append('date_from', params.dateFrom);
|
||
if (params.dateTo) queryParams.append('date_to', params.dateTo);
|
||
if (params.search) queryParams.append('search', params.search);
|
||
|
||
return fetchApi<Order[]>(`/orders?${queryParams.toString()}`);
|
||
}
|
||
|
||
export async function fetchOrder(id: number): Promise<ApiResponse<Order>> {
|
||
return fetchApi<Order>(`/orders/${id}`);
|
||
}
|
||
|
||
export async function updateOrder(id: number, data: Partial<Order>): Promise<ApiResponse<Order>> {
|
||
return fetchApi<Order>(`/orders/${id}`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(data),
|
||
});
|
||
}
|
||
|
||
export async function cancelOrder(id: number): Promise<ApiResponse<Order>> {
|
||
return fetchApi<Order>(`/orders/${id}/cancel`, {
|
||
method: 'POST',
|
||
});
|
||
}
|
||
|
||
// Создаем класс api для обеспечения обратной совместимости
|
||
const api = {
|
||
get: async <T>(endpoint: string, options: any = {}): Promise<{ data: T }> => {
|
||
const params = options.params ?
|
||
new URLSearchParams(Object.entries(options.params)
|
||
.filter(([_, v]) => v !== undefined)
|
||
.map(([k, v]) => [k, String(v)])) :
|
||
new URLSearchParams();
|
||
|
||
const queryString = params.toString();
|
||
const url = queryString ? `${endpoint}?${queryString}` : endpoint;
|
||
|
||
const response = await fetchApi<T>(url);
|
||
return { data: response.data };
|
||
},
|
||
|
||
post: async <T>(endpoint: string, data: any, options: any = {}): Promise<{ data: T }> => {
|
||
const response = await fetchApi<T>(endpoint, {
|
||
method: 'POST',
|
||
body: JSON.stringify(data),
|
||
...options
|
||
});
|
||
return { data: response.data };
|
||
},
|
||
|
||
put: async <T>(endpoint: string, data: any, options: any = {}): Promise<{ data: T }> => {
|
||
const response = await fetchApi<T>(endpoint, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(data),
|
||
...options
|
||
});
|
||
return { data: response.data };
|
||
},
|
||
|
||
delete: async <T>(endpoint: string, options: any = {}): Promise<{ data: T }> => {
|
||
const response = await fetchApi<T>(endpoint, {
|
||
method: 'DELETE',
|
||
...options
|
||
});
|
||
return { data: response.data };
|
||
}
|
||
};
|
||
|
||
// Экспортируем API класс
|
||
export default api;
|