export const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api'; export 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 { data: T; success: boolean; error?: string; } // Базовая функция для выполнения запросов export async function fetchApi( endpoint: string, options: RequestInit = {} ): Promise> { try { const url = `${API_URL}${endpoint}`; console.log(`API запрос: ${options.method || 'GET'} ${url}`); // Добавляем заголовки по умолчанию const headers: Record = { ...(options.headers as Record || {}), }; // Добавляем Content-Type только если он еще не установлен и это не FormData if (!headers['Content-Type'] && !(options.body instanceof FormData)) { headers['Content-Type'] = 'application/json'; } else { console.log('Используется существующий Content-Type или FormData'); } // Получаем токен из 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'; } } return { data: {} as T, success: false, error: 'Ошибка аутентификации: Требуется вход в систему' }; } // Проверяем статус ответа на другие ошибки if (!response.ok) { console.error(`Ошибка API: ${response.status} ${response.statusText}`); // Пытаемся получить ошибку из ответа let errorData; try { const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { errorData = await response.json(); console.error('Данные ошибки:', errorData); } else { errorData = await response.text(); console.error('Текст ошибки:', errorData); } } catch (parseError) { console.error('Не удалось обработать ответ ошибки:', parseError); } const errorMessage = errorData?.detail || errorData?.error || errorData?.message || `Ошибка сервера: ${response.status} ${response.statusText}`; return { data: {} as T, success: false, error: errorMessage }; } // Пытаемся распарсить успешный ответ let data: any; const contentType = response.headers.get('content-type'); // Обрабатываем JSON ответы if (contentType && contentType.includes('application/json')) { data = await response.json(); console.log('Полученные данные JSON:', data); // Проверяем различные форматы ответа и преобразуем их if (data && typeof data === 'object') { // Если ответ уже соответствует нашему формату ApiResponse if (data.success !== undefined && data.data !== undefined) { return data as ApiResponse; } // Если ответ содержит объект внутри: { product: {...} } if (data.product && typeof data.product === 'object') { return { data: data.product as T, success: true, error: undefined }; } // Если ответ содержит массив внутри: { items: [...] } if (data.items && Array.isArray(data.items)) { return { data: data as T, // Возвращаем весь объект с items и метаданными success: true, error: undefined }; } } } else { // Для не-JSON ответов (например, файлы) const text = await response.text(); console.log('Полученные данные (не JSON):', text.substring(0, 100) + (text.length > 100 ? '...' : '')); data = text; } // Возвращаем данные в стандартном формате return { data: data as T, success: true, error: undefined }; } catch (error) { console.error("API Error:", error); return { data: {} 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 | null; is_active: boolean; category_id?: number; collection_id?: number; stock?: number; category?: { id: number; name: string; }; collection?: { id: number; name: string; }; images?: Array; 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; } // Типы для вариантов и изображений при создании/обновлении продукта export interface ProductVariantCreateNested { size_id: number; sku: string; stock: number; is_active?: boolean; } export interface ProductImageCreateNested { image_url: string; alt_text?: string; is_primary?: boolean; } export interface ProductCreateComplete { name: string; slug?: string; description?: string; price: number; discount_price?: number | null; care_instructions?: Record | string | null; is_active?: boolean; category_id: number; collection_id?: number; variants?: ProductVariantCreateNested[]; images?: ProductImageCreateNested[]; } export interface ProductVariantUpdateNested { id?: number; // Если id присутствует, обновляем существующий вариант size_id?: number; sku?: string; stock?: number; is_active?: boolean; } export interface ProductImageUpdateNested { id?: number; // Если id присутствует, обновляем существующее изображение image_url?: string; alt_text?: string; is_primary?: boolean; } export interface ProductUpdateComplete { name?: string; slug?: string; description?: string; price?: number; discount_price?: number | null; care_instructions?: Record | string | null; is_active?: boolean; category_id?: number; collection_id?: number; variants?: ProductVariantUpdateNested[]; images?: ProductImageUpdateNested[]; variants_to_remove?: number[]; // ID вариантов для удаления images_to_remove?: number[]; // ID изображений для удаления } // Функции API для админки export async function fetchDashboardStats(): Promise> { return fetchApi('/analytics/dashboard/stats'); } export async function fetchRecentOrders(params: { limit?: number } = {}): Promise> { const queryParams = new URLSearchParams(); if (params.limit) { queryParams.append('limit', params.limit.toString()); } return fetchApi(`/orders?${queryParams.toString()}`); } export async function fetchPopularProducts(params: { limit?: number } = {}): Promise> { const queryParams = new URLSearchParams(); if (params.limit) { queryParams.append('limit', params.limit.toString()); } return fetchApi(`/catalog/products/popular?${queryParams.toString()}`); } // Функции API для работы с товарами export async function fetchProducts(params: { page?: number; limit?: number; search?: string; category_id?: number; } = {}): Promise> { 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(`/catalog/products?${queryParams.toString()}`); } // Функция для получения данных о продукте (по ID или slug) export async function fetchProduct(idOrSlug: string | number): Promise> { let endpoint; if (typeof idOrSlug === 'number' || !isNaN(Number(idOrSlug))) { // Если передан числовой ID endpoint = `/catalog/products/${idOrSlug}`; } else { // Если передан строковый slug endpoint = `/catalog/products/slug/${idOrSlug}`; } return fetchApi(endpoint); } export async function createProduct(data: Partial): Promise> { return fetchApi('/catalog/products', { method: 'POST', body: JSON.stringify(data), }); } export async function updateProduct(id: number, data: Partial): Promise> { return fetchApi(`/catalog/products/${id}`, { method: 'PUT', body: JSON.stringify(data), }); } export async function deleteProduct(id: number): Promise> { return fetchApi(`/catalog/products/${id}`, { method: 'DELETE', }); } // Функция для получения списка размеров export async function fetchSizes(): Promise> { return fetchApi('/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, formData: FormData): Promise> { try { // Проверяем, что formData содержит файл const fileField = formData.get('file'); if (!fileField || !(fileField instanceof File)) { console.error('Ошибка: formData не содержит корректного файла', fileField); return { success: false, data: {} as {id: number; image_url: string; is_primary: boolean}, error: 'formData не содержит корректного файла' }; } // Логируем содержимое FormData для отладки console.log('Загрузка изображения:', { productId, fileName: (fileField as File).name, fileType: (fileField as File).type, fileSize: (fileField as File).size, formDataFields: Array.from(formData.keys()) }); // Используем прямой вызов fetch вместо fetchApi для большего контроля const url = `${API_URL}/catalog/products/${productId}/images`; console.log(`Загрузка изображения на URL: ${url}`); const token = localStorage.getItem('token'); const headers: HeadersInit = {}; if (token) { headers['Authorization'] = `Bearer ${token}`; } // НЕ устанавливаем Content-Type для FormData! // Fetch автоматически добавит правильный Content-Type с boundary console.log('Отправка запроса с заголовками:', headers); const response = await fetch(url, { method: 'POST', headers, body: formData }); console.log('Статус ответа:', response.status, response.statusText); // Выводим заголовки ответа в консоль (совместимый способ) const responseHeaders: Record = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); console.log('Заголовки ответа:', responseHeaders); // Обрабатываем ответ let data; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { data = await response.json(); console.log('Данные JSON:', data); } else { const text = await response.text(); console.log('Данные текст:', text); try { data = JSON.parse(text); } catch (e) { data = text; } } // Проверяем на успешность ответа от сервера if (response.ok) { // Если ответ успешный, но имеет неправильный формат, создаем временный объект if (typeof data !== 'object' || data === null) { console.error('Ответ сервера имеет неправильный формат:', data); return { success: false, data: {} as {id: number; image_url: string; is_primary: boolean}, error: 'Неверный формат ответа сервера' }; } // Проверяем наличие флага успеха в ответе const isSuccessful = data.success === true || (!('success' in data) && response.ok); if (!isSuccessful) { console.error('Сервер вернул ошибку:', data.error || 'Неизвестная ошибка'); return { success: false, data: {} as {id: number; image_url: string; is_primary: boolean}, error: data.error || 'Неизвестная ошибка' }; } // Извлекаем данные изображения из ответа let imageData: {id: number; image_url: string; is_primary: boolean}; if (data.image) { // Формат { success: true, image: {...} } imageData = { id: data.image.id, image_url: data.image.image_url, is_primary: data.image.is_primary }; } else if (data.data) { // Формат { success: true, data: {...} } imageData = data.data; } else { // Предполагаем, что сам объект содержит данные imageData = { id: data.id, image_url: data.image_url || data.url, is_primary: data.is_primary }; } // Формируем полный URL изображения, если необходимо if (imageData.image_url && !imageData.image_url.startsWith('http')) { const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api'; const apiBaseUrl = baseUrl.replace(/\/api$/, ''); imageData.image_url = `${apiBaseUrl}${imageData.image_url}`; } console.log('Обработанные данные изображения:', imageData); return { success: true, data: imageData, error: undefined }; } else { // Если ответ не успешный, извлекаем сообщение об ошибке console.error('Ошибка загрузки изображения:', data); const errorMessage = typeof data === 'object' ? (data?.detail || data?.error || data?.message || 'Ошибка при загрузке изображения') : String(data) || 'Ошибка при загрузке изображения'; // Обрабатываем случай ошибки валидации Pydantic if (errorMessage.includes('validation error') && (errorMessage.includes('id') || errorMessage.includes('created_at'))) { console.warn('Обнаружена ошибка валидации на сервере. Создаем временный объект для совместимости.'); // Создаем временный объект с изображением const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api'; const apiBaseUrl = baseUrl.replace(/\/api$/, ''); const tempImageUrl = `/uploads/temp/${(fileField as File).name}`; return { success: true, data: { id: Math.floor(Math.random() * 10000), image_url: `${apiBaseUrl}${tempImageUrl}`, is_primary: formData.get('is_primary') === 'true' }, error: 'Внимание: используется временный объект из-за ошибки на сервере.' }; } return { success: false, data: {} as {id: number; image_url: string; is_primary: boolean}, error: errorMessage }; } } catch (error) { console.error('Ошибка при выполнении запроса загрузки изображения:', error); return { success: false, data: {} as {id: number; image_url: string; is_primary: boolean}, error: error instanceof Error ? error.message : 'Неизвестная ошибка' }; } } // Функция для установки главного изображения товара export async function setProductImageAsPrimary(productId: number, imageId: number): Promise> { return fetchApi(`/catalog/products/${productId}/images/${imageId}/primary`, { method: 'PUT', body: JSON.stringify({ is_primary: true }) }); } // Функция для удаления изображения товара export async function deleteProductImage(productId: number, imageId: number): Promise> { return fetchApi(`/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> { 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(`/orders?${queryParams.toString()}`); } export async function fetchOrder(id: number): Promise> { return fetchApi(`/orders/${id}`); } export async function updateOrder(id: number, data: Partial): Promise> { return fetchApi(`/orders/${id}`, { method: 'PUT', body: JSON.stringify(data), }); } export async function cancelOrder(id: number): Promise> { return fetchApi(`/orders/${id}/cancel`, { method: 'POST', }); } // Функция для комплексного создания продукта export async function createProductComplete(data: ProductCreateComplete): Promise> { try { console.log('Вызов createProductComplete с данными:', data); // Добавляем обработку случая с ошибкой транзакции const maxRetries = 3; let retryCount = 0; let lastError: any = null; while (retryCount < maxRetries) { try { // Если это повторная попытка, добавляем небольшую задержку if (retryCount > 0) { console.log(`Повторная попытка #${retryCount} после ошибки:`, lastError); // Увеличиваем время ожидания с каждой попыткой await new Promise(resolve => setTimeout(resolve, 1000 * retryCount)); } const response = await fetchApi('/catalog/products/complete', { method: 'POST', body: JSON.stringify(data), }); console.log('Ответ от fetchApi:', response); // Проверяем успешность ответа if (!response.success) { // Проверяем на ошибку транзакции в тексте ошибки if (response.error && response.error.includes('transaction')) { lastError = new Error(response.error); retryCount++; console.log(`Обнаружена ошибка транзакции, попытка ${retryCount}/${maxRetries}`); continue; // Пробуем еще раз } // Если это не ошибка транзакции или исчерпаны попытки, возвращаем ошибку return response; } // Если ответ успешный, возвращаем его return response; } catch (error) { console.error(`Ошибка при создании продукта (попытка ${retryCount + 1}/${maxRetries}):`, error); lastError = error; // Проверяем, является ли ошибка проблемой с транзакцией const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('transaction')) { retryCount++; console.log(`Обнаружена ошибка транзакции, попытка ${retryCount}/${maxRetries}`); continue; // Пробуем еще раз } // Если это не ошибка транзакции, выбрасываем ее throw error; } } // Если мы исчерпали все попытки и все еще получаем ошибку транзакции console.error('Исчерпаны все попытки создания продукта'); return { success: false, data: {} as Product, error: 'Не удалось создать продукт после нескольких попыток. Возможно, проблема с базой данных.' }; } catch (error: any) { console.error('Ошибка в createProductComplete:', error); return { success: false, data: {} as Product, error: error.message || 'Ошибка при создании продукта' }; } } // Обновление продукта с вариантами и изображениями в одном запросе export async function updateProductComplete( productId: number, product: ProductUpdateComplete ): Promise> { return fetchApi(`/catalog/products/${productId}/complete`, { method: 'PUT', body: JSON.stringify(product), }); } // Создаем класс api для обеспечения обратной совместимости const api = { get: async (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(url); return { data: response.data }; }, post: async (endpoint: string, data: any, options: any = {}): Promise<{ data: T }> => { const response = await fetchApi(endpoint, { method: 'POST', body: JSON.stringify(data), ...options }); return { data: response.data }; }, put: async (endpoint: string, data: any, options: any = {}): Promise<{ data: T }> => { const response = await fetchApi(endpoint, { method: 'PUT', body: JSON.stringify(data), ...options }); return { data: response.data }; }, delete: async (endpoint: string, options: any = {}): Promise<{ data: T }> => { const response = await fetchApi(endpoint, { method: 'DELETE', ...options }); return { data: response.data }; } }; // API для авторизации export const authApi = { // Вход в систему login: async (email: string, password: string) => { try { // Формируем данные для отправки const formData = new URLSearchParams(); formData.append('username', email); formData.append('password', password); // Прямой вызов fetch для большего контроля console.log('Вызов API логина с данными:', formData.toString()); const response = await fetch(`${API_URL}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData.toString() }); console.log('Статус ответа:', response.status, response.statusText); // Обрабатываем ответ let data; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { data = await response.json(); console.log('Данные JSON:', data); } else { const text = await response.text(); console.log('Данные текст:', text); try { data = JSON.parse(text); } catch (e) { data = text; } } // Возвращаем в формате ApiResponse if (response.ok) { return { success: true, data, error: undefined }; } else { return { success: false, data: {} as any, error: data?.detail || 'Ошибка при входе' }; } } catch (error) { console.error('Ошибка при выполнении запроса логина:', error); return { success: false, data: {} as any, error: error instanceof Error ? error.message : 'Неизвестная ошибка' }; } }, // Получение данных профиля getProfile: async () => { return fetchApi('/users/me'); }, // Регистрация register: async (userData: any) => { return fetchApi('/auth/register', { method: 'POST', body: JSON.stringify(userData), }); } }; // Экспортируем API класс export default api;