dressed_for_succes_store/frontend/lib/api.ts

443 lines
14 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.

// Базовый 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;