Nouvelles fonctionnalités : - Cartes de statistiques : Factures traitées, Montant total, Score moyen, Dernière activité - Graphique en barres : Évolution des factures par semaine (30 derniers jours) - Graphique circulaire : Top 10 fournisseurs avec pourcentages - Liste détaillée : Tous les fournisseurs avec nombre de factures et montants - Bouton "Voir les statistiques détaillées" pour afficher/masquer les graphiques - Calculs automatiques : montant moyen par facture, score de qualité avec label (Excellent/Bon/Moyen/Faible) Le dashboard utilise Recharts pour les visualisations et offre une vue complète de l'activité de traitement des factures.
264 lines
10 KiB
TypeScript
264 lines
10 KiB
TypeScript
import DashboardLayout from "@/components/DashboardLayout";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { trpc } from "@/lib/trpc";
|
|
import { FileText, DollarSign, TrendingUp, Clock } from "lucide-react";
|
|
import { BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
|
|
import { Button } from "@/components/ui/button";
|
|
import { useState } from "react";
|
|
|
|
const COLORS = [
|
|
'#3b82f6', // blue
|
|
'#8b5cf6', // purple
|
|
'#ec4899', // pink
|
|
'#f59e0b', // amber
|
|
'#10b981', // emerald
|
|
'#06b6d4', // cyan
|
|
'#f97316', // orange
|
|
'#6366f1', // indigo
|
|
'#14b8a6', // teal
|
|
'#a855f7', // violet
|
|
];
|
|
|
|
export default function Dashboard() {
|
|
const { data: stats, isLoading } = trpc.invoices.getStats.useQuery();
|
|
const [showDetailedStats, setShowDetailedStats] = useState(false);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="flex items-center justify-center h-96">
|
|
<div className="text-muted-foreground">Chargement des statistiques...</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
const formatCurrency = (amount: number) => {
|
|
return new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR',
|
|
}).format(amount);
|
|
};
|
|
|
|
const formatDate = (date: Date | string | null) => {
|
|
if (!date) return 'N/A';
|
|
return new Date(date).toLocaleDateString('fr-FR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
const getScoreLabel = (score: number) => {
|
|
if (score >= 90) return 'Excellent';
|
|
if (score >= 75) return 'Bon';
|
|
if (score >= 60) return 'Moyen';
|
|
return 'Faible';
|
|
};
|
|
|
|
const getScoreColor = (score: number) => {
|
|
if (score >= 90) return 'text-green-600';
|
|
if (score >= 75) return 'text-blue-600';
|
|
if (score >= 60) return 'text-yellow-600';
|
|
return 'text-red-600';
|
|
};
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Tableau de bord</h1>
|
|
<p className="text-muted-foreground">Activité des 30 derniers jours</p>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowDetailedStats(!showDetailedStats)}
|
|
>
|
|
{showDetailedStats ? 'Masquer' : 'Voir les statistiques détaillées'}
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Stats Cards */}
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Factures traitées</CardTitle>
|
|
<FileText className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{stats?.completed || 0}</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{stats?.completed || 0} complétées, {stats?.error || 0} en erreur
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Montant total</CardTitle>
|
|
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{formatCurrency(stats?.totalAmount || 0)}</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
Moyenne : {stats?.completed ? formatCurrency((stats.totalAmount || 0) / stats.completed) : '0 €'} / facture
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Score moyen</CardTitle>
|
|
<TrendingUp className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center gap-3">
|
|
<div className={`text-2xl font-bold ${getScoreColor(stats?.averageScore || 0)}`}>
|
|
{stats?.averageScore || 0}
|
|
</div>
|
|
<div className={`text-sm font-medium ${getScoreColor(stats?.averageScore || 0)}`}>
|
|
{getScoreLabel(stats?.averageScore || 0)}
|
|
</div>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">Qualité d'extraction</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Dernière activité</CardTitle>
|
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{stats?.lastActivity
|
|
? new Date(stats.lastActivity).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' })
|
|
: 'N/A'}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{stats?.lastActivity
|
|
? new Date(stats.lastActivity).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
|
|
: ''}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Detailed Stats */}
|
|
{showDetailedStats && (
|
|
<>
|
|
{/* Charts Row */}
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
{/* Weekly Chart */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Factures par semaine</CardTitle>
|
|
<CardDescription>
|
|
Nombre de factures traitées chaque semaine (30 derniers jours)
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{stats?.weeklyData && stats.weeklyData.length > 0 ? (
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<BarChart data={stats.weeklyData}>
|
|
<CartesianGrid strokeDasharray="3 3" />
|
|
<XAxis dataKey="week" />
|
|
<YAxis />
|
|
<Tooltip />
|
|
<Bar dataKey="count" fill="#3b82f6" />
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
) : (
|
|
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
|
|
Aucune donnée disponible
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Top Suppliers Pie Chart */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Top 10 fournisseurs</CardTitle>
|
|
<CardDescription>
|
|
Répartition des factures par fournisseur (30 derniers jours)
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{stats?.topSuppliers && stats.topSuppliers.length > 0 ? (
|
|
<ResponsiveContainer width="100%" height={300}>
|
|
<PieChart>
|
|
<Pie
|
|
data={stats.topSuppliers}
|
|
cx="50%"
|
|
cy="50%"
|
|
labelLine={false}
|
|
label={(entry) => `${entry.name}: ${entry.percentage.toFixed(0)}%`}
|
|
outerRadius={80}
|
|
fill="#8884d8"
|
|
dataKey="count"
|
|
>
|
|
{stats.topSuppliers.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
) : (
|
|
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
|
|
Aucune donnée disponible
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Suppliers List */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Détail par fournisseur</CardTitle>
|
|
<CardDescription>Liste des fournisseurs (30 derniers jours)</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{stats?.suppliersList && stats.suppliersList.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{stats.suppliersList.map((supplier, index) => (
|
|
<div
|
|
key={index}
|
|
className="flex items-center justify-between p-3 rounded-lg border hover:bg-accent transition-colors"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className="w-3 h-3 rounded-full"
|
|
style={{ backgroundColor: COLORS[index % COLORS.length] }}
|
|
/>
|
|
<span className="font-medium">{supplier.name}</span>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="font-semibold">{supplier.count} factures</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{formatCurrency(supplier.amount)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
Aucun fournisseur trouvé
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</>
|
|
)}
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|