import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, CatalogCategory, CatalogCategoryPayload, CatalogProduct, CatalogProductPayload, CatalogSummary, ClientAnalyticsItem, ClientDetailsAnalytics, ClientFilterOptions, ClientMetadataFilters, ClientPurchasePatternAnalytics, ConsumptionReference, ConsumptionReferencePayload, CreateProductionOrdersResult, CreateUserResult, CutPlanLotReservationPayload, CuttingSettings, DashboardAnalytics, DateRange, GraphsStockBalanceHistoryItem, GraphsStockBalancePayload, ManagedUser, MaterialTransformationStatus, OperationalUser, OrderData, OlistRunDetails, OlistStatus, ProductAnalyticsItem, ProductComposition, ProductCompositionImportSummary, ProductCompositionPayload, ProductDetailsAnalytics, ProductionOrderDefectPayload, ProductionOrderExecutionPayload, ProductionOrderImportData, ProductionOrderItem, ProductionOrderPayload, ProductionOrderStatus, ProductionOrderSummary, ProductionPlan, ProductionPlanPayload, ProductionPlanReviewPayload, ProductionWorkstation, ProductionWorkstationPayload, RfmAnalytics, SellerAnalyticsItem, SellerDetailsAnalytics, StockData, SupplyFabricPlan, SupplyFabricPlanPayload, SupplyInventoryAdjustmentPayload, SupplyLot, SupplyMaterialDetailsPayload, SupplyMaterialTransformation, SupplyMaterialTransformationPayload, SupplyProductionExitPayload, SupplyPurchaseNeed, SupplyPurchaseOrder, SupplyPurchaseOrderPayload, SupplyPurchaseOrderStatus, SupplyReceipt, SupplyReceiptPayload, SupplySummary } 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 type LoginConfig = { captchaRequired: boolean; captchaConfigured: boolean; turnstileSiteKey: string; }; export type LoginResult = 'success' | 'invalid_credentials' | 'captcha_failed' | 'server_error'; export const getLoginConfig = async (): Promise => { const response = await fetch(`${API_URL}/login/config`, { cache: 'no-store' }); if (!response.ok) throw new Error('Failed to load login config'); return response.json() as Promise; }; export const login = async (email: string, password: string, captchaToken?: string): Promise => { try { const response = await fetch(`${API_URL}/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email, password, captchaToken }), }); if (response.ok) { const data = await response.json() as { token?: string; user?: AuthUser }; if (!data.token || !data.user) return 'server_error'; localStorage.setItem('auth_token', data.token); localStorage.setItem('auth_user', JSON.stringify(data.user)); return 'success'; } if (response.status === 403) return 'captcha_failed'; return 'invalid_credentials'; } catch (error) { console.error('Login failed', error); return 'server_error'; } }; 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) { logout(); return []; } if (!response.ok) return []; return await response.json(); } catch { return []; } }; export const fetchGraphsStockBalanceHistory = async (productId: string): Promise => { const response = await authFetch(`/stock/${encodeURIComponent(productId)}/graphs-balance-history`); if (!response.ok) throw new Error('Não foi possível consultar o histórico de saldo Graphs.'); return response.json() as Promise; }; export const saveGraphsStockBalance = async (productId: string, payload: GraphsStockBalancePayload): Promise => { const response = await authFetch(`/stock/${encodeURIComponent(productId)}/graphs-balance`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json().catch(() => null) as StockData | { error?: string } | null; if (!response.ok) throw new Error((data as { error?: string } | null)?.error || 'Não foi possível salvar o saldo Graphs.'); return data as StockData; }; export const deleteGraphsStockBalance = async (productId: string): Promise => { const response = await authFetch(`/stock/${encodeURIComponent(productId)}/graphs-balance`, { method: 'DELETE' }); const data = await response.json().catch(() => null) as StockData | { error?: string } | null; if (!response.ok) throw new Error((data as { error?: string } | null)?.error || 'Não foi possível remover o saldo Graphs.'); return data as StockData; }; 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) { 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}` } }); // A 403 means the signed-in user lacks permission for this specific // resource. Keep their session intact so protected optional data cannot // force a logout from an otherwise accessible page. if (response.status === 401) { logout(); } return response; }; export const downloadDatabaseDiagnostic = async (): Promise => { const response = await authFetch('/admin/database-diagnostic', { cache: 'no-store' }); const data = await response.json().catch(() => null); if (!response.ok) { throw new Error(data?.error || 'Não foi possível exportar o diagnóstico do banco.'); } const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json;charset=utf-8;' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = `graphs-db-diagnostic-${new Date().toISOString().slice(0, 10)}.json`; anchor.click(); URL.revokeObjectURL(url); }; export const fetchProductionOrders = async ( dateRange: DateRange, filters?: { search?: string; activeOnly?: boolean }, options?: CacheOptions ): Promise => { const params = buildDateRangeParams(dateRange); const search = filters?.search?.trim(); if (search) params.set('search', search); if (filters?.activeOnly) params.set('activeOnly', 'true'); 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, cutting: 0, sewing: 0, review: 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, cutting: 0, sewing: 0, review: 0, finished: 0, canceled: 0 } }; } }, options); }; export const createProductionOrders = async (orders: ProductionOrderPayload[], lotReservations: CutPlanLotReservationPayload[] = [], plan?: ProductionPlanPayload): Promise => { const response = await authFetch('/production-orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ orders, lotReservations, plan }) }); if (!response.ok) throw new Error('Não foi possível criar as ordens de produção.'); analyticsCache.clear(); return await response.json(); }; export const fetchProductionPlans = async (): Promise => { const response = await authFetch('/production-plans', { cache: 'no-store' }); if (!response.ok) throw new Error('Não foi possível carregar os planos de produção.'); return response.json() as Promise; }; export const updateProductionPlan = async (id: number, payload: ProductionPlanPayload): Promise => { const response = await authFetch(`/production-plans/${id}`, { method: 'PUT', 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 atualizar o plano de produção.'); analyticsCache.clear(); return data as ProductionPlan; }; export const updateProductionPlanStatus = async (id: number, status: ProductionOrderStatus): Promise => { const response = await authFetch(`/production-plans/${id}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); const data = await response.json().catch(() => null); if (!response.ok) throw new Error(data?.error || 'Não foi possível mover o plano de produção.'); analyticsCache.clear(); return data as ProductionPlan; }; export const saveProductionPlanReview = async (id: number, payload: ProductionPlanReviewPayload): Promise => { const response = await authFetch(`/production-plans/${id}/review`, { method: 'PUT', 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 salvar a revisão operacional.'); analyticsCache.clear(); return data as ProductionPlan; }; export const updateProductionOrderStatus = async (id: number, status: ProductionOrderStatus): Promise => { const response = await authFetch(`/production-orders/${id}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (!response.ok) throw new Error('Não foi possível atualizar o status da ordem.'); analyticsCache.clear(); return await response.json(); }; export const updateProductionOrderExecution = async (id: number, payload: ProductionOrderExecutionPayload): Promise => { const response = await authFetch(`/production-orders/${id}/execution`, { 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 registrar a execução da OP.'); analyticsCache.clear(); return data as ProductionOrderItem; }; export const fetchProductionWorkstations = async (includeInactive = false): Promise => { const response = await authFetch(`/production-workstations${includeInactive ? '?includeInactive=true' : ''}`, { cache: 'no-store' }); if (!response.ok) throw new Error('Não foi possível carregar os postos de trabalho.'); return response.json() as Promise; }; export const createProductionWorkstation = async (payload: ProductionWorkstationPayload): Promise => { const response = await authFetch('/production-workstations', { 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 posto de trabalho.'); return data as ProductionWorkstation; }; export const updateProductionWorkstation = async (id: number, payload: ProductionWorkstationPayload): Promise => { const response = await authFetch(`/production-workstations/${id}`, { method: 'PUT', 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 atualizar o posto de trabalho.'); return data as ProductionWorkstation; }; export const addProductionOrderDefect = async (id: number, payload: ProductionOrderDefectPayload): Promise => { const response = await authFetch(`/production-orders/${id}/defects`, { 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 registrar o defeito.'); analyticsCache.clear(); return data as ProductionOrderItem; }; 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 fetchCatalogSummary = async (): Promise => { try { const response = await authFetch('/catalog'); if (!response.ok) return { categories: [], products: [], consumptionReferences: [] }; return await response.json(); } catch (error) { console.error('Fetch catalog summary failed', error); return { categories: [], products: [], consumptionReferences: [] }; } }; export const fetchOlistStatus = async (runsPage = 1, runsPageSize = 5): Promise => { const params = new URLSearchParams({ runsPage: String(runsPage), runsPageSize: String(runsPageSize) }); const response = await authFetch(`/olist/status?${params}`, { cache: 'no-store' }); const data = await response.json().catch(() => null); if (!response.ok) throw new Error(data?.error || 'Não foi possível consultar a integração Olist.'); return data as OlistStatus; }; export const getOlistAuthorizationUrl = async (): Promise => { const response = await authFetch('/olist/authorization-url', { method: 'POST' }); const data = await response.json().catch(() => null); if (!response.ok || !data?.url) throw new Error(data?.error || 'Não foi possível iniciar a conexão com a Olist.'); return data.url as string; }; export const startOlistCompositionSync = async (fullSync = false): Promise => { const response = await authFetch('/olist/sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fullSync }) }); const data = await response.json().catch(() => null); if (!response.ok) throw new Error(data?.error || 'Não foi possível iniciar a sincronização Olist.'); }; export const stopOlistCompositionSync = async (): Promise => { const response = await authFetch('/olist/sync/stop', { method: 'POST' }); const data = await response.json().catch(() => null); if (!response.ok) throw new Error(data?.error || 'Não foi possível interromper a sincronização Olist.'); }; export const fetchOlistRunDetails = async (runId: number, eventsPage = 1, productsPage = 1): Promise => { const params = new URLSearchParams({ eventsPage: String(eventsPage), productsPage: String(productsPage) }); const response = await authFetch(`/olist/runs/${runId}?${params}`, { cache: 'no-store' }); const data = await response.json().catch(() => null); if (!response.ok) throw new Error(data?.error || 'Não foi possível consultar os detalhes da execução Olist.'); return data as OlistRunDetails; }; export const fetchProductionOrderImportData = async (runsPage = 1, runsPageSize = 5): Promise => { const params = new URLSearchParams({ runsPage: String(runsPage), runsPageSize: String(runsPageSize) }); const response = await authFetch(`/olist/production-order-imports?${params}`, { cache: 'no-store' }); const data = await response.json().catch(() => null); if (!response.ok) throw new Error(data?.error || 'Não foi possível consultar as importações de OP.'); return data as ProductionOrderImportData; }; export const exportMissingProductionOrderDataCsv = async (): Promise => { const response = await authFetch('/olist/production-order-imports/pending-csv', { cache: 'no-store' }); const data = await response.json().catch(() => null); if (!response.ok || !data?.csv || !data?.filename) { throw new Error(data?.error || 'Não foi possível exportar as pendências das OPs.'); } const url = URL.createObjectURL(new Blob([data.csv], { type: 'text/csv;charset=utf-8' })); const anchor = document.createElement('a'); anchor.href = url; anchor.download = data.filename; document.body.appendChild(anchor); anchor.click(); anchor.remove(); URL.revokeObjectURL(url); return Number(data.totalRows) || 0; }; export const importFinalizedProductionOrderCsv = async (filename: string, csv: string): Promise }> => { const response = await authFetch('/olist/production-order-imports', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename, csv }) }); const data = await response.json().catch(() => null); if (!response.ok) throw new Error(data?.error || 'Não foi possível importar as OPs finalizadas.'); return data; }; export const saveCatalogCategory = async (payload: CatalogCategoryPayload): Promise => { const response = await authFetch('/catalog/categories', { 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 salvar a categoria.'); } return data as CatalogCategory; }; export const updateCatalogCategory = async (id: number, payload: CatalogCategoryPayload): Promise => { const response = await authFetch(`/catalog/categories/${id}`, { method: 'PUT', 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 atualizar a categoria.'); } return data as CatalogCategory; }; export const deleteCatalogCategory = async (id: number): Promise => { const response = await authFetch(`/catalog/categories/${id}`, { method: 'DELETE' }); if (!response.ok) { const data = await response.json().catch(() => null); throw new Error(data?.error || 'Não foi possível excluir a categoria.'); } }; export const saveCatalogProduct = async (payload: CatalogProductPayload): Promise => { const response = await authFetch('/catalog/products', { 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 salvar o produto.'); } return data as CatalogProduct; }; export const updateCatalogProduct = async (id: number, payload: CatalogProductPayload): Promise => { const response = await authFetch(`/catalog/products/${id}`, { method: 'PUT', 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 atualizar o produto.'); return data as CatalogProduct; }; export const deleteCatalogProduct = async (id: number): Promise => { const response = await authFetch(`/catalog/products/${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 produto.'); } }; export const saveConsumptionReference = async (payload: ConsumptionReferencePayload): Promise => { const response = await authFetch('/catalog/consumption-references', { 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 salvar a referência de consumo.'); } return data as ConsumptionReference; }; export const updateConsumptionReference = async (id: number, payload: ConsumptionReferencePayload): Promise => { const response = await authFetch(`/catalog/consumption-references/${id}`, { method: 'PUT', 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 atualizar a referência de consumo.'); return data as ConsumptionReference; }; export const deleteConsumptionReference = async (id: number): Promise => { const response = await authFetch(`/catalog/consumption-references/${id}`, { method: 'DELETE' }); if (!response.ok) { const data = await response.json().catch(() => null); throw new Error(data?.error || 'Não foi possível excluir a referência.'); } }; export const importProductCompositions = async (payload: unknown): Promise => { const response = await authFetch('/catalog/product-compositions/import', { 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 importar as composições.'); } analyticsCache.clear(); return data as ProductCompositionImportSummary; }; export const fetchSupplySummary = async (): Promise => { const emptySummary: SupplySummary = { receipts: [], lots: [], movements: [], fabricPlans: [], purchaseNeeds: [], stats: { totalQuantityKg: 0, activeLots: 0, rolls: 0, alerts: 0, pendingReceipts: 0, approvedReceipts: 0 } }; try { const response = await authFetch('/supply'); if (!response.ok) return emptySummary; return await response.json(); } catch (error) { console.error('Fetch supply summary failed', error); return emptySummary; } }; export const fetchSupplySuppliers = async (): Promise => { const response = await authFetch('/supply/suppliers'); if (!response.ok) throw new Error('Não foi possível carregar os fornecedores cadastrados.'); return response.json() as Promise; }; export const fetchSupplyPurchaseOrders = async (): Promise => { const response = await authFetch('/supply/purchase-orders'); if (!response.ok) throw new Error('Não foi possível carregar as ordens de compra.'); return response.json() as Promise; }; export const createSupplyPurchaseOrder = async (payload: SupplyPurchaseOrderPayload): Promise => { const response = await authFetch('/supply/purchase-orders', { 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 a ordem de compra.'); return data as SupplyPurchaseOrder; }; export const updateSupplyPurchaseOrderStatus = async (id: number, status: SupplyPurchaseOrderStatus): Promise => { const response = await authFetch(`/supply/purchase-orders/${id}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); const data = await response.json().catch(() => null); if (!response.ok) throw new Error(data?.error || 'Não foi possível atualizar a ordem de compra.'); return data as SupplyPurchaseOrder; }; export const saveSupplyMaterialDetails = async ( materialKey: string, payload: SupplyMaterialDetailsPayload ): Promise => { const response = await authFetch(`/supply/materials/${encodeURIComponent(materialKey)}`, { method: 'PUT', 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 salvar os dados do material.'); } analyticsCache.clear(); }; export const createSupplyReceipt = async (payload: SupplyReceiptPayload): Promise => { const response = await authFetch('/supply/receipts', { 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 registrar o recebimento.'); } return data as SupplyReceipt; }; export const approveSupplyReceipt = async (id: number): Promise => { const response = await authFetch(`/supply/receipts/${id}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); const data = await response.json().catch(() => null); if (!response.ok) { throw new Error(data?.error || 'Não foi possível aprovar o recebimento.'); } return data as SupplyReceipt; }; export const deleteSupplyReceipt = async (id: number): Promise => { const response = await authFetch(`/supply/receipts/${id}`, { method: 'DELETE' }); if (!response.ok) { const data = await response.json().catch(() => null); throw new Error(data?.error || 'Não foi possível remover o recebimento.'); } }; export const fetchSupplyFabricPlans = async (): Promise => { try { const response = await authFetch('/supply/fabric-plans'); if (!response.ok) return []; return await response.json(); } catch (error) { console.error('Fetch supply fabric plans failed', error); return []; } }; export const createSupplyFabricPlan = async (payload: SupplyFabricPlanPayload): Promise => { const response = await authFetch('/supply/fabric-plans', { 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 salvar o plano de malha.'); } return data as SupplyFabricPlan; }; export const deleteSupplyFabricPlan = async (id: number): Promise => { const response = await authFetch(`/supply/fabric-plans/${id}`, { method: 'DELETE' }); if (!response.ok) { const data = await response.json().catch(() => null); throw new Error(data?.error || 'Não foi possível remover o plano de malha.'); } }; export const fetchSupplyMaterialTransformations = async (): Promise => { try { const response = await authFetch('/supply/material-transformations'); if (!response.ok) return []; return await response.json(); } catch (error) { console.error('Fetch material transformations failed', error); return []; } }; export const createSupplyMaterialTransformation = async (payload: SupplyMaterialTransformationPayload): Promise => { const response = await authFetch('/supply/material-transformations', { 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 salvar a transformação.'); return data as SupplyMaterialTransformation; }; export const updateSupplyMaterialTransformationStatus = async (id: number, status: MaterialTransformationStatus): Promise => { const response = await authFetch(`/supply/material-transformations/${id}/status`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); const data = await response.json().catch(() => null); if (!response.ok) throw new Error(data?.error || 'Não foi possível atualizar a transformação.'); return data as SupplyMaterialTransformation; }; export const fetchSupplyPurchaseNeeds = async (): Promise => { try { const response = await authFetch('/supply/purchase-needs'); if (!response.ok) return []; return await response.json(); } catch (error) { console.error('Fetch supply purchase needs failed', error); return []; } }; export const adjustSupplyLotInventory = async (lotId: number, payload: SupplyInventoryAdjustmentPayload): Promise => { const response = await authFetch(`/supply/lots/${lotId}/inventory-adjustment`, { 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 ajustar o inventário.'); } return data as SupplyLot; }; export const consumeSupplyLotForProduction = async (lotId: number, payload: SupplyProductionExitPayload): Promise => { const response = await authFetch(`/supply/lots/${lotId}/production-exit`, { 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 registrar a saída para produção.'); } return data as SupplyLot; }; 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 fetchSellerAnalytics = async (dateRange: DateRange): Promise => { const path = `/analytics/sellers?${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 seller analytics failed', error); return []; } }); }; export const fetchSellerDetailsAnalytics = async (sellerId: string, dateRange: DateRange): Promise => { const path = `/analytics/sellers/${encodeURIComponent(sellerId)}/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 seller details analytics failed', error); return null; } }); }; 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; } }); }; export const fetchProductComposition = async (productId: string): Promise => { try { const response = await authFetch(`/analytics/products/${encodeURIComponent(productId)}/composition`); if (!response.ok) return null; const data = await response.json() as { composition?: ProductComposition | null }; return data.composition || null; } catch (error) { console.error('Fetch product composition failed', error); return null; } }; export const saveManualProductComposition = async (productId: string, payload: ProductCompositionPayload): Promise => { const response = await authFetch(`/analytics/products/${encodeURIComponent(productId)}/composition`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json().catch(() => null) as { composition?: ProductComposition; error?: string } | null; if (!response.ok || !data?.composition) throw new Error(data?.error || 'Não foi possível salvar a composição.'); return data.composition; }; export const deleteManualProductComposition = async (productId: string): Promise => { const response = await authFetch(`/analytics/products/${encodeURIComponent(productId)}/composition`, { method: 'DELETE' }); const data = await response.json().catch(() => null) as { error?: string } | null; if (!response.ok) throw new Error(data?.error || 'Não foi possível excluir a composição manual.'); }; export const fetchProductCompositions = async (): Promise => { try { const response = await authFetch('/analytics/product-compositions'); if (!response.ok) return []; const data = await response.json() as { compositions?: ProductComposition[] }; return data.compositions || []; } catch (error) { console.error('Fetch product compositions failed', error); return []; } }; export const exportProductCompositions = async (): Promise => { const response = await authFetch('/analytics/product-compositions'); const data = await response.json().catch(() => null) as { compositions?: ProductComposition[]; error?: string } | null; if (!response.ok) { throw new Error(data?.error || 'Não foi possível exportar as composições.'); } const compositions = data?.compositions || []; const blob = new Blob([JSON.stringify({ exportedAt: new Date().toISOString(), compositions }, null, 2)], { type: 'application/json;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `composicoes_produtos_${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); return compositions.length; }; 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 fetchOperationalUsers = async (): Promise => { try { const response = await authFetch('/users/operational'); if (!response.ok) return []; const data = await response.json() as { users?: OperationalUser[] }; return data.users || []; } catch (error) { console.error('Fetch operational 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); } };