**Corrections d'erreurs :** 1. **Erreurs SQL DATE_FORMAT corrigées** (analyticsDb.ts) : - Remplacement des sous-requêtes corrélées par des JOIN dans getTauxRemplissageByMonth() - Utilisation de COUNT(CASE WHEN...) au lieu de SUM(SELECT COUNT(*)) - Ajout de leftJoin pour éviter les erreurs de sous-requêtes 2. **Erreur React "Cannot read properties of null" corrigée** (AdminRapportPublicCible.tsx) : - Ajout de l'opérateur null-safe `?.` pour accéder à `insc.apprenant?.fonction` - Vérification explicite si la fonction est null/undefined - Gestion du cas "public cible = tous" qui correspond toujours 3. **Tests réussis** : - Page rapport-public-cible : affiche correctement les statistiques et recommandations - Tableau de bord analytique : graphiques et statistiques fonctionnels - Page suivi questionnaires : affiche correctement les données **Harmonisation charte graphique :** - Application du style bleu clair (#6B9FE8) avec ombre portée à tous les titres des 32 pages - Création de la classe CSS `.page-title` réutilisable dans index.css - Cohérence visuelle sur toute l'application **Résultat :** ✅ Toutes les erreurs SQL et React sont corrigées ✅ Les pages fonctionnent correctement sans erreur ✅ Charte graphique harmonisée sur toutes les pages
167 lines
6.4 KiB
TypeScript
167 lines
6.4 KiB
TypeScript
import { useState } from "react";
|
|
import { trpc } from "@/lib/trpc";
|
|
import DashboardLayout from "@/components/DashboardLayout";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Loader2, FileText, CheckCircle2, XCircle, ExternalLink } from "lucide-react";
|
|
import { format } from "date-fns";
|
|
import { fr } from "date-fns/locale";
|
|
|
|
export default function HistoriqueAttestations() {
|
|
const { data: historique, isLoading } = trpc.attestations.historique.useQuery();
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="flex items-center justify-center min-h-[400px]">
|
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="container max-w-7xl py-8">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<FileText className="h-8 w-8 text-primary" />
|
|
<div>
|
|
<h1 className="page-title">Historique des attestations</h1>
|
|
<p className="text-muted-foreground">
|
|
Suivi de tous les envois d'attestations de formation
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Envois d'attestations</CardTitle>
|
|
<CardDescription>
|
|
Liste complète des attestations envoyées avec leur statut
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{!historique || historique.length === 0 ? (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
|
<p>Aucun envoi d'attestation enregistré</p>
|
|
</div>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Date d'envoi</TableHead>
|
|
<TableHead>Formation</TableHead>
|
|
<TableHead>Séquence</TableHead>
|
|
<TableHead>Apprenant</TableHead>
|
|
<TableHead>Email</TableHead>
|
|
<TableHead>Statut</TableHead>
|
|
<TableHead>Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{historique.map((item) => (
|
|
<TableRow key={item.id}>
|
|
<TableCell className="whitespace-nowrap">
|
|
{format(new Date(item.dateEnvoi), "dd/MM/yyyy HH:mm", { locale: fr })}
|
|
</TableCell>
|
|
<TableCell className="font-medium">
|
|
{item.formation.nom}
|
|
</TableCell>
|
|
<TableCell>{item.sequence.nom}</TableCell>
|
|
<TableCell>
|
|
{item.apprenant.prenom} {item.apprenant.nom}
|
|
</TableCell>
|
|
<TableCell className="text-muted-foreground">
|
|
{item.apprenant.email}
|
|
</TableCell>
|
|
<TableCell>
|
|
{item.statut === "envoye" ? (
|
|
<Badge variant="default" className="gap-1">
|
|
<CheckCircle2 className="h-3 w-3" />
|
|
Envoyé
|
|
</Badge>
|
|
) : (
|
|
<Badge variant="destructive" className="gap-1">
|
|
<XCircle className="h-3 w-3" />
|
|
Erreur
|
|
</Badge>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{item.urlAttestation && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
asChild
|
|
>
|
|
<a
|
|
href={item.urlAttestation}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="gap-1"
|
|
>
|
|
<ExternalLink className="h-4 w-4" />
|
|
Voir PDF
|
|
</a>
|
|
</Button>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Statistiques */}
|
|
{historique && historique.length > 0 && (
|
|
<div className="grid gap-4 md:grid-cols-3 mt-6">
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
Total envoyé
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{historique.length}</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
Envois réussis
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-green-600">
|
|
{historique.filter((h) => h.statut === "envoye").length}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
Erreurs
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold text-red-600">
|
|
{historique.filter((h) => h.statut === "erreur").length}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|