Checkpoint: Améliorations de la gestion des attestations :

- Affichage de la date de début dans l'en-tête de chaque séquence
- Bouton d'envoi groupé par séquence
- Filtre séquences terminées/à venir
This commit is contained in:
Manus
2026-01-25 09:16:22 -05:00
parent 8e20ac40dd
commit b32487aa4d
3 changed files with 104 additions and 4 deletions

View File

@@ -171,6 +171,7 @@ export default function AdminGestionAttestations() {
const [selectedInscriptionId, setSelectedInscriptionId] = useState<number | null>(null); const [selectedInscriptionId, setSelectedInscriptionId] = useState<number | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null); const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [previewDialogOpen, setPreviewDialogOpen] = useState(false); const [previewDialogOpen, setPreviewDialogOpen] = useState(false);
const [filtreStatut, setFiltreStatut] = useState<"toutes" | "terminees" | "a_venir">("toutes");
// Récupérer la liste des formations // Récupérer la liste des formations
const { data: formations, isLoading: loadingFormations } = trpc.formations.list.useQuery(); const { data: formations, isLoading: loadingFormations } = trpc.formations.list.useQuery();
@@ -353,6 +354,40 @@ export default function AdminGestionAttestations() {
</CardContent> </CardContent>
</Card> </Card>
{/* Filtre par statut */}
{selectedFormationId && (
<Card>
<CardHeader>
<CardTitle>Filtrer les séquences</CardTitle>
<CardDescription>
Afficher uniquement les séquences terminées ou à venir
</CardDescription>
</CardHeader>
<CardContent>
<RadioGroup value={filtreStatut} onValueChange={(value: "toutes" | "terminees" | "a_venir") => setFiltreStatut(value)}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="toutes" id="toutes" />
<Label htmlFor="toutes" className="font-normal cursor-pointer">
Toutes les séquences
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="terminees" id="terminees" />
<Label htmlFor="terminees" className="font-normal cursor-pointer">
Séquences terminées
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="a_venir" id="a_venir" />
<Label htmlFor="a_venir" className="font-normal cursor-pointer">
Séquences à venir
</Label>
</div>
</RadioGroup>
</CardContent>
</Card>
)}
{/* Configuration */} {/* Configuration */}
{selectedFormationId && ( {selectedFormationId && (
<Card> <Card>
@@ -471,20 +506,64 @@ export default function AdminGestionAttestations() {
const idB = parseInt(b[0]); const idB = parseInt(b[0]);
return idA - idB; return idA - idB;
}) })
.filter(([sequenceId, sequenceApprenants]) => {
// Filtrer selon le statut sélectionné
if (filtreStatut === "toutes") return true;
const premiereDate = sequenceApprenants[0]?.sequence?.premiereDate;
if (!premiereDate) return true; // Afficher les séquences sans date
const now = new Date();
const sequenceDate = new Date(premiereDate);
if (filtreStatut === "terminees") {
return sequenceDate < now;
} else if (filtreStatut === "a_venir") {
return sequenceDate >= now;
}
return true;
})
.map(([sequenceId, sequenceApprenants]) => { .map(([sequenceId, sequenceApprenants]) => {
const sequenceName = sequenceApprenants[0]?.sequence?.nom || 'Séquence inconnue'; const sequenceName = sequenceApprenants[0]?.sequence?.nom || 'Séquence inconnue';
const premiereDate = sequenceApprenants[0]?.sequence?.premiereDate;
const dateFormatted = premiereDate ? new Date(premiereDate).toLocaleDateString('fr-FR') : null;
return ( return (
<div key={sequenceId} className="border rounded-lg overflow-hidden"> <div key={sequenceId} className="border rounded-lg overflow-hidden">
{/* En-tête de séquence */} {/* En-tête de séquence */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b px-4 py-3"> <div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b px-4 py-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="font-semibold text-lg text-blue-900">{sequenceName}</h3> <h3 className="font-semibold text-lg text-blue-900">
{sequenceName}
{dateFormatted && <span className="text-blue-600 ml-2">- {dateFormatted}</span>}
</h3>
<p className="text-sm text-blue-700">{sequenceApprenants.length} apprenant(s)</p> <p className="text-sm text-blue-700">{sequenceApprenants.length} apprenant(s)</p>
</div> </div>
<Badge variant="outline" className="bg-white"> <div className="flex items-center gap-3">
{sequenceApprenants.filter(a => a.attestation?.emailEnvoye).length} / {sequenceApprenants.length} envoyé(es) <Badge variant="outline" className="bg-white">
</Badge> {sequenceApprenants.filter(a => a.attestation?.emailEnvoye).length} / {sequenceApprenants.length} envoyé(es)
</Badge>
<Button
size="sm"
variant="default"
className="bg-blue-600 hover:bg-blue-700"
onClick={() => {
const inscriptionIds = sequenceApprenants
.filter(a => a.attestation?.documentUrl && !a.attestation?.emailEnvoye)
.map(a => a.inscription.id);
if (inscriptionIds.length === 0) {
toast.info("Aucune attestation à envoyer pour cette séquence");
return;
}
if (confirm(`Envoyer ${inscriptionIds.length} attestation(s) pour cette séquence ?`)) {
inscriptionIds.forEach(id => {
sendAttestationMutation.mutate({ inscriptionId: id });
});
}
}}
disabled={sendAttestationMutation.isPending}
>
<Send className="h-4 w-4 mr-1" />
Envoyer tout
</Button>
</div>
</div> </div>
</div> </div>

View File

@@ -159,6 +159,18 @@ export async function getApprenantsWithAttestationStatus(formationId: number) {
const db = await getDb(); const db = await getDb();
if (!db) return []; if (!db) return [];
// Récupérer les premières dates de chaque séquence
const premieresDates = await db
.select({
sequenceId: datesFormation.sequenceId,
premiereDate: sql<Date>`MIN(${datesFormation.date})`,
})
.from(datesFormation)
.groupBy(datesFormation.sequenceId);
// Créer un map pour accès rapide
const datesMap = new Map(premieresDates.map(d => [d.sequenceId, d.premiereDate]));
const results = await db const results = await db
.select({ .select({
// Apprenant // Apprenant
@@ -205,6 +217,7 @@ export async function getApprenantsWithAttestationStatus(formationId: number) {
sequence: { sequence: {
id: r.sequenceId, id: r.sequenceId,
nom: r.sequenceNom, nom: r.sequenceNom,
premiereDate: datesMap.get(r.sequenceId) || null,
}, },
attestation: r.attestationId ? { attestation: r.attestationId ? {
id: r.attestationId, id: r.attestationId,

View File

@@ -1285,4 +1285,12 @@
- [x] Identifier la cause du problème (structure imbriquée Drizzle ORM) - [x] Identifier la cause du problème (structure imbriquée Drizzle ORM)
- [x] Corriger le code backend (aplatir la structure de la requête) - [x] Corriger le code backend (aplatir la structure de la requête)
- [x] Corriger le code frontend (utiliser l'ID de séquence pour le tri) - [x] Corriger le code frontend (utiliser l'ID de séquence pour le tri)
- [x] Déployer sur le VPS
## Améliorations de la gestion des attestations
- [x] Modifier le backend pour inclure la première date de chaque séquence
- [x] Afficher la date de début dans l'en-tête de chaque séquence (ex: "Groupe A - 15/01/2026")
- [x] Ajouter un bouton "Envoyer toutes les attestations de cette séquence" dans l'en-tête
- [x] Créer un filtre "Séquences terminées / à venir"
- [ ] Déployer sur le VPS - [ ] Déployer sur le VPS