Files
graphs/src/dataService.ts
Cauê Faleiros 9d99f2387b
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 53s
perf: limit cut plan to active orders
2026-08-28 13:14:21 -03:00

1241 lines
50 KiB
TypeScript

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<T> = {
expiresAt: number;
data?: T;
promise?: Promise<T>;
};
const analyticsCache = new Map<string, ApiCacheEntry<unknown>>();
const getCachedAnalyticsValue = <T>(key: string): T | undefined => {
const entry = analyticsCache.get(key) as ApiCacheEntry<T> | undefined;
if (!entry || entry.data === undefined || entry.expiresAt <= Date.now()) return undefined;
return entry.data;
};
const getCachedAnalytics = async <T>(key: string, loader: () => Promise<T>, options: CacheOptions = {}): Promise<T> => {
const now = Date.now();
const cached = analyticsCache.get(key) as ApiCacheEntry<T> | 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<LoginConfig> => {
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<LoginConfig>;
};
export const login = async (email: string, password: string, captchaToken?: string): Promise<LoginResult> => {
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<StockData[]> => {
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<GraphsStockBalanceHistoryItem[]> => {
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<GraphsStockBalanceHistoryItem[]>;
};
export const saveGraphsStockBalance = async (productId: string, payload: GraphsStockBalancePayload): Promise<StockData> => {
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<StockData> => {
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<OrderData[]> => {
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<Response> => {
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<void> => {
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<ProductionOrderSummary> => {
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<CreateProductionOrdersResult> => {
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<ProductionPlan[]> => {
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<ProductionPlan[]>;
};
export const updateProductionPlan = async (id: number, payload: ProductionPlanPayload): Promise<ProductionPlan> => {
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<ProductionPlan> => {
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<ProductionPlan> => {
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<ProductionOrderItem> => {
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<ProductionOrderItem> => {
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<ProductionWorkstation[]> => {
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<ProductionWorkstation[]>;
};
export const createProductionWorkstation = async (payload: ProductionWorkstationPayload): Promise<ProductionWorkstation> => {
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<ProductionWorkstation> => {
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<ProductionOrderItem> => {
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<CuttingSettings> => {
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<CuttingSettings> => {
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<CatalogSummary> => {
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<OlistStatus> => {
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<string> => {
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<void> => {
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<void> => {
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<OlistRunDetails> => {
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<ProductionOrderImportData> => {
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<number> => {
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<ProductionOrderImportData & { runId: number; totalRows: number; imported: number; updated: number; skipped: number; failed: number; issues: Array<{ line?: number; number?: string; message: string }> }> => {
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<CatalogCategory> => {
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<CatalogCategory> => {
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<void> => {
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<CatalogProduct> => {
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<CatalogProduct> => {
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<void> => {
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<ConsumptionReference> => {
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<ConsumptionReference> => {
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<void> => {
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<ProductCompositionImportSummary> => {
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<SupplySummary> => {
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<string[]> => {
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<string[]>;
};
export const fetchSupplyPurchaseOrders = async (): Promise<SupplyPurchaseOrder[]> => {
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<SupplyPurchaseOrder[]>;
};
export const createSupplyPurchaseOrder = async (payload: SupplyPurchaseOrderPayload): Promise<SupplyPurchaseOrder> => {
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<SupplyPurchaseOrder> => {
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<void> => {
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<SupplyReceipt> => {
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<SupplyReceipt> => {
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<void> => {
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<SupplyFabricPlan[]> => {
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<SupplyFabricPlan> => {
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<void> => {
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<SupplyMaterialTransformation[]> => {
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<SupplyMaterialTransformation> => {
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<SupplyMaterialTransformation> => {
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<SupplyPurchaseNeed[]> => {
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<SupplyLot> => {
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<SupplyLot> => {
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<DashboardAnalytics | null> => {
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<ProductAnalyticsItem[]> => {
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<SellerAnalyticsItem[]> => {
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<SellerDetailsAnalytics | null> => {
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<ProductDetailsAnalytics | null> => {
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<ProductComposition | null> => {
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<ProductComposition> => {
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<void> => {
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<ProductComposition[]> => {
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<number> => {
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<ClientMetadataFilters>) => {
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<ClientFilterOptions> => {
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<ClientMetadataFilters>) => {
const params = buildDateRangeParams(dateRange);
appendClientMetadataFilterParams(params, filters);
return `/analytics/rfm?${params.toString()}`;
};
export const getCachedRfmAnalytics = (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>): RfmAnalytics | undefined => {
return getCachedAnalyticsValue<RfmAnalytics>(buildRfmAnalyticsPath(dateRange, filters));
};
export const fetchRfmAnalytics = async (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>, options?: CacheOptions): Promise<RfmAnalytics | null> => {
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<ClientMetadataFilters>) => {
const params = buildDateRangeParams(dateRange);
appendClientMetadataFilterParams(params, filters);
return `/analytics/clients?${params.toString()}`;
};
export const getCachedClientAnalytics = (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>): ClientAnalyticsItem[] | undefined => {
return getCachedAnalyticsValue<ClientAnalyticsItem[]>(buildClientAnalyticsPath(dateRange, filters));
};
export const getCachedClientFilterOptions = (dateRange: DateRange): ClientFilterOptions | undefined => {
return getCachedAnalyticsValue<ClientFilterOptions>(`/analytics/clients/filters?${buildDateRangeParams(dateRange).toString()}`);
};
export const fetchClientAnalytics = async (dateRange: DateRange, filters?: Partial<ClientMetadataFilters>, options?: CacheOptions): Promise<ClientAnalyticsItem[]> => {
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<ClientPurchasePatternAnalytics> => {
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<ClientDetailsAnalytics | null> => {
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<CampaignQueueSummary | null> => {
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<CampaignPreview | null> => {
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<CampaignProcessSummary | null> => {
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<boolean> => {
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<ManagedUser[]> => {
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<OperationalUser[]> => {
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<CreateUserResult> => {
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<ManagedUser> => {
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<void> => {
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<string, unknown>[], 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);
}
};