Rollback to 8e20ac40
This commit is contained in:
@@ -171,7 +171,6 @@ 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();
|
||||||
@@ -354,40 +353,6 @@ 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>
|
||||||
@@ -506,64 +471,20 @@ 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">
|
<h3 className="font-semibold text-lg text-blue-900">{sequenceName}</h3>
|
||||||
{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>
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Badge variant="outline" className="bg-white">
|
<Badge variant="outline" className="bg-white">
|
||||||
{sequenceApprenants.filter(a => a.attestation?.emailEnvoye).length} / {sequenceApprenants.length} envoyé(es)
|
{sequenceApprenants.filter(a => a.attestation?.emailEnvoye).length} / {sequenceApprenants.length} envoyé(es)
|
||||||
</Badge>
|
</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>
|
||||||
|
|
||||||
|
|||||||
@@ -159,22 +159,6 @@ export async function getApprenantsWithAttestationStatus(formationId: number) {
|
|||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) return [];
|
if (!db) return [];
|
||||||
|
|
||||||
// Récupérer toutes les dates de formation
|
|
||||||
const toutesLesDates = await db
|
|
||||||
.select({
|
|
||||||
sequenceId: datesFormation.sequenceId,
|
|
||||||
date: datesFormation.date,
|
|
||||||
})
|
|
||||||
.from(datesFormation);
|
|
||||||
|
|
||||||
// Calculer la première date de chaque séquence en JavaScript
|
|
||||||
const datesMap = new Map<number, Date>();
|
|
||||||
toutesLesDates.forEach(({ sequenceId, date }) => {
|
|
||||||
if (!datesMap.has(sequenceId) || date < datesMap.get(sequenceId)!) {
|
|
||||||
datesMap.set(sequenceId, date);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const results = await db
|
const results = await db
|
||||||
.select({
|
.select({
|
||||||
// Apprenant
|
// Apprenant
|
||||||
@@ -221,7 +205,6 @@ 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,
|
||||||
|
|||||||
15
todo.md
15
todo.md
@@ -1285,17 +1285,4 @@
|
|||||||
- [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
|
- [ ] 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"
|
|
||||||
- [x] Déployer sur le VPS
|
|
||||||
|
|
||||||
## Corriger l'erreur SQL MIN() vide dans la récupération de la première date
|
|
||||||
|
|
||||||
- [x] Corriger la requête SQL dans gestionAttestationsDb.ts (remplacé par post-traitement JS)
|
|
||||||
- [x] Déployer la correction sur le VPS
|
|
||||||
|
|||||||
Reference in New Issue
Block a user