Files
graphs/src/pages/Dashboard.tsx
Cauê Faleiros 285587bda5
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 47s
Animate seller dashboard chart
2026-06-26 15:02:16 -03:00

748 lines
30 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from 'react';
import { useOutletContext, useNavigate } from 'react-router-dom';
import { AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
import { DollarSign, ShoppingCart, TrendingUp } from 'lucide-react';
import DateRangePicker from '../components/DateRangePicker';
import type { DashboardAnalytics, OrderData, DateRange } from '../types';
import { applyDashboardColors, buildDashboardMetrics } from '../analytics/dashboard';
import { fetchDashboardAnalytics } from '../dataService';
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
};
const formatCompactCurrency = (value: number) => {
const absValue = Math.abs(value);
const formatNumber = (nextValue: number) => Number.isInteger(nextValue)
? String(nextValue)
: nextValue.toLocaleString('pt-BR', { maximumFractionDigits: 1 });
if (absValue >= 1_000_000) return `${formatNumber(value / 1_000_000)}M`;
if (absValue >= 1_000) return `${formatNumber(value / 1_000)}k`;
return formatNumber(value);
};
const formatDateTick = (value: string) => {
if (!value) return '';
const date = new Date(`${value}T00:00:00`);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat('pt-BR', { day: '2-digit', month: '2-digit' }).format(date);
};
const formatDateLabel = (value: string) => {
if (!value) return '';
const date = new Date(`${value}T00:00:00`);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(date);
};
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}`;
};
const SELLER_COLORS = [
'#25C2FF',
'#52DFA0',
'#A06BFF',
'#FF8A63',
'#FFC247',
'#FF7A9B',
'#18D6B5',
'#FFA24A',
'#82B8FF',
'#B8E84D'
];
const BAR_FILL_OPACITY = 0.5;
const BAR_STROKE_OPACITY = 0.85;
const PIE_FILL_OPACITY = 0.68;
const CHART_GRID_COLOR = 'var(--chart-grid)';
const CHART_AXIS_COLOR = 'var(--chart-axis)';
const CHART_CURSOR_COLOR = 'var(--chart-cursor)';
type SellerMetricKey = 'revenue' | 'ticket' | 'orders';
type SellerTimeSeriesSeller = {
id: string;
seriesKey: string;
name: string;
fill: string;
total: number;
revenue: number;
orders: number;
};
type SellerTimeSeriesChartData = {
date: string;
[key: string]: string | number;
};
type SellerMetricConfig = {
key: SellerMetricKey;
label: string;
title: string;
fallbackDescription: string;
trendDescription: string;
emptyText: string;
formatValue: (value: number) => string;
formatTick: (value: number) => string;
};
const sellerMetricOptions: SellerMetricConfig[] = [
{
key: 'revenue',
label: 'Receita',
title: 'Receita por Vendedor',
fallbackDescription: 'Total por vendedor no período selecionado.',
trendDescription: 'Evolução por data no período selecionado.',
emptyText: 'Nenhuma receita com vendedor no período.',
formatValue: formatCurrency,
formatTick: formatCompactCurrency
},
{
key: 'ticket',
label: 'Ticket médio',
title: 'Ticket Médio por Vendedor',
fallbackDescription: 'Ticket médio por vendedor no período selecionado.',
trendDescription: 'Ticket médio por data no período selecionado.',
emptyText: 'Nenhum ticket médio com vendedor no período.',
formatValue: formatCurrency,
formatTick: formatCompactCurrency
},
{
key: 'orders',
label: 'Pedidos',
title: 'Pedidos por Vendedor',
fallbackDescription: 'Pedidos por vendedor no período selecionado.',
trendDescription: 'Pedidos por data no período selecionado.',
emptyText: 'Nenhum pedido com vendedor no período.',
formatValue: (value) => new Intl.NumberFormat('pt-BR').format(value),
formatTick: (value) => new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 0 }).format(value)
}
];
const getSellerMetricValue = (metric: SellerMetricKey, revenue: number, orders: number) => {
if (metric === 'orders') return orders;
if (metric === 'ticket') return orders ? revenue / orders : 0;
return revenue;
};
const getBarGlowStyle = (color: string) => ({
filter: `drop-shadow(0 0 3px ${color}40)`
});
const getPieGlowStyle = (color: string) => ({
filter: `drop-shadow(0 0 8px ${color}80)`,
cursor: 'pointer'
});
const kpiIconStyle = (color: string) => ({
backgroundColor: `${color}14`,
borderColor: `${color}26`,
color
});
const DashboardSkeleton = () => (
<div className="space-y-6" aria-label="Carregando dashboard">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[0, 1, 2].map(item => (
<div key={`dashboard-kpi-skeleton-${item}`} className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm">
<div className="flex justify-between gap-6">
<div className="w-full">
<div className="skeleton h-4 w-32" />
<div className="skeleton mt-3 h-8 w-44" />
</div>
<div className="skeleton h-12 w-12 shrink-0 rounded-xl" />
</div>
</div>
))}
</div>
<div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="skeleton h-5 w-48" />
</div>
<div className="mt-8">
<div className="skeleton h-80 w-full" />
<div className="mt-5 flex flex-wrap gap-2">
{[0, 1, 2, 3, 4, 5].map(row => (
<div key={`dashboard-seller-legend-skeleton-${row}`} className="skeleton h-9 rounded-full" style={{ width: `${150 - row * 7}px` }} />
))}
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{[0, 1].map(item => (
<div key={`dashboard-product-chart-skeleton-${item}`} className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm">
<div className="skeleton h-5 w-44" />
<div className="mt-8 h-80">
<div className="flex h-full items-end justify-center gap-3">
{[0, 1, 2, 3, 4, 5, 6, 7].map(bar => (
<div key={`dashboard-column-skeleton-${item}-${bar}`} className="skeleton w-10" style={{ height: `${35 + ((bar * 17) % 55)}%` }} />
))}
</div>
</div>
</div>
))}
</div>
</div>
);
type ChartTooltipPayload = {
value: number;
name?: string;
color?: string;
stroke?: string;
dataKey?: string;
payload?: {
fill?: string;
};
};
type CustomTooltipProps = {
active?: boolean;
payload?: ChartTooltipPayload[];
label?: string;
isCurrency?: boolean;
valueLabel?: string;
};
const CustomTooltip = ({ active, payload, label, isCurrency, valueLabel }: CustomTooltipProps) => {
if (active && payload && payload.length) {
const color = payload[0].payload?.fill || payload[0].stroke || payload[0].color || 'var(--chart-detail-bar)';
const displayLabel = label || payload[0].name;
const value = isCurrency ? formatCurrency(payload[0].value) : payload[0].value;
const displayValueLabel = valueLabel || (isCurrency ? 'Receita:' : 'Vendas:');
return (
<div
className="rounded-xl border p-3 shadow-lg"
style={{ backgroundColor: 'var(--chart-tooltip-bg)', borderColor: 'var(--chart-tooltip-border)' }}
>
<p className="font-bold mb-1" style={{ color }}>{displayLabel}</p>
<p className="m-0" style={{ color: 'var(--chart-tooltip-text)' }}>{displayValueLabel} {value}</p>
</div>
);
}
return null;
};
type SellerMetricTooltipProps = {
active?: boolean;
payload?: ChartTooltipPayload[];
label?: string;
focusedSellerId: string | null;
sellers: SellerTimeSeriesSeller[];
metricConfig: SellerMetricConfig;
};
const SellerMetricTooltip = ({ active, payload, label, focusedSellerId, sellers, metricConfig }: SellerMetricTooltipProps) => {
if (!active || !payload?.length) return null;
const sellersByKey = new Map(sellers.map(seller => [seller.seriesKey, seller]));
const rows = payload
.map(item => {
const seller = sellersByKey.get(String(item.dataKey || ''));
return seller ? { seller, value: Number(item.value || 0) } : null;
})
.filter((item): item is { seller: SellerTimeSeriesSeller; value: number } => Boolean(item && item.value > 0))
.sort((a, b) => {
if (focusedSellerId) {
if (a.seller.id === focusedSellerId) return -1;
if (b.seller.id === focusedSellerId) return 1;
}
return b.value - a.value;
})
.slice(0, focusedSellerId ? 4 : 6);
return (
<div
className="min-w-56 rounded-xl border p-3 shadow-lg"
style={{ backgroundColor: 'var(--chart-tooltip-bg)', borderColor: 'var(--chart-tooltip-border)' }}
>
<p className="mb-2 text-xs font-bold uppercase tracking-wide text-dark-muted">{formatDateLabel(String(label || ''))}</p>
<div className="space-y-2 text-sm" style={{ color: 'var(--chart-tooltip-text)' }}>
{rows.length ? rows.map(({ seller, value }) => (
<div key={`seller-tooltip-${seller.id}`} className="flex items-center justify-between gap-4">
<span className="flex min-w-0 items-center gap-2 font-semibold">
<span className="h-2.5 w-2.5 shrink-0 rounded-full" style={{ backgroundColor: seller.fill }} />
<span className="truncate">{seller.name}</span>
</span>
<span className="shrink-0 font-bold">{metricConfig.formatValue(value)}</span>
</div>
)) : (
<p className="m-0 text-dark-muted">Sem receita nesse dia.</p>
)}
</div>
</div>
);
};
const Dashboard = () => {
const navigate = useNavigate();
const { dateRange, setDateRange, ordersData, refreshInterval, setRefreshInterval } = useOutletContext<{
dateRange: DateRange,
setDateRange: (range: DateRange) => void,
ordersData: OrderData[],
refreshInterval: number,
setRefreshInterval: (interval: number) => void,
}>();
const [serverMetrics, setServerMetrics] = useState<DashboardAnalytics | null>(null);
const [isMetricsLoading, setIsMetricsLoading] = useState(true);
const [focusedSellerId, setFocusedSellerId] = useState<string | null>(null);
const [sellerMetric, setSellerMetric] = useState<SellerMetricKey>('revenue');
const loadDashboardMetrics = useCallback(async (range: DateRange, options?: { force?: boolean }) => {
setIsMetricsLoading(true);
const metrics = await fetchDashboardAnalytics(range, options);
setServerMetrics(metrics);
setIsMetricsLoading(false);
}, []);
useEffect(() => {
// Dashboard metrics are synchronized with the selected server-side date range.
// eslint-disable-next-line react-hooks/set-state-in-effect
void loadDashboardMetrics(dateRange);
}, [dateRange, loadDashboardMetrics]);
useEffect(() => {
if (refreshInterval === 0) return;
const intervalId = setInterval(() => {
void loadDashboardMetrics(dateRange, { force: true });
}, refreshInterval);
return () => clearInterval(intervalId);
}, [dateRange, loadDashboardMetrics, refreshInterval]);
const { totalRevenue, totalOrders, averageOrderValue, salesByProduct, revenueByProduct, revenueBySeller, ordersBySeller, sellerRevenueByDate } = useMemo(() => {
if (serverMetrics) return applyDashboardColors(serverMetrics);
return buildDashboardMetrics(ordersData, dateRange);
}, [dateRange, ordersData, serverMetrics]);
const sellerColorMap = useMemo(() => {
const colorMap = new Map<string, string>();
[...revenueBySeller, ...ordersBySeller].forEach((seller) => {
const key = seller.id || seller.name;
if (!colorMap.has(key)) {
colorMap.set(key, SELLER_COLORS[colorMap.size % SELLER_COLORS.length]);
}
});
return colorMap;
}, [ordersBySeller, revenueBySeller]);
const sellerMetricConfig = sellerMetricOptions.find(option => option.key === sellerMetric) || sellerMetricOptions[0];
const sellerTimeSeries = useMemo(() => {
if (!sellerRevenueByDate.length && revenueBySeller.length) {
const startDate = formatDateKey(dateRange.start);
const endDate = formatDateKey(dateRange.end);
const chartDates = startDate === endDate ? [endDate] : [startDate, endDate];
const sellersById = new Map<string, Omit<SellerTimeSeriesSeller, 'seriesKey' | 'total'>>();
revenueBySeller.forEach(seller => {
const id = seller.id || seller.name;
sellersById.set(id, {
id,
name: seller.name,
fill: sellerColorMap.get(id) || seller.fill,
revenue: seller.value,
orders: 0
});
});
ordersBySeller.forEach(seller => {
const id = seller.id || seller.name;
const existing = sellersById.get(id);
sellersById.set(id, {
id,
name: existing?.name || seller.name,
fill: existing?.fill || sellerColorMap.get(id) || seller.fill,
revenue: existing?.revenue || 0,
orders: seller.value
});
});
const sellers: SellerTimeSeriesSeller[] = [...sellersById.values()]
.map(seller => ({
...seller,
total: getSellerMetricValue(sellerMetric, seller.revenue, seller.orders),
seriesKey: ''
}))
.filter(seller => seller.total > 0)
.sort((a, b) => b.total - a.total)
.slice(0, 8)
.map((seller, index) => {
const id = seller.id || seller.name;
return {
...seller,
id,
seriesKey: `seller_${index}`
};
});
const chartData: SellerTimeSeriesChartData[] = chartDates.map(date => {
const row: SellerTimeSeriesChartData = { date };
sellers.forEach(seller => {
row[seller.seriesKey] = seller.total;
});
return row;
});
return { sellers, chartData, isFallback: true };
}
const sellersById = new Map<string, Omit<SellerTimeSeriesSeller, 'seriesKey' | 'total'>>();
const valuesByDate = new Map<string, Map<string, { revenue: number; orders: number }>>();
sellerRevenueByDate.forEach(point => {
const id = point.id || point.name;
const existing = sellersById.get(id);
sellersById.set(id, {
id,
name: point.name,
fill: sellerColorMap.get(id) || point.fill,
revenue: (existing?.revenue || 0) + point.value,
orders: (existing?.orders || 0) + (point.orders || 0)
});
if (!valuesByDate.has(point.date)) {
valuesByDate.set(point.date, new Map());
}
const dateValues = valuesByDate.get(point.date);
if (dateValues) {
const existingValue = dateValues.get(id);
dateValues.set(id, {
revenue: (existingValue?.revenue || 0) + point.value,
orders: (existingValue?.orders || 0) + (point.orders || 0)
});
}
});
const sellers: SellerTimeSeriesSeller[] = [...sellersById.values()]
.map(seller => ({
...seller,
total: getSellerMetricValue(sellerMetric, seller.revenue, seller.orders),
seriesKey: ''
}))
.filter(seller => seller.total > 0)
.sort((a, b) => b.total - a.total)
.slice(0, 8)
.map((seller, index) => ({
...seller,
seriesKey: `seller_${index}`
}));
const chartData: SellerTimeSeriesChartData[] = [...valuesByDate.entries()]
.sort(([dateA], [dateB]) => dateA.localeCompare(dateB))
.map(([date, values]) => {
const row: SellerTimeSeriesChartData = { date };
sellers.forEach(seller => {
const value = values.get(seller.id);
row[seller.seriesKey] = value ? getSellerMetricValue(sellerMetric, value.revenue, value.orders) : 0;
});
return row;
});
return { sellers, chartData, isFallback: false };
}, [dateRange.end, dateRange.start, ordersBySeller, revenueBySeller, sellerColorMap, sellerMetric, sellerRevenueByDate]);
const focusedSeller = focusedSellerId ? sellerTimeSeries.sellers.find(seller => seller.id === focusedSellerId) : null;
const effectiveFocusedSellerId = focusedSeller?.id || null;
const handleManualRefresh = () => {
void loadDashboardMetrics(dateRange, { force: true });
};
const shouldShowSkeleton = isMetricsLoading && !serverMetrics;
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-dark-text">Visão Geral</h1>
<p className="text-dark-muted font-medium">Resumo de vendas e performance dos produtos.</p>
</div>
<DateRangePicker
dateRange={dateRange}
onChange={setDateRange}
refreshInterval={refreshInterval}
setRefreshInterval={setRefreshInterval}
onManualRefresh={handleManualRefresh}
/>
</div>
{shouldShowSkeleton ? (
<DashboardSkeleton />
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm">
<div className="flex justify-between items-start">
<div>
<p className="text-dark-muted text-sm font-medium mb-1">Receita Total</p>
<h3 className="text-3xl font-bold text-dark-text">{formatCurrency(totalRevenue)}</h3>
</div>
<div className="rounded-xl border p-3" style={kpiIconStyle('#52DFA0')}>
<DollarSign className="w-6 h-6" />
</div>
</div>
</div>
<div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm">
<div className="flex justify-between items-start">
<div>
<p className="text-dark-muted text-sm font-medium mb-1">Total de Produtos Vendidos</p>
<h3 className="text-3xl font-bold text-dark-text">{totalOrders}</h3>
</div>
<div className="rounded-xl border p-3" style={kpiIconStyle('#82B8FF')}>
<ShoppingCart className="w-6 h-6" />
</div>
</div>
</div>
<div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm">
<div className="flex justify-between items-start">
<div>
<p className="text-dark-muted text-sm font-medium mb-1">Ticket Médio (Por Item)</p>
<h3 className="text-3xl font-bold text-dark-text">{formatCurrency(averageOrderValue)}</h3>
</div>
<div className="rounded-xl border p-3" style={kpiIconStyle('#B992FF')}>
<TrendingUp className="w-6 h-6" />
</div>
</div>
</div>
</div>
<div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm flex flex-col">
<div className="mb-6 flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div>
<h3 className="text-lg font-bold text-dark-text">{sellerMetricConfig.title}</h3>
<p className="mt-1 text-sm font-medium text-dark-muted">
{focusedSeller
? `Foco em ${focusedSeller.name}`
: sellerTimeSeries.isFallback
? sellerMetricConfig.fallbackDescription
: sellerMetricConfig.trendDescription}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="inline-flex rounded-xl border border-dark-border bg-dark-input p-1">
{sellerMetricOptions.map(option => (
<button
key={option.key}
type="button"
onClick={() => setSellerMetric(option.key)}
className={`h-8 cursor-pointer rounded-lg px-3 text-xs font-bold transition-colors ${
sellerMetric === option.key
? 'bg-dark-card text-dark-text shadow-sm'
: 'text-dark-muted hover:text-dark-text'
}`}
>
{option.label}
</button>
))}
</div>
{focusedSeller && (
<button
type="button"
onClick={() => setFocusedSellerId(null)}
className="h-9 rounded-lg border border-dark-border bg-dark-input px-3 text-xs font-bold text-dark-muted transition-colors hover:text-dark-text"
>
Ver todos
</button>
)}
</div>
</div>
{sellerTimeSeries.sellers.length ? (
<div>
<div className="h-96 min-w-0">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={sellerTimeSeries.chartData} margin={{ top: 14, right: 24, left: 4, bottom: 12 }}>
<defs>
{sellerTimeSeries.sellers.map(seller => (
<linearGradient key={`seller-gradient-${seller.seriesKey}`} id={`seller-gradient-${seller.seriesKey}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={seller.fill} stopOpacity={effectiveFocusedSellerId && effectiveFocusedSellerId !== seller.id ? 0.08 : 0.42} />
<stop offset="95%" stopColor={seller.fill} stopOpacity={effectiveFocusedSellerId && effectiveFocusedSellerId !== seller.id ? 0.01 : 0.05} />
</linearGradient>
))}
</defs>
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID_COLOR} vertical={false} />
<XAxis
dataKey="date"
stroke={CHART_AXIS_COLOR}
fontSize={11}
tickLine={false}
axisLine={false}
minTickGap={18}
tickFormatter={formatDateTick}
/>
<YAxis
stroke={CHART_AXIS_COLOR}
fontSize={11}
tickLine={false}
axisLine={false}
tickFormatter={(value) => sellerMetricConfig.formatTick(Number(value))}
width={54}
/>
<Tooltip
content={<SellerMetricTooltip focusedSellerId={effectiveFocusedSellerId} sellers={sellerTimeSeries.sellers} metricConfig={sellerMetricConfig} />}
cursor={{ stroke: CHART_AXIS_COLOR, strokeDasharray: '4 4' }}
/>
{sellerTimeSeries.sellers.map(seller => {
const isFocused = effectiveFocusedSellerId === seller.id;
const isDimmed = Boolean(effectiveFocusedSellerId && !isFocused);
return (
<Area
key={seller.id}
type="monotone"
dataKey={seller.seriesKey}
name={seller.name}
stroke={seller.fill}
strokeWidth={isFocused ? 3 : 2}
strokeOpacity={isDimmed ? 0.2 : 0.95}
fill={`url(#seller-gradient-${seller.seriesKey})`}
fillOpacity={isDimmed ? 0.25 : 1}
dot={sellerTimeSeries.isFallback ? {
r: 3,
strokeWidth: 2,
fill: seller.fill,
stroke: 'var(--color-dark-card)'
} : false}
activeDot={{
r: isFocused ? 5.5 : 4,
strokeWidth: 2,
fill: seller.fill,
stroke: 'var(--color-dark-card)',
onClick: () => setFocusedSellerId(current => current === seller.id ? null : seller.id)
}}
onClick={() => setFocusedSellerId(current => current === seller.id ? null : seller.id)}
style={{ cursor: 'pointer' }}
isAnimationActive
animationBegin={120}
animationDuration={700}
animationEasing="ease-out"
/>
);
})}
</AreaChart>
</ResponsiveContainer>
</div>
<div className="mt-5 flex flex-wrap gap-2 border-t border-dark-border pt-4">
{sellerTimeSeries.sellers.map(seller => {
const isFocused = effectiveFocusedSellerId === seller.id;
const isDimmed = Boolean(effectiveFocusedSellerId && !isFocused);
return (
<button
key={`seller-legend-${seller.id}`}
type="button"
onClick={() => setFocusedSellerId(current => current === seller.id ? null : seller.id)}
className={`flex max-w-full cursor-pointer items-center gap-2 rounded-full border px-3 py-2 text-left transition-colors ${
isFocused
? 'border-dark-text bg-dark-input text-dark-text'
: 'border-dark-border bg-dark-input/40 text-dark-muted hover:bg-dark-input hover:text-dark-text'
}`}
style={{ opacity: isDimmed ? 0.48 : 1 }}
title={seller.name}
>
<span className="h-2.5 w-2.5 shrink-0 rounded-full" style={{ backgroundColor: seller.fill }} />
<span className="max-w-[220px] truncate text-sm font-bold">{seller.name}</span>
<span className="shrink-0 text-xs font-bold opacity-75">{sellerMetricConfig.formatTick(seller.total)}</span>
</button>
);
})}
</div>
</div>
) : (
<div className="flex h-80 items-center justify-center">
<p className="text-sm font-semibold text-dark-muted">{sellerMetricConfig.emptyText}</p>
</div>
)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm flex flex-col">
<h3 className="text-lg font-bold mb-6 text-dark-text">Produtos Mais Vendidos</h3>
<div className="h-80 w-full flex items-center justify-center">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={salesByProduct} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke={CHART_GRID_COLOR} vertical={false} />
<XAxis
dataKey="name" stroke={CHART_AXIS_COLOR} fontSize={10} tickLine={false} axisLine={false}
tick={false}
/>
<YAxis stroke={CHART_AXIS_COLOR} fontSize={12} tickLine={false} axisLine={false} />
<Tooltip content={<CustomTooltip />} cursor={{ fill: CHART_CURSOR_COLOR }} />
<Bar dataKey="value" radius={[4, 4, 0, 0]} onClick={(data) => { if(data?.payload?.id) navigate(`/products/${data.payload.id}`) }} style={{ cursor: 'pointer' }}>
{salesByProduct.map((entry) => (
<Cell
key={`cell-${entry.name}`}
fill={entry.fill}
fillOpacity={BAR_FILL_OPACITY}
stroke={entry.fill}
strokeOpacity={BAR_STROKE_OPACITY}
strokeWidth={1.25}
style={{ ...getBarGlowStyle(entry.fill), cursor: 'pointer' }}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<div className="mt-4 grid grid-cols-2 gap-2">
{salesByProduct.map((entry) => (
<div key={`bar-legend-${entry.name}`} className="flex items-center text-[10px] cursor-pointer hover:opacity-80 transition-opacity" onClick={() => navigate(`/products/${entry.id}`)}>
<span className="w-2.5 h-2.5 rounded-full mr-2 shrink-0" style={{ backgroundColor: entry.fill }}></span>
<span className="text-dark-muted truncate font-semibold" title={entry.name}>{entry.name}</span>
</div>
))}
</div>
</div>
<div className="bg-dark-card p-6 rounded-2xl border border-dark-border shadow-sm flex flex-col">
<h3 className="text-lg font-bold mb-6 text-dark-text">Receita por Produto</h3>
<div className="h-80 w-full flex items-center justify-center">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie data={revenueByProduct} cx="50%" cy="50%" innerRadius={80} outerRadius={110} paddingAngle={5} dataKey="value" onClick={(data) => { if(data?.payload?.id) navigate(`/products/${data.payload.id}`) }} style={{ cursor: 'pointer' }}>
{revenueByProduct.map((entry) => (
<Cell
key={`cell-${entry.name}`}
fill={entry.fill}
fillOpacity={PIE_FILL_OPACITY}
stroke={entry.fill}
strokeOpacity={BAR_STROKE_OPACITY}
strokeWidth={1.5}
style={getPieGlowStyle(entry.fill)}
/>
))}
</Pie>
<Tooltip content={<CustomTooltip isCurrency={true} />} cursor={{ fill: CHART_CURSOR_COLOR }} />
</PieChart>
</ResponsiveContainer>
</div>
<div className="mt-4 grid grid-cols-2 gap-2">
{revenueByProduct.map((entry) => (
<div key={`pie-legend-${entry.name}`} className="flex items-center text-[10px] cursor-pointer hover:opacity-80 transition-opacity" onClick={() => navigate(`/products/${entry.id}`)}>
<span className="w-2.5 h-2.5 rounded-full mr-2 shrink-0" style={{ backgroundColor: entry.fill }}></span>
<span className="text-dark-muted truncate font-semibold">{entry.name}</span>
</div>
))}
</div>
</div>
</div>
</>
)}
</div>
);
};
export default Dashboard;