1241 lines
50 KiB
TypeScript
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);
|
|
}
|
|
};
|