xpeditis2.0/apps/frontend/app/dashboard/page.tsx
David a1e255e816
Some checks failed
CI/CD Pipeline / Discord Notification (Failure) (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Deployment Summary (push) Blocked by required conditions
CI/CD Pipeline / Deploy to Portainer (push) Blocked by required conditions
CI/CD Pipeline / Discord Notification (Success) (push) Blocked by required conditions
CI/CD Pipeline / Backend - Build, Test & Push (push) Failing after 1m20s
CI/CD Pipeline / Frontend - Build, Test & Push (push) Has been cancelled
fix v1.0.0
2025-12-23 11:49:57 +01:00

422 lines
17 KiB
TypeScript

/**
* Dashboard Home Page - Clean & Colorful with Charts
* Professional design with data visualization
*/
'use client';
import { useQuery } from '@tanstack/react-query';
import { dashboardApi } from '@/lib/api';
import Link from 'next/link';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Package,
PackageCheck,
PackageX,
Clock,
Weight,
TrendingUp,
Plus,
ArrowRight,
} from 'lucide-react';
import {
PieChart,
Pie,
Cell,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
LineChart,
Line,
Legend,
} from 'recharts';
export default function DashboardPage() {
// Fetch CSV booking KPIs
const { data: csvKpis, isLoading: csvKpisLoading } = useQuery({
queryKey: ['dashboard', 'csv-booking-kpis'],
queryFn: () => dashboardApi.getCsvBookingKPIs(),
});
// Fetch top carriers
const { data: topCarriers, isLoading: carriersLoading } = useQuery({
queryKey: ['dashboard', 'top-carriers'],
queryFn: () => dashboardApi.getTopCarriers(),
});
// Prepare data for charts
const statusDistribution = csvKpis
? [
{ name: 'Acceptés', value: csvKpis.totalAccepted, color: '#10b981' },
{ name: 'Refusés', value: csvKpis.totalRejected, color: '#ef4444' },
{ name: 'En Attente', value: csvKpis.totalPending, color: '#f59e0b' },
]
: [];
const carrierWeightData = topCarriers
? topCarriers.slice(0, 5).map(c => ({
name: c.carrierName.length > 15 ? c.carrierName.substring(0, 15) + '...' : c.carrierName,
poids: Math.round(c.totalWeightKG),
}))
: [];
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-6 py-8 space-y-6">
{/* Header - Compact */}
<div className="flex items-center justify-between pb-4 border-b border-gray-200">
<div>
<h1 className="text-3xl font-semibold text-gray-900">Tableau de Bord</h1>
<p className="text-gray-600 mt-1 text-sm">
Vue d'ensemble de vos bookings et performances
</p>
</div>
<Link href="/dashboard/bookings">
<Button className="bg-blue-600 hover:bg-blue-700 text-white gap-2 shadow-sm">
<Plus className="h-4 w-4" />
Nouveau Booking
</Button>
</Link>
</div>
{/* KPI Cards - Compact with Color */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{/* Bookings Acceptés */}
<Card className="border border-gray-200 shadow-sm hover:shadow-md transition-shadow bg-white">
<CardContent className="p-4">
<div className="flex flex-col items-center text-center">
<div className="h-10 w-10 rounded-lg bg-green-100 flex items-center justify-center mb-2">
<PackageCheck className="h-5 w-5 text-green-600" />
</div>
<p className="text-xs font-medium text-gray-600 mb-1">Acceptés</p>
{csvKpisLoading ? (
<div className="h-8 w-16 bg-gray-100 animate-pulse rounded" />
) : (
<>
<p className="text-2xl font-bold text-gray-900">
{csvKpis?.totalAccepted || 0}
</p>
<p className="text-xs text-gray-500 mt-1">
+{csvKpis?.acceptedThisMonth || 0} ce mois
</p>
</>
)}
</div>
</CardContent>
</Card>
{/* Bookings Refusés */}
<Card className="border border-gray-200 shadow-sm hover:shadow-md transition-shadow bg-white">
<CardContent className="p-4">
<div className="flex flex-col items-center text-center">
<div className="h-10 w-10 rounded-lg bg-red-100 flex items-center justify-center mb-2">
<PackageX className="h-5 w-5 text-red-600" />
</div>
<p className="text-xs font-medium text-gray-600 mb-1">Refusés</p>
{csvKpisLoading ? (
<div className="h-8 w-16 bg-gray-100 animate-pulse rounded" />
) : (
<>
<p className="text-2xl font-bold text-gray-900">
{csvKpis?.totalRejected || 0}
</p>
<p className="text-xs text-gray-500 mt-1">
+{csvKpis?.rejectedThisMonth || 0} ce mois
</p>
</>
)}
</div>
</CardContent>
</Card>
{/* Bookings En Attente */}
<Card className="border border-gray-200 shadow-sm hover:shadow-md transition-shadow bg-white">
<CardContent className="p-4">
<div className="flex flex-col items-center text-center">
<div className="h-10 w-10 rounded-lg bg-amber-100 flex items-center justify-center mb-2">
<Clock className="h-5 w-5 text-amber-600" />
</div>
<p className="text-xs font-medium text-gray-600 mb-1">En Attente</p>
{csvKpisLoading ? (
<div className="h-8 w-16 bg-gray-100 animate-pulse rounded" />
) : (
<>
<p className="text-2xl font-bold text-gray-900">
{csvKpis?.totalPending || 0}
</p>
<p className="text-xs text-gray-500 mt-1">
{csvKpis?.acceptanceRate.toFixed(1)}% acceptés
</p>
</>
)}
</div>
</CardContent>
</Card>
{/* Poids Total */}
<Card className="border border-gray-200 shadow-sm hover:shadow-md transition-shadow bg-white">
<CardContent className="p-4">
<div className="flex flex-col items-center text-center">
<div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center mb-2">
<Weight className="h-5 w-5 text-blue-600" />
</div>
<p className="text-xs font-medium text-gray-600 mb-1">Poids Total</p>
{csvKpisLoading ? (
<div className="h-8 w-16 bg-gray-100 animate-pulse rounded" />
) : (
<>
<p className="text-2xl font-bold text-gray-900">
{(csvKpis?.totalWeightAcceptedKG || 0).toLocaleString()}
</p>
<p className="text-xs text-gray-500 mt-1">
KG • {(csvKpis?.totalVolumeAcceptedCBM || 0).toFixed(1)} CBM
</p>
</>
)}
</div>
</CardContent>
</Card>
</div>
{/* Charts Section */}
<div className="grid gap-4 md:grid-cols-2">
{/* Distribution des Statuts - Pie Chart */}
<Card className="border border-gray-200 shadow-sm bg-white">
<CardHeader className="pb-4 border-b border-gray-100">
<CardTitle className="text-base font-semibold text-gray-900">
Distribution des Bookings
</CardTitle>
<CardDescription className="text-xs text-gray-600">
Répartition par statut
</CardDescription>
</CardHeader>
<CardContent className="pt-4">
{csvKpisLoading ? (
<div className="h-48 bg-gray-50 animate-pulse rounded" />
) : (
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie
data={statusDistribution}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }: any) =>
`${name} ${((percent || 0) * 100).toFixed(0)}%`
}
outerRadius={70}
fill="#8884d8"
dataKey="value"
>
{statusDistribution.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
)}
</CardContent>
</Card>
{/* Poids par Transporteur - Bar Chart */}
<Card className="border border-gray-200 shadow-sm bg-white">
<CardHeader className="pb-4 border-b border-gray-100">
<CardTitle className="text-base font-semibold text-gray-900">
Poids par Transporteur
</CardTitle>
<CardDescription className="text-xs text-gray-600">
Top 5 carriers par poids (KG)
</CardDescription>
</CardHeader>
<CardContent className="pt-4">
{carriersLoading ? (
<div className="h-48 bg-gray-50 animate-pulse rounded" />
) : (
<ResponsiveContainer width="100%" height={200}>
<BarChart data={carrierWeightData}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis
dataKey="name"
tick={{ fontSize: 11 }}
angle={-15}
textAnchor="end"
height={50}
/>
<YAxis tick={{ fontSize: 11 }} />
<Tooltip />
<Bar dataKey="poids" fill="#3b82f6" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
)}
</CardContent>
</Card>
</div>
{/* Performance Overview - Compact */}
<div className="grid gap-4 md:grid-cols-3">
<Card className="border border-gray-200 shadow-sm bg-white">
<CardContent className="p-4">
<div className="flex flex-col items-center text-center">
<div className="h-10 w-10 rounded-lg bg-green-100 flex items-center justify-center mb-2">
<TrendingUp className="h-5 w-5 text-green-600" />
</div>
<p className="text-xs font-medium text-gray-600 mb-1">Taux d'Acceptation</p>
<p className="text-2xl font-bold text-gray-900">
{csvKpisLoading ? '--' : `${csvKpis?.acceptanceRate.toFixed(1)}%`}
</p>
</div>
</CardContent>
</Card>
<Card className="border border-gray-200 shadow-sm bg-white">
<CardContent className="p-4">
<div className="flex flex-col items-center text-center">
<div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center mb-2">
<Package className="h-5 w-5 text-blue-600" />
</div>
<p className="text-xs font-medium text-gray-600 mb-1">Total Bookings</p>
<p className="text-2xl font-bold text-gray-900">
{csvKpisLoading
? '--'
: (csvKpis?.totalAccepted || 0) +
(csvKpis?.totalRejected || 0) +
(csvKpis?.totalPending || 0)}
</p>
</div>
</CardContent>
</Card>
<Card className="border border-gray-200 shadow-sm bg-white">
<CardContent className="p-4">
<div className="flex flex-col items-center text-center">
<div className="h-10 w-10 rounded-lg bg-purple-100 flex items-center justify-center mb-2">
<Weight className="h-5 w-5 text-purple-600" />
</div>
<p className="text-xs font-medium text-gray-600 mb-1">Volume Total</p>
<p className="text-2xl font-bold text-gray-900">
{csvKpisLoading ? '--' : `${(csvKpis?.totalVolumeAcceptedCBM || 0).toFixed(1)}`}
<span className="text-sm font-normal text-gray-500 ml-1">CBM</span>
</p>
</div>
</CardContent>
</Card>
</div>
{/* Top Carriers - Compact Table */}
<Card className="border border-gray-200 shadow-sm bg-white">
<CardHeader className="pb-3 border-b border-gray-100">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base font-semibold text-gray-900">
Top Transporteurs
</CardTitle>
<CardDescription className="text-xs text-gray-600 mt-1">
Classement des meilleures compagnies
</CardDescription>
</div>
<Link href="/dashboard/bookings">
<Button
variant="ghost"
size="sm"
className="gap-2 text-gray-600 hover:text-gray-900 text-xs"
>
Voir tout
<ArrowRight className="h-3 w-3" />
</Button>
</Link>
</div>
</CardHeader>
<CardContent className="p-0">
{carriersLoading ? (
<div className="divide-y divide-gray-100">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="px-4 py-3">
<div className="h-12 bg-gray-50 animate-pulse rounded" />
</div>
))}
</div>
) : topCarriers && topCarriers.length > 0 ? (
<div className="divide-y divide-gray-100">
{topCarriers.slice(0, 5).map((carrier, index) => (
<div
key={carrier.carrierName}
className="px-4 py-3 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1">
<div className="flex items-center justify-center w-6 h-6 rounded-md bg-gray-100 text-gray-700 font-semibold text-xs">
{index + 1}
</div>
<div className="flex-1">
<h3 className="font-semibold text-gray-900 text-sm">
{carrier.carrierName}
</h3>
<div className="flex items-center gap-3 text-xs text-gray-500 mt-0.5">
<span>{carrier.totalBookings} bookings</span>
<span></span>
<span>{carrier.totalWeightKG.toLocaleString()} KG</span>
</div>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<div className="flex items-center gap-1.5 justify-end mb-0.5">
<Badge
variant="secondary"
className="bg-green-50 text-green-700 border-green-200 text-xs px-1.5 py-0"
>
{carrier.acceptedBookings}
</Badge>
{carrier.rejectedBookings > 0 && (
<Badge
variant="secondary"
className="bg-red-50 text-red-700 border-red-200 text-xs px-1.5 py-0"
>
{carrier.rejectedBookings}
</Badge>
)}
</div>
<p className="text-xs text-gray-500">
{carrier.acceptanceRate.toFixed(0)}% ${carrier.avgPriceUSD.toFixed(0)}
</p>
</div>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12 px-4">
<div className="mx-auto mb-3 h-12 w-12 rounded-full bg-gray-100 flex items-center justify-center">
<Package className="h-6 w-6 text-gray-400" />
</div>
<h3 className="text-sm font-semibold text-gray-900 mb-1">
Aucun booking
</h3>
<p className="text-xs text-gray-500 mb-4 max-w-sm mx-auto">
Créez votre premier booking pour voir vos statistiques
</p>
<Link href="/dashboard/bookings">
<Button size="sm" className="bg-blue-600 hover:bg-blue-700">
<Plus className="mr-1.5 h-3 w-3" />
Créer un booking
</Button>
</Link>
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}