432 lines
15 KiB
TypeScript
432 lines
15 KiB
TypeScript
import type { AuthUser, CampaignPreview, CampaignProcessSummary, CampaignQueueSummary, ClientAnalyticsItem, ClientDetailsAnalytics, ClientFilterOptions, ClientMetadataFilters, CreateUserResult, DashboardAnalytics, DateRange, ManagedUser, OrderData, ProductAnalyticsItem, ProductDetailsAnalytics, RfmAnalytics, StockData } from './types';
|
|
import { formatDateParam } from './dateRanges';
|
|
|
|
const API_URL = import.meta.env.VITE_API_URL || '/api';
|
|
const ANALYTICS_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
const IN_FLIGHT_CACHE_TTL_MS = 15 * 1000;
|
|
|
|
type CacheOptions = {
|
|
force?: boolean;
|
|
};
|
|
|
|
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) {
|
|
analyticsCache.delete(key);
|
|
} else {
|
|
analyticsCache.set(key, { data, expiresAt: Date.now() + ANALYTICS_CACHE_TTL_MS });
|
|
}
|
|
return data;
|
|
} catch (error) {
|
|
analyticsCache.delete(key);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const buildDateRangeParams = (dateRange: DateRange) => new URLSearchParams({
|
|
start: formatDateParam(dateRange.start),
|
|
end: formatDateParam(dateRange.end)
|
|
});
|
|
|
|
export const login = async (email: string, password: string): Promise<boolean> => {
|
|
try {
|
|
const response = await fetch(`${API_URL}/login`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json() as { token?: string; user?: AuthUser };
|
|
if (!data.token || !data.user) return false;
|
|
localStorage.setItem('auth_token', data.token);
|
|
localStorage.setItem('auth_user', JSON.stringify(data.user));
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
console.error('Login failed', error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const logout = () => {
|
|
localStorage.removeItem('auth_token');
|
|
localStorage.removeItem('auth_user');
|
|
window.location.href = '/#/login';
|
|
};
|
|
|
|
export const isAuthenticated = (): boolean => {
|
|
return !!localStorage.getItem('auth_token');
|
|
};
|
|
|
|
export const getCurrentUser = (): AuthUser | null => {
|
|
const rawUser = localStorage.getItem('auth_user');
|
|
if (!rawUser) return null;
|
|
|
|
try {
|
|
return JSON.parse(rawUser) as AuthUser;
|
|
} catch {
|
|
localStorage.removeItem('auth_user');
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export const isSuperAdmin = (): boolean => {
|
|
return getCurrentUser()?.role === 'super_admin';
|
|
};
|
|
|
|
export const fetchStock = async (): Promise<StockData[]> => {
|
|
try {
|
|
const token = localStorage.getItem('auth_token');
|
|
const response = await fetch(`${API_URL}/stock`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
if (response.status === 401 || response.status === 403) {
|
|
logout();
|
|
return [];
|
|
}
|
|
if (!response.ok) return [];
|
|
return await response.json();
|
|
} catch {
|
|
return [];
|
|
}
|
|
};
|
|
|
|
export const fetchData = async (): Promise<OrderData[]> => {
|
|
try {
|
|
const token = localStorage.getItem('auth_token');
|
|
const response = await fetch(`${API_URL}/data`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
if (response.status === 401 || response.status === 403) {
|
|
logout();
|
|
return [];
|
|
}
|
|
if (!response.ok) return [];
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error("Fetch failed", error);
|
|
return [];
|
|
}
|
|
};
|
|
|
|
const authFetch = async (path: string, options: RequestInit = {}): Promise<Response> => {
|
|
const token = localStorage.getItem('auth_token');
|
|
const response = await fetch(`${API_URL}${path}`, {
|
|
...options,
|
|
headers: {
|
|
...(options.headers || {}),
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (response.status === 401 || response.status === 403) {
|
|
logout();
|
|
}
|
|
|
|
return response;
|
|
};
|
|
|
|
export const 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);
|
|
if (!response.ok) return null;
|
|
return await response.json();
|
|
} catch (error) {
|
|
console.error('Fetch dashboard analytics failed', error);
|
|
return null;
|
|
}
|
|
}, options);
|
|
};
|
|
|
|
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 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;
|
|
}
|
|
});
|
|
};
|
|
|
|
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);
|
|
};
|
|
|
|
export const fetchClientFilterOptions = async (dateRange: DateRange): Promise<ClientFilterOptions> => {
|
|
const path = `/analytics/clients/filters?${buildDateRangeParams(dateRange).toString()}`;
|
|
return getCachedAnalytics(path, async () => {
|
|
try {
|
|
const response = await authFetch(path);
|
|
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: [] };
|
|
}
|
|
});
|
|
};
|
|
|
|
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);
|
|
};
|
|
|
|
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 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);
|
|
}
|
|
};
|