import axios from 'axios'; import { notification } from 'antd'; import type { CatalogItem, CreateInvoiceRequest, MatchRequest, HealthResponse, InvoiceResponse, ProductMatch, Recommendation, UnmatchedItem, UserSettings, InvoiceStats, ProductGroup, Store, Supplier, DraftInvoice, DraftItem, UpdateDraftItemRequest, CommitDraftRequest, ReorderDraftItemsRequest, ProductSearchResult, AddContainerRequest, AddContainerResponse, DictionariesResponse, UnifiedInvoice, ServerUser, UserRole, InvoiceDetails, GetPhotosResponse } from './types'; // Базовый URL export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api'; // Хелпер для получения полного URL картинки (убирает /api если путь статики идет от корня, или добавляет как есть) // В данном ТЗ сказано просто склеивать. export const getStaticUrl = (path: string | null | undefined): string => { if (!path) return ''; if (path.startsWith('http')) return path; return `${API_BASE_URL}${path}`; }; // Телеграм объект const tg = window.Telegram?.WebApp; // Событие для глобальной обработки 401 export const UNAUTHORIZED_EVENT = 'rms_unauthorized'; // Событие для режима технического обслуживания (503) export const MAINTENANCE_EVENT = 'rms_maintenance'; const apiClient = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }); // --- Request Interceptor (Авторизация через initData) --- apiClient.interceptors.request.use((config) => { const initData = tg?.initData; // Если initData пустая — мы не в Telegram. Блокируем запрос. if (!initData) { console.error('Запрос заблокирован: приложение запущено вне Telegram.'); return Promise.reject(new Error('MISSING_TELEGRAM_DATA')); } // Устанавливаем заголовок согласно новым требованиям config.headers['Authorization'] = `Bearer ${initData}`; return config; }); // --- Response Interceptor (Обработка ошибок и уведомления) --- apiClient.interceptors.response.use( (response) => response, (error) => { if (error.response && error.response.status === 401) { // Глобальное уведомление об ошибке авторизации notification.error({ message: 'Ошибка авторизации', description: 'Ваша сессия в Telegram истекла или данные неверны. Попробуйте перезапустить бота.', placement: 'top', }); window.dispatchEvent(new Event(UNAUTHORIZED_EVENT)); } if (error.response && error.response.status === 503) { // Режим технического обслуживания window.dispatchEvent(new Event(MAINTENANCE_EVENT)); } // Если запрос был отменен нами (нет initData), не выводим стандартную ошибку API if (error.message === 'MISSING_TELEGRAM_DATA') { return Promise.reject(error); } console.error('API Error:', error); return Promise.reject(error); } ); export const api = { checkHealth: async (): Promise => { const { data } = await apiClient.get('/health'); return data; }, getRecommendations: async (): Promise => { const { data } = await apiClient.get('/recommendations'); return data; }, getCatalogItems: async (): Promise => { const { data } = await apiClient.get('/ocr/catalog'); return data; }, searchProducts: async (query: string): Promise => { const { data } = await apiClient.get('/ocr/search', { params: { q: query } }); return data; }, createContainer: async (payload: AddContainerRequest): Promise => { const { data } = await apiClient.post('/drafts/container', payload); return data; }, getMatches: async (): Promise => { const { data } = await apiClient.get('/ocr/matches'); return data; }, getUnmatched: async (): Promise => { const { data } = await apiClient.get('/ocr/unmatched'); return data; }, createMatch: async (payload: MatchRequest): Promise<{ status: string }> => { const { data } = await apiClient.post<{ status: string }>('/ocr/match', payload); return data; }, deleteMatch: async (rawName: string): Promise<{ status: string }> => { const { data } = await apiClient.delete<{ status: string }>('/ocr/match', { params: { raw_name: rawName } }); return data; }, deleteUnmatched: async (rawName: string): Promise<{ status: string }> => { const { data } = await apiClient.delete<{ status: string }>('/ocr/unmatched', { params: { raw_name: rawName } }); return data; }, createInvoice: async (payload: CreateInvoiceRequest): Promise => { const { data } = await apiClient.post('/invoices/send', payload); return data; }, // --- НОВЫЙ МЕТОД: Получение всех справочников --- getDictionaries: async (): Promise => { const { data } = await apiClient.get('/dictionaries'); return data; }, // Старые методы оставляем для совместимости, но они могут вызывать getDictionaries внутри или deprecated endpoint getStores: async (): Promise => { // Можно использовать новый эндпоинт и возвращать часть данных const { data } = await apiClient.get('/dictionaries'); return data.stores; }, getSuppliers: async (): Promise => { // Реальный запрос вместо мока const { data } = await apiClient.get('/dictionaries'); return data.suppliers; }, // Обновленный метод получения списка накладных с фильтрацией getDrafts: async (from?: string, to?: string): Promise => { const { data } = await apiClient.get('/drafts', { params: { from, to } }); return data; }, getDraft: async (id: string): Promise => { const { data } = await apiClient.get(`/drafts/${id}`); return data; }, updateDraftItem: async (draftId: string, itemId: string, payload: UpdateDraftItemRequest): Promise => { const { data } = await apiClient.patch(`/drafts/${draftId}/items/${itemId}`, payload); return data; }, addDraftItem: async (draftId: string): Promise => { const { data } = await apiClient.post(`/drafts/${draftId}/items`, {}); return data; }, deleteDraftItem: async (draftId: string, itemId: string): Promise => { await apiClient.delete(`/drafts/${draftId}/items/${itemId}`); }, reorderDraftItems: async (draftId: string, payload: ReorderDraftItemsRequest): Promise => { await apiClient.post(`/drafts/${draftId}/reorder`, payload); }, commitDraft: async (draftId: string, payload: CommitDraftRequest): Promise<{ document_number: string }> => { const { data } = await apiClient.post<{ document_number: string }>(`/drafts/${draftId}/commit`, payload); return data; }, deleteDraft: async (id: string): Promise => { await apiClient.delete(`/drafts/${id}`); }, // --- Настройки и Статистика --- getSettings: async (): Promise => { const { data } = await apiClient.get('/settings'); return data; }, updateSettings: async (payload: UserSettings): Promise => { const { data } = await apiClient.post('/settings', payload); return data; }, getStats: async (): Promise => { const { data } = await apiClient.get('/stats/invoices'); return data; }, getProductGroups: async (): Promise => { const { data } = await apiClient.get('/dictionaries/groups'); return data; }, // --- Управление командой --- getUsers: async (): Promise => { const { data } = await apiClient.get('/settings/users'); return data; }, updateUserRole: async (userId: string, newRole: UserRole): Promise<{ status: string }> => { const { data } = await apiClient.patch<{ status: string }>(`/settings/users/${userId}`, { new_role: newRole }); return data; }, removeUser: async (userId: string): Promise<{ status: string }> => { const { data } = await apiClient.delete<{ status: string }>(`/settings/users/${userId}`); return data; }, getInvoice: async (id: string): Promise => { const { data } = await apiClient.get(`/invoices/${id}`); return data; }, syncInvoices: async (): Promise => { await apiClient.post('/invoices/sync'); }, getPhotos: async (page = 1, limit = 20): Promise => { const { data } = await apiClient.get('/photos', { params: { page, limit } }); return data; }, deletePhoto: async (id: string, force = false): Promise => { await apiClient.delete(`/photos/${id}`, { params: { force } }); }, regenerateDraftFromPhoto: async (id: string): Promise => { await apiClient.post(`/photos/${id}/regenerate`); }, };