import { useEffect, useState } from 'react'; import { useParams, Link, useOutletContext } from 'react-router-dom'; import { Package, DollarSign, Pencil, ReceiptText, Warehouse } from 'lucide-react'; import { AreaChart, Area, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import BackButton from '../components/BackButton'; import DateRangePicker from '../components/DateRangePicker'; import SkuPlanningModal from '../components/SkuPlanningModal'; import ProductTypeBadge from '../components/ProductTypeBadge'; import RefreshStatus from '../components/RefreshStatus'; import type { CutProductOverride, CuttingSettings, DateRange, ProductComposition, ProductDetailsAnalytics } from '../types'; import { fetchCuttingSettings, fetchProductComposition, fetchProductDetailsAnalytics, saveCuttingSettings } from '../dataService'; import { parseProductName } from '../productParsing'; import { formatColorLabel } from '../displayFormatters'; import { averageRecentValues, formatDateBucketLabel, formatDateBucketLongLabel, getAutoDateBucket, getMovingAverageWindow, getRangeDayCount, getDateBucketKey, type DateBucket } from '../chartUtils'; import { getProductTypeConfig, resolveProductType } from '../productClassification'; import { getPlanningStock } from '../planningStock'; const CHART_GRID_COLOR = 'var(--chart-grid)'; const CHART_AXIS_COLOR = 'var(--chart-axis)'; const CHART_CURSOR_COLOR = 'var(--chart-cursor)'; const CHART_DETAIL_BAR_COLOR = 'var(--chart-detail-bar)'; const VARIANT_BAR_COLOR = '#52DFA0'; type ProductChartMetric = 'quantity' | 'revenue' | 'ticket'; type ProductMetricChartPoint = ProductDetailsAnalytics['chartData'][number] & { selectedValue: number; movingAverage?: number; }; const formatDateKey = (date: Date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; type CustomTooltipProps = { active?: boolean; payload?: Array<{ value: number; dataKey?: string; payload?: ProductMetricChartPoint; }>; label?: string; metric: ProductChartMetric; formatCurrency: (value: number) => string; formatNumber: (value: number) => string; isHourly: boolean; dateBucket: DateBucket; }; const CustomTooltip = ({ active, payload, label, metric, formatCurrency, formatNumber, isHourly, dateBucket }: CustomTooltipProps) => { if (active && payload && payload.length) { const primaryPayload = payload.find(item => item.dataKey === 'selectedValue') || payload[0]; const point = primaryPayload.payload; const value = primaryPayload.value; const displayValue = metric === 'quantity' ? `${formatNumber(value)} un.` : formatCurrency(value); const displayLabel = isHourly ? String(label || '') : formatDateBucketLongLabel(String(label || ''), dateBucket); return (

{displayLabel}

{displayValue}

{point && (

Unidades: {formatNumber(point.quantitySold ?? point.value ?? 0)}

Receita: {formatCurrency(point.revenue ?? 0)}

Pedidos: {formatNumber(point.orderCount ?? 0)}

)}
); } return null; }; const ProductDetailsSkeleton = () => (
{[0, 1, 2, 3].map(item => (
))}
{[0, 1, 2, 3].map(item => (
))}
); const ProductDetails = () => { const { id } = useParams<{ id: string }>(); const { dateRange, setDateRange } = useOutletContext<{ dateRange: DateRange, setDateRange: (range: DateRange) => void }>(); const [details, setDetails] = useState(null); const [composition, setComposition] = useState(null); const [isCompositionLoading, setIsCompositionLoading] = useState(true); const [isLoading, setIsLoading] = useState(true); const [chartMetric, setChartMetric] = useState('quantity'); const [selectedProductBucket, setSelectedProductBucket] = useState(null); const [planningSettings, setPlanningSettings] = useState({ familyYields: {}, productOverrides: {} }); const [isPlanningModalOpen, setIsPlanningModalOpen] = useState(false); const [isSavingPlanning, setIsSavingPlanning] = useState(false); useEffect(() => { let isMounted = true; const loadPlanningSettings = async () => { const settings = await fetchCuttingSettings(); if (isMounted) setPlanningSettings(settings); }; void loadPlanningSettings(); return () => { isMounted = false; }; }, []); useEffect(() => { let isMounted = true; const loadProductDetails = async () => { if (!id) { if (isMounted) { setDetails(null); setComposition(null); setIsLoading(false); setIsCompositionLoading(false); } return; } setIsLoading(true); setIsCompositionLoading(true); const [productDetails, productComposition] = await Promise.all([ fetchProductDetailsAnalytics(id, dateRange), fetchProductComposition(id) ]); if (isMounted) { setDetails(productDetails); setComposition(productComposition); setIsLoading(false); setIsCompositionLoading(false); } }; void loadProductDetails(); return () => { isMounted = false; }; }, [dateRange, id]); const formatCurrency = (value: number) => { return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value); }; const formatNumber = (value: number) => { return new Intl.NumberFormat('pt-BR').format(value); }; const saveProductOverride = async (productId: string, override: CutProductOverride | null) => { const productOverrides = { ...planningSettings.productOverrides }; if (override) { productOverrides[productId] = override; } else { delete productOverrides[productId]; } const nextSettings = { ...planningSettings, productOverrides }; setIsSavingPlanning(true); try { const savedSettings = await saveCuttingSettings(nextSettings); setPlanningSettings(savedSettings); setIsPlanningModalOpen(false); } finally { setIsSavingPlanning(false); } }; if (isLoading && !details) { return ; } if (!details?.productInfo) { return (

Produto não encontrado.

Voltar para produtos
); } const { productInfo, chartData, totalSold, totalRevenue, totalOrders = 0, averageTicket = 0, variantBreakdown = [] } = details; const productType = resolveProductType(productInfo.name, planningSettings.productOverrides[productInfo.id]); const productTypeConfig = getProductTypeConfig(productType); const isRefreshing = isLoading && Boolean(details); const isSingleDayRange = formatDateKey(dateRange.start) === formatDateKey(dateRange.end); const isHourlyChart = isSingleDayRange && chartData.some(point => /h$|:/.test(point.date)); const dateBucket = isHourlyChart ? 'day' : getAutoDateBucket(dateRange); const periodDays = getRangeDayCount(dateRange); const dailyAverageSold = totalSold / periodDays; const projectedStockDays = dailyAverageSold > 0 ? getPlanningStock(productInfo.stock) / dailyAverageSold : null; const stockActionLabel = projectedStockDays === null ? 'Sem venda no período' : projectedStockDays <= 7 ? 'Reposição crítica' : projectedStockDays <= 21 ? 'Planejar reposição' : 'Estoque confortável'; const metricConfig = { quantity: { label: 'Unidades', title: `Volume por ${isHourlyChart ? 'Horário' : 'Data'}`, subtitle: 'Quantidade vendida no período selecionado.', tickFormatter: (value: number) => formatNumber(value) }, revenue: { label: 'Receita', title: `Receita por ${isHourlyChart ? 'Horário' : 'Data'}`, subtitle: 'Faturamento do produto no período selecionado.', tickFormatter: (value: number) => value >= 1000 ? `${formatNumber(value / 1000)}k` : formatCurrency(value) }, ticket: { label: 'Ticket médio', title: `Ticket médio por ${isHourlyChart ? 'Horário' : 'Data'}`, subtitle: 'Receita média por pedido neste produto.', tickFormatter: (value: number) => value >= 1000 ? `${formatNumber(value / 1000)}k` : formatCurrency(value) } } satisfies Record string; }>; const selectedMetric = metricConfig[chartMetric]; const metricChartData = (() => { const bucketMap = new Map(); chartData.forEach(point => { const sourceKey = isHourlyChart ? point.date : getDateBucketKey(point.date, dateBucket); const current = bucketMap.get(sourceKey) || { date: sourceKey, value: 0, quantitySold: 0, revenue: 0, orderCount: 0, averageTicket: 0, selectedValue: 0 }; const quantity = point.quantitySold ?? point.value ?? 0; const revenue = point.revenue ?? 0; const orderCount = point.orderCount ?? 0; current.value = (current.value || 0) + quantity; current.quantitySold = (current.quantitySold || 0) + quantity; current.revenue = (current.revenue || 0) + revenue; current.orderCount = (current.orderCount || 0) + orderCount; current.averageTicket = current.orderCount ? current.revenue / current.orderCount : 0; bucketMap.set(sourceKey, current); }); const rows = [...bucketMap.values()] .filter(point => (point.quantitySold || point.value || point.revenue || point.orderCount)) .sort((a, b) => a.date.localeCompare(b.date)) .map(point => ({ ...point, selectedValue: chartMetric === 'quantity' ? (point.quantitySold ?? point.value) : chartMetric === 'revenue' ? (point.revenue ?? 0) : (point.averageTicket ?? 0) })); const movingAverageWindow = isHourlyChart ? 0 : getMovingAverageWindow(dateBucket); if (movingAverageWindow > 1) { const values = rows.map(point => point.selectedValue); rows.forEach((point, index) => { point.movingAverage = averageRecentValues(values, index, movingAverageWindow); }); } return rows; })(); const selectedProductPoint = selectedProductBucket ? metricChartData.find(point => point.date === selectedProductBucket) : null; const selectedProductBucketLabel = selectedProductPoint ? isHourlyChart ? selectedProductPoint.date : formatDateBucketLongLabel(selectedProductPoint.date, dateBucket) : ''; const maxVariantQuantity = Math.max(...variantBreakdown.map(variant => variant.quantitySold), 0); return (
ID: #{productInfo.id}

{productInfo.name}

{productTypeConfig.description}

Unidades Vendidas

{formatNumber(totalSold)}

{formatNumber(dailyAverageSold)} un./dia

Receita Total

{formatCurrency(totalRevenue)}

Ticket Médio

{formatCurrency(averageTicket)}

Estoque

{formatNumber(productInfo.stock)}

{projectedStockDays === null ? stockActionLabel : `${formatNumber(projectedStockDays)} dias · ${stockActionLabel}`}

Composição

{composition && (

Para produzir 1 UN de {composition.finishedProductSku || productInfo.id}

)}
{isCompositionLoading ? (
Carregando composição…
) : !composition ? (
Nenhuma composição sincronizada para este produto.
) : composition.components.length === 0 ? (
Esta composição não possui insumos.
) : (
{composition.components.map(component => ( ))}
Produto / insumo SKU Quantidade por unidade Unidade
{component.productId ? ( {component.componentName} ) : component.componentName} {component.componentSku || '—'} {formatNumber(component.quantityPerUnit)} {component.unit || '—'}
)}

{selectedMetric.title}

{isHourlyChart ? selectedMetric.subtitle : dateBucket === 'day' ? `${selectedMetric.subtitle} Média móvel de 7 dias.` : dateBucket === 'week' ? 'Valores agrupados por semana com média móvel de 4 semanas.' : 'Valores agrupados por mês com média móvel de 3 meses.'}

{(Object.keys(metricConfig) as ProductChartMetric[]).map(metric => ( ))}
{metricChartData.length === 0 ? (
Nenhuma venda no período selecionado.
) : (
{ if (event?.activeLabel) setSelectedProductBucket(String(event.activeLabel)); }} > ( isHourlyChart ? String(value) : formatDateBucketLabel(String(value), dateBucket) )} /> selectedMetric.tickFormatter(Number(value))} /> } cursor={{ fill: CHART_CURSOR_COLOR }} /> {!isHourlyChart && metricChartData.some(point => point.movingAverage !== undefined) && ( )}
)} {selectedProductPoint && (

{selectedProductBucketLabel}

Detalhe do ponto selecionado.

Unidades
{formatNumber(selectedProductPoint.quantitySold ?? selectedProductPoint.value ?? 0)}
Receita
{formatCurrency(selectedProductPoint.revenue ?? 0)}
Pedidos
{formatNumber(selectedProductPoint.orderCount ?? 0)}
Ticket
{formatCurrency(selectedProductPoint.averageTicket ?? 0)}
Cobertura
{projectedStockDays === null ? '-' : `${formatNumber(projectedStockDays)} dias`}
)}

Variações do Produto

Tamanhos, cores ou SKUs parecidos no mesmo período.

{formatNumber(totalOrders)} {totalOrders === 1 ? 'pedido' : 'pedidos'}
{variantBreakdown.length === 0 ? (
Nenhuma variação encontrada para este produto.
) : (
{variantBreakdown.map(variant => { const width = maxVariantQuantity ? Math.max(4, (variant.quantitySold / maxVariantQuantity) * 100) : 0; const metadata = parseProductName(variant.name); return (
{variant.name}
#{variant.id} {metadata.color && ( {formatColorLabel(metadata.color)} )} {metadata.size && ( Tam. {metadata.size} )}
{formatNumber(variant.quantitySold)} un.
vendidas
{formatCurrency(variant.revenue)}
receita
); })}
)}
{isPlanningModalOpen && ( setIsPlanningModalOpen(false)} onSave={(override) => saveProductOverride(productInfo.id, override)} /> )}
); }; export default ProductDetails;