dressed_for_succes_store/frontend/lib/api.ts

878 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<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 (!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<T>;
}
// Если ответ содержит объект внутри: { 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, 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;
}
// Типы для вариантов и изображений при создании/обновлении продукта
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, string> | 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, string> | 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<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, formData: FormData): Promise<ApiResponse<{id: number; image_url: string; is_primary: boolean}>> {
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<string, string> = {};
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<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',
});
}
// Функция для комплексного создания продукта
export async function createProductComplete(data: ProductCreateComplete): Promise<ApiResponse<Product>> {
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<Product>('/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<ApiResponse<Product>> {
return fetchApi<Product>(`/catalog/products/${productId}/complete`, {
method: 'PUT',
body: JSON.stringify(product),
});
}
// Создаем класс 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 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;