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:
@@ -214,7 +214,7 @@ function DashboardLayoutContent({
|
||||
// État pour gérer les sections ouvertes/fermées (persisté dans localStorage)
|
||||
const [openSections, setOpenSections] = useState<Record<string, boolean>>(() => {
|
||||
// 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');
|
||||
|
||||
// Si la version a changé, ignorer le localStorage existant
|
||||
@@ -243,13 +243,27 @@ function DashboardLayoutContent({
|
||||
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) => {
|
||||
setOpenSections(prev => {
|
||||
const newState = {
|
||||
...prev,
|
||||
[sectionTitle]: !prev[sectionTitle]
|
||||
};
|
||||
const isCurrentlyOpen = prev[sectionTitle];
|
||||
|
||||
// Si le menu est déjà ouvert, on le ferme
|
||||
if (isCurrentlyOpen) {
|
||||
const newState = {
|
||||
...prev,
|
||||
[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
|
||||
localStorage.setItem('menuSectionsState', JSON.stringify(newState));
|
||||
return newState;
|
||||
|
||||
@@ -12,6 +12,129 @@ import { toast } from "sonner";
|
||||
import { Eye, Send, Upload, Trash2, FileText } from "lucide-react";
|
||||
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() {
|
||||
const [selectedFormationId, setSelectedFormationId] = useState<number | null>(null);
|
||||
const [modeAttestation, setModeAttestation] = useState<"auto" | "manuel">("auto");
|
||||
@@ -358,24 +481,16 @@ export default function AdminGestionAttestations() {
|
||||
)}
|
||||
|
||||
{/* Dialog d'upload */}
|
||||
<Dialog open={uploadDialogOpen} onOpenChange={setUploadDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Charger une attestation</DialogTitle>
|
||||
<DialogDescription>
|
||||
Sélectionnez un fichier PDF à uploader pour cet apprenant
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<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>
|
||||
<UploadAttestationDialog
|
||||
open={uploadDialogOpen}
|
||||
onOpenChange={setUploadDialogOpen}
|
||||
inscriptionId={selectedInscriptionId}
|
||||
onSuccess={() => {
|
||||
refetchApprenants();
|
||||
setUploadDialogOpen(false);
|
||||
toast.success("Attestation chargée avec succès");
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Dialog de prévisualisation */}
|
||||
<Dialog open={previewDialogOpen} onOpenChange={setPreviewDialogOpen}>
|
||||
|
||||
@@ -102,11 +102,27 @@ export async function uploadAttestationDocument(
|
||||
|
||||
return existing[0].id;
|
||||
} 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
|
||||
const result = await db
|
||||
.insert(attestations)
|
||||
.values({
|
||||
inscriptionId,
|
||||
apprenantId: inscription[0].apprenantId,
|
||||
sequenceId: inscription[0].sequenceId,
|
||||
documentUrl,
|
||||
documentS3Key,
|
||||
uploadedBy,
|
||||
|
||||
6
todo.md
6
todo.md
@@ -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 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] 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)
|
||||
|
||||
Reference in New Issue
Block a user