extract frontend analytics helpers
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m12s

This commit is contained in:
Cauê Faleiros
2026-05-27 15:58:12 -03:00
parent 8c2590c56a
commit 72ded82ec7
9 changed files with 362 additions and 258 deletions

122
src/analytics/clients.ts Normal file
View 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 };
};

View 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
View 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
View 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 };
};

View File

@@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { useParams, Link, useOutletContext } from 'react-router-dom';
import { ArrowLeft, User, Tag, Package, DollarSign, Clock, Phone } from 'lucide-react';
import type { OrderData } from '../types';
import { parseOrderDate } from '../dataService';
import { buildClientDetailsMetrics } from '../analytics/clients';
const ClientDetails = () => {
const { name } = useParams<{ name: string }>();
@@ -10,41 +10,7 @@ const ClientDetails = () => {
const { ordersData } = useOutletContext<{ ordersData: OrderData[] }>();
const { groupedOrders, totalSpent, totalItems, clientPhone } = useMemo(() => {
const orders = ordersData;
const clientOrders = orders.filter(order => {
const clientName = order.Nome_Cliente || `Cliente Desconhecido (Pedido ${order.Valor_Pedido})`;
return clientName === decodedName;
});
let clientPhone = '';
const groupedOrdersMap: Record<string, { date: string, orderId: string, orderTotal: number, items: OrderData[] }> = {};
let totalSpent = 0;
let totalItems = 0;
clientOrders.forEach(order => {
if (order.Fone_Cliente && !clientPhone) clientPhone = order.Fone_Cliente;
totalSpent += (order.Quantidade * order.Valor_Unitario);
totalItems += order.Quantidade;
// Use ID_Pedido if available, otherwise fallback to date and total order value
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);
});
// Sort grouped orders by date descending
const groupedOrders = Object.values(groupedOrdersMap).sort((a, b) => {
return parseOrderDate(b.date).getTime() - parseOrderDate(a.date).getTime();
});
return { groupedOrders, totalSpent, totalItems, clientPhone };
return buildClientDetailsMetrics(ordersData, decodedName);
}, [decodedName, ordersData]);
const formatCurrency = (value: number) => {

View File

@@ -2,10 +2,9 @@ import { useMemo, useState } from 'react';
import { Link, useOutletContext } from 'react-router-dom';
import { Search, ChevronRight, Filter, ChevronLeft, Download } from 'lucide-react';
import type { OrderData, DateRange } from '../types';
import { parseOrderDate, exportToCSV } from '../dataService';
import { exportToCSV } from '../dataService';
import DateRangePicker from '../components/DateRangePicker';
type SortOption = 'recent' | 'spent_desc' | 'spent_asc' | 'items_desc' | 'items_asc';
import { buildClientsSummary, type ClientSortOption } from '../analytics/clients';
const Clients = () => {
const { dateRange, setDateRange, ordersData } = useOutletContext<{
@@ -14,66 +13,14 @@ const Clients = () => {
ordersData: OrderData[]
}>();
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState<SortOption>('recent');
const [sortBy, setSortBy] = useState<ClientSortOption>('recent');
// Pagination state
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const clientsData = useMemo(() => {
const orders = ordersData.filter(order => {
const orderDate = parseOrderDate(order.Data_Pedido);
return orderDate >= dateRange.start && orderDate <= dateRange.end;
});
const clientMap: Record<string, { totalSpent: number, totalItems: number, uniqueOrders: Set<string>, lastPurchase: number, phone: string }> = {};
orders.forEach(order => {
const clientName = order.Nome_Cliente || `Cliente Desconhecido (Pedido ${order.Valor_Pedido})`;
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;
}
// Calculate total spent based on quantity * unit price
clientMap[clientName].totalSpent += (order.Quantidade * order.Valor_Unitario);
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;
}
});
let result = Object.keys(clientMap).map(name => ({
name,
phone: clientMap[name].phone,
totalSpent: clientMap[name].totalSpent,
totalItems: clientMap[name].totalItems,
orderCount: clientMap[name].uniqueOrders.size, // Grouped by unique date+value combinations
lastPurchase: clientMap[name].lastPurchase
}));
if (searchTerm) {
result = result.filter(c => c.name.toLowerCase().includes(searchTerm.toLowerCase()));
}
return result.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;
}
});
return buildClientsSummary(ordersData, dateRange, searchTerm, sortBy);
}, [searchTerm, sortBy, ordersData, dateRange]);
// Pagination logic
@@ -107,7 +54,7 @@ const Clients = () => {
<select
value={sortBy}
onChange={(e) => {
setSortBy(e.target.value as SortOption);
setSortBy(e.target.value as ClientSortOption);
setCurrentPage(1);
}}
className="appearance-none bg-dark-card border border-dark-border text-dark-text text-sm rounded-xl pl-9 pr-8 py-2.5 focus:outline-none focus:border-brand-primary transition-colors shadow-sm cursor-pointer"

View File

@@ -4,27 +4,7 @@ import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContaine
import { DollarSign, ShoppingCart, TrendingUp } from 'lucide-react';
import DateRangePicker from '../components/DateRangePicker';
import type { OrderData, DateRange } from '../types';
import { parseOrderDate } from '../dataService';
const COLORS = [
// 10 Strong Base Colors
'#10b981', '#3b82f6', '#8b5cf6', '#f43f5e', '#f97316',
'#06b6d4', '#ec4899', '#eab308', '#6366f1', '#14b8a6',
// 10 Softer Versions
'#6ee7b7', '#93c5fd', '#c4b5fd', '#fda4af', '#fdba74',
'#67e8f9', '#f9a8d4', '#fde047', '#a5b4fc', '#5eead4'
];
const globalColorMap: Record<string, string> = {};
let globalColorIndex = 0;
const getProductColor = (name: string) => {
if (!globalColorMap[name]) {
globalColorMap[name] = COLORS[globalColorIndex % COLORS.length];
globalColorIndex++;
}
return globalColorMap[name];
};
import { buildDashboardMetrics } from '../analytics/dashboard';
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
@@ -73,65 +53,9 @@ const Dashboard = () => {
loadData: (showLoading?: boolean) => void
}>();
const filteredData = useMemo(() => {
const orders = ordersData;
return orders.filter(order => {
const orderDate = parseOrderDate(order.Data_Pedido);
return orderDate >= dateRange.start && orderDate <= dateRange.end;
});
}, [dateRange, ordersData]);
const { totalRevenue, totalOrders, averageOrderValue, salesByProduct, revenueByProduct } = useMemo(() => {
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 = order.Quantidade * order.Valor_Unitario;
revenue += itemRevenue;
totalItems += order.Quantidade;
const productName = order.Descricao_Produto.split(' TAMANHO')[0];
productNameIdMap[productName] = order.ID_Produto;
if (productSalesMap[productName]) {
productSalesMap[productName] += order.Quantidade;
productRevenueMap[productName] += itemRevenue;
} else {
productSalesMap[productName] = order.Quantidade;
productRevenueMap[productName] = itemRevenue;
}
});
// Identify which products will actually be displayed in both charts (Top 10 of each)
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);
// Combine them into a unique set to assign colors only to the VISIBLE products
const displayProducts = Array.from(new Set([...topSalesNames, ...topRevenueNames])).sort();
const productColors: Record<string, string> = {};
displayProducts.forEach((name) => {
productColors[name] = getProductColor(name);
});
const productsData = topSalesNames.map(name => ({
name,
id: productNameIdMap[name],
value: productSalesMap[name],
fill: productColors[name]
}));
const revenueData = topRevenueNames.map(name => ({
name,
id: productNameIdMap[name],
value: productRevenueMap[name],
fill: productColors[name]
}));
return { totalRevenue: revenue, totalOrders: totalItems, averageOrderValue: revenue / (filteredData.length || 1), salesByProduct: productsData, revenueByProduct: revenueData };
}, [filteredData]);
return buildDashboardMetrics(ordersData, dateRange);
}, [dateRange, ordersData]);
return (
<div className="space-y-6">

View File

@@ -4,7 +4,7 @@ import { ArrowLeft, Package, DollarSign } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import DateRangePicker from '../components/DateRangePicker';
import type { OrderData, DateRange } from '../types';
import { parseOrderDate } from '../dataService';
import { buildProductDetailsMetrics } from '../analytics/products';
type CustomTooltipProps = {
active?: boolean;
@@ -36,41 +36,7 @@ const ProductDetails = () => {
}>();
const { productInfo, chartData, totalSold, totalRevenue } = useMemo(() => {
const orders = ordersData.filter(order => order.ID_Produto === id);
if (orders.length === 0) return { productInfo: null, chartData: [], totalSold: 0, totalRevenue: 0 };
const info = {
id: orders[0].ID_Produto,
name: orders[0].Descricao_Produto.split(' TAMANHO')[0],
price: orders[0].Valor_Unitario
};
const salesByDate: Record<string, number> = {};
let sold = 0;
let revenue = 0;
orders.forEach(order => {
const orderDate = parseOrderDate(order.Data_Pedido);
if (orderDate >= dateRange.start && orderDate <= dateRange.end) {
const dateStr = order.Data_Pedido;
salesByDate[dateStr] = (salesByDate[dateStr] || 0) + order.Quantidade;
sold += order.Quantidade;
revenue += (order.Quantidade * order.Valor_Unitario);
}
});
const chart = 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: info, chartData: chart, totalSold: sold, totalRevenue: revenue };
return buildProductDetailsMetrics(ordersData, id, dateRange);
}, [id, dateRange, ordersData]);
const formatCurrency = (value: number) => {

View File

@@ -3,7 +3,8 @@ import { Link, useOutletContext } from 'react-router-dom';
import { Search, Package, TrendingUp, ChevronLeft, ChevronRight, Download } from 'lucide-react';
import DateRangePicker from '../components/DateRangePicker';
import type { OrderData, DateRange, StockData } from '../types';
import { parseOrderDate, exportToCSV } from '../dataService';
import { exportToCSV } from '../dataService';
import { buildProductsSummary } from '../analytics/products';
const Products = () => {
const { dateRange, setDateRange, ordersData, stockData } = useOutletContext<{
@@ -22,54 +23,7 @@ const Products = () => {
const [itemsPerPage, setItemsPerPage] = useState(10);
const productsData = useMemo(() => {
const orders = ordersData;
const productMap: Record<string, { id: string, name: string, totalSold: number, revenue: number, lastPrice: number, stock: number }> = {};
// Initialize with stock data
if (stockData && Array.isArray(stockData)) {
stockData.forEach(item => {
productMap[item.produto_id] = {
id: item.produto_id,
name: item.nome,
totalSold: 0,
revenue: 0,
lastPrice: 0,
stock: item.saldo || 0
};
});
}
orders.forEach(order => {
const orderDate = parseOrderDate(order.Data_Pedido);
if (orderDate < dateRange.start || orderDate > dateRange.end) return;
if (!productMap[order.ID_Produto]) {
productMap[order.ID_Produto] = {
id: order.ID_Produto,
name: order.Descricao_Produto.split(' TAMANHO')[0],
totalSold: 0,
revenue: 0,
lastPrice: order.Valor_Unitario,
stock: 0
};
}
// Update name if we didn't get it from stock
if (productMap[order.ID_Produto].name === 'Unknown' || !productMap[order.ID_Produto].name) {
productMap[order.ID_Produto].name = order.Descricao_Produto.split(' TAMANHO')[0];
}
productMap[order.ID_Produto].totalSold += order.Quantidade;
productMap[order.ID_Produto].revenue += (order.Quantidade * order.Valor_Unitario);
productMap[order.ID_Produto].lastPrice = order.Valor_Unitario;
});
let result = Object.values(productMap);
if (searchTerm) {
result = result.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase()) || p.id.includes(searchTerm));
}
return result.sort((a, b) => b.totalSold - a.totalSold);
return buildProductsSummary(ordersData, stockData, dateRange, searchTerm);
}, [dateRange, searchTerm, ordersData, stockData]);
// Pagination logic