Checkpoint: Système d'attestations de formation implémenté

 Infrastructure backend :
- Tables attestations et configAttestation en base de données
- Service de génération PDF avec PDFKit (attestationService.ts)
- Génération automatique de PDF avec template personnalisable
- Upload automatique vers S3 avec URLs publiques
- Procédures tRPC pour générer, récupérer et configurer

 Interface utilisateur :
- Page de configuration des attestations (/admin/attestations)
- Personnalisation du texte avec variables dynamiques (nomComplet, nomFormation, dateDebut, dateFin)
- Configuration du signataire (nom et fonction)
- Bouton "Générer" dans la page de validation des présences formateur
- Génération automatique avec ouverture du PDF dans un nouvel onglet

 Fonctionnalités :
- Vérification des doublons (une seule attestation par apprenant/séquence)
- Génération uniquement pour les apprenants marqués comme présents
- PDF professionnel avec détails de la formation et dates
- Stockage permanent sur S3

📋 Améliorations futures possibles :
- Upload de logo et signature personnalisés
- Envoi automatique par email aux apprenants
- Espace apprenant pour télécharger les attestations
This commit is contained in:
Manus
2026-01-07 10:06:29 -05:00
parent cc3badffa3
commit 023711df92
15 changed files with 6691 additions and 2 deletions

View File

@@ -13,9 +13,48 @@ import {
} from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { trpc } from "@/lib/trpc";
import { Calendar, FileText, MapPin, Upload, Users } from "lucide-react";
import { Calendar, FileText, MapPin, Upload, Users, Award } from "lucide-react";
import { useParams } from "wouter";
import { toast } from "sonner";
import { useState } from "react";
function AttestationButton({ inscriptionId, present }: { inscriptionId: number; present: boolean }) {
const [isGenerating, setIsGenerating] = useState(false);
const genererAttestation = trpc.attestations.generer.useMutation();
const handleGenerer = async () => {
if (!present) {
toast.error("L'apprenant doit être marqué comme présent pour générer une attestation");
return;
}
setIsGenerating(true);
try {
const result = await genererAttestation.mutateAsync({ inscriptionId });
if (result.success && result.attestation) {
toast.success(result.message || "Attestation générée avec succès");
// Ouvrir le PDF dans un nouvel onglet
window.open(result.attestation.pdfUrl, "_blank");
}
} catch (error: any) {
toast.error(error.message || "Erreur lors de la génération de l'attestation");
} finally {
setIsGenerating(false);
}
};
return (
<Button
size="sm"
variant="outline"
onClick={handleGenerer}
disabled={!present || isGenerating}
>
<Award className="h-4 w-4 mr-1" />
{isGenerating ? "Génération..." : "Générer"}
</Button>
);
}
export default function FormateurSequence() {
const { id } = useParams();
@@ -189,6 +228,7 @@ export default function FormateurSequence() {
<TableHead>Établissement</TableHead>
<TableHead>Statut</TableHead>
<TableHead className="text-center">Présent</TableHead>
<TableHead className="text-center">Attestation</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -224,6 +264,9 @@ export default function FormateurSequence() {
}
/>
</TableCell>
<TableCell className="text-center">
<AttestationButton inscriptionId={apprenant.inscriptionId} present={apprenant.statut === "confirmee"} />
</TableCell>
</TableRow>
))}
</TableBody>