From f5e2de9a350fb66434261d8b0d0fc21a0094f859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Wed, 1 Jul 2026 10:57:33 -0300 Subject: [PATCH] Improve product analytics UI --- backend/services/analyticsService.js | 88 ++++++++++-- backend/test/analyticsService.test.js | 65 +++++++-- src/analytics/products.ts | 2 + src/pages/ProductDetails.tsx | 198 +++++++++++++++++++++++--- src/pages/Products.tsx | 126 +++++++++++----- src/types.ts | 14 ++ 6 files changed, 417 insertions(+), 76 deletions(-) diff --git a/backend/services/analyticsService.js b/backend/services/analyticsService.js index 5dee928..35ddd03 100644 --- a/backend/services/analyticsService.js +++ b/backend/services/analyticsService.js @@ -759,18 +759,24 @@ const getProductDetailsAnalytics = async (productId, range = {}) => { 'produto_id = $1', 'data_pedido_date IS NOT NULL' ]; + const variantParams = [normalizedProductId]; + const variantFilters = ['data_pedido_date IS NOT NULL']; if (normalizedStart) { periodParams.push(normalizedStart); periodFilters.push(`data_pedido_date >= $${periodParams.length}::date`); + variantParams.push(normalizedStart); + variantFilters.push(`data_pedido_date >= $${variantParams.length}::date`); } if (normalizedEnd) { periodParams.push(normalizedEnd); periodFilters.push(`data_pedido_date <= $${periodParams.length}::date`); + variantParams.push(normalizedEnd); + variantFilters.push(`data_pedido_date <= $${variantParams.length}::date`); } - const [summaryResult, periodResult] = await Promise.all([ + const [summaryResult, periodResult, variantResult] = await Promise.all([ pool.query(` WITH selected_product AS ( SELECT $1::text as id @@ -778,7 +784,8 @@ const getProductDetailsAnalytics = async (productId, range = {}) => { stock_info AS ( SELECT produto_id as id, - MAX(NULLIF(nome, '')) as name + MAX(NULLIF(nome, '')) as name, + COALESCE(MAX(saldo), 0) as stock FROM stock WHERE produto_id = $1 GROUP BY produto_id @@ -795,7 +802,8 @@ const getProductDetailsAnalytics = async (productId, range = {}) => { SELECT selected_product.id, COALESCE(stock_info.name, order_info.name, 'Unknown') as name, - COALESCE(order_info.price, 0) as price + COALESCE(order_info.price, 0) as price, + COALESCE(stock_info.stock, 0) as stock FROM selected_product LEFT JOIN stock_info ON stock_info.id = selected_product.id LEFT JOIN order_info ON order_info.id = selected_product.id @@ -805,7 +813,8 @@ const getProductDetailsAnalytics = async (productId, range = {}) => { SELECT EXTRACT(HOUR FROM created_at AT TIME ZONE 'America/Sao_Paulo')::int as hour, COALESCE(SUM(quantidade), 0) as quantity_sold, - COALESCE(SUM(quantidade * valor_unitario), 0) as revenue + COALESCE(SUM(quantidade * valor_unitario), 0) as revenue, + COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as order_count FROM orders WHERE ${periodFilters.join(' AND ')} GROUP BY hour @@ -815,12 +824,35 @@ const getProductDetailsAnalytics = async (productId, range = {}) => { data_pedido_date, MAX(data_pedido) as date_label, COALESCE(SUM(quantidade), 0) as quantity_sold, - COALESCE(SUM(quantidade * valor_unitario), 0) as revenue + COALESCE(SUM(quantidade * valor_unitario), 0) as revenue, + COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as order_count FROM orders WHERE ${periodFilters.join(' AND ')} GROUP BY data_pedido_date ORDER BY data_pedido_date ASC; - `, periodParams) + `, periodParams), + pool.query(` + WITH selected_base AS ( + SELECT COALESCE(${PRODUCT_NAME_SQL}, 'Unknown') as base_name + FROM orders + WHERE produto_id = $1 + AND data_pedido_date IS NOT NULL + ORDER BY data_pedido_date DESC NULLS LAST, data_pedido DESC NULLS LAST + LIMIT 1 + ) + SELECT + produto_id as id, + MAX(COALESCE(NULLIF(produto_descricao, ''), 'Unknown')) as name, + COALESCE(SUM(quantidade), 0) as quantity_sold, + COALESCE(SUM(quantidade * valor_unitario), 0) as revenue, + COUNT(DISTINCT COALESCE(NULLIF(pedido_id, ''), data_pedido || '_' || valor_pedido::text))::int as order_count + FROM orders + WHERE ${variantFilters.join(' AND ')} + AND COALESCE(${PRODUCT_NAME_SQL}, 'Unknown') = (SELECT base_name FROM selected_base) + GROUP BY produto_id + ORDER BY quantity_sold DESC, revenue DESC, name ASC + LIMIT 8; + `, variantParams) ]); const summary = summaryResult.rows[0]; @@ -829,17 +861,43 @@ const getProductDetailsAnalytics = async (productId, range = {}) => { const chartData = useHourlyChart ? Array.from({ length: 24 }, (_, hour) => { const row = periodResult.rows.find(item => toNumber(item.hour) === hour); + const quantitySold = row ? toNumber(row.quantity_sold) : 0; + const revenue = row ? toNumber(row.revenue) : 0; + const orderCount = row ? toNumber(row.order_count) : 0; + return { date: `${String(hour).padStart(2, '0')}h`, - value: row ? toNumber(row.quantity_sold) : 0 + value: quantitySold, + quantitySold, + revenue, + orderCount, + averageTicket: orderCount ? revenue / orderCount : 0 }; }) - : periodResult.rows.map(row => ({ - date: row.date_label || getDateOnly(row.data_pedido_date) || '', - value: toNumber(row.quantity_sold) - })); + : periodResult.rows.map(row => { + const quantitySold = toNumber(row.quantity_sold); + const revenue = toNumber(row.revenue); + const orderCount = toNumber(row.order_count); + + return { + date: row.date_label || getDateOnly(row.data_pedido_date) || '', + value: quantitySold, + quantitySold, + revenue, + orderCount, + averageTicket: orderCount ? revenue / orderCount : 0 + }; + }); const totalSold = periodResult.rows.reduce((sum, row) => sum + toNumber(row.quantity_sold), 0); const totalRevenue = periodResult.rows.reduce((sum, row) => sum + toNumber(row.revenue), 0); + const totalOrders = periodResult.rows.reduce((sum, row) => sum + toNumber(row.order_count), 0); + const variantBreakdown = variantResult.rows.map(row => ({ + id: row.id, + name: row.name, + quantitySold: toNumber(row.quantity_sold), + revenue: toNumber(row.revenue), + orderCount: toNumber(row.order_count) + })); return { range: { @@ -849,11 +907,15 @@ const getProductDetailsAnalytics = async (productId, range = {}) => { productInfo: { id: summary.id, name: summary.name, - price: toNumber(summary.price) + price: toNumber(summary.price), + stock: toNumber(summary.stock) }, chartData, totalSold, - totalRevenue + totalRevenue, + totalOrders, + averageTicket: totalOrders ? totalRevenue / totalOrders : 0, + variantBreakdown }; }; diff --git a/backend/test/analyticsService.test.js b/backend/test/analyticsService.test.js index 2bda131..76ad62d 100644 --- a/backend/test/analyticsService.test.js +++ b/backend/test/analyticsService.test.js @@ -526,24 +526,44 @@ test('getProductDetailsAnalytics returns product identity and period chart witho rows: [{ id: '919483307', name: 'BASE LISA CAMISETA COR PRETO TAMANHO - G', - price: 11.9 + price: 11.9, + stock: 11731 }] }; } + if (sql.includes('selected_base AS')) { + assert.match(sql, /COALESCE\(\s+CASE/); + assert.match(sql, /GROUP BY produto_id/); + assert.deepEqual(params, ['919483307', '2026-06-01', '2026-06-22']); + return { + rows: [ + { + id: '919483307', + name: 'BASE LISA CAMISETA COR PRETO TAMANHO - G', + quantity_sold: 3, + revenue: 35.7, + order_count: 1 + } + ] + }; + } + return { rows: [ { data_pedido_date: '2026-06-01', date_label: '01-06-2026', quantity_sold: 3, - revenue: 35.7 + revenue: 35.7, + order_count: 1 }, { data_pedido_date: '2026-06-02', date_label: '02-06-2026', quantity_sold: 2, - revenue: 23.8 + revenue: 23.8, + order_count: 1 } ] }; @@ -552,7 +572,7 @@ test('getProductDetailsAnalytics returns product identity and period chart witho try { const details = await getProductDetailsAnalytics('919483307', { start: '2026-06-01', end: '2026-06-22' }); - assert.equal(calls.length, 2); + assert.equal(calls.length, 3); assert.match(calls[0].sql, /WITH selected_product AS/); assert.match(calls[0].sql, /WHERE produto_id = \$1/); assert.deepEqual(calls[0].params, ['919483307']); @@ -563,14 +583,26 @@ test('getProductDetailsAnalytics returns product identity and period chart witho assert.deepEqual(details.productInfo, { id: '919483307', name: 'BASE LISA CAMISETA COR PRETO TAMANHO - G', - price: 11.9 + price: 11.9, + stock: 11731 }); assert.deepEqual(details.chartData, [ - { date: '01-06-2026', value: 3 }, - { date: '02-06-2026', value: 2 } + { date: '01-06-2026', value: 3, quantitySold: 3, revenue: 35.7, orderCount: 1, averageTicket: 35.7 }, + { date: '02-06-2026', value: 2, quantitySold: 2, revenue: 23.8, orderCount: 1, averageTicket: 23.8 } ]); assert.equal(details.totalSold, 5); assert.equal(details.totalRevenue, 59.5); + assert.equal(details.totalOrders, 2); + assert.equal(details.averageTicket, 29.75); + assert.deepEqual(details.variantBreakdown, [ + { + id: '919483307', + name: 'BASE LISA CAMISETA COR PRETO TAMANHO - G', + quantitySold: 3, + revenue: 35.7, + orderCount: 1 + } + ]); } finally { pool.query = originalQuery; } @@ -585,7 +617,8 @@ test('getProductDetailsAnalytics keeps known products visible with zero period s rows: [{ id: 'stock-only', name: 'Produto sem venda no período', - price: 0 + price: 0, + stock: 12 }] }; } @@ -600,6 +633,7 @@ test('getProductDetailsAnalytics keeps known products visible with zero period s assert.equal(details.totalSold, 0); assert.equal(details.totalRevenue, 0); assert.deepEqual(details.chartData, []); + assert.deepEqual(details.variantBreakdown, []); } finally { pool.query = originalQuery; } @@ -614,16 +648,21 @@ test('getProductDetailsAnalytics groups single-day chart by hour', async () => { rows: [{ id: '919483307', name: 'Produto com venda por hora', - price: 11.9 + price: 11.9, + stock: 4 }] }; } + if (sql.includes('selected_base AS')) { + return { rows: [] }; + } + assert.match(sql, /EXTRACT\(HOUR FROM created_at AT TIME ZONE 'America\/Sao_Paulo'\)::int as hour/); return { rows: [ - { hour: 9, quantity_sold: 2, revenue: 23.8 }, - { hour: 18, quantity_sold: 3, revenue: 35.7 } + { hour: 9, quantity_sold: 2, revenue: 23.8, order_count: 1 }, + { hour: 18, quantity_sold: 3, revenue: 35.7, order_count: 1 } ] }; }; @@ -632,8 +671,8 @@ test('getProductDetailsAnalytics groups single-day chart by hour', async () => { const details = await getProductDetailsAnalytics('919483307', { start: '2026-06-22', end: '2026-06-22' }); assert.equal(details.chartData.length, 24); - assert.deepEqual(details.chartData[9], { date: '09h', value: 2 }); - assert.deepEqual(details.chartData[18], { date: '18h', value: 3 }); + assert.deepEqual(details.chartData[9], { date: '09h', value: 2, quantitySold: 2, revenue: 23.8, orderCount: 1, averageTicket: 23.8 }); + assert.deepEqual(details.chartData[18], { date: '18h', value: 3, quantitySold: 3, revenue: 35.7, orderCount: 1, averageTicket: 35.7 }); assert.equal(details.totalSold, 5); assert.equal(details.totalRevenue, 59.5); } finally { diff --git a/src/analytics/products.ts b/src/analytics/products.ts index e6090b5..9d8c2b8 100644 --- a/src/analytics/products.ts +++ b/src/analytics/products.ts @@ -8,6 +8,8 @@ export interface ProductSummary { revenue: number; lastPrice: number; stock: number; + firstSaleDate?: string | null; + lastSaleDate?: string | null; } export interface ProductDetailsMetrics { diff --git a/src/pages/ProductDetails.tsx b/src/pages/ProductDetails.tsx index 5a22b86..0ddc68d 100644 --- a/src/pages/ProductDetails.tsx +++ b/src/pages/ProductDetails.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useParams, Link, useOutletContext } from 'react-router-dom'; -import { ArrowLeft, Package, DollarSign } from 'lucide-react'; +import { ArrowLeft, Package, DollarSign, ReceiptText, Warehouse } from 'lucide-react'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import DateRangePicker from '../components/DateRangePicker'; import RefreshStatus from '../components/RefreshStatus'; @@ -11,6 +11,9 @@ 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'; const formatDateKey = (date: Date) => { const year = date.getFullYear(); @@ -21,19 +24,36 @@ const formatDateKey = (date: Date) => { type CustomTooltipProps = { active?: boolean; - payload?: Array<{ value: number }>; + payload?: Array<{ + value: number; + payload?: ProductDetailsAnalytics['chartData'][number] & { selectedValue?: number }; + }>; label?: string; + metric: ProductChartMetric; + formatCurrency: (value: number) => string; + formatNumber: (value: number) => string; }; -const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { +const CustomTooltip = ({ active, payload, label, metric, formatCurrency, formatNumber }: CustomTooltipProps) => { if (active && payload && payload.length) { + const point = payload[0].payload; + const value = payload[0].value; + const displayValue = metric === 'quantity' ? `${formatNumber(value)} un.` : formatCurrency(value); + return (

{label}

-

Vendas: {payload[0].value}

+

{displayValue}

+ {point && ( +
+

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

+

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

+

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

+
+ )}
); } @@ -56,8 +76,8 @@ const ProductDetailsSkeleton = () => (
-
- {[0, 1].map(item => ( +
+ {[0, 1, 2, 3].map(item => (
@@ -74,6 +94,15 @@ const ProductDetailsSkeleton = () => (
+ +
+
+
+ {[0, 1, 2, 3].map(item => ( +
+ ))} +
+
); @@ -85,6 +114,7 @@ const ProductDetails = () => { }>(); const [details, setDetails] = useState(null); const [isLoading, setIsLoading] = useState(true); + const [chartMetric, setChartMetric] = useState('quantity'); useEffect(() => { let isMounted = true; @@ -118,6 +148,10 @@ const ProductDetails = () => { return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value); }; + const formatNumber = (value: number) => { + return new Intl.NumberFormat('pt-BR').format(value); + }; + if (isLoading && !details) { return ; } @@ -131,9 +165,44 @@ const ProductDetails = () => { ); } - const { productInfo, chartData, totalSold, totalRevenue } = details; + const { productInfo, chartData, totalSold, totalRevenue, totalOrders = 0, averageTicket = 0, variantBreakdown = [] } = details; const isRefreshing = isLoading && Boolean(details); const isSingleDayRange = formatDateKey(dateRange.start) === formatDateKey(dateRange.end); + const metricConfig = { + quantity: { + label: 'Unidades', + title: `Volume por ${isSingleDayRange ? 'Horário' : 'Data'}`, + subtitle: 'Quantidade vendida no período selecionado.', + tickFormatter: (value: number) => formatNumber(value) + }, + revenue: { + label: 'Receita', + title: `Receita por ${isSingleDayRange ? '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 ${isSingleDayRange ? '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 = chartData.map(point => ({ + ...point, + selectedValue: chartMetric === 'quantity' + ? (point.quantitySold ?? point.value) + : chartMetric === 'revenue' + ? (point.revenue ?? 0) + : (point.averageTicket ?? 0) + })); + const maxVariantQuantity = Math.max(...variantBreakdown.map(variant => variant.quantitySold), 0); return (
@@ -163,11 +232,11 @@ const ProductDetails = () => {
-
+

Unidades Vendidas

-

{totalSold}

+

{formatNumber(totalSold)}

@@ -182,15 +251,57 @@ const ProductDetails = () => {
+
+
+

Ticket Médio

+

{formatCurrency(averageTicket)}

+
+
+ +
+
+
+
+

Estoque

+

{formatNumber(productInfo.stock)}

+
+
+ +
+
-

- Volume de Vendas por {isSingleDayRange ? 'Horário' : 'Data'} -

+
+
+

{selectedMetric.title}

+

{selectedMetric.subtitle}

+
+
+ {(Object.keys(metricConfig) as ProductChartMetric[]).map(metric => ( + + ))} +
+
+ {metricChartData.length === 0 ? ( +
+ Nenhuma venda no período selecionado. +
+ ) : (
- + @@ -205,11 +316,11 @@ const ProductDetails = () => { textAnchor={isSingleDayRange ? 'middle' : 'end'} height={isSingleDayRange ? 24 : 80} /> - - } cursor={{ fill: CHART_CURSOR_COLOR }} /> + selectedMetric.tickFormatter(Number(value))} /> + } cursor={{ fill: CHART_CURSOR_COLOR }} /> {
+ )} +
+ +
+
+
+

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; + + return ( +
+
+
+
{variant.name}
+
#{variant.id}
+
+
+
+
{formatNumber(variant.quantitySold)} un.
+
vendidas
+
+
+
{formatCurrency(variant.revenue)}
+
receita
+
+
+
+
+
+
+
+ ); + })} +
+ )}
diff --git a/src/pages/Products.tsx b/src/pages/Products.tsx index 4d62688..97e9837 100644 --- a/src/pages/Products.tsx +++ b/src/pages/Products.tsx @@ -7,24 +7,68 @@ import type { DateRange, ProductAnalyticsItem } from '../types'; import { exportToCSV, fetchProductAnalytics } from '../dataService'; import type { ProductSummary } from '../analytics/products'; +type ProductHealth = { + label: string; + className: string; +}; + +const getDateOnlyTime = (value?: string | null) => { + if (!value) return 0; + const date = new Date(`${String(value).slice(0, 10)}T00:00:00`); + return Number.isNaN(date.getTime()) ? 0 : date.getTime(); +}; + +const getProductHealth = (product: ProductSummary, dateRange: DateRange): ProductHealth => { + const endTime = new Date(dateRange.end.getFullYear(), dateRange.end.getMonth(), dateRange.end.getDate()).getTime(); + const startTime = new Date(dateRange.start.getFullYear(), dateRange.start.getMonth(), dateRange.start.getDate()).getTime(); + const rangeDays = Math.max(1, Math.round((endTime - startTime) / 86400000) + 1); + const lastSaleTime = getDateOnlyTime(product.lastSaleDate); + const firstSaleTime = getDateOnlyTime(product.firstSaleDate); + const daysSinceLastSale = lastSaleTime ? Math.max(0, Math.round((endTime - lastSaleTime) / 86400000)) : Infinity; + const daysSinceFirstSale = firstSaleTime ? Math.max(0, Math.round((endTime - firstSaleTime) / 86400000)) : Infinity; + + if (product.totalSold > 0 && product.stock > 0 && product.stock <= Math.max(3, product.totalSold * 0.15)) { + return { label: 'Estoque baixo', className: 'border-amber-500/35 bg-amber-500/10 text-amber-700 dark:text-amber-300' }; + } + + if (!product.totalSold) { + return { label: 'Sem venda', className: 'border-zinc-500/25 bg-zinc-500/10 text-zinc-600 dark:text-zinc-400' }; + } + + if (daysSinceFirstSale <= Math.min(14, rangeDays)) { + return { label: 'Novo', className: 'border-cyan-500/35 bg-cyan-500/10 text-cyan-700 dark:text-cyan-300' }; + } + + if (daysSinceLastSale <= Math.max(1, Math.min(7, Math.ceil(rangeDays * 0.2)))) { + return { label: 'Quente', className: 'border-emerald-500/35 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300' }; + } + + if (daysSinceLastSale > Math.max(14, Math.ceil(rangeDays * 0.55))) { + return { label: 'Esfriando', className: 'border-orange-500/35 bg-orange-500/10 text-orange-700 dark:text-orange-300' }; + } + + return { label: 'Estável', className: 'border-sky-500/35 bg-sky-500/10 text-sky-700 dark:text-sky-300' }; +}; + const ProductTableSkeleton = () => (
-
- {[0, 1, 2, 3, 4, 5].map(item => ( +
+ {[0, 1, 2, 3, 4, 5, 6].map(item => (
))}
{[0, 1, 2, 3, 4, 5, 6, 7].map(row => ( -
+
+
@@ -79,7 +123,9 @@ const Products = () => { totalSold: product.quantitySold, revenue: product.revenue, lastPrice: product.lastPrice, - stock: product.stock + stock: product.stock, + firstSaleDate: product.firstSaleDate, + lastSaleDate: product.lastSaleDate })); const filteredProducts = normalizedSearch ? products.filter(product => @@ -139,6 +185,8 @@ const Products = () => { 'Descrição': product.name, 'Preço Atual (R$)': product.lastPrice.toFixed(2).replace('.', ','), 'Total Vendido (un.)': product.totalSold, + 'Status': getProductHealth(product, dateRange).label, + 'Estoque': product.stock, 'Receita Gerada (R$)': product.revenue.toFixed(2).replace('.', ',') })); exportToCSV(exportData, `produtos_${new Date().toISOString().split('T')[0]}.csv`); @@ -165,42 +213,52 @@ const Products = () => { ID Produto Descrição Total Vendido + Status Estoque Receita Gerada Ações - {paginatedData.map((product) => ( - - #{product.id} - -
{product.name}
-
Preço Atual: {formatCurrency(product.lastPrice)}
- - -
- - {product.totalSold} un. -
- - - - {product.stock} un. - - - {formatCurrency(product.revenue)} - - - - Ver Gráfico - - - - ))} + {paginatedData.map((product) => { + const health = getProductHealth(product, dateRange); + + return ( + + #{product.id} + +
{product.name}
+
Preço Atual: {formatCurrency(product.lastPrice)}
+ + +
+ + {product.totalSold} un. +
+ + + + {health.label} + + + + + {product.stock} un. + + + {formatCurrency(product.revenue)} + + + + Ver Gráfico + + + + ); + })}
diff --git a/src/types.ts b/src/types.ts index df2a753..12e7745 100644 --- a/src/types.ts +++ b/src/types.ts @@ -115,13 +115,27 @@ export interface ProductDetailsAnalytics { id: string; name: string; price: number; + stock: number; }; chartData: Array<{ date: string; value: number; + quantitySold?: number; + revenue?: number; + orderCount?: number; + averageTicket?: number; }>; totalSold: number; totalRevenue: number; + totalOrders?: number; + averageTicket?: number; + variantBreakdown?: Array<{ + id: string; + name: string; + quantitySold: number; + revenue: number; + orderCount: number; + }>; } export interface ClientAnalyticsItem {