All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m26s
147 lines
7.2 KiB
TypeScript
147 lines
7.2 KiB
TypeScript
import { useMemo, useState } from 'react';
|
|
import { Link, useOutletContext } from 'react-router-dom';
|
|
import { Search, ChevronRight, Filter } from 'lucide-react';
|
|
import type { OrderData } from '../types';
|
|
import { parseOrderDate } from '../dataService';
|
|
|
|
type SortOption = 'recent' | 'spent_desc' | 'spent_asc' | 'items_desc' | 'items_asc';
|
|
|
|
const Clients = () => {
|
|
const { ordersData } = useOutletContext<{ ordersData: OrderData[] }>();
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [sortBy, setSortBy] = useState<SortOption>('recent');
|
|
|
|
const clientsData = useMemo(() => {
|
|
const orders = ordersData;
|
|
const clientMap: Record<string, { totalSpent: number, totalItems: number, uniqueOrders: Set<string>, lastPurchase: number }> = {};
|
|
|
|
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 };
|
|
}
|
|
|
|
// 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,
|
|
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;
|
|
}
|
|
});
|
|
}, [searchTerm, sortBy, ordersData]);
|
|
|
|
const formatCurrency = (value: number) => {
|
|
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold mb-2 text-zinc-900 dark:text-dark-text">Clientes</h1>
|
|
<p className="text-zinc-500 dark:text-dark-muted font-medium">Métricas de engajamento e histórico de consumo dos seus clientes.</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row gap-3">
|
|
<div className="relative">
|
|
<Filter className="absolute left-3 top-1/2 transform -translate-y-1/2 text-zinc-400 dark:text-dark-muted w-4 h-4" />
|
|
<select
|
|
value={sortBy}
|
|
onChange={(e) => setSortBy(e.target.value as SortOption)}
|
|
className="appearance-none bg-white dark:bg-dark-input border border-zinc-200 dark:border-dark-border text-zinc-700 dark:text-dark-text text-sm rounded-xl pl-9 pr-8 py-2.5 focus:outline-none focus:border-brand-primary transition-all shadow-sm cursor-pointer"
|
|
>
|
|
<option value="recent">Mais Recentes</option>
|
|
<option value="spent_desc">Maior Gasto</option>
|
|
<option value="spent_asc">Menor Gasto</option>
|
|
<option value="items_desc">Mais Produtos</option>
|
|
<option value="items_asc">Menos Produtos</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-zinc-400 dark:text-dark-muted w-5 h-5" />
|
|
<input
|
|
type="text"
|
|
placeholder="Buscar cliente..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-full md:w-64 bg-white dark:bg-dark-input border border-zinc-200 dark:border-dark-border text-zinc-900 dark:text-dark-text rounded-xl pl-10 pr-4 py-2.5 focus:outline-none focus:border-brand-primary focus:ring-2 focus:ring-brand-primary/20 transition-all shadow-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white dark:bg-dark-card border border-zinc-200 dark:border-dark-border rounded-2xl overflow-hidden shadow-sm">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left text-sm">
|
|
<thead className="bg-zinc-50 dark:bg-dark-header border-b border-zinc-100 dark:border-dark-border text-zinc-500 dark:text-dark-muted">
|
|
<tr>
|
|
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Posição</th>
|
|
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Nome do Cliente</th>
|
|
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Total Gasto</th>
|
|
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px]">Produtos Comprados</th>
|
|
<th className="px-6 py-4 font-bold uppercase tracking-wider text-[10px] text-right">Ações</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-zinc-100 dark:divide-dark-border">
|
|
{clientsData.map((client, index) => (
|
|
<tr key={client.name} className="hover:bg-zinc-50/80 dark:hover:bg-dark-input/50 transition-colors group">
|
|
<td className="px-6 py-2.5">
|
|
<span className="inline-flex items-center justify-center w-7 h-7 rounded-full text-xs font-bold bg-zinc-100 dark:bg-dark-border text-zinc-500 dark:text-dark-muted">
|
|
{index + 1}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-2.5 font-semibold text-zinc-900 dark:text-dark-text">{client.name}</td>
|
|
<td className="px-6 py-2.5 text-brand-primary font-bold">{formatCurrency(client.totalSpent)}</td>
|
|
<td className="px-6 py-2.5 text-zinc-500 dark:text-dark-muted text-xs font-medium">
|
|
{client.totalItems} un. ({client.orderCount} {client.orderCount === 1 ? 'pedido' : 'pedidos'})
|
|
</td>
|
|
<td className="px-6 py-2.5 text-right">
|
|
<Link
|
|
to={`/clients/${encodeURIComponent(client.name)}`}
|
|
className="inline-flex items-center text-xs font-bold text-brand-primary hover:opacity-80 transition-opacity"
|
|
>
|
|
Ver detalhes
|
|
<ChevronRight className="w-3.5 h-3.5 ml-1" />
|
|
</Link>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Clients;
|