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)
|
// É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 newState = {
|
const isCurrentlyOpen = prev[sectionTitle];
|
||||||
...prev,
|
|
||||||
[sectionTitle]: !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
|
// Sauvegarder dans localStorage
|
||||||
localStorage.setItem('menuSectionsState', JSON.stringify(newState));
|
localStorage.setItem('menuSectionsState', JSON.stringify(newState));
|
||||||
return newState;
|
return newState;
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user