mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
290 lines
9.8 KiB
TypeScript
290 lines
9.8 KiB
TypeScript
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<HealthResponse> => {
|
||
const { data } = await apiClient.get<HealthResponse>('/health');
|
||
return data;
|
||
},
|
||
|
||
getRecommendations: async (): Promise<Recommendation[]> => {
|
||
const { data } = await apiClient.get<Recommendation[]>('/recommendations');
|
||
return data;
|
||
},
|
||
|
||
getCatalogItems: async (): Promise<CatalogItem[]> => {
|
||
const { data } = await apiClient.get<CatalogItem[]>('/ocr/catalog');
|
||
return data;
|
||
},
|
||
|
||
searchProducts: async (query: string): Promise<ProductSearchResult[]> => {
|
||
const { data } = await apiClient.get<ProductSearchResult[]>('/ocr/search', {
|
||
params: { q: query }
|
||
});
|
||
return data;
|
||
},
|
||
|
||
createContainer: async (payload: AddContainerRequest): Promise<AddContainerResponse> => {
|
||
const { data } = await apiClient.post<AddContainerResponse>('/drafts/container', payload);
|
||
return data;
|
||
},
|
||
|
||
getMatches: async (): Promise<ProductMatch[]> => {
|
||
const { data } = await apiClient.get<ProductMatch[]>('/ocr/matches');
|
||
return data;
|
||
},
|
||
|
||
getUnmatched: async (): Promise<UnmatchedItem[]> => {
|
||
const { data } = await apiClient.get<UnmatchedItem[]>('/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<InvoiceResponse> => {
|
||
const { data } = await apiClient.post<InvoiceResponse>('/invoices/send', payload);
|
||
return data;
|
||
},
|
||
|
||
// --- НОВЫЙ МЕТОД: Получение всех справочников ---
|
||
getDictionaries: async (): Promise<DictionariesResponse> => {
|
||
const { data } = await apiClient.get<DictionariesResponse>('/dictionaries');
|
||
return data;
|
||
},
|
||
|
||
// Старые методы оставляем для совместимости, но они могут вызывать getDictionaries внутри или deprecated endpoint
|
||
getStores: async (): Promise<Store[]> => {
|
||
// Можно использовать новый эндпоинт и возвращать часть данных
|
||
const { data } = await apiClient.get<DictionariesResponse>('/dictionaries');
|
||
return data.stores;
|
||
},
|
||
|
||
getSuppliers: async (): Promise<Supplier[]> => {
|
||
// Реальный запрос вместо мока
|
||
const { data } = await apiClient.get<DictionariesResponse>('/dictionaries');
|
||
return data.suppliers;
|
||
},
|
||
|
||
// Обновленный метод получения списка накладных с фильтрацией
|
||
getDrafts: async (from?: string, to?: string): Promise<UnifiedInvoice[]> => {
|
||
const { data } = await apiClient.get<UnifiedInvoice[]>('/drafts', {
|
||
params: { from, to }
|
||
});
|
||
return data;
|
||
},
|
||
|
||
getDraft: async (id: string): Promise<DraftInvoice> => {
|
||
const { data } = await apiClient.get<DraftInvoice>(`/drafts/${id}`);
|
||
return data;
|
||
},
|
||
|
||
updateDraftItem: async (draftId: string, itemId: string, payload: UpdateDraftItemRequest): Promise<DraftInvoice> => {
|
||
const { data } = await apiClient.patch<DraftInvoice>(`/drafts/${draftId}/items/${itemId}`, payload);
|
||
return data;
|
||
},
|
||
|
||
addDraftItem: async (draftId: string): Promise<DraftItem> => {
|
||
const { data } = await apiClient.post<DraftItem>(`/drafts/${draftId}/items`, {});
|
||
return data;
|
||
},
|
||
|
||
deleteDraftItem: async (draftId: string, itemId: string): Promise<void> => {
|
||
await apiClient.delete(`/drafts/${draftId}/items/${itemId}`);
|
||
},
|
||
|
||
reorderDraftItems: async (draftId: string, payload: ReorderDraftItemsRequest): Promise<void> => {
|
||
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<void> => {
|
||
await apiClient.delete(`/drafts/${id}`);
|
||
},
|
||
|
||
// --- Настройки и Статистика ---
|
||
|
||
getSettings: async (): Promise<UserSettings> => {
|
||
const { data } = await apiClient.get<UserSettings>('/settings');
|
||
return data;
|
||
},
|
||
|
||
updateSettings: async (payload: UserSettings): Promise<UserSettings> => {
|
||
const { data } = await apiClient.post<UserSettings>('/settings', payload);
|
||
return data;
|
||
},
|
||
|
||
getStats: async (): Promise<InvoiceStats> => {
|
||
const { data } = await apiClient.get<InvoiceStats>('/stats/invoices');
|
||
return data;
|
||
},
|
||
|
||
getProductGroups: async (): Promise<ProductGroup[]> => {
|
||
const { data } = await apiClient.get<ProductGroup[]>('/dictionaries/groups');
|
||
return data;
|
||
},
|
||
|
||
// --- Управление командой ---
|
||
|
||
getUsers: async (): Promise<ServerUser[]> => {
|
||
const { data } = await apiClient.get<ServerUser[]>('/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<InvoiceDetails> => {
|
||
const { data } = await apiClient.get<InvoiceDetails>(`/invoices/${id}`);
|
||
return data;
|
||
},
|
||
|
||
syncInvoices: async (): Promise<void> => {
|
||
await apiClient.post('/invoices/sync');
|
||
},
|
||
getPhotos: async (page = 1, limit = 20): Promise<GetPhotosResponse> => {
|
||
const { data } = await apiClient.get<GetPhotosResponse>('/photos', {
|
||
params: { page, limit }
|
||
});
|
||
return data;
|
||
},
|
||
|
||
deletePhoto: async (id: string, force = false): Promise<void> => {
|
||
await apiClient.delete(`/photos/${id}`, {
|
||
params: { force }
|
||
});
|
||
},
|
||
|
||
regenerateDraftFromPhoto: async (id: string): Promise<void> => {
|
||
await apiClient.post(`/photos/${id}/regenerate`);
|
||
},
|
||
};
|
||
|