diff --git a/.manus/db/db-query-1767799415071.json b/.manus/db/db-query-1767799415071.json new file mode 100644 index 0000000..bf39062 --- /dev/null +++ b/.manus/db/db-query-1767799415071.json @@ -0,0 +1,9 @@ +{ + "query": "ALTER TABLE users \nADD COLUMN formateurId INT NULL AFTER isActive,\nMODIFY COLUMN role ENUM('user', 'admin', 'formateur') NOT NULL DEFAULT 'user';", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute ALTER TABLE users \nADD COLUMN formateurId INT NULL AFTER isActive,\nMODIFY COLUMN role ENUM('user', 'admin', 'formateur') NOT NULL DEFAULT 'user';", + "rows": [], + "messages": [], + "stdout": "", + "stderr": "", + "execution_time_ms": 526 +} \ No newline at end of file diff --git a/client/src/components/ImageUpload.tsx b/client/src/components/ImageUpload.tsx new file mode 100644 index 0000000..e710231 --- /dev/null +++ b/client/src/components/ImageUpload.tsx @@ -0,0 +1,160 @@ +import { useState, useRef } from "react"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Upload, X, Loader2, Image as ImageIcon } from "lucide-react"; +import { toast } from "sonner"; + +interface ImageUploadProps { + label: string; + currentImageUrl?: string | null; + onUploadComplete: (url: string, s3Key: string) => void; + accept?: string; + maxSizeMB?: number; +} + +export default function ImageUpload({ + label, + currentImageUrl, + onUploadComplete, + accept = "image/png,image/jpeg,image/jpg", + maxSizeMB = 5, +}: ImageUploadProps) { + const [uploading, setUploading] = useState(false); + const [previewUrl, setPreviewUrl] = useState(currentImageUrl || null); + const fileInputRef = useRef(null); + + const handleFileSelect = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Vérifier la taille du fichier + const fileSizeMB = file.size / (1024 * 1024); + if (fileSizeMB > maxSizeMB) { + toast.error(`Le fichier est trop volumineux (max ${maxSizeMB}MB)`); + return; + } + + // Vérifier le type de fichier + if (!file.type.startsWith("image/")) { + toast.error("Veuillez sélectionner une image"); + return; + } + + setUploading(true); + + try { + // Créer un aperçu local + const localPreview = URL.createObjectURL(file); + setPreviewUrl(localPreview); + + // Préparer les données pour l'upload + const formData = new FormData(); + formData.append("file", file); + + // Envoyer le fichier au serveur + const response = await fetch("/api/upload-image", { + method: "POST", + body: formData, + }); + + if (!response.ok) { + throw new Error("Erreur lors de l'upload"); + } + + const data = await response.json(); + + if (data.success && data.url && data.s3Key) { + toast.success("Image uploadée avec succès"); + setPreviewUrl(data.url); + onUploadComplete(data.url, data.s3Key); + } else { + throw new Error(data.message || "Erreur lors de l'upload"); + } + } catch (error) { + console.error("Erreur upload:", error); + toast.error("Erreur lors de l'upload de l'image"); + setPreviewUrl(currentImageUrl || null); + } finally { + setUploading(false); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + } + }; + + const handleRemove = () => { + setPreviewUrl(null); + onUploadComplete("", ""); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + return ( +
+ + +
+ {/* Aperçu de l'image */} +
+ {previewUrl ? ( +
+ Aperçu + +
+ ) : ( +
+ +
+ )} +
+ + {/* Bouton d'upload */} +
+ + +

+ Formats acceptés: PNG, JPG, JPEG (max {maxSizeMB}MB) +

+
+
+
+ ); +} diff --git a/client/src/pages/AdminUsers.tsx b/client/src/pages/AdminUsers.tsx index b791436..fccb7e2 100644 --- a/client/src/pages/AdminUsers.tsx +++ b/client/src/pages/AdminUsers.tsx @@ -46,17 +46,20 @@ export default function AdminUsers() { email: "", username: "", password: "", - role: "user" as "user" | "admin", + role: "user" as "user" | "admin" | "formateur", + formateurId: undefined as number | undefined, }); const [editForm, setEditForm] = useState({ name: "", email: "", username: "", password: "", - role: "user" as "user" | "admin", + role: "user" as "user" | "admin" | "formateur", + formateurId: undefined as number | undefined, }); const { data: users = [], refetch } = trpc.users.list.useQuery(); + const { data: formateurs = [] } = trpc.formateurs.list.useQuery(); const createMutation = trpc.users.create.useMutation({ onSuccess: () => { @@ -70,6 +73,7 @@ export default function AdminUsers() { username: "", password: "", role: "user", + formateurId: undefined, }); }, onError: (error) => { @@ -137,6 +141,7 @@ export default function AdminUsers() { password: createForm.password || undefined, role: createForm.role, isActive: true, + formateurId: createForm.formateurId, }); }; @@ -148,6 +153,7 @@ export default function AdminUsers() { username: user.username || "", password: "", // Toujours vide pour la sécurité role: user.role, + formateurId: user.formateurId, }); setEditDialogOpen(true); }; @@ -161,6 +167,7 @@ export default function AdminUsers() { name: editForm.name || undefined, email: editForm.email || undefined, role: editForm.role, + formateurId: editForm.formateurId, }; if (editForm.username) { @@ -268,6 +275,7 @@ export default function AdminUsers() { Tous les rôles Administrateur Utilisateur + Formateur @@ -325,8 +333,8 @@ export default function AdminUsers() { {user.email || "N/A"} {user.username || "N/A"} - - {user.role === "admin" ? "Administrateur" : "Utilisateur"} + + {user.role === "admin" ? "Administrateur" : user.role === "formateur" ? "Formateur" : "Utilisateur"} @@ -452,7 +460,7 @@ export default function AdminUsers() { + + {createForm.role === "formateur" && ( +
+ + +
+ )} @@ -545,7 +577,7 @@ export default function AdminUsers() { + + {editForm.role === "formateur" && ( +
+ + +
+ )} diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx index e2aab44..fa1a8d0 100644 --- a/client/src/pages/Login.tsx +++ b/client/src/pages/Login.tsx @@ -37,8 +37,12 @@ export default function Login() { if (response.ok && data.success) { toast.success("Connexion réussie !"); - // Rediriger vers le tableau de bord - setLocation("/admin"); + // Rediriger selon le rôle de l'utilisateur + if (data.user.role === "formateur") { + setLocation("/formateur"); + } else { + setLocation("/admin"); + } } else { toast.error(data.message || "Identifiant ou mot de passe incorrect"); } diff --git a/client/src/pages/admin/AdminAttestations.tsx b/client/src/pages/admin/AdminAttestations.tsx index 33bbfa6..daec329 100644 --- a/client/src/pages/admin/AdminAttestations.tsx +++ b/client/src/pages/admin/AdminAttestations.tsx @@ -8,6 +8,7 @@ import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { toast } from "sonner"; import { FileText, Save } from "lucide-react"; +import ImageUpload from "@/components/ImageUpload"; export default function AdminAttestations() { const { data: config, isLoading, refetch } = trpc.attestations.getConfig.useQuery(); @@ -17,6 +18,10 @@ export default function AdminAttestations() { nomSignataire: "", fonctionSignataire: "", texteAttestation: "", + logoUrl: "", + logoS3Key: "", + signatureUrl: "", + signatureS3Key: "", }); // Mettre à jour le formulaire quand les données sont chargées @@ -26,6 +31,10 @@ export default function AdminAttestations() { nomSignataire: config.nomSignataire || "", fonctionSignataire: config.fonctionSignataire || "", texteAttestation: config.texteAttestation || "", + logoUrl: config.logoUrl || "", + logoS3Key: config.logoS3Key || "", + signatureUrl: config.signatureUrl || "", + signatureS3Key: config.signatureS3Key || "", }); } }, [config]); @@ -101,6 +110,32 @@ export default function AdminAttestations() { + + + Logo et signature + + Ajoutez un logo et une signature pour personnaliser vos attestations + + + + + setFormData({ ...formData, logoUrl: url, logoS3Key: s3Key }) + } + /> + + + setFormData({ ...formData, signatureUrl: url, signatureS3Key: s3Key }) + } + /> + + + Texte de l'attestation @@ -137,6 +172,10 @@ export default function AdminAttestations() { nomSignataire: config.nomSignataire || "", fonctionSignataire: config.fonctionSignataire || "", texteAttestation: config.texteAttestation || "", + logoUrl: config.logoUrl || "", + logoS3Key: config.logoS3Key || "", + signatureUrl: config.signatureUrl || "", + signatureS3Key: config.signatureS3Key || "", }); } }} diff --git a/drizzle/meta/0027_snapshot.json b/drizzle/meta/0027_snapshot.json new file mode 100644 index 0000000..8594f78 --- /dev/null +++ b/drizzle/meta/0027_snapshot.json @@ -0,0 +1,2035 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "f42716b3-6290-4d8d-986d-c85a6f805e71", + "prevId": "f0782a6b-0d76-4702-9f2d-ea3f55d57977", + "tables": { + "alertes": { + "name": "alertes", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "nom": { + "name": "nom", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('taux_remplissage','seuil_inscriptions')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "condition": { + "name": "condition", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "valeurSeuil": { + "name": "valeurSeuil", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "codeEtablissement": { + "name": "codeEtablissement", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailsDestinataires": { + "name": "emailsDestinataires", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actif": { + "name": "actif", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "derniereVerification": { + "name": "derniereVerification", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dernierEnvoi": { + "name": "dernierEnvoi", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "alertes_id": { + "name": "alertes_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "apprenants": { + "name": "apprenants", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "nom": { + "name": "nom", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prenom": { + "name": "prenom", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "codeEtablissement": { + "name": "codeEtablissement", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fonction": { + "name": "fonction", + "type": "enum('directeur','chef_service','autre')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "apprenants_id": { + "name": "apprenants_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "apprenants_email_unique": { + "name": "apprenants_email_unique", + "columns": [ + "email" + ] + } + }, + "checkConstraint": {} + }, + "attestations": { + "name": "attestations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "inscriptionId": { + "name": "inscriptionId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "s3Key": { + "name": "s3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pdfUrl": { + "name": "pdfUrl", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateGeneration": { + "name": "dateGeneration", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "emailEnvoye": { + "name": "emailEnvoye", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "dateEnvoiEmail": { + "name": "dateEnvoiEmail", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "attestations_id": { + "name": "attestations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "configAttestation": { + "name": "configAttestation", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "logoS3Key": { + "name": "logoS3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logoUrl": { + "name": "logoUrl", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureS3Key": { + "name": "signatureS3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureUrl": { + "name": "signatureUrl", + "type": "varchar(1000)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nomSignataire": { + "name": "nomSignataire", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fonctionSignataire": { + "name": "fonctionSignataire", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "texteAttestation": { + "name": "texteAttestation", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "configAttestation_id": { + "name": "configAttestation_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "datesFormation": { + "name": "datesFormation", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateDebut": { + "name": "dateDebut", + "type": "datetime", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateFin": { + "name": "dateFin", + "type": "datetime", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ordre": { + "name": "ordre", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "datesFormation_id": { + "name": "datesFormation_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "emailConfig": { + "name": "emailConfig", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "provider": { + "name": "provider", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'resend'" + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fromEmail": { + "name": "fromEmail", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fromName": { + "name": "fromName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Formation Manager Itinova'" + }, + "mode": { + "name": "mode", + "type": "enum('simulation','production')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'simulation'" + }, + "domainVerified": { + "name": "domainVerified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "smtpHost": { + "name": "smtpHost", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "smtpPort": { + "name": "smtpPort", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "smtpSecure": { + "name": "smtpSecure", + "type": "enum('none','tls','ssl')", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'tls'" + }, + "smtpUser": { + "name": "smtpUser", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "smtpPassword": { + "name": "smtpPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "emailConfig_id": { + "name": "emailConfig_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "emailTemplates": { + "name": "emailTemplates", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logoUrl": { + "name": "logoUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "primaryColor": { + "name": "primaryColor", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#2563eb'" + }, + "headerBgColor": { + "name": "headerBgColor", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#2563eb'" + }, + "headerTextColor": { + "name": "headerTextColor", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#ffffff'" + }, + "headerTitle": { + "name": "headerTitle", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Formation Manager Itinova'" + }, + "footerText": { + "name": "footerText", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bodyContent": { + "name": "bodyContent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "emailTemplates_id": { + "name": "emailTemplates_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "emailTemplates_type_unique": { + "name": "emailTemplates_type_unique", + "columns": [ + "type" + ] + } + }, + "checkConstraint": {} + }, + "envoisQuestionnaires": { + "name": "envoisQuestionnaires", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "questionnaireId": { + "name": "questionnaireId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateEnvoi": { + "name": "dateEnvoi", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "token": { + "name": "token", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateReponse": { + "name": "dateReponse", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "envoisQuestionnaires_id": { + "name": "envoisQuestionnaires_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "envoisQuestionnaires_token_unique": { + "name": "envoisQuestionnaires_token_unique", + "columns": [ + "token" + ] + } + }, + "checkConstraint": {} + }, + "formateurs": { + "name": "formateurs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "nom": { + "name": "nom", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "formateurs_id": { + "name": "formateurs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "formations": { + "name": "formations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "nom": { + "name": "nom", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lienUnique": { + "name": "lienUnique", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actif": { + "name": "actif", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "formations_id": { + "name": "formations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "formations_lienUnique_unique": { + "name": "formations_lienUnique_unique", + "columns": [ + "lienUnique" + ] + } + }, + "checkConstraint": {} + }, + "inscriptions": { + "name": "inscriptions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "statut": { + "name": "statut", + "type": "enum('confirmee','liste_attente','annulee')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateInscription": { + "name": "dateInscription", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "inscriptions_id": { + "name": "inscriptions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "logsNotifications": { + "name": "logsNotifications", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "enum('remerciement','notification_formateur_inscription','notification_formateur_annulation','alerte_capacite','notification_liste_attente')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "formateurId": { + "name": "formateurId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailDestinataire": { + "name": "emailDestinataire", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sujet": { + "name": "sujet", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateEnvoi": { + "name": "dateEnvoi", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "statut": { + "name": "statut", + "type": "enum('success','failed')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messageErreur": { + "name": "messageErreur", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "logsNotifications_id": { + "name": "logsNotifications_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "logsRappels": { + "name": "logsRappels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rappelId": { + "name": "rappelId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailDestinataire": { + "name": "emailDestinataire", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "typeRappel": { + "name": "typeRappel", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateEnvoi": { + "name": "dateEnvoi", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "statut": { + "name": "statut", + "type": "enum('success','failed')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messageErreur": { + "name": "messageErreur", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateSequence": { + "name": "dateSequence", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nbTentatives": { + "name": "nbTentatives", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "prochainEssai": { + "name": "prochainEssai", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "logsRappels_id": { + "name": "logsRappels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "passwordResetTokens": { + "name": "passwordResetTokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used": { + "name": "used", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "passwordResetTokens_id": { + "name": "passwordResetTokens_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "passwordResetTokens_token_unique": { + "name": "passwordResetTokens_token_unique", + "columns": [ + "token" + ] + } + }, + "checkConstraint": {} + }, + "questionnaires": { + "name": "questionnaires", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "nom": { + "name": "nom", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('satisfaction','evaluation_pre','evaluation_post')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "formationId": { + "name": "formationId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actif": { + "name": "actif", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "envoiAutomatique": { + "name": "envoiAutomatique", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "delaiEnvoiJours": { + "name": "delaiEnvoiJours", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "questionnaires_id": { + "name": "questionnaires_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "questions": { + "name": "questions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "questionnaireId": { + "name": "questionnaireId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ordre": { + "name": "ordre", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "texte": { + "name": "texte", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "typeQuestion": { + "name": "typeQuestion", + "type": "enum('choix_multiple','echelle','texte_libre','oui_non')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "echelleMin": { + "name": "echelleMin", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "echelleMax": { + "name": "echelleMax", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "echelleLabelMin": { + "name": "echelleLabelMin", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "echelleLabelMax": { + "name": "echelleLabelMax", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "obligatoire": { + "name": "obligatoire", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "questions_id": { + "name": "questions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rappels": { + "name": "rappels", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "nom": { + "name": "nom", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "templateType": { + "name": "templateType", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "joursAvant": { + "name": "joursAvant", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "heureEnvoi": { + "name": "heureEnvoi", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'09:00'" + }, + "actif": { + "name": "actif", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "derniereExecution": { + "name": "derniereExecution", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rappels_id": { + "name": "rappels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "reponsesQuestionnaires": { + "name": "reponsesQuestionnaires", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "questionnaireId": { + "name": "questionnaireId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateReponse": { + "name": "dateReponse", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "complete": { + "name": "complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "reponsesQuestionnaires_id": { + "name": "reponsesQuestionnaires_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "reponsesQuestions": { + "name": "reponsesQuestions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "reponseQuestionnaireId": { + "name": "reponseQuestionnaireId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "questionId": { + "name": "questionId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reponseNumerique": { + "name": "reponseNumerique", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reponseTexte": { + "name": "reponseTexte", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "reponsesQuestions_id": { + "name": "reponsesQuestions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sequences": { + "name": "sequences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "formationId": { + "name": "formationId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nom": { + "name": "nom", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lieu": { + "name": "lieu", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "publicCible": { + "name": "publicCible", + "type": "enum('directeur','chef_service','tous','autre')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "formateurId": { + "name": "formateurId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "capaciteMax": { + "name": "capaciteMax", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 12 + }, + "dateBlocage": { + "name": "dateBlocage", + "type": "datetime", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "statut": { + "name": "statut", + "type": "enum('ouverte','bloquee','terminee')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ouverte'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sequences_id": { + "name": "sequences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "supportsFormation": { + "name": "supportsFormation", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "formateurId": { + "name": "formateurId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nomFichier": { + "name": "nomFichier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "typeFichier": { + "name": "typeFichier", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tailleFichier": { + "name": "tailleFichier", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "urlFichier": { + "name": "urlFichier", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "s3Key": { + "name": "s3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "supportsFormation_id": { + "name": "supportsFormation_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "openId": { + "name": "openId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loginMethod": { + "name": "loginMethod", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('user','admin','formateur')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "isActive": { + "name": "isActive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "formateurId": { + "name": "formateurId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "lastSignedIn": { + "name": "lastSignedIn", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "users_openId_unique": { + "name": "users_openId_unique", + "columns": [ + "openId" + ] + }, + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 8c4ca6c..b3de802 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -190,6 +190,13 @@ "when": 1767798385842, "tag": "0026_panoramic_king_bedlam", "breakpoints": true + }, + { + "idx": 27, + "version": "5", + "when": 1767799408213, + "tag": "0027_naive_jackal", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index b8f3083..f4e8624 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -20,9 +20,11 @@ export const users = mysqlTable("users", { /** Mot de passe hashé (bcrypt) */ password: varchar("password", { length: 255 }), loginMethod: varchar("loginMethod", { length: 64 }), - role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(), + role: mysqlEnum("role", ["user", "admin", "formateur"]).default("user").notNull(), /** Statut du compte (actif/inactif) */ isActive: boolean("isActive").default(true).notNull(), + /** ID du formateur associé (si role = formateur) */ + formateurId: int("formateurId"), createdAt: timestamp("createdAt").defaultNow().notNull(), updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(), diff --git a/package.json b/package.json index f053d3a..ed56425 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@trpc/client": "^11.6.0", "@trpc/react-query": "^11.6.0", "@trpc/server": "^11.6.0", + "@types/multer": "^2.0.0", "@types/pdfkit": "^0.17.4", "axios": "^1.12.0", "bcryptjs": "^3.0.3", @@ -70,6 +71,7 @@ "jspdf": "^3.0.3", "jspdf-autotable": "^5.0.2", "lucide-react": "^0.453.0", + "multer": "^2.0.2", "mysql2": "^3.15.0", "nanoid": "^5.1.5", "next-themes": "^0.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0018611..959e811 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,6 +127,9 @@ importers: '@trpc/server': specifier: ^11.6.0 version: 11.6.0(typescript@5.9.3) + '@types/multer': + specifier: ^2.0.0 + version: 2.0.0 '@types/pdfkit': specifier: ^0.17.4 version: 0.17.4 @@ -187,6 +190,9 @@ importers: lucide-react: specifier: ^0.453.0 version: 0.453.0(react@19.2.0) + multer: + specifier: ^2.0.2 + version: 2.0.2 mysql2: specifier: ^3.15.0 version: 3.15.1 @@ -2752,6 +2758,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/multer@2.0.0': + resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==} + '@types/node-fetch@2.6.13': resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} @@ -2873,6 +2882,9 @@ packages: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + archiver-utils@2.1.0: resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} engines: {node: '>= 6'} @@ -2998,6 +3010,10 @@ packages: resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} engines: {node: '>=0.2.0'} + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -3106,6 +3122,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -4414,6 +4434,10 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + multer@2.0.2: + resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} + engines: {node: '>= 10.16.0'} + mysql2@3.15.1: resolution: {integrity: sha512-WZMIRZstT2MFfouEaDz/AGFnGi1A2GwaDe7XvKTdRJEYiAHbOrh4S3d8KFmQeh11U85G+BFjIvS1Di5alusZsw==} engines: {node: '>= 8.0'} @@ -4985,6 +5009,10 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -5110,6 +5138,9 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -5404,6 +5435,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -8539,6 +8574,10 @@ snapshots: '@types/ms@2.1.0': {} + '@types/multer@2.0.0': + dependencies: + '@types/express': 4.17.21 + '@types/node-fetch@2.6.13': dependencies: '@types/node': 24.7.0 @@ -8679,6 +8718,8 @@ snapshots: dependencies: humanize-ms: 1.2.1 + append-field@1.0.0: {} + archiver-utils@2.1.0: dependencies: glob: 7.2.3 @@ -8834,6 +8875,10 @@ snapshots: buffers@0.1.1: {} + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + bytes@3.1.2: {} cac@6.7.14: {} @@ -8948,6 +8993,13 @@ snapshots: concat-map@0.0.1: {} + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + confbox@0.1.8: {} confbox@0.2.2: {} @@ -10522,6 +10574,16 @@ snapshots: ms@2.1.3: {} + multer@2.0.2: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + mysql2@3.15.1: dependencies: aws-ssl-profiles: 1.1.2 @@ -11229,6 +11291,8 @@ snapshots: - '@types/react' - supports-color + streamsearch@1.1.0: {} + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -11343,6 +11407,8 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + typedarray@0.0.6: {} + typescript@5.9.3: {} uc.micro@2.1.0: {} @@ -11650,6 +11716,8 @@ snapshots: xmlchars@2.2.0: {} + xtend@4.0.2: {} + yallist@3.1.1: {} yallist@5.0.0: {} diff --git a/server/_core/index.ts b/server/_core/index.ts index c9bf45e..f2c7005 100644 --- a/server/_core/index.ts +++ b/server/_core/index.ts @@ -7,6 +7,7 @@ import { registerOAuthRoutes } from "./oauth"; import { appRouter } from "../routers"; import { createContext } from "./context"; import localAuthRouter from "./localAuth"; +import uploadImageRouter from "./uploadImage"; import { serveStatic, setupVite } from "./vite"; import { initRappelScheduler } from "../rappelScheduler"; import { initRappelRetryScheduler } from "../rappelRetry"; @@ -40,6 +41,8 @@ async function startServer() { registerOAuthRoutes(app); // Local authentication under /api/auth/local app.use("/api/auth/local", localAuthRouter); + // Image upload under /api/upload-image + app.use("/api/upload-image", uploadImageRouter); // tRPC API app.use( "/api/trpc", diff --git a/server/_core/uploadImage.ts b/server/_core/uploadImage.ts new file mode 100644 index 0000000..78f0c01 --- /dev/null +++ b/server/_core/uploadImage.ts @@ -0,0 +1,67 @@ +import { Router } from "express"; +import multer from "multer"; +import { storagePut } from "../storage"; +import { randomBytes } from "crypto"; + +const router = Router(); + +// Configuration de multer pour gérer les uploads en mémoire +const upload = multer({ + storage: multer.memoryStorage(), + limits: { + fileSize: 5 * 1024 * 1024, // 5MB max + }, + fileFilter: (req, file, cb) => { + // Vérifier que c'est bien une image + if (file.mimetype.startsWith("image/")) { + cb(null, true); + } else { + cb(new Error("Le fichier doit être une image")); + } + }, +}); + +/** + * Route d'upload d'images vers S3 + * POST /api/upload-image + * Body: multipart/form-data avec un champ "file" + */ +router.post("/", upload.single("file"), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ + success: false, + message: "Aucun fichier fourni", + }); + } + + const file = req.file; + + // Générer un nom de fichier unique + const randomSuffix = randomBytes(8).toString("hex"); + const extension = file.originalname.split(".").pop() || "jpg"; + const fileName = `attestations/${Date.now()}-${randomSuffix}.${extension}`; + + // Upload vers S3 + const result = await storagePut( + fileName, + file.buffer, + file.mimetype + ); + + return res.json({ + success: true, + url: result.url, + s3Key: fileName, + message: "Image uploadée avec succès", + }); + } catch (error) { + console.error("[UploadImage] Error:", error); + return res.status(500).json({ + success: false, + message: "Erreur lors de l'upload de l'image", + }); + } +}); + +export default router; diff --git a/server/attestationService.ts b/server/attestationService.ts index 94da1c3..a80c865 100644 --- a/server/attestationService.ts +++ b/server/attestationService.ts @@ -79,7 +79,19 @@ export async function genererAttestationPDF(inscriptionId: number): Promise<{ s3 doc.on("error", reject); // Construction du PDF - try { + (async () => { + try { + // Logo en haut à droite si disponible + if (config?.logoUrl) { + try { + const logoResponse = await fetch(config.logoUrl); + const logoBuffer = Buffer.from(await logoResponse.arrayBuffer()); + doc.image(logoBuffer, doc.page.width - 150, 50, { width: 100 }); + } catch (error) { + console.error("Erreur lors du chargement du logo:", error); + } + } + // Titre doc.fontSize(24).font("Helvetica-Bold").text("ATTESTATION DE FORMATION", { align: "center" }); doc.moveDown(2); @@ -125,18 +137,33 @@ export async function genererAttestationPDF(inscriptionId: number): Promise<{ s3 doc.text(`Fait le ${format(new Date(), "dd MMMM yyyy", { locale: fr })}`, { align: "right" }); doc.moveDown(3); - if (config?.nomSignataire) { - doc.text(config.nomSignataire, { align: "right" }); - } - if (config?.fonctionSignataire) { - doc.text(config.fonctionSignataire, { align: "right" }); - } + // Signature si disponible + if (config?.signatureUrl) { + try { + const signatureResponse = await fetch(config.signatureUrl); + const signatureBuffer = Buffer.from(await signatureResponse.arrayBuffer()); + const signatureX = doc.page.width - 200; + const signatureY = doc.y; + doc.image(signatureBuffer, signatureX, signatureY, { width: 150, height: 50 }); + doc.moveDown(3); + } catch (error) { + console.error("Erreur lors du chargement de la signature:", error); + } + } - doc.end(); - } catch (error) { - doc.end(); - reject(error); - } + if (config?.nomSignataire) { + doc.text(config.nomSignataire, { align: "right" }); + } + if (config?.fonctionSignataire) { + doc.text(config.fonctionSignataire, { align: "right" }); + } + + doc.end(); + } catch (error) { + doc.end(); + reject(error); + } + })(); }); } diff --git a/server/routers.ts b/server/routers.ts index e0e0bde..5a4758e 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -904,8 +904,9 @@ export const appRouter = router({ email: z.string().email().optional(), username: z.string().min(3).optional(), password: z.string().min(6).optional(), - role: z.enum(["user", "admin"]).default("user"), + role: z.enum(["user", "admin", "formateur"]).default("user"), isActive: z.boolean().default(true), + formateurId: z.number().optional(), })).mutation(async ({ input }) => { await db.createUser(input); return { success: true }; @@ -917,8 +918,9 @@ export const appRouter = router({ email: z.string().email().optional(), username: z.string().min(3).optional(), password: z.string().min(6).optional(), - role: z.enum(["user", "admin"]).optional(), + role: z.enum(["user", "admin", "formateur"]).optional(), isActive: z.boolean().optional(), + formateurId: z.number().optional(), })).mutation(async ({ input }) => { const { id, ...data } = input; await db.updateUser(id, data); @@ -1821,6 +1823,10 @@ export const appRouter = router({ nomSignataire: z.string().optional(), fonctionSignataire: z.string().optional(), texteAttestation: z.string().optional(), + logoUrl: z.string().optional(), + logoS3Key: z.string().optional(), + signatureUrl: z.string().optional(), + signatureS3Key: z.string().optional(), })) .mutation(async ({ input }) => { const { updateConfigAttestation } = await import("./attestationService"); diff --git a/todo.md b/todo.md index 9ecdf42..68c3d19 100644 --- a/todo.md +++ b/todo.md @@ -450,3 +450,19 @@ - [x] Créer la page de configuration - [x] Intégrer le bouton dans la page de validation des présences - [x] Tester la génération complète + +## Authentification et accès formateurs +- [x] Analyser le système d'authentification actuel (users, roles) +- [x] Créer le rôle "formateur" dans la base de données +- [x] Lier les comptes utilisateurs aux formateurs existants (champ formateurId) +- [x] Utiliser la page de connexion locale existante +- [x] Implémenter la redirection selon le rôle (admin → /admin, formateur → /formateur) +- [ ] Tester la connexion et l'accès aux fonctionnalités formateur + +## Upload logo et signature pour attestations +- [x] Ajouter les champs logoUrl et signatureUrl dans configAttestation (déjà existants) +- [x] Créer le composant d'upload d'images vers S3 (ImageUpload.tsx) +- [x] Créer l'endpoint d'upload serveur (/api/upload-image) +- [x] Intégrer l'upload dans la page de configuration des attestations +- [x] Modifier le service PDF pour inclure le logo et la signature +- [ ] Tester la génération d'attestations avec logo et signature personnalisés