testes/src_2/features/prafrot/views/StatisticsView.jsx

530 lines
26 KiB
JavaScript

import React, { useState } from 'react';
import { usePrafrotStatistics } from '../hooks/usePrafrotStatistics';
import { useVehicles } from '../hooks/useVehicles';
import { StatsGrid } from '@/components/shared/StatsGrid';
import {
Truck, Wrench, CheckCircle2, AlertTriangle,
BarChart3, PieChart as PieChartIcon, LineChart as LineChartIcon,
RefreshCw, Download, Calendar, Layers, MapPin, Gauge
} from 'lucide-react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import {
ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip,
PieChart, Pie, Cell, LineChart, Line, Legend, CartesianGrid
} from 'recharts';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter
} from "@/components/ui/dialog";
import ExcelTable from '../components/ExcelTable';
// Cores premium para os gráficos
const COLORS = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#ec4899', '#14b8a6'];
const Skeleton = ({ className }) => (
<div className={`animate-pulse bg-slate-200 dark:bg-slate-800 rounded-xl ${className}`} />
);
// Componente de Tooltip Customizado e Premium
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] p-4 rounded-2xl shadow-2xl backdrop-blur-md bg-opacity-95 dark:bg-opacity-95 ring-1 ring-black/5">
<p className="text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-slate-500 mb-2">{label}</p>
<div className="space-y-1.5">
{payload.map((item, index) => (
<div key={index} className="flex items-center gap-3">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: item.color || item.fill }} />
<div className="flex flex-col">
<span className="text-xs font-bold text-slate-700 dark:text-slate-200">{item.name}</span>
<span className="text-sm font-black text-slate-900 dark:text-white">{item.value.toLocaleString()}</span>
</div>
</div>
))}
</div>
</div>
);
}
return null;
};
export default function StatisticsView() {
const { data, loading: statsLoading, refresh } = usePrafrotStatistics();
const { deleteVehicle } = useVehicles();
const [selectedStatus, setSelectedStatus] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
if (statsLoading) {
return (
<div className="space-y-8 animate-in fade-in duration-500">
<div className="flex justify-between items-center">
<div className="space-y-2">
<Skeleton className="h-10 w-64" />
<Skeleton className="h-4 w-96" />
</div>
<div className="flex gap-2">
<Skeleton className="h-10 w-32" />
<Skeleton className="h-10 w-32" />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => <Skeleton key={i} className="h-32 w-full" />)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Skeleton className="h-[400px] w-full" />
<Skeleton className="h-[400px] w-full" />
</div>
</div>
);
}
// Prepara dados para os KPIs - Baseado nos dados reais fornecidos
const totalVeiculos = data.totalPlacas;
// Em manutenção: Somar status "Aberto" de manutencao_frota ou status relacionados
const emManutencao = data.placasPorManutencao
.filter(m => m.manutencao === "Aberto" || m.manutencao === "ABERTO")
.reduce((acc, curr) => acc + curr.total, 0);
// Disponíveis: Filtrar pela data mais recente e status DISPONÍVEL ou ROTA
const disponiveis = data.placasPorDisponibilidade
.filter(d => d.status_disponibilidade === 'DISPONÍVEL' || d.status_disponibilidade === 'ROTA' || d.status_disponibilidade === 'DISP')
.reduce((acc, curr) => acc + curr.total, 0);
const sinistros = data.placasPorSinistro.reduce((acc, curr) => acc + curr.total, 0);
const stats = [
{
label: 'Frota Total',
value: totalVeiculos.toLocaleString(),
icon: <Truck />,
color: 'bg-emerald-500/10 text-emerald-600'
},
{
label: 'Em Manutenção (Aberta)',
value: emManutencao.toLocaleString(),
icon: <Wrench />,
color: 'bg-orange-500/10 text-orange-600'
},
{
label: 'Em Operação/Disp.',
value: disponiveis.toLocaleString(),
icon: <CheckCircle2 />,
color: 'bg-blue-500/10 text-blue-600'
},
/* {
label: 'Sinistros/Vendas/Dev.',
value: sinistros.toLocaleString(),
icon: <AlertTriangle />,
color: 'bg-red-500/10 text-red-600'
} */
];
// Nomes dos meses para o gráfico quantitativo
const monthNames = ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"];
const formattedQuantitativo = data.quantitativoManutencao.map(item => ({
...item,
name: monthNames[item.mes - 1]
}));
const chartStatusData = data.placasPorStatus.map(item => ({
...item,
status: (item.status || item.status_frota || 'NÃO INFORMADO').trim()
}));
return (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 pb-12">
{/* Header com Design Premium */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="space-y-1">
<h1 className="text-4xl font-black text-slate-900 dark:text-white tracking-tight">
Dashboard <span className="text-emerald-500">Estatístico</span>
</h1>
<p className="text-slate-500 dark:text-slate-400 font-medium text-lg">
Monitoramento de BI e KPIs em tempo real da operação de frota.
</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={refresh}
className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-white dark:bg-[#1c1c1c] border border-slate-200 dark:border-[#2a2a2a] text-slate-600 dark:text-slate-300 font-bold text-sm hover:shadow-lg hover:-translate-y-0.5 transition-all"
>
<RefreshCw size={18} className={statsLoading ? "animate-spin" : ""} />
Sincronizar Dados
</button>
{/* <button className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-emerald-600 text-white font-bold text-sm hover:bg-emerald-700 hover:shadow-xl hover:shadow-emerald-500/20 hover:-translate-y-0.5 transition-all">
<Download size={18} />
Exportar BI
</button> */}
</div>
</div>
{/* KPI Section */}
<div className="relative">
<div className="absolute -inset-1 bg-gradient-to-r from-emerald-500/10 to-blue-500/10 blur-2xl opacity-50 -z-10" />
<StatsGrid stats={stats} />
</div>
{/* Status Distribution - Full Width */}
<Card className="border-none shadow-sm hover:shadow-md transition-all overflow-hidden bg-white dark:bg-[#1c1c1c]">
<CardHeader className="flex flex-row items-center justify-between border-b dark:border-[#2a2a2a] mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/10 rounded-xl text-blue-600">
<PieChartIcon size={20} />
</div>
<CardTitle className="text-xl font-bold">Distribuição por Status</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col lg:flex-row items-center gap-8">
{/* Graph Left */}
<div className="h-[300px] w-full lg:w-1/2 min-h-[300px] relative">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={chartStatusData}
cx="50%"
cy="50%"
innerRadius={80}
outerRadius={110}
paddingAngle={5}
dataKey="total"
nameKey="status"
stroke="none"
>
{chartStatusData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
</PieChart>
</ResponsiveContainer>
{/* Center Text Overlay */}
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
<span className="text-3xl font-black text-slate-800 dark:text-white">{totalVeiculos.toLocaleString()}</span>
<span className="text-xs uppercase font-bold text-slate-400">Veículos</span>
</div>
</div>
{/* Cards Right - Row Visualization */}
<div className="w-full lg:w-1/2 flex flex-col gap-3 h-[300px] overflow-y-auto custom-scrollbar pr-2">
{chartStatusData.map((entry, index) => (
<div
key={index}
onClick={() => {
setSelectedStatus(entry);
setIsModalOpen(true);
}}
className="flex items-center justify-between p-4 rounded-xl bg-slate-50 dark:bg-[#252525] border border-slate-100 dark:border-[#333] hover:border-blue-500/30 transition-all cursor-pointer group hover:scale-[1.01] hover:shadow-md"
>
<div className="flex items-center gap-4">
<div className="w-3 h-3 rounded-full shrink-0 shadow-sm" style={{ backgroundColor: COLORS[index % COLORS.length] }} />
<span className="text-sm font-bold text-slate-600 dark:text-slate-300 uppercase tracking-tight">{entry.status}</span>
</div>
<div className="flex items-center gap-4">
<div className="h-2 w-24 bg-slate-200 dark:bg-[#333] rounded-full overflow-hidden hidden sm:block">
<div className="h-full rounded-full" style={{ width: `${(entry.total / totalVeiculos) * 100}%`, backgroundColor: COLORS[index % COLORS.length] }} />
</div>
<span className="text-lg font-black text-slate-900 dark:text-white min-w-[40px] text-right">{entry.total}</span>
</div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
{/* Status Records Modal */}
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="max-w-7xl w-[95vw] h-[90vh] bg-white dark:bg-[#0f0f0f] border-slate-200 dark:border-[#2a2a2a] p-0 overflow-hidden shadow-2xl flex flex-col !outline-none">
<DialogHeader className="p-6 border-b border-slate-200 dark:border-[#2a2a2a] shrink-0 bg-slate-50/50 dark:bg-[#0f0f0f]">
<div className="flex items-center gap-4">
<div className="p-4 bg-emerald-500/10 rounded-2xl text-emerald-600 shadow-inner">
<Truck size={28} />
</div>
<div>
<DialogTitle className="text-2xl font-black text-slate-800 dark:text-white uppercase tracking-tight">
Veículos: {selectedStatus?.status}
</DialogTitle>
<DialogDescription className="text-slate-500 dark:text-slate-400 font-medium text-sm">
Listagem completa dos {selectedStatus?.total} veículos com status <span className="text-emerald-500 font-bold">"{selectedStatus?.status}"</span>.
</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="flex-1 overflow-hidden p-6 bg-white dark:bg-[#1c1c1c]">
<div className="h-full w-full rounded-2xl border border-slate-200 dark:border-[#2a2a2a] overflow-hidden shadow-sm">
<ExcelTable
data={selectedStatus?.registros ? JSON.parse(selectedStatus.registros).filter(r => r !== null) : []}
columns={[
{ header: 'Placa', field: 'placa', width: '100px', className: 'font-mono font-bold text-emerald-600 dark:text-emerald-500' },
{ header: 'Modelo', field: 'modelo', width: '150px' },
{ header: 'Unidade', field: 'base', width: '120px' },
{ header: 'Motorista', field: 'motorista', width: '180px' },
{ header: 'Status', field: 'status', width: '140px' },
{ header: 'Manutenção', field: 'manutencao', width: '120px' },
{ header: 'Atuação', field: 'atuacao', width: '140px' },
{ header: 'Proprietário', field: 'proprietario', width: '140px' },
{ header: 'VecFleet', field: 'vecfleet', width: '140px' },
{ header: 'OBS', field: 'obs', width: '250px' },
]}
filterDefs={[
{ field: 'placa', label: 'Placa', type: 'text' },
{ field: 'motorista', label: 'Motorista', type: 'text' },
{ field: 'base', label: 'Unidade', type: 'select' }
]}
onDelete={(item) => deleteVehicle(item.idveiculo_frota)}
/>
</div>
</div>
<DialogFooter className="p-4 border-t border-slate-200 dark:border-[#2a2a2a] bg-slate-50/50 dark:bg-[#1c1c1c]">
<button
onClick={() => setIsModalOpen(false)}
className="px-6 py-2 rounded-xl bg-slate-100 dark:bg-[#2a2a2a] hover:bg-slate-200 dark:hover:bg-[#333] text-slate-700 dark:text-slate-200 font-bold text-sm transition-all border border-slate-200 dark:border-[#333]"
>
Fechar Listagem
</button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Main Charts - Line Chart Full Width now */}
<div className="grid grid-cols-1 gap-6">
{/* Fluxo de Manutenção */}
<Card className="border-none shadow-sm hover:shadow-md transition-all overflow-hidden bg-white dark:bg-[#1c1c1c]">
<CardHeader className="flex flex-row items-center justify-between border-b dark:border-[#2a2a2a] mb-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-emerald-500/10 rounded-xl text-emerald-600">
<LineChartIcon size={20} />
</div>
<CardTitle className="text-xl font-bold">Fluxo de Manutenção Mensal</CardTitle>
</div>
</CardHeader>
<CardContent className="h-[400px]">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={formattedQuantitativo} margin={{ top: 20, right: 30, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="colorEntrada" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#f59e0b" stopOpacity={0.1}/>
<stop offset="95%" stopColor="#f59e0b" stopOpacity={0}/>
</linearGradient>
<linearGradient id="colorSaida" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.1}/>
<stop offset="95%" stopColor="#10b981" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
<XAxis
dataKey="name"
stroke="#94a3b8"
fontSize={12}
tickLine={false}
axisLine={false}
dy={10}
/>
<YAxis
stroke="#94a3b8"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<Tooltip content={<CustomTooltip />} />
<Legend iconType="circle" wrapperStyle={{ paddingTop: '20px' }} />
<Line
type="monotone"
dataKey="total_entrada"
name="Veículos Entrando"
stroke="#f59e0b"
strokeWidth={4}
dot={{ r: 4, fill: '#f59e0b', strokeWidth: 2, stroke: '#fff' }}
activeDot={{ r: 6, strokeWidth: 0 }}
/>
<Line
type="monotone"
dataKey="total_saida"
name="Veículos Liberados"
stroke="#10b981"
strokeWidth={4}
dot={{ r: 4, fill: '#10b981', strokeWidth: 2, stroke: '#fff' }}
activeDot={{ r: 6, strokeWidth: 0 }}
/>
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* Row 3 - Regional (Full Width) */}
<div className="grid grid-cols-1 gap-6">
{/* Veículos por Base */}
<Card className="border-none shadow-sm hover:shadow-md transition-all bg-white dark:bg-[#1c1c1c]">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400">Regional</span>
<MapPin size={14} className="text-emerald-500" />
</div>
<CardTitle className="text-lg font-bold">Veículos por Base</CardTitle>
</CardHeader>
<CardContent className="h-[350px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data.placasPorBase} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
<XAxis dataKey="base" stroke="#94a3b8" fontSize={11} axisLine={false} tickLine={false} />
<YAxis stroke="#94a3b8" fontSize={11} axisLine={false} tickLine={false} />
<Tooltip cursor={{ fill: 'transparent' }} content={<CustomTooltip />} />
<Bar dataKey="total" fill="#10b981" radius={[6, 6, 0, 0]} barSize={40} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* Row 4 - Chronology and Category */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Ano de Fabricação */}
<Card className="border-none shadow-sm hover:shadow-md transition-all bg-white dark:bg-[#1c1c1c]">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400">Cronologia</span>
<Calendar size={14} className="text-blue-500" />
</div>
<CardTitle className="text-lg font-bold">Idade da Frota (Ano)</CardTitle>
</CardHeader>
<CardContent className="h-[280px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data.placasPorAno}>
<XAxis dataKey="ano_fabricacao" stroke="#94a3b8" fontSize={9} axisLine={false} tickLine={false} />
<YAxis hide />
<Tooltip cursor={{ fill: 'transparent' }} content={<CustomTooltip />} />
<Bar dataKey="total" name="Total de Veículos" fill="#3b82f6" radius={[6, 6, 0, 0]} barSize={32} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Categorias */}
<Card className="border-none shadow-sm hover:shadow-md transition-all bg-white dark:bg-[#1c1c1c]">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400">Classificação</span>
<Layers size={14} className="text-purple-500" />
</div>
<CardTitle className="text-lg font-bold">Frota por Categoria</CardTitle>
</CardHeader>
<CardContent className="h-[280px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data.placasPorCategoria} layout="vertical" margin={{ left: 0, right: 40, top: 10, bottom: 10 }}>
<defs>
<linearGradient id="barGradient" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stopColor="#8b5cf6" />
<stop offset="100%" stopColor="#a78bfa" />
</linearGradient>
</defs>
<XAxis type="number" hide />
<YAxis
dataKey="categoria"
type="category"
fontSize={10}
stroke="#94a3b8"
axisLine={false}
tickLine={false}
width={90}
tick={{ fontWeight: '600' }}
/>
<Tooltip cursor={{ fill: 'rgba(139, 92, 246, 0.05)' }} content={<CustomTooltip />} />
<Bar
dataKey="total"
name="Total de Veículos"
fill="url(#barGradient)"
radius={[0, 10, 10, 0]}
barSize={18}
label={{
position: 'right',
fill: '#94a3b8',
fontSize: 10,
fontWeight: 'bold',
formatter: (val) => val.toLocaleString()
}}
/>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
{/* Row 4 - Details and Units */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="border-none shadow-sm bg-white dark:bg-[#1c1c1c]">
<CardHeader className="flex flex-row items-center justify-between overflow-hidden">
<div>
<CardTitle className="text-xl font-bold">Disponibilidade Detalhada</CardTitle>
<p className="text-sm text-slate-500">Breakdown por status de operação.</p>
</div>
<div className="p-3 bg-emerald-500/10 rounded-2xl text-emerald-600">
<Gauge size={24} strokeWidth={2.5} />
</div>
</CardHeader>
<CardContent className="pt-2">
<div className="space-y-5">
{data.placasPorDisponibilidade.slice(0, 10).map((item, i) => (
<div key={i} className="group cursor-default">
<div className="flex items-center justify-between mb-2">
<div className="flex flex-col min-w-0">
<span className="text-sm font-black text-slate-800 dark:text-slate-100 group-hover:text-emerald-500 transition-colors uppercase tracking-tight truncate">{item.status_disponibilidade || 'NÃO INFORMADO'}</span>
<span className="text-[10px] font-bold text-slate-400 tracking-widest uppercase truncate">{item.disponibilidade}</span>
</div>
<div className="flex items-center gap-3 shrink-0">
<span className="text-2xl font-black text-slate-900 dark:text-white">{item.total}</span>
<span className="text-xs font-bold px-2 py-0.5 rounded bg-slate-100 dark:bg-[#2a2a2a] text-slate-500">
{((item.total / (totalVeiculos || 1)) * 100).toFixed(1)}%
</span>
</div>
</div>
<div className="w-full h-2 bg-slate-100 dark:bg-[#2a2a2a] rounded-full overflow-hidden">
<div
className="h-full bg-emerald-500 rounded-full transition-all duration-1000 ease-out"
style={{ width: `${(item.total / (totalVeiculos || 1)) * 100}%` }}
/>
</div>
</div>
))}
</div>
</CardContent>
</Card>
<Card className="border-none shadow-sm bg-white dark:bg-[#1c1c1c]">
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle className="text-xl font-bold">Monitoramento por Unidade</CardTitle>
<p className="text-sm text-slate-500">Volume de monitoramento ativo por central.</p>
</div>
<div className="p-3 bg-blue-500/10 rounded-2xl text-blue-600">
<BarChart3 size={24} strokeWidth={2.5} />
</div>
</CardHeader>
<CardContent className="pt-2">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 max-h-[500px] overflow-y-auto custom-scrollbar pr-2">
{data.placasPorUnidade.map((item, i) => (
<div key={i} className="p-4 rounded-2xl bg-slate-50 dark:bg-[#252525] border border-slate-100 dark:border-[#2a2a2a] flex justify-between items-center hover:border-blue-500/50 hover:shadow-md transition-all">
<div className="flex flex-col min-w-0">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Unidade</span>
<span className="text-sm font-bold text-slate-700 dark:text-slate-200 truncate">{item.unidade}</span>
</div>
<span className="text-2xl font-black text-blue-500 shrink-0">{item.total}</span>
</div>
))}
{data.placasPorUnidade.length === 0 && (
<div className="col-span-2 py-12 flex flex-col items-center justify-center text-slate-400">
<Layers size={48} strokeWidth={1} className="mb-2 opacity-20" />
<p className="text-sm font-medium">Nenhum dado de unidade encontrado</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
</div>
);
}