extract frontend analytics helpers
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m12s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m12s
This commit is contained in:
122
src/analytics/clients.ts
Normal file
122
src/analytics/clients.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { DateRange, OrderData } from '../types';
|
||||
import { parseOrderDate } from '../dataService';
|
||||
import { filterOrdersByDateRange, getClientDisplayName, getOrderItemRevenue } from './orders';
|
||||
|
||||
export type ClientSortOption = 'recent' | 'spent_desc' | 'spent_asc' | 'items_desc' | 'items_asc';
|
||||
|
||||
export interface ClientSummary {
|
||||
name: string;
|
||||
phone: string;
|
||||
totalSpent: number;
|
||||
totalItems: number;
|
||||
orderCount: number;
|
||||
lastPurchase: number;
|
||||
}
|
||||
|
||||
export interface GroupedClientOrder {
|
||||
date: string;
|
||||
orderId: string;
|
||||
orderTotal: number;
|
||||
items: OrderData[];
|
||||
}
|
||||
|
||||
export interface ClientDetailsMetrics {
|
||||
groupedOrders: GroupedClientOrder[];
|
||||
totalSpent: number;
|
||||
totalItems: number;
|
||||
clientPhone: string;
|
||||
}
|
||||
|
||||
export const buildClientsSummary = (
|
||||
ordersData: OrderData[],
|
||||
dateRange: DateRange,
|
||||
searchTerm: string,
|
||||
sortBy: ClientSortOption
|
||||
): ClientSummary[] => {
|
||||
const orders = filterOrdersByDateRange(ordersData, dateRange);
|
||||
const clientMap: Record<string, {
|
||||
totalSpent: number;
|
||||
totalItems: number;
|
||||
uniqueOrders: Set<string>;
|
||||
lastPurchase: number;
|
||||
phone: string;
|
||||
}> = {};
|
||||
|
||||
orders.forEach(order => {
|
||||
const clientName = getClientDisplayName(order);
|
||||
|
||||
if (!clientMap[clientName]) {
|
||||
clientMap[clientName] = { totalSpent: 0, totalItems: 0, uniqueOrders: new Set(), lastPurchase: 0, phone: '' };
|
||||
}
|
||||
|
||||
if (order.Fone_Cliente) {
|
||||
clientMap[clientName].phone = order.Fone_Cliente;
|
||||
}
|
||||
|
||||
clientMap[clientName].totalSpent += getOrderItemRevenue(order);
|
||||
clientMap[clientName].totalItems += order.Quantidade;
|
||||
clientMap[clientName].uniqueOrders.add(`${order.Data_Pedido}_${order.Valor_Pedido}`);
|
||||
|
||||
const orderTime = parseOrderDate(order.Data_Pedido).getTime();
|
||||
if (orderTime > clientMap[clientName].lastPurchase) {
|
||||
clientMap[clientName].lastPurchase = orderTime;
|
||||
}
|
||||
});
|
||||
|
||||
const normalizedSearch = searchTerm.trim().toLowerCase();
|
||||
const clients = Object.keys(clientMap).map(name => ({
|
||||
name,
|
||||
phone: clientMap[name].phone,
|
||||
totalSpent: clientMap[name].totalSpent,
|
||||
totalItems: clientMap[name].totalItems,
|
||||
orderCount: clientMap[name].uniqueOrders.size,
|
||||
lastPurchase: clientMap[name].lastPurchase
|
||||
}));
|
||||
|
||||
const filteredClients = normalizedSearch
|
||||
? clients.filter(client => client.name.toLowerCase().includes(normalizedSearch))
|
||||
: clients;
|
||||
|
||||
return filteredClients.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'recent': return b.lastPurchase - a.lastPurchase;
|
||||
case 'spent_desc': return b.totalSpent - a.totalSpent;
|
||||
case 'spent_asc': return a.totalSpent - b.totalSpent;
|
||||
case 'items_desc': return b.totalItems - a.totalItems;
|
||||
case 'items_asc': return a.totalItems - b.totalItems;
|
||||
default: return 0;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const buildClientDetailsMetrics = (ordersData: OrderData[], clientName: string): ClientDetailsMetrics => {
|
||||
const clientOrders = ordersData.filter(order => getClientDisplayName(order) === clientName);
|
||||
const groupedOrdersMap: Record<string, GroupedClientOrder> = {};
|
||||
let clientPhone = '';
|
||||
let totalSpent = 0;
|
||||
let totalItems = 0;
|
||||
|
||||
clientOrders.forEach(order => {
|
||||
if (order.Fone_Cliente && !clientPhone) clientPhone = order.Fone_Cliente;
|
||||
|
||||
totalSpent += getOrderItemRevenue(order);
|
||||
totalItems += order.Quantidade;
|
||||
|
||||
const key = order.ID_Pedido || `${order.Data_Pedido}_${order.Valor_Pedido}`;
|
||||
if (!groupedOrdersMap[key]) {
|
||||
groupedOrdersMap[key] = {
|
||||
date: order.Data_Pedido,
|
||||
orderId: order.ID_Pedido || key,
|
||||
orderTotal: order.Valor_Pedido,
|
||||
items: []
|
||||
};
|
||||
}
|
||||
groupedOrdersMap[key].items.push(order);
|
||||
});
|
||||
|
||||
const groupedOrders = Object.values(groupedOrdersMap).sort((a, b) => {
|
||||
return parseOrderDate(b.date).getTime() - parseOrderDate(a.date).getTime();
|
||||
});
|
||||
|
||||
return { groupedOrders, totalSpent, totalItems, clientPhone };
|
||||
};
|
||||
85
src/analytics/dashboard.ts
Normal file
85
src/analytics/dashboard.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { DateRange, OrderData } from '../types';
|
||||
import { filterOrdersByDateRange, getBaseProductName, getOrderItemRevenue } from './orders';
|
||||
|
||||
const COLORS = [
|
||||
'#10b981', '#3b82f6', '#8b5cf6', '#f43f5e', '#f97316',
|
||||
'#06b6d4', '#ec4899', '#eab308', '#6366f1', '#14b8a6',
|
||||
'#6ee7b7', '#93c5fd', '#c4b5fd', '#fda4af', '#fdba74',
|
||||
'#67e8f9', '#f9a8d4', '#fde047', '#a5b4fc', '#5eead4'
|
||||
];
|
||||
|
||||
const globalColorMap: Record<string, string> = {};
|
||||
let globalColorIndex = 0;
|
||||
|
||||
const getProductColor = (name: string): string => {
|
||||
if (!globalColorMap[name]) {
|
||||
globalColorMap[name] = COLORS[globalColorIndex % COLORS.length];
|
||||
globalColorIndex += 1;
|
||||
}
|
||||
return globalColorMap[name];
|
||||
};
|
||||
|
||||
export interface ChartProductMetric {
|
||||
name: string;
|
||||
id: string;
|
||||
value: number;
|
||||
fill: string;
|
||||
}
|
||||
|
||||
export interface DashboardMetrics {
|
||||
totalRevenue: number;
|
||||
totalOrders: number;
|
||||
averageOrderValue: number;
|
||||
salesByProduct: ChartProductMetric[];
|
||||
revenueByProduct: ChartProductMetric[];
|
||||
}
|
||||
|
||||
export const buildDashboardMetrics = (ordersData: OrderData[], dateRange: DateRange): DashboardMetrics => {
|
||||
const filteredData = filterOrdersByDateRange(ordersData, dateRange);
|
||||
let revenue = 0;
|
||||
let totalItems = 0;
|
||||
const productSalesMap: Record<string, number> = {};
|
||||
const productRevenueMap: Record<string, number> = {};
|
||||
const productNameIdMap: Record<string, string> = {};
|
||||
|
||||
filteredData.forEach(order => {
|
||||
const itemRevenue = getOrderItemRevenue(order);
|
||||
const productName = getBaseProductName(order.Descricao_Produto);
|
||||
|
||||
revenue += itemRevenue;
|
||||
totalItems += order.Quantidade;
|
||||
productNameIdMap[productName] = order.ID_Produto;
|
||||
productSalesMap[productName] = (productSalesMap[productName] || 0) + order.Quantidade;
|
||||
productRevenueMap[productName] = (productRevenueMap[productName] || 0) + itemRevenue;
|
||||
});
|
||||
|
||||
const topSalesNames = Object.keys(productSalesMap).sort((a, b) => productSalesMap[b] - productSalesMap[a]).slice(0, 10);
|
||||
const topRevenueNames = Object.keys(productRevenueMap).sort((a, b) => productRevenueMap[b] - productRevenueMap[a]).slice(0, 10);
|
||||
const displayProducts = Array.from(new Set([...topSalesNames, ...topRevenueNames])).sort();
|
||||
const productColors = displayProducts.reduce<Record<string, string>>((colors, name) => {
|
||||
colors[name] = getProductColor(name);
|
||||
return colors;
|
||||
}, {});
|
||||
|
||||
const salesByProduct = topSalesNames.map(name => ({
|
||||
name,
|
||||
id: productNameIdMap[name],
|
||||
value: productSalesMap[name],
|
||||
fill: productColors[name]
|
||||
}));
|
||||
|
||||
const revenueByProduct = topRevenueNames.map(name => ({
|
||||
name,
|
||||
id: productNameIdMap[name],
|
||||
value: productRevenueMap[name],
|
||||
fill: productColors[name]
|
||||
}));
|
||||
|
||||
return {
|
||||
totalRevenue: revenue,
|
||||
totalOrders: totalItems,
|
||||
averageOrderValue: revenue / (filteredData.length || 1),
|
||||
salesByProduct,
|
||||
revenueByProduct
|
||||
};
|
||||
};
|
||||
23
src/analytics/orders.ts
Normal file
23
src/analytics/orders.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { DateRange, OrderData } from '../types';
|
||||
import { parseOrderDate } from '../dataService';
|
||||
|
||||
export const getBaseProductName = (description: string): string => {
|
||||
return description.split(' TAMANHO')[0];
|
||||
};
|
||||
|
||||
export const getClientDisplayName = (order: OrderData): string => {
|
||||
return order.Nome_Cliente || `Cliente Desconhecido (Pedido ${order.Valor_Pedido})`;
|
||||
};
|
||||
|
||||
export const isOrderInDateRange = (order: OrderData, dateRange: DateRange): boolean => {
|
||||
const orderDate = parseOrderDate(order.Data_Pedido);
|
||||
return orderDate >= dateRange.start && orderDate <= dateRange.end;
|
||||
};
|
||||
|
||||
export const filterOrdersByDateRange = (orders: OrderData[], dateRange: DateRange): OrderData[] => {
|
||||
return orders.filter(order => isOrderInDateRange(order, dateRange));
|
||||
};
|
||||
|
||||
export const getOrderItemRevenue = (order: OrderData): number => {
|
||||
return order.Quantidade * order.Valor_Unitario;
|
||||
};
|
||||
117
src/analytics/products.ts
Normal file
117
src/analytics/products.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { DateRange, OrderData, StockData } from '../types';
|
||||
import { getBaseProductName, getOrderItemRevenue, isOrderInDateRange } from './orders';
|
||||
|
||||
export interface ProductSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
totalSold: number;
|
||||
revenue: number;
|
||||
lastPrice: number;
|
||||
stock: number;
|
||||
}
|
||||
|
||||
export interface ProductDetailsMetrics {
|
||||
productInfo: {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
} | null;
|
||||
chartData: Array<{
|
||||
date: string;
|
||||
value: number;
|
||||
}>;
|
||||
totalSold: number;
|
||||
totalRevenue: number;
|
||||
}
|
||||
|
||||
export const buildProductsSummary = (
|
||||
ordersData: OrderData[],
|
||||
stockData: StockData[],
|
||||
dateRange: DateRange,
|
||||
searchTerm: string
|
||||
): ProductSummary[] => {
|
||||
const productMap: Record<string, ProductSummary> = {};
|
||||
|
||||
stockData.forEach(item => {
|
||||
productMap[item.produto_id] = {
|
||||
id: item.produto_id,
|
||||
name: item.nome,
|
||||
totalSold: 0,
|
||||
revenue: 0,
|
||||
lastPrice: 0,
|
||||
stock: item.saldo || 0
|
||||
};
|
||||
});
|
||||
|
||||
ordersData.forEach(order => {
|
||||
if (!isOrderInDateRange(order, dateRange)) return;
|
||||
|
||||
if (!productMap[order.ID_Produto]) {
|
||||
productMap[order.ID_Produto] = {
|
||||
id: order.ID_Produto,
|
||||
name: getBaseProductName(order.Descricao_Produto),
|
||||
totalSold: 0,
|
||||
revenue: 0,
|
||||
lastPrice: order.Valor_Unitario,
|
||||
stock: 0
|
||||
};
|
||||
}
|
||||
|
||||
if (productMap[order.ID_Produto].name === 'Unknown' || !productMap[order.ID_Produto].name) {
|
||||
productMap[order.ID_Produto].name = getBaseProductName(order.Descricao_Produto);
|
||||
}
|
||||
|
||||
productMap[order.ID_Produto].totalSold += order.Quantidade;
|
||||
productMap[order.ID_Produto].revenue += getOrderItemRevenue(order);
|
||||
productMap[order.ID_Produto].lastPrice = order.Valor_Unitario;
|
||||
});
|
||||
|
||||
const normalizedSearch = searchTerm.trim().toLowerCase();
|
||||
const products = Object.values(productMap);
|
||||
const filteredProducts = normalizedSearch
|
||||
? products.filter(product => product.name.toLowerCase().includes(normalizedSearch) || product.id.includes(searchTerm))
|
||||
: products;
|
||||
|
||||
return filteredProducts.sort((a, b) => b.totalSold - a.totalSold);
|
||||
};
|
||||
|
||||
export const buildProductDetailsMetrics = (
|
||||
ordersData: OrderData[],
|
||||
productId: string | undefined,
|
||||
dateRange: DateRange
|
||||
): ProductDetailsMetrics => {
|
||||
const productOrders = ordersData.filter(order => order.ID_Produto === productId);
|
||||
|
||||
if (productOrders.length === 0) {
|
||||
return { productInfo: null, chartData: [], totalSold: 0, totalRevenue: 0 };
|
||||
}
|
||||
|
||||
const productInfo = {
|
||||
id: productOrders[0].ID_Produto,
|
||||
name: getBaseProductName(productOrders[0].Descricao_Produto),
|
||||
price: productOrders[0].Valor_Unitario
|
||||
};
|
||||
|
||||
const salesByDate: Record<string, number> = {};
|
||||
let totalSold = 0;
|
||||
let totalRevenue = 0;
|
||||
|
||||
productOrders.forEach(order => {
|
||||
if (!isOrderInDateRange(order, dateRange)) return;
|
||||
|
||||
salesByDate[order.Data_Pedido] = (salesByDate[order.Data_Pedido] || 0) + order.Quantidade;
|
||||
totalSold += order.Quantidade;
|
||||
totalRevenue += getOrderItemRevenue(order);
|
||||
});
|
||||
|
||||
const chartData = Object.keys(salesByDate).map(date => ({
|
||||
date,
|
||||
value: salesByDate[date]
|
||||
})).sort((a, b) => {
|
||||
const [da, ma, ya] = a.date.split('-').map(Number);
|
||||
const [db, mb, yb] = b.date.split('-').map(Number);
|
||||
return new Date(ya, ma - 1, da).getTime() - new Date(yb, mb - 1, db).getTime();
|
||||
});
|
||||
|
||||
return { productInfo, chartData, totalSold, totalRevenue };
|
||||
};
|
||||
Reference in New Issue
Block a user