Files
graphs/src/pages/Sellers.tsx
Cauê Faleiros a93068e86b
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m30s
Remove seller overview cards
2026-08-11 09:25:31 -03:00

128 lines
8.4 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { Link, useOutletContext } from 'react-router-dom';
import { BriefcaseBusiness, Eye, Search } from 'lucide-react';
import DateRangePicker from '../components/DateRangePicker';
import PaginationControls from '../components/PaginationControls';
import RefreshStatus from '../components/RefreshStatus';
import { fetchSellerAnalytics } from '../dataService';
import { formatDisplayName, removeTrailingSellerId } from '../displayFormatters';
import type { DateRange, SellerAnalyticsItem } from '../types';
const formatNumber = (value: number) => new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 0 }).format(value);
const formatCurrency = (value: number) => new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
const SellersTableSkeleton = () => (
<div className="overflow-x-auto">
<div className="min-w-[980px]">
<div className="grid grid-cols-[90px_1.5fr_repeat(5,120px)_80px] gap-5 border-b border-dark-border bg-dark-header px-6 py-4">
{[1, 2, 3, 4, 5, 6, 7, 8].map(item => <div key={item} className="skeleton h-3" />)}
</div>
<div className="divide-y divide-dark-border">
{[1, 2, 3, 4, 5, 6].map(row => <div key={row} className="grid grid-cols-[90px_1.5fr_repeat(5,120px)_80px] items-center gap-5 px-6 py-3"><div className="skeleton h-7 w-7 rounded-full" /><div><div className="skeleton h-4 w-40" /><div className="skeleton mt-2 h-3 w-20" /></div>{[1, 2, 3, 4, 5].map(column => <div key={column} className="skeleton h-4" />)}<div className="skeleton h-8 w-8 rounded-lg" /></div>)}
</div>
</div>
</div>
);
const Sellers = () => {
const { dateRange, setDateRange } = useOutletContext<{ dateRange: DateRange; setDateRange: (range: DateRange) => void }>();
const [sellers, setSellers] = useState<SellerAnalyticsItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [search, setSearch] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
useEffect(() => {
let isMounted = true;
const load = async () => {
setIsLoading(true);
const rows = await fetchSellerAnalytics(dateRange);
if (isMounted) {
setSellers(rows);
setIsLoading(false);
}
};
void load();
return () => { isMounted = false; };
}, [dateRange]);
const visibleSellers = useMemo(() => {
const term = search.trim().toLocaleLowerCase('pt-BR');
if (!term) return sellers;
return sellers.filter(seller => `${seller.name} ${seller.id}`.toLocaleLowerCase('pt-BR').includes(term));
}, [search, sellers]);
const totalPages = Math.ceil(visibleSellers.length / itemsPerPage);
const safeCurrentPage = Math.min(currentPage, totalPages || 1);
const startIndex = (safeCurrentPage - 1) * itemsPerPage;
const paginatedSellers = visibleSellers.slice(startIndex, startIndex + itemsPerPage);
return (
<div className="space-y-6">
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<div>
<h1 className="text-2xl font-bold text-dark-text">Vendedores</h1>
<p className="mt-2 font-medium text-dark-muted">Performance comercial, carteira e produtos vendidos por responsável.</p>
</div>
<DateRangePicker dateRange={dateRange} onChange={setDateRange} />
</div>
<RefreshStatus isRefreshing={isLoading && sellers.length > 0} />
<div className="rounded-2xl border border-dark-border bg-dark-card shadow-sm">
<div className="flex flex-col gap-4 border-b border-dark-border p-4 sm:p-5 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 className="text-base font-bold text-dark-text">Equipe comercial</h2>
<p className="mt-1 text-sm font-semibold text-dark-muted">Selecione um vendedor para ver carteira, produtos e evolução.</p>
</div>
<label className="relative block w-full lg:max-w-sm">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-dark-muted" />
<input value={search} onChange={event => { setSearch(event.target.value); setCurrentPage(1); }} placeholder="Buscar vendedor..." className="h-11 w-full rounded-xl border border-dark-border bg-dark-input pl-10 pr-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" />
</label>
</div>
{isLoading && !sellers.length ? (
<SellersTableSkeleton />
) : visibleSellers.length ? (
<>
<div className="overflow-x-auto">
<table className="w-full min-w-[980px] text-left text-sm">
<thead className="border-b border-dark-border bg-dark-header text-dark-muted">
<tr>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Posição</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Nome do vendedor</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Receita</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Pedidos</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Clientes</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Ticket médio</th>
<th className="px-6 py-4 text-[10px] font-bold uppercase tracking-wider">Produtos vendidos</th>
<th className="px-6 py-4 text-right text-[10px] font-bold uppercase tracking-wider">Ações</th>
</tr>
</thead>
<tbody className="divide-y divide-dark-border">
{paginatedSellers.map((seller, index) => (
<tr key={seller.id} className="group transition-colors hover:bg-dark-input/50">
<td className="px-6 py-2.5"><span className="inline-flex h-7 w-7 items-center justify-center rounded-full bg-dark-border text-xs font-bold text-dark-muted">{startIndex + index + 1}</span></td>
<td className="px-6 py-2.5"><p className="font-semibold text-dark-text">{formatDisplayName(removeTrailingSellerId(seller.name))}</p><p className="mt-1 text-[10px] font-semibold text-dark-muted">{seller.id.startsWith('name:') ? 'Sem ID Tiny' : `ID ${seller.id}`}</p></td>
<td className="px-6 py-2.5 font-bold text-brand-primary">{formatCurrency(seller.revenue)}</td>
<td className="px-6 py-2.5 font-semibold text-dark-text">{formatNumber(seller.orderCount)}</td>
<td className="px-6 py-2.5 font-semibold text-dark-text">{formatNumber(seller.customerCount)}</td>
<td className="px-6 py-2.5 font-semibold text-dark-text">{formatCurrency(seller.averageTicket)}</td>
<td className="px-6 py-2.5 text-xs font-medium text-dark-muted">{formatNumber(seller.quantitySold)} un. ({formatNumber(seller.orderCount)} {seller.orderCount === 1 ? 'pedido' : 'pedidos'})</td>
<td className="px-6 py-2.5 text-right"><Link to={`/sellers/${encodeURIComponent(seller.id)}`} className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-brand-primary/10 text-brand-primary transition-opacity hover:opacity-80" title={`Ver detalhes de ${seller.name}`} aria-label={`Ver detalhes de ${seller.name}`}><Eye className="h-3.5 w-3.5" /></Link></td>
</tr>
))}
</tbody>
</table>
</div>
<PaginationControls totalItems={visibleSellers.length} currentPage={safeCurrentPage} totalPages={totalPages} pageSize={itemsPerPage} pageSizeOptions={[10, 20, 50]} itemLabel="vendedores" pageSizeLabel="itens por página" startIndex={startIndex} endIndex={Math.min(startIndex + itemsPerPage, visibleSellers.length)} onPageChange={setCurrentPage} onPageSizeChange={pageSize => { setItemsPerPage(pageSize); setCurrentPage(1); }} />
</>
) : (
<div className="flex flex-col items-center px-6 py-16 text-center"><BriefcaseBusiness className="h-10 w-10 text-dark-muted" /><p className="mt-4 text-sm font-bold text-dark-text">Nenhum vendedor encontrado.</p><p className="mt-1 text-sm font-semibold text-dark-muted">Ajuste a busca ou o período selecionado.</p></div>
)}
</div>
</div>
);
};
export default Sellers;