530 lines
26 KiB
JavaScript
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>
|
|
);
|
|
}
|