import { useEffect, useMemo, useState } from 'react'; import { Link, useOutletContext, useParams } from 'react-router-dom'; import { DollarSign, Package, Palette, Ruler, TrendingDown, TrendingUp, Warehouse } from 'lucide-react'; import BackButton from '../components/BackButton'; import DateRangePicker from '../components/DateRangePicker'; import PaginationControls from '../components/PaginationControls'; import RefreshStatus from '../components/RefreshStatus'; import { fetchProductAnalytics } from '../dataService'; import { decodeProductGroupKey, normalizeProductText, parseProductName } from '../productParsing'; import type { DateRange, ProductAnalyticsItem } from '../types'; type VariantRow = ProductAnalyticsItem & { color: string; size: string; dailySales: number; daysOfCover: number | null; }; type BreakdownRow = { label: string; quantitySold: number; revenue: number; stock: number; skuCount: number; }; const BREAKDOWN_LIMIT = 12; const COLOR_SWATCHES: Array<{ pattern: string; color: string }> = [ { pattern: 'preto', color: '#171717' }, { pattern: 'branco', color: '#f8fafc' }, { pattern: 'bege', color: '#d7bf9a' }, { pattern: 'café', color: '#79553d' }, { pattern: 'cafe', color: '#79553d' }, { pattern: 'perola', color: '#e7dfcf' }, { pattern: 'pérola', color: '#e7dfcf' }, { pattern: 'marinho', color: '#172554' }, { pattern: 'bordo', color: '#6b1226' }, { pattern: 'bordô', color: '#6b1226' }, { pattern: 'verde', color: '#166534' }, { pattern: 'rosa', color: '#f0a6bf' }, { pattern: 'cinza', color: '#8f8f8f' }, { pattern: 'vermelho', color: '#b91c1c' }, { pattern: 'grafite', color: '#3f3f46' }, { pattern: 'azul', color: '#2563eb' } ]; const getRangeDays = (range: DateRange) => { const start = new Date(range.start); const end = new Date(range.end); start.setHours(0, 0, 0, 0); end.setHours(0, 0, 0, 0); return Math.max(1, Math.round((end.getTime() - start.getTime()) / 86_400_000) + 1); }; const formatNumber = (value: number, maximumFractionDigits = 0) => ( new Intl.NumberFormat('pt-BR', { maximumFractionDigits }).format(value) ); const formatCurrency = (value: number) => ( new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value) ); const formatDays = (value: number | null) => { if (value === null) return '-'; if (value > 999) return '999+ dias'; return `${formatNumber(value, value < 10 ? 1 : 0)} dias`; }; const getSwatchColor = (label: string) => { const normalizedLabel = label.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase(); return COLOR_SWATCHES.find(item => normalizedLabel.includes(item.pattern.normalize('NFD').replace(/\p{Diacritic}/gu, '')))?.color || '#64748b'; }; const getBarColor = (label: string) => { const color = getSwatchColor(label); return `color-mix(in srgb, ${color} 74%, var(--color-dark-text) 26%)`; }; const ColorSwatch = ({ label, className = 'h-2.5 w-2.5' }: { label: string; className?: string }) => ( ); const buildBreakdown = (rows: VariantRow[], field: 'color' | 'size') => { const totals = new Map(); rows.forEach(row => { const label = row[field] || (field === 'color' ? 'Sem cor' : 'Sem tamanho'); const current = totals.get(label) || { label, quantitySold: 0, revenue: 0, stock: 0, skuCount: 0 }; totals.set(label, { label, quantitySold: current.quantitySold + row.quantitySold, revenue: current.revenue + row.revenue, stock: current.stock + row.stock, skuCount: current.skuCount + 1 }); }); return [...totals.values()].sort((a, b) => b.quantitySold - a.quantitySold); }; const BreakdownPanel = ({ title, subtitle, rows, type }: { title: string; subtitle: string; rows: BreakdownRow[]; type: 'color' | 'size'; }) => { const maxSold = Math.max(...rows.map(row => row.quantitySold), 0); const visibleRows = rows.slice(0, BREAKDOWN_LIMIT); const hiddenCount = Math.max(0, rows.length - visibleRows.length); return (

{title}

{subtitle}

{type === 'color' ? ( ) : ( )}
{rows.length === 0 ? (
Sem dados para este grupo.
) : (
{visibleRows.map(row => { const width = maxSold ? Math.max(4, (row.quantitySold / maxSold) * 100) : 0; const barColor = type === 'color' ? getBarColor(row.label) : '#25c2ff'; return (
{type === 'color' ? : } {type === 'size' && row.label !== 'Sem tamanho' ? `Tam. ${row.label}` : row.label}
{formatNumber(row.quantitySold)} un.
{row.skuCount} SKUs
); })} {hiddenCount > 0 && (
+{formatNumber(hiddenCount)} itens fora do top {BREAKDOWN_LIMIT}
)}
)}
); }; const ProductGroupDetailsSkeleton = () => (
{[0, 1, 2, 3].map(item => (
))}
); const ProductGroupDetails = () => { const { groupKey } = useParams<{ groupKey: string }>(); const { dateRange, setDateRange } = useOutletContext<{ dateRange: DateRange, setDateRange: (range: DateRange) => void }>(); const [products, setProducts] = useState([]); const [isLoading, setIsLoading] = useState(true); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(20); const groupName = useMemo(() => { if (!groupKey) return ''; try { return decodeProductGroupKey(groupKey); } catch { return ''; } }, [groupKey]); useEffect(() => { let isMounted = true; const loadProducts = async () => { setIsLoading(true); const data = await fetchProductAnalytics(dateRange); if (isMounted) { setProducts(data); setIsLoading(false); } }; void loadProducts(); return () => { isMounted = false; }; }, [dateRange]); const groupRows = useMemo(() => { const rangeDays = getRangeDays(dateRange); const normalizedGroupName = normalizeProductText(groupName).toLowerCase(); return products .map(product => { const metadata = parseProductName(product.name); const dailySales = product.quantitySold / rangeDays; return { ...product, color: metadata.color, size: metadata.size, baseName: metadata.baseName, dailySales, daysOfCover: dailySales > 0 ? product.stock / dailySales : null }; }) .filter(product => normalizeProductText(product.baseName).toLowerCase() === normalizedGroupName) .sort((a, b) => b.quantitySold - a.quantitySold); }, [dateRange, groupName, products]); const totals = useMemo(() => { const totalSold = groupRows.reduce((total, row) => total + row.quantitySold, 0); const totalRevenue = groupRows.reduce((total, row) => total + row.revenue, 0); const totalStock = groupRows.reduce((total, row) => total + row.stock, 0); const dailySales = groupRows.reduce((total, row) => total + row.dailySales, 0); const daysOfCover = dailySales > 0 ? totalStock / dailySales : null; const colors = new Set(groupRows.map(row => row.color).filter(Boolean)); const sizes = new Set(groupRows.map(row => row.size).filter(Boolean)); return { totalSold, totalRevenue, totalStock, dailySales, daysOfCover, colorCount: colors.size, sizeCount: sizes.size }; }, [groupRows]); const colorBreakdown = useMemo(() => buildBreakdown(groupRows, 'color'), [groupRows]); const sizeBreakdown = useMemo(() => buildBreakdown(groupRows, 'size'), [groupRows]); const isRefreshing = isLoading && products.length > 0; const totalPages = Math.ceil(groupRows.length / itemsPerPage); const safeCurrentPage = Math.min(currentPage, totalPages || 1); const startIndex = (safeCurrentPage - 1) * itemsPerPage; const paginatedRows = groupRows.slice(startIndex, startIndex + itemsPerPage); if (isLoading && products.length === 0) { return ; } if (!groupName || groupRows.length === 0) { return (

Grupo de produtos não encontrado.

Voltar para produtos
); } return (

Grupo · {formatNumber(groupRows.length)} SKUs · {formatNumber(totals.colorCount)} cores · {formatNumber(totals.sizeCount)} tamanhos

{groupName}

{ setDateRange(range); setCurrentPage(1); }} />

Unidades vendidas

{formatNumber(totals.totalSold)}

Receita total

{formatCurrency(totals.totalRevenue)}

Estoque

{formatNumber(totals.totalStock)}

Cobertura estimada

{formatDays(totals.daysOfCover)}

Variações do grupo

SKUs, cores e tamanhos que formam este grupo.

{formatNumber(groupRows.length)} SKUs
{paginatedRows.map(row => ( ))}
ID Produto Descrição Cor Tamanho Vendido Estoque Média diária Receita Ações
#{row.id}
{row.name}
Preço Atual: {formatCurrency(row.lastPrice)}
{row.color || '-'} {row.size || '-'}
{formatNumber(row.quantitySold)} un.
{formatNumber(row.stock)} un. {formatNumber(row.dailySales, 2)} un./dia {formatCurrency(row.revenue)} Ver SKU
{ setItemsPerPage(pageSize); setCurrentPage(1); }} />
); }; export default ProductGroupDetails;