import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, ClientAnalyticsItem, ClientDetailsAnalytics, ClientFilterOptions, ClientMetadataFilters, ClientPurchasePatternAnalytics, CreateUserResult, CuttingSettings, DashboardAnalytics, DateRange, ManagedUser, OrderData, ProductAnalyticsItem, ProductDetailsAnalytics, ProductionOrderSummary, RfmAnalytics, StockData } from './types'; import { formatDateParam } from './dateRanges'; const API_URL = import.meta.env.VITE_API_URL || '/api'; const ANALYTICS_CACHE_TTL_MS = 5 * 60 * 1000; const IN_FLIGHT_CACHE_TTL_MS = 15 * 1000; type CacheOptions = { force?: boolean; shouldCache?: (data: unknown) => boolean; }; type ApiCacheEntry = { expiresAt: number; data?: T; promise?: Promise; }; const analyticsCache = new Map>(); const getCachedAnalyticsValue = (key: string): T | undefined => { const entry = analyticsCache.get(key) as ApiCacheEntry | undefined; if (!entry || entry.data === undefined || entry.expiresAt <= Date.now()) return undefined; return entry.data; }; const getCachedAnalytics = async (key: string, loader: () => Promise, options: CacheOptions = {}): Promise => { const now = Date.now(); const cached = analyticsCache.get(key) as ApiCacheEntry | undefined; if (!options.force) { if (cached?.data !== undefined && cached.expiresAt > now) return cached.data; if (cached?.promise && cached.expiresAt > now) return cached.promise; } const promise = loader(); analyticsCache.set(key, { expiresAt: now + IN_FLIGHT_CACHE_TTL_MS, promise }); try { const data = await promise; if (data === null || options.shouldCache?.(data) === false) { analyticsCache.delete(key); } else { analyticsCache.set(key, { data, expiresAt: Date.now() + ANALYTICS_CACHE_TTL_MS }); } return data; } catch (error) { analyticsCache.delete(key); throw error; } }; const buildDateRangeParams = (dateRange: DateRange) => new URLSearchParams({ start: formatDateParam(dateRange.start), end: formatDateParam(dateRange.end) }); export const login = async (email: string, password: string): Promise => { try { const response = await fetch(`${API_URL}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, password }), }); if (response.ok) { const data = await response.json() as { token?: string; user?: AuthUser }; if (!data.token || !data.user) return false; localStorage.setItem('auth_token', data.token); localStorage.setItem('auth_user', JSON.stringify(data.user)); return true; } return false; } catch (error) { console.error('Login failed', error); return false; } }; export const logout = () => { localStorage.removeItem('auth_token'); localStorage.removeItem('auth_user'); window.location.href = '/#/login'; }; export const isAuthenticated = (): boolean => { return !!localStorage.getItem('auth_token'); }; export const getCurrentUser = (): AuthUser | null => { const rawUser = localStorage.getItem('auth_user'); if (!rawUser) return null; try { return JSON.parse(rawUser) as AuthUser; } catch { localStorage.removeItem('auth_user'); return null; } }; export const isSuperAdmin = (): boolean => { return getCurrentUser()?.role === 'super_admin'; }; export const fetchStock = async (): Promise => { try { const token = localStorage.getItem('auth_token'); const response = await fetch(`${API_URL}/stock`, { headers: { 'Authorization': `Bearer ${token}` } }); if (response.status === 401 || response.status === 403) { logout(); return []; } if (!response.ok) return []; return await response.json(); } catch { return []; } }; export const fetchData = async (): Promise => { try { const token = localStorage.getItem('auth_token'); const response = await fetch(`${API_URL}/data`, { headers: { 'Authorization': `Bearer ${token}` } }); if (response.status === 401 || response.status === 403) { logout(); return []; } if (!response.ok) return []; return await response.json(); } catch (error) { console.error("Fetch failed", error); return []; } }; const authFetch = async (path: string, options: RequestInit = {}): Promise => { const token = localStorage.getItem('auth_token'); const response = await fetch(`${API_URL}${path}`, { ...options, headers: { ...(options.headers || {}), 'Authorization': `Bearer ${token}` } }); if (response.status === 401 || response.status === 403) { logout(); } return response; }; export const fetchProductionOrders = async ( dateRange: DateRange, filters?: { search?: string }, options?: CacheOptions ): Promise => { const params = buildDateRangeParams(dateRange); const search = filters?.search?.trim(); if (search) params.set('search', search); const path = `/production-orders?${params.toString()}`; return getCachedAnalytics(path, async () => { try { const response = await authFetch(path, options?.force ? { cache: 'no-store' } : {}); if (!response.ok) return { orders: [], counts: { all: 0, open: 0, in_progress: 0, finished: 0, canceled: 0 } }; return await response.json(); } catch (error) { console.error('Fetch production orders failed', error); return { orders: [], counts: { all: 0, open: 0, in_progress: 0, finished: 0, canceled: 0 } }; } }, options); }; export const fetchCuttingSettings = async (): Promise => { try { const response = await authFetch('/cutting-settings'); if (!response.ok) return { familyYields: {}, productOverrides: {} }; return await response.json(); } catch (error) { console.error('Fetch cutting settings failed', error); return { familyYields: {}, productOverrides: {} }; } }; export const saveCuttingSettings = async (settings: CuttingSettings): Promise => { const response = await authFetch('/cutting-settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings) }); const data = await response.json().catch(() => null); if (!response.ok) { throw new Error(data?.error || 'Não foi possível salvar as regras de corte.'); } return data as CuttingSettings; }; export const fetchDashboardAnalytics = async (dateRange: DateRange, options?: CacheOptions): Promise => { const path = `/analytics/dashboard?${buildDateRangeParams(dateRange).toString()}`; return getCachedAnalytics(path, async () => { try { const response = await authFetch(path, options?.force ? { cache: 'no-store' } : {}); if (!response.ok) return null; return await response.json(); } catch (error) { console.error('Fetch dashboard analytics failed', error); return null; } }, { ...options, shouldCache: (data) => { const metrics = data as DashboardAnalytics | null; return Boolean( metrics && Array.isArray(metrics.revenueBySeller) && Array.isArray(metrics.ordersBySeller) && Array.isArray(metrics.sellerRevenueByDate) && Array.isArray(metrics.sellerRevenueByHour) ); } }); }; export const fetchProductAnalytics = async (dateRange: DateRange): Promise => { const path = `/analytics/products?${buildDateRangeParams(dateRange).toString()}`; return getCachedAnalytics(path, async () => { try { const response = await authFetch(path); if (!response.ok) return []; return await response.json(); } catch (error) { console.error('Fetch product analytics failed', error); return []; } }); }; export const fetchProductDetailsAnalytics = async (productId: string, dateRange: DateRange): Promise => { const path = `/analytics/products/${encodeURIComponent(productId)}/details?${buildDateRangeParams(dateRange).toString()}`; return getCachedAnalytics(path, async () => { try { const response = await authFetch(path); if (!response.ok) return null; return await response.json(); } catch (error) { console.error('Fetch product details analytics failed', error); return null; } }); }; const appendClientMetadataFilterParams = (params: URLSearchParams, filters?: Partial) => { if (!filters) return; if (filters.marketplace) params.set('marketplace', filters.marketplace); if (filters.canal_venda) params.set('canal_venda', filters.canal_venda); if (filters.seller) params.set('seller', filters.seller); }; const hasClientFilterOptions = (data: unknown) => { const options = data as ClientFilterOptions | null; return Boolean( options?.marketplaces?.length || options?.salesChannels?.length || options?.sellers?.length ); }; export const fetchClientFilterOptions = async (dateRange: DateRange, options?: CacheOptions): Promise => { const path = `/analytics/clients/filters?${buildDateRangeParams(dateRange).toString()}`; return getCachedAnalytics(path, async () => { try { const response = await authFetch(path, options?.force ? { cache: 'no-store' } : {}); if (!response.ok) return { marketplaces: [], salesChannels: [], sellers: [] }; return await response.json(); } catch (error) { console.error('Fetch client filter options failed', error); return { marketplaces: [], salesChannels: [], sellers: [] }; } }, { ...options, shouldCache: hasClientFilterOptions }); }; const buildRfmAnalyticsPath = (dateRange: DateRange, filters?: Partial) => { const params = buildDateRangeParams(dateRange); appendClientMetadataFilterParams(params, filters); return `/analytics/rfm?${params.toString()}`; }; export const getCachedRfmAnalytics = (dateRange: DateRange, filters?: Partial): RfmAnalytics | undefined => { return getCachedAnalyticsValue(buildRfmAnalyticsPath(dateRange, filters)); }; export const fetchRfmAnalytics = async (dateRange: DateRange, filters?: Partial, options?: CacheOptions): Promise => { const path = buildRfmAnalyticsPath(dateRange, filters); return getCachedAnalytics(path, async () => { try { const response = await authFetch(path); if (!response.ok) return null; return await response.json(); } catch (error) { console.error('Fetch RFM analytics failed', error); return null; } }, options); }; const buildClientAnalyticsPath = (dateRange: DateRange, filters?: Partial) => { const params = buildDateRangeParams(dateRange); appendClientMetadataFilterParams(params, filters); return `/analytics/clients?${params.toString()}`; }; export const getCachedClientAnalytics = (dateRange: DateRange, filters?: Partial): ClientAnalyticsItem[] | undefined => { return getCachedAnalyticsValue(buildClientAnalyticsPath(dateRange, filters)); }; export const getCachedClientFilterOptions = (dateRange: DateRange): ClientFilterOptions | undefined => { return getCachedAnalyticsValue(`/analytics/clients/filters?${buildDateRangeParams(dateRange).toString()}`); }; export const fetchClientAnalytics = async (dateRange: DateRange, filters?: Partial, options?: CacheOptions): Promise => { const path = buildClientAnalyticsPath(dateRange, filters); return getCachedAnalytics(path, async () => { try { const response = await authFetch(path); if (!response.ok) return []; return await response.json(); } catch (error) { console.error('Fetch client analytics failed', error); return []; } }, options); }; const emptyClientPurchasePattern = (): ClientPurchasePatternAnalytics => ({ weekdayRangeLabel: 'Todo período', hourRangeLabel: 'Últimos 60 dias', purchaseWeekdays: ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sab'].map(label => ({ label, value: 0 })), purchaseHours: Array.from({ length: 24 }, (_, hour) => ({ label: `${String(hour).padStart(2, '0')}h`, value: 0 })) }); export const fetchClientPurchasePatternAnalytics = async (options?: CacheOptions): Promise => { const path = '/analytics/clients/purchase-pattern'; return getCachedAnalytics(path, async () => { try { const response = await authFetch(path, options?.force ? { cache: 'no-store' } : {}); if (!response.ok) return emptyClientPurchasePattern(); return await response.json(); } catch (error) { console.error('Fetch client purchase pattern analytics failed', error); return emptyClientPurchasePattern(); } }, options); }; export const fetchClientDetailsAnalytics = async (clientToken: string, dateRange: DateRange): Promise => { const path = `/analytics/clients/${encodeURIComponent(clientToken)}/details?${buildDateRangeParams(dateRange).toString()}`; return getCachedAnalytics(path, async () => { try { const response = await authFetch(path); if (!response.ok) return null; return await response.json(); } catch (error) { console.error('Fetch client details analytics failed', error); return null; } }); }; export const clearAnalyticsCache = () => { analyticsCache.clear(); }; export const fetchCampaigns = async (): Promise => { try { const response = await authFetch('/campaigns'); if (!response.ok) return null; return await response.json(); } catch (error) { console.error('Fetch campaigns failed', error); return null; } }; export const fetchCampaignPreview = async (): Promise => { try { const response = await authFetch('/campaigns/preview'); if (!response.ok) return null; return await response.json(); } catch (error) { console.error('Fetch campaign preview failed', error); return null; } }; export const processCampaignsNow = async (): Promise => { try { const response = await authFetch('/campaigns/process', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); if (!response.ok) return null; return await response.json(); } catch (error) { console.error('Process campaigns failed', error); return null; } }; export const retryCampaignGroup = async (baseProductName: string): Promise => { try { const response = await authFetch('/campaigns/retry', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ baseProductName }) }); return response.ok; } catch (error) { console.error('Retry campaign failed', error); return false; } }; export const fetchUsers = async (): Promise => { try { const response = await authFetch('/users'); if (!response.ok) return []; const data = await response.json() as { users?: ManagedUser[] }; return data.users || []; } catch (error) { console.error('Fetch users failed', error); return []; } }; export const createUser = async (payload: { name: string; email: string; password?: string }): Promise => { const response = await authFetch('/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json().catch(() => null); if (!response.ok) { throw new Error(data?.error || 'Não foi possível criar o usuário.'); } return data as CreateUserResult; }; export const updateUser = async (id: number, payload: { name: string; email: string; isActive: boolean; password?: string }): Promise => { const response = await authFetch(`/users/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json().catch(() => null); if (!response.ok) { throw new Error(data?.error || 'Não foi possível editar o usuário.'); } return data.user as ManagedUser; }; export const deleteUser = async (id: number): Promise => { const response = await authFetch(`/users/${id}`, { method: 'DELETE' }); if (!response.ok) { const data = await response.json().catch(() => null); throw new Error(data?.error || 'Não foi possível excluir o usuário.'); } }; export const exportToCSV = (data: Record[], filename: string) => { if (!data || !data.length) return; const headers = Object.keys(data[0]); const csvRows = []; // Add headers csvRows.push(headers.join(',')); // Add rows for (const row of data) { const values = headers.map(header => { const val = row[header]; const escaped = String(val ?? '').replace(/"/g, '""'); return `"${escaped}"`; }); csvRows.push(values.join(',')); } const csvString = csvRows.join('\n'); const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); if (link.download !== undefined) { const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } };