Checkpoint: Correction de l'erreur "refetchInscrits is not defined" lors de l'upload d'attestation manuelle :

- Remplacement de refetchInscrits() par refetchApprenants() dans AdminGestionAttestations.tsx ligne 489
- La fonction refetchApprenants() est la bonne fonction définie dans le composant (ligne 157)
- Déployé sur le VPS de production (https://formations.itinova.org)
- L'upload d'attestation manuelle fonctionne maintenant correctement sans erreur JavaScript
This commit is contained in:
Manus
2026-01-17 18:30:13 -05:00
parent a203b70e27
commit a79132e921
4 changed files with 175 additions and 24 deletions

View File

@@ -214,7 +214,7 @@ function DashboardLayoutContent({
// État pour gérer les sections ouvertes/fermées (persisté dans localStorage) // État pour gérer les sections ouvertes/fermées (persisté dans localStorage)
const [openSections, setOpenSections] = useState<Record<string, boolean>>(() => { const [openSections, setOpenSections] = useState<Record<string, boolean>>(() => {
// Version du système de menu (incrémenter pour forcer la réinitialisation) // Version du système de menu (incrémenter pour forcer la réinitialisation)
const MENU_VERSION = 4; // Incrémenté pour ne garder ouvert que "Gestion" par défaut const MENU_VERSION = 5; // Incrémenté pour forcer la réinitialisation : seul "Gestion" ouvert par défaut
const savedVersion = localStorage.getItem('menuVersion'); const savedVersion = localStorage.getItem('menuVersion');
// Si la version a changé, ignorer le localStorage existant // Si la version a changé, ignorer le localStorage existant
@@ -243,13 +243,27 @@ function DashboardLayoutContent({
return initial; return initial;
}); });
// Fonction pour toggle une section // Fonction pour toggle une section (mode accordéon : un seul menu ouvert à la fois)
const toggleSection = (sectionTitle: string) => { const toggleSection = (sectionTitle: string) => {
setOpenSections(prev => { setOpenSections(prev => {
const isCurrentlyOpen = prev[sectionTitle];
// Si le menu est déjà ouvert, on le ferme
if (isCurrentlyOpen) {
const newState = { const newState = {
...prev, ...prev,
[sectionTitle]: !prev[sectionTitle] [sectionTitle]: false
}; };
localStorage.setItem('menuSectionsState', JSON.stringify(newState));
return newState;
}
// Sinon, on ferme tous les menus et on ouvre celui-ci
const newState: Record<string, boolean> = {};
Object.keys(prev).forEach(key => {
newState[key] = key === sectionTitle;
});
// Sauvegarder dans localStorage // Sauvegarder dans localStorage
localStorage.setItem('menuSectionsState', JSON.stringify(newState)); localStorage.setItem('menuSectionsState', JSON.stringify(newState));
return newState; return newState;

View File

@@ -12,6 +12,129 @@ import { toast } from "sonner";
import { Eye, Send, Upload, Trash2, FileText } from "lucide-react"; import { Eye, Send, Upload, Trash2, FileText } from "lucide-react";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
// Composant de dialogue d'upload d'attestation
function UploadAttestationDialog({
open,
onOpenChange,
inscriptionId,
onSuccess,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
inscriptionId: number | null;
onSuccess: () => void;
}) {
const [uploading, setUploading] = useState(false);
const [fileData, setFileData] = useState<{
nomFichier: string;
urlFichier: string;
s3Key: string;
typeFichier: string;
tailleFichier: number;
} | null>(null);
const uploadMutation = trpc.gestionAttestations.uploadDocument.useMutation({
onSuccess: () => {
onSuccess();
setFileData(null);
},
onError: (error) => {
toast.error(`Erreur lors de l'upload : ${error.message}`);
setUploading(false);
},
});
const handleUpload = () => {
if (!fileData || !inscriptionId) return;
setUploading(true);
uploadMutation.mutate({
inscriptionId,
documentUrl: fileData.urlFichier,
documentS3Key: fileData.s3Key,
});
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Charger une attestation</DialogTitle>
<DialogDescription>
Sélectionnez un fichier PDF à uploader pour cet apprenant
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Fichier PDF</Label>
<input
type="file"
accept=".pdf"
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
if (file.type !== 'application/pdf') {
toast.error('Seuls les fichiers PDF sont acceptés');
return;
}
if (file.size > 10 * 1024 * 1024) {
toast.error('Le fichier ne doit pas dépasser 10 Mo');
return;
}
setUploading(true);
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload-file', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Erreur lors de l\'upload');
}
const data = await response.json();
setFileData({
nomFichier: file.name,
urlFichier: data.url,
s3Key: data.key,
typeFichier: file.type,
tailleFichier: file.size,
});
toast.success('Fichier uploadé avec succès');
} catch (error) {
toast.error('Erreur lors de l\'upload du fichier');
} finally {
setUploading(false);
}
}}
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-primary file:text-primary-foreground hover:file:bg-primary/90"
/>
{fileData && (
<p className="text-sm text-muted-foreground mt-2">
Fichier sélectionné : {fileData.nomFichier}
</p>
)}
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={uploading}>
Annuler
</Button>
<Button onClick={handleUpload} disabled={!fileData || uploading}>
{uploading ? 'Chargement...' : 'Valider'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}
export default function AdminGestionAttestations() { export default function AdminGestionAttestations() {
const [selectedFormationId, setSelectedFormationId] = useState<number | null>(null); const [selectedFormationId, setSelectedFormationId] = useState<number | null>(null);
const [modeAttestation, setModeAttestation] = useState<"auto" | "manuel">("auto"); const [modeAttestation, setModeAttestation] = useState<"auto" | "manuel">("auto");
@@ -358,24 +481,16 @@ export default function AdminGestionAttestations() {
)} )}
{/* Dialog d'upload */} {/* Dialog d'upload */}
<Dialog open={uploadDialogOpen} onOpenChange={setUploadDialogOpen}> <UploadAttestationDialog
<DialogContent> open={uploadDialogOpen}
<DialogHeader> onOpenChange={setUploadDialogOpen}
<DialogTitle>Charger une attestation</DialogTitle> inscriptionId={selectedInscriptionId}
<DialogDescription> onSuccess={() => {
Sélectionnez un fichier PDF à uploader pour cet apprenant refetchApprenants();
</DialogDescription> setUploadDialogOpen(false);
</DialogHeader> toast.success("Attestation chargée avec succès");
<div className="space-y-4"> }}
<p className="text-sm text-muted-foreground"> />
Fonctionnalité d'upload à implémenter avec un composant FileUpload similaire à celui des rappels
</p>
<Button variant="outline" onClick={() => setUploadDialogOpen(false)}>
Fermer
</Button>
</div>
</DialogContent>
</Dialog>
{/* Dialog de prévisualisation */} {/* Dialog de prévisualisation */}
<Dialog open={previewDialogOpen} onOpenChange={setPreviewDialogOpen}> <Dialog open={previewDialogOpen} onOpenChange={setPreviewDialogOpen}>

View File

@@ -102,11 +102,27 @@ export async function uploadAttestationDocument(
return existing[0].id; return existing[0].id;
} else { } else {
// Récupérer les informations de l'inscription
const inscription = await db
.select({
apprenantId: inscriptions.apprenantId,
sequenceId: inscriptions.sequenceId,
})
.from(inscriptions)
.where(eq(inscriptions.id, inscriptionId))
.limit(1);
if (inscription.length === 0) {
throw new Error("Inscription not found");
}
// Créer une nouvelle attestation // Créer une nouvelle attestation
const result = await db const result = await db
.insert(attestations) .insert(attestations)
.values({ .values({
inscriptionId, inscriptionId,
apprenantId: inscription[0].apprenantId,
sequenceId: inscription[0].sequenceId,
documentUrl, documentUrl,
documentS3Key, documentS3Key,
uploadedBy, uploadedBy,

View File

@@ -853,3 +853,9 @@
- [x] Corriger la détection de la pièce jointe dans le tableau des rappels (vérification de nomFichier et urlFichier au lieu de pieceJointe) - [x] Corriger la détection de la pièce jointe dans le tableau des rappels (vérification de nomFichier et urlFichier au lieu de pieceJointe)
- [x] Corriger l'envoi de la pièce jointe dans les emails de rappel (support des fichiers locaux et S3) - [x] Corriger l'envoi de la pièce jointe dans les emails de rappel (support des fichiers locaux et S3)
- [x] Corriger la corruption de la pièce jointe lors de l'envoi par email (ajout de l'encoding base64 pour SMTP/Nodemailer) - [x] Corriger la corruption de la pièce jointe lors de l'envoi par email (ajout de l'encoding base64 pour SMTP/Nodemailer)
- [x] Modifier le comportement par défaut des menus de la sidebar : tous repliés à la connexion sauf le menu "Gestion" (MENU_VERSION incrémentée à 5)
- [x] Modifier le comportement des menus de la sidebar en mode accordéon : lorsqu'un menu s'ouvre, les autres se replient automatiquement
- [x] Implémenter la fonctionnalité du bouton "Charger" dans la gestion des attestations de formation (upload de PDF avec validation)
- [x] Corriger l'erreur "No procedure found on path attestations.uploadDocument" lors de l'upload d'attestations (utilisation de gestionAttestations.uploadDocument)
- [x] Corriger l'erreur SQL "Failed query: insert into attestations" lors de l'upload d'attestation manuelle (ajout de apprenantId et sequenceId)
- [x] Corriger l'erreur "refetchInscrits is not defined" lors de l'upload d'attestation manuelle (remplacement par refetchApprenants)