Remove dashboard operational copilot
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
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 { AlertTriangle, Bot, ClipboardList, DollarSign, Package, ShoppingCart, TrendingUp } from 'lucide-react';
|
||||
import { DollarSign, ShoppingCart, TrendingUp } from 'lucide-react';
|
||||
import DateRangePicker from '../components/DateRangePicker';
|
||||
import RefreshStatus from '../components/RefreshStatus';
|
||||
import type { DashboardAnalytics, OrderData, DateRange } from '../types';
|
||||
@@ -161,15 +161,6 @@ const kpiIconStyle = (color: string) => ({
|
||||
color
|
||||
});
|
||||
|
||||
type OperationalInsight = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: typeof Bot;
|
||||
accentClassName: string;
|
||||
actionLabel: string;
|
||||
actionTo: string;
|
||||
};
|
||||
|
||||
const DashboardSkeleton = () => (
|
||||
<div className="space-y-6" aria-label="Carregando dashboard">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
@@ -511,60 +502,6 @@ const Dashboard = () => {
|
||||
|
||||
const focusedSeller = focusedSellerId ? sellerTimeSeries.sellers.find(seller => seller.id === focusedSellerId) : null;
|
||||
const effectiveFocusedSellerId = focusedSeller?.id || null;
|
||||
const operationalInsights = useMemo<OperationalInsight[]>(() => {
|
||||
const insights: OperationalInsight[] = [];
|
||||
const topProduct = salesByProduct[0];
|
||||
const topRevenueProduct = revenueByProduct[0];
|
||||
const topSeller = chartRevenueBySeller[0];
|
||||
const soldUnits = salesByProduct.reduce((total, product) => total + product.value, 0);
|
||||
const topProductShare = topProduct && soldUnits ? topProduct.value / soldUnits : 0;
|
||||
|
||||
if (topProduct && topProductShare >= 0.35) {
|
||||
insights.push({
|
||||
title: `${topProduct.name} concentra ${Math.round(topProductShare * 100)}% das unidades vendidas`,
|
||||
description: 'Priorize a conferência de estoque e corte para evitar ruptura no item mais sensível do período.',
|
||||
icon: AlertTriangle,
|
||||
accentClassName: 'border-amber-400/30 bg-amber-400/10 text-amber-300',
|
||||
actionLabel: 'Abrir corte',
|
||||
actionTo: '/cutting'
|
||||
});
|
||||
}
|
||||
|
||||
if (topRevenueProduct && topRevenueProduct.value > 0) {
|
||||
insights.push({
|
||||
title: `${topRevenueProduct.name} lidera a receita do período`,
|
||||
description: `${formatCurrency(topRevenueProduct.value)} em receita. Compare disponibilidade e necessidade de compra antes de vender sem cobertura.`,
|
||||
icon: Package,
|
||||
accentClassName: 'border-sky-400/30 bg-sky-400/10 text-sky-300',
|
||||
actionLabel: 'Ver suprimentos',
|
||||
actionTo: '/supplies'
|
||||
});
|
||||
}
|
||||
|
||||
if (topSeller && topSeller.value > 0) {
|
||||
insights.push({
|
||||
title: `${topSeller.name} puxa a maior receita por vendedor`,
|
||||
description: `${formatCurrency(topSeller.value)} no período selecionado. Use o gráfico abaixo para comparar ritmo e concentração.`,
|
||||
icon: TrendingUp,
|
||||
accentClassName: 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300',
|
||||
actionLabel: 'Analisar vendas',
|
||||
actionTo: '/graph'
|
||||
});
|
||||
}
|
||||
|
||||
if (totalOrders > 0 && averageOrderValue > 0) {
|
||||
insights.push({
|
||||
title: `Ticket médio por item em ${formatCurrency(averageOrderValue)}`,
|
||||
description: `${new Intl.NumberFormat('pt-BR').format(totalOrders)} unidades vendidas no período. Revise mix de produtos e abastecimento dos campeões.`,
|
||||
icon: ClipboardList,
|
||||
accentClassName: 'border-brand-primary/30 bg-brand-primary/10 text-brand-primary',
|
||||
actionLabel: 'Ver produtos',
|
||||
actionTo: '/products'
|
||||
});
|
||||
}
|
||||
|
||||
return insights.slice(0, 4);
|
||||
}, [averageOrderValue, chartRevenueBySeller, revenueByProduct, salesByProduct, totalOrders]);
|
||||
|
||||
const handleManualRefresh = () => {
|
||||
void loadDashboardMetrics(dateRange, { force: true });
|
||||
@@ -594,63 +531,6 @@ const Dashboard = () => {
|
||||
<DashboardSkeleton />
|
||||
) : (
|
||||
<div className={isRefreshing ? 'refreshing-content space-y-6' : 'space-y-6'} aria-busy={isRefreshing}>
|
||||
|
||||
<div className="overflow-hidden rounded-2xl border border-brand-primary/20 bg-dark-card shadow-sm">
|
||||
<div className="border-b border-dark-border p-5">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="rounded-xl border border-brand-primary/25 bg-brand-primary/10 p-3 text-brand-primary">
|
||||
<Bot className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-dark-text">Copiloto operacional</h2>
|
||||
<p className="mt-1 max-w-3xl text-sm font-semibold text-dark-muted">
|
||||
Leituras automáticas do período selecionado para orientar vendas, estoque e produção.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleManualRefresh}
|
||||
className="inline-flex h-10 items-center justify-center rounded-xl border border-dark-border bg-dark-input px-4 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary cursor-pointer"
|
||||
>
|
||||
Recalcular
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{operationalInsights.length ? (
|
||||
<div className="grid grid-cols-1 gap-3 p-5 xl:grid-cols-2">
|
||||
{operationalInsights.map((insight) => (
|
||||
<div key={insight.title} className="rounded-xl border border-dark-border bg-dark-input/35 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`rounded-lg border p-2 ${insight.accentClassName}`}>
|
||||
<insight.icon className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-sm font-bold text-dark-text">{insight.title}</h3>
|
||||
<p className="mt-1 text-sm font-semibold text-dark-muted">{insight.description}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(insight.actionTo)}
|
||||
className="mt-3 inline-flex h-8 items-center rounded-lg border border-dark-border bg-dark-card px-3 text-xs font-bold text-dark-text transition-colors hover:border-brand-primary cursor-pointer"
|
||||
>
|
||||
{insight.actionLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-5">
|
||||
<div className="rounded-xl border border-dark-border bg-dark-input/35 p-4 text-sm font-semibold text-dark-muted">
|
||||
Sem sinais operacionais suficientes no período selecionado.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user