diff --git a/client/src/App.tsx b/client/src/App.tsx index dce7178..c31bdd8 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -40,6 +40,7 @@ import Login from "./pages/Login"; import FormateurDashboard from "./pages/FormateurDashboard"; import FormateurEmargement from "./pages/FormateurEmargement"; import EmargementScan from "./pages/EmargementScan"; +import EmargementFormateurScan from "./pages/EmargementFormateurScan"; import SignerAttestation from "./pages/SignerAttestation"; import ForgotPassword from "./pages/ForgotPassword"; import ResetPassword from "./pages/ResetPassword"; @@ -85,6 +86,7 @@ function Router() { + {/* Final fallback route */} diff --git a/client/src/pages/EmargementFormateurScan.tsx b/client/src/pages/EmargementFormateurScan.tsx new file mode 100644 index 0000000..359198e --- /dev/null +++ b/client/src/pages/EmargementFormateurScan.tsx @@ -0,0 +1,258 @@ +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { trpc } from "@/lib/trpc"; +import { CheckCircle2, Loader2, XCircle, PenTool } from "lucide-react"; +import { useState } from "react"; +import { useParams } from "wouter"; +import { toast } from "sonner"; +import SignaturePadComponent from "@/components/SignaturePad"; + +export default function EmargementFormateurScan() { + const { token } = useParams<{ token: string }>(); + const [email, setEmail] = useState(""); + const [selectedPeriode, setSelectedPeriode] = useState<"matin" | "apres_midi">("matin"); + const [success, setSuccess] = useState(false); + const [heureEnregistrement, setHeureEnregistrement] = useState(null); + const [error, setError] = useState(null); + const [showSignaturePad, setShowSignaturePad] = useState(false); + const [signatureDataUrl, setSignatureDataUrl] = useState(null); + + // Valider la présence formateur + const { mutate: validerPresence, isPending: validating } = trpc.presences.valider.useMutation({ + onSuccess: () => { + const now = new Date(); + setHeureEnregistrement(now); + setSuccess(true); + toast.success("Présence formateur validée avec succès !"); + }, + onError: (error) => { + setError(error.message); + toast.error("Erreur lors de la validation", { + description: error.message, + }); + }, + }); + + const handleSaveSignature = (dataUrl: string) => { + setSignatureDataUrl(dataUrl); + setShowSignaturePad(false); + // Valider automatiquement après la signature + handleValiderWithSignature(dataUrl); + }; + + const handleValider = () => { + if (!email) { + toast.error("Veuillez saisir votre email"); + return; + } + + // Afficher le pad de signature + setShowSignaturePad(true); + }; + + const handleValiderWithSignature = (dataUrl: string) => { + if (!email) { + toast.error("Veuillez saisir votre email"); + return; + } + + validerPresence({ + token: token || "", + email, + periode: selectedPeriode, + modeValidation: "qrcode", + signatureDataUrl: dataUrl, + typeUtilisateur: "formateur", + }); + }; + + if (success) { + return ( +
+ + +
+ +
+ + Présence formateur validée ! + + + Votre présence en tant que formateur a été enregistrée avec succès + +
+ + {heureEnregistrement && ( +
+

+ 🕒 Heure d'enregistrement +

+

+ {heureEnregistrement.toLocaleTimeString("fr-FR", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + })} +

+

+ {heureEnregistrement.toLocaleDateString("fr-FR", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + })} +

+
+ )} +

+ Merci d'avoir confirmé votre présence. Vous pouvez maintenant fermer cette page. +

+ +
+
+
+ ); + } + + if (error) { + return ( +
+ + +
+ +
+ + Erreur + + + {error} + +
+ + + +
+
+ ); + } + + if (showSignaturePad) { + return ( +
+ + +
+ +
+ + Signature formateur + + + Signez avec votre doigt ou votre souris pour valider votre présence + +
+ + setShowSignaturePad(false)} + /> + +
+
+ ); + } + + return ( +
+ + + + Émargement Formateur + + + Validez votre présence en tant que formateur + + + +
+ + setEmail(e.target.value)} + disabled={validating} + /> +
+ +
+ + +
+ + +
+
+
+ ); +} diff --git a/client/src/pages/FormateurEmargement.tsx b/client/src/pages/FormateurEmargement.tsx index 0b54af9..b117782 100644 --- a/client/src/pages/FormateurEmargement.tsx +++ b/client/src/pages/FormateurEmargement.tsx @@ -26,13 +26,14 @@ export default function FormateurEmargement() { const { user } = useAuth(); const [selectedSequenceId, setSelectedSequenceId] = useState(null); const [showQRDialog, setShowQRDialog] = useState(false); + const [showQRFormateurDialog, setShowQRFormateurDialog] = useState(false); const [lastRefreshTime, setLastRefreshTime] = useState(new Date()); const [secondsSinceRefresh, setSecondsSinceRefresh] = useState(0); // Récupérer toutes les séquences (filtrées côté serveur pour les formateurs) const { data: sequences, isLoading: loadingSequences } = trpc.sequences.list.useQuery(); - // Générer le QR code + // Générer le QR code apprenants const { data: qrData, mutate: generateQR, isPending: generatingQR } = trpc.presences.generateQRCode.useMutation({ onSuccess: () => { setShowQRDialog(true); @@ -44,6 +45,18 @@ export default function FormateurEmargement() { }, }); + // Générer le QR code formateur + const { data: qrFormateurData, mutate: generateQRFormateur, isPending: generatingQRFormateur } = trpc.presences.generateQRCodeFormateur.useMutation({ + onSuccess: () => { + setShowQRFormateurDialog(true); + }, + onError: (error) => { + toast.error("Erreur lors de la génération du QR code formateur", { + description: error.message, + }); + }, + }); + // Récupérer les inscrits avec les dates de formation const { data: inscrits, isLoading: loadingInscrits, refetch: refetchInscrits } = trpc.inscriptions.listWithDates.useQuery( { sequenceId: selectedSequenceId || 0 }, @@ -121,6 +134,14 @@ export default function FormateurEmargement() { generateQR({ sequenceId: selectedSequenceId }); }; + const handleGenerateQRFormateur = () => { + if (!selectedSequenceId) { + toast.error("Veuillez sélectionner une séquence"); + return; + } + generateQRFormateur({ sequenceId: selectedSequenceId }); + }; + const handleValiderPresence = (inscriptionId: number, dateFormationId: number, periode: "matin" | "apres_midi") => { validerPresence({ inscriptionId, @@ -209,7 +230,7 @@ export default function FormateurEmargement() { {selectedSequenceId && (
-
+
+ @@ -451,11 +485,11 @@ export default function FormateurEmargement() { )} - {/* Dialog QR Code */} + {/* Dialog QR Code Apprenants */} - QR Code d'émargement + QR Code d'émargement apprenants Les apprenants doivent scanner ce QR code pour valider leur présence @@ -475,6 +509,30 @@ export default function FormateurEmargement() { + {/* Dialog QR Code Formateur */} + + + + QR Code d'émargement formateur + + Scannez ce QR code pour valider votre présence en tant que formateur + + + {qrFormateurData && ( +
+ QR Code Formateur +

+ Mode formateur : redirige vers la page d'émargement dédiée +

+
+ )} +
+
+ {/* Dialog Signature */} diff --git a/drizzle/meta/0017_snapshot.json b/drizzle/meta/0017_snapshot.json new file mode 100644 index 0000000..d6392aa --- /dev/null +++ b/drizzle/meta/0017_snapshot.json @@ -0,0 +1,2298 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "6682663d-4fa8-4b5d-80ff-8ce549ff3118", + "prevId": "e32d3d82-3520-4f4d-888a-1faa027d6128", + "tables": { + "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": false, + "autoincrement": false + }, + "fonction": { + "name": "fonction", + "type": "varchar(100)", + "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": { + "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 + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inscriptionId": { + "name": "inscriptionId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "urlPdf": { + "name": "urlPdf", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "s3Key": { + "name": "s3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "documentUrl": { + "name": "documentUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "documentS3Key": { + "name": "documentS3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uploadedBy": { + "name": "uploadedBy", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "uploadedAt": { + "name": "uploadedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailEnvoye": { + "name": "emailEnvoye", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "dateEnvoiEmail": { + "name": "dateEnvoiEmail", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureUrl": { + "name": "signatureUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureS3Key": { + "name": "signatureS3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateSigned": { + "name": "dateSigned", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateGeneration": { + "name": "dateGeneration", + "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 + }, + "texteAttestation": { + "name": "texteAttestation", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nomSignataire": { + "name": "nomSignataire", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fonctionSignataire": { + "name": "fonctionSignataire", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logoUrl": { + "name": "logoUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logoS3Key": { + "name": "logoS3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureUrl": { + "name": "signatureUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureS3Key": { + "name": "signatureS3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "ordre": { + "name": "ordre", + "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 + }, + "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": "enum('resend','smtp','simulation')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'simulation'" + }, + "apiKey": { + "name": "apiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "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 + }, + "smtpUser": { + "name": "smtpUser", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "smtpPassword": { + "name": "smtpPassword", + "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 + }, + "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": "enum('inscription','teaser','rappel','rappelJ1','rappel1','rappel2','rappel3','rappel4','rappel5','rappel6','reset_password','attestation')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "titre": { + "name": "titre", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bodyContent": { + "name": "bodyContent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "couleurPrincipale": { + "name": "couleurPrincipale", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#283581'" + }, + "couleurSecondaire": { + "name": "couleurSecondaire", + "type": "varchar(7)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#0578BE'" + }, + "piedDePage": { + "name": "piedDePage", + "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": { + "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 + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateEnvoi": { + "name": "dateEnvoi", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "dateReponse": { + "name": "dateReponse", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "statut": { + "name": "statut", + "type": "enum('envoye','repondu')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'envoye'" + } + }, + "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 + }, + "modeAttestation": { + "name": "modeAttestation", + "type": "enum('auto','manuel')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'auto'" + }, + "modeEnvoi": { + "name": "modeEnvoi", + "type": "enum('auto','manuel')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manuel'" + }, + "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": {} + }, + "historiqueAttestations": { + "name": "historiqueAttestations", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateEnvoi": { + "name": "dateEnvoi", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "statut": { + "name": "statut", + "type": "enum('envoye','erreur')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messageErreur": { + "name": "messageErreur", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "urlAttestation": { + "name": "urlAttestation", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "modeEnvoi": { + "name": "modeEnvoi", + "type": "enum('manuel','auto')", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'manuel'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "historiqueAttestations_id": { + "name": "historiqueAttestations_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "historiqueParametres": { + "name": "historiqueParametres", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userName": { + "name": "userName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "champModifie": { + "name": "champModifie", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ancienneValeur": { + "name": "ancienneValeur", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nouvelleValeur": { + "name": "nouvelleValeur", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateModification": { + "name": "dateModification", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "historiqueParametres_id": { + "name": "historiqueParametres_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "inscriptions": { + "name": "inscriptions", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "statut": { + "name": "statut", + "type": "enum('confirmee','en_attente','annulee')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'confirmee'" + }, + "dateInscription": { + "name": "dateInscription", + "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','attestation')", + "primaryKey": false, + "notNull": true, + "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 + }, + "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 + }, + "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 + }, + "modeEnvoi": { + "name": "modeEnvoi", + "type": "enum('manuel','auto')", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'manuel'" + }, + "dateEnvoi": { + "name": "dateEnvoi", + "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 + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('rappel1','rappel2','rappel3','rappel4','rappel5','rappel6')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "typeEnvoi": { + "name": "typeEnvoi", + "type": "enum('automatique','test')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'automatique'" + }, + "statut": { + "name": "statut", + "type": "enum('succes','echec')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "messageErreur": { + "name": "messageErreur", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nbTentatives": { + "name": "nbTentatives", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "prochainEssai": { + "name": "prochainEssai", + "type": "datetime", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateEnvoi": { + "name": "dateEnvoi", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "logsrappels_id": { + "name": "logsrappels_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "parametres": { + "name": "parametres", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "urlPublique": { + "name": "urlPublique", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'https://formations.itinova.org'" + }, + "delaiExpirationQR": { + "name": "delaiExpirationQR", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "dureeValiditeToken": { + "name": "dureeValiditeToken", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 24 + }, + "notificationsActives": { + "name": "notificationsActives", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "parametres_id": { + "name": "parametres_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "passwordResetTokens": { + "name": "passwordResetTokens", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "email": { + "name": "email", + "type": "varchar(320)", + "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": {} + }, + "presences": { + "name": "presences", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "inscriptionId": { + "name": "inscriptionId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateFormationId": { + "name": "dateFormationId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "periode": { + "name": "periode", + "type": "enum('matin','apres_midi')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'matin'" + }, + "heurePresence": { + "name": "heurePresence", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "modeValidation": { + "name": "modeValidation", + "type": "enum('qrcode','manuel')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "validateurId": { + "name": "validateurId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commentaire": { + "name": "commentaire", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureUrl": { + "name": "signatureUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureS3Key": { + "name": "signatureS3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "presences_id": { + "name": "presences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "questionnaires": { + "name": "questionnaires", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "titre": { + "name": "titre", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "enum('satisfaction','evaluation_pre','evaluation_post')", + "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": { + "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 + }, + "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 + }, + "valeurMin": { + "name": "valeurMin", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "valeurMax": { + "name": "valeurMax", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "obligatoire": { + "name": "obligatoire", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 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": { + "questions_id": { + "name": "questions_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "rappelDates": { + "name": "rappelDates", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "rappelId": { + "name": "rappelId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateFormationId": { + "name": "dateFormationId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rappelDates_id": { + "name": "rappelDates_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": "enum('rappel1','rappel2','rappel3','rappel4','rappel5','rappel6')", + "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'" + }, + "timing": { + "name": "timing", + "type": "enum('pre_formation','post_formation')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pre_formation'" + }, + "nomFichier": { + "name": "nomFichier", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "urlFichier": { + "name": "urlFichier", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "s3Key": { + "name": "s3Key", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typeFichier": { + "name": "typeFichier", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tailleFichier": { + "name": "tailleFichier", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nomFichier2": { + "name": "nomFichier2", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "urlFichier2": { + "name": "urlFichier2", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "s3Key2": { + "name": "s3Key2", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typeFichier2": { + "name": "typeFichier2", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tailleFichier2": { + "name": "tailleFichier2", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actif": { + "name": "actif", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "derniereExecution": { + "name": "derniereExecution", + "type": "datetime", + "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 + }, + "sequenceId": { + "name": "sequenceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "apprenantId": { + "name": "apprenantId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dateReponse": { + "name": "dateReponse", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "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 + }, + "reponseTexte": { + "name": "reponseTexte", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reponseNumerique": { + "name": "reponseNumerique", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "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(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "capaciteMax": { + "name": "capaciteMax", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "statut": { + "name": "statut", + "type": "enum('ouverte','bloquee','terminee','fermee','annulee')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'ouverte'" + }, + "publicCible": { + "name": "publicCible", + "type": "enum('directeur','chef_service','autre','tous')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'tous'" + }, + "formateurId": { + "name": "formateurId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dateBlocage": { + "name": "dateBlocage", + "type": "datetime", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "qrCodeToken": { + "name": "qrCodeToken", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "qrCodeFormateurToken": { + "name": "qrCodeFormateurToken", + "type": "varchar(100)", + "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": { + "sequences_id": { + "name": "sequences_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "sequences_qrCodeToken_unique": { + "name": "sequences_qrCodeToken_unique", + "columns": [ + "qrCodeToken" + ] + }, + "sequences_qrCodeFormateurToken_unique": { + "name": "sequences_qrCodeFormateurToken_unique", + "columns": [ + "qrCodeFormateurToken" + ] + } + }, + "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(100)", + "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())" + } + }, + "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','super_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 b928c8b..9657d9b 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -120,6 +120,13 @@ "when": 1770047974825, "tag": "0016_aromatic_king_cobra", "breakpoints": true + }, + { + "idx": 17, + "version": "5", + "when": 1770134309991, + "tag": "0017_slimy_azazel", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 53cdc4c..71a033c 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -90,8 +90,10 @@ export const sequences = mysqlTable("sequences", { formateurId: int("formateurId"), /** Date limite d'inscription (blocage) */ dateBlocage: datetime("dateBlocage"), - /** Token unique pour le QR code d'émargement */ + /** Token unique pour le QR code d'émargement apprenants */ qrCodeToken: varchar("qrCodeToken", { length: 100 }).unique(), + /** Token unique pour le QR code d'émargement formateur */ + qrCodeFormateurToken: varchar("qrCodeFormateurToken", { length: 100 }).unique(), createdAt: timestamp("createdAt").defaultNow().notNull(), updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), }); diff --git a/server/presenceDb.ts b/server/presenceDb.ts index 3b231db..5de7c37 100644 --- a/server/presenceDb.ts +++ b/server/presenceDb.ts @@ -185,3 +185,76 @@ export async function supprimerPresence(presenceId: number) { return { success: true }; } + +/** + * Valider la présence d'un formateur (par QR code ou manuellement) + */ +export async function validerPresenceFormateur(params: { + formateurId: number; + sequenceId: number; + dateFormationId: number; + periode: "matin" | "apres_midi"; + modeValidation: "qrcode" | "manuel"; + commentaire?: string; + signatureUrl?: string; + signatureS3Key?: string; +}) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Récupérer la date de formation + const dateFormation = await db + .select() + .from(datesFormation) + .where(eq(datesFormation.id, params.dateFormationId)) + .limit(1); + + if (dateFormation.length === 0) { + throw new Error("Date de formation introuvable"); + } + + // Vérifier que la date actuelle est le jour de la formation ou après + const now = new Date(); + const dateDebut = new Date(dateFormation[0].dateDebut); + + // Réinitialiser les heures pour comparer uniquement les dates + now.setHours(0, 0, 0, 0); + dateDebut.setHours(0, 0, 0, 0); + + if (now < dateDebut) { + throw new Error("Vous ne pouvez pas émarger avant la date de formation"); + } + + // Vérifier si la présence existe déjà pour cette période + const presenceExistante = await db + .select() + .from(presences) + .where( + and( + eq(presences.formateurId, params.formateurId), + eq(presences.dateFormationId, params.dateFormationId), + eq(presences.periode, params.periode), + eq(presences.typeUtilisateur, 'formateur') + ) + ); + + if (presenceExistante.length > 0) { + throw new Error(`Présence déjà validée pour ${params.periode === 'matin' ? 'le matin' : 'l\'après-midi'}`); + } + + // Créer la présence formateur + await db.insert(presences).values({ + formateurId: params.formateurId, + dateFormationId: params.dateFormationId, + periode: params.periode, + typeUtilisateur: 'formateur', + heurePresence: new Date(), + modeValidation: params.modeValidation, + commentaire: params.commentaire, + signatureUrl: params.signatureUrl, + signatureS3Key: params.signatureS3Key, + dateSigned: params.signatureUrl ? new Date() : undefined, + }); + + return { success: true }; +} diff --git a/server/routers.ts b/server/routers.ts index 881ea28..f1f0c42 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -2509,17 +2509,69 @@ export const appRouter = router({ }; }), + // Générer un QR code pour le formateur + generateQRCodeFormateur: protectedProcedure + .input(z.object({ + sequenceId: z.number(), + })) + .mutation(async ({ input, ctx }) => { + const { generateQRToken } = await import("./qrCodeGenerator"); + const { getDb } = await import("./db"); + const { sequences } = await import("../drizzle/schema"); + const { eq } = await import("drizzle-orm"); + const QRCode = (await import("qrcode")).default; + const { getParametres } = await import("./parametresDb"); + + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Vérifier que la séquence existe + const sequence = await db.select().from(sequences).where(eq(sequences.id, input.sequenceId)).limit(1); + if (sequence.length === 0) { + throw new Error("Séquence introuvable"); + } + + // Générer un nouveau token pour le formateur + let token = sequence[0].qrCodeFormateurToken; + if (!token) { + token = generateQRToken(); + await db.update(sequences).set({ qrCodeFormateurToken: token }).where(eq(sequences.id, input.sequenceId)); + } + + // Générer le QR code avec URL spécifique formateur + const parametres = await getParametres(); + const url = `${parametres.urlPublique}/emargement-formateur/${token}`; + + const qrCodeDataURL = await QRCode.toDataURL(url, { + errorCorrectionLevel: "H", + type: "image/png", + width: 400, + margin: 2, + color: { + dark: "#000000", + light: "#FFFFFF", + }, + }); + + return { + token, + qrCodeDataURL, + }; + }), + // Valider une présence (scan QR code ou manuel) valider: publicProcedure .input(z.object({ token: z.string().optional(), - inscriptionId: z.number(), - dateFormationId: z.number(), + inscriptionId: z.number().optional(), + dateFormationId: z.number().optional(), periode: z.enum(["matin", "apres_midi"]), modeValidation: z.enum(["qrcode", "manuel"]), validateurId: z.number().optional(), commentaire: z.string().optional(), signatureDataUrl: z.string().optional(), + email: z.string().optional(), + typeUtilisateur: z.enum(["apprenant", "formateur"]).optional(), })) .mutation(async ({ input }) => { const { validerPresence } = await import("./presenceDb"); @@ -2530,10 +2582,91 @@ export const appRouter = router({ const db = await getDb(); if (!db) throw new Error("Database not available"); + // Cas formateur : logique spécifique + if (input.typeUtilisateur === "formateur") { + const { formateurs, datesFormation } = await import("../drizzle/schema"); + const { and, sql } = await import("drizzle-orm"); + const { storagePut } = await import("./storage"); + const crypto = await import("crypto"); + + // Vérifier le token formateur + if (input.modeValidation === "qrcode" && input.token) { + const sequence = await db.select().from(sequences).where(eq(sequences.qrCodeFormateurToken, input.token)).limit(1); + if (sequence.length === 0) { + throw new Error("QR code formateur invalide"); + } + } + + // Récupérer le formateur par email + if (!input.email) throw new Error("Email formateur requis"); + const formateurData = await db.select().from(formateurs).where(eq(formateurs.email, input.email)).limit(1); + if (formateurData.length === 0) { + throw new Error("Formateur introuvable avec cet email"); + } + + // Trouver la séquence associée au token + const sequenceData = await db.select().from(sequences).where(eq(sequences.qrCodeFormateurToken, input.token || "")).limit(1); + if (sequenceData.length === 0) { + throw new Error("Séquence introuvable"); + } + const finalSequenceId = sequenceData[0].id; + + // Trouver automatiquement la date du jour + const today = new Date(); + today.setHours(0, 0, 0, 0); + const datesToday = await db + .select() + .from(datesFormation) + .where( + and( + eq(datesFormation.sequenceId, finalSequenceId), + sql`DATE(${datesFormation.dateDebut}) <= CURDATE()`, + sql`DATE(${datesFormation.dateFin}) >= CURDATE()` + ) + ) + .limit(1); + if (datesToday.length === 0) { + throw new Error("Aucune date de formation ne correspond à aujourd'hui"); + } + const dateFormId = datesToday[0].id; + + // Stocker la signature sur S3 + let signatureUrl: string | undefined; + let signatureS3Key: string | undefined; + if (input.signatureDataUrl) { + const formationId = sequenceData[0].formationId; + const sequenceId = sequenceData[0].id; + const randomSuffix = crypto.randomBytes(8).toString("hex"); + const fileName = `signature-formateur-${formateurData[0].id}-${dateFormId}-${randomSuffix}.png`; + const signatureS3Key = `signatures/${formationId}-${sequenceId}/${fileName}`; + const base64Data = input.signatureDataUrl.split(",")[1]; + const buffer = Buffer.from(base64Data, "base64"); + const { url } = await storagePut(signatureS3Key, buffer, "image/png"); + signatureUrl = url; + } + + // Valider la présence formateur + const { validerPresenceFormateur } = await import("./presenceDb"); + const result = await validerPresenceFormateur({ + formateurId: formateurData[0].id, + sequenceId: finalSequenceId, + dateFormationId: dateFormId, + periode: input.periode, + modeValidation: input.modeValidation, + commentaire: input.commentaire, + signatureUrl, + signatureS3Key, + }); + + return { success: true, presence: result }; + } + + // Cas apprenant : logique existante // Si mode QR code, vérifier le token if (input.modeValidation === "qrcode" && input.token) { // Récupérer l'inscription pour vérifier la séquence + if (!input.inscriptionId) throw new Error("Inscription requise pour un apprenant"); const inscription = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1); if (inscription.length === 0) { throw new Error("Inscription introuvable"); @@ -2546,6 +2679,10 @@ export const appRouter = router({ } } + // Vérifier que inscriptionId et dateFormationId sont fournis pour un apprenant + if (!input.inscriptionId) throw new Error("Inscription requise pour un apprenant"); + if (!input.dateFormationId) throw new Error("Date de formation requise pour un apprenant"); + // Stocker la signature localement si fournie let signatureUrl: string | undefined; let signatureS3Key: string | undefined; diff --git a/todo.md b/todo.md index fafc099..6d4a5a6 100644 --- a/todo.md +++ b/todo.md @@ -1502,3 +1502,28 @@ - [x] Empêcher l'émargement avant la date de formation concernée (vérification backend) - [ ] Permettre aux formateurs d'émarger par QR code (matin et après-midi) avec signature tactile (en cours - à finaliser lors d'une prochaine session) + +## QR Code Formateur (Réimplémentation méthodique) + +### Phase 1 : Analyse +- [x] Analyser le code existant pour le QR code apprenant +- [x] Identifier les modifications nécessaires pour le formateur +- [x] Planifier l'architecture de la solution + +### Phase 2 : Implémentation +- [ ] Ajouter le bouton "QR Code Formateur" dans FormateurEmargement.tsx +- [ ] Créer/modifier la procédure backend generateQRCodeFormateur +- [ ] Implémenter la logique de validation de présence formateur +- [ ] Gérer la détection automatique de la date du jour + +### Phase 3 : Tests locaux +- [ ] Tester la génération du QR code formateur +- [ ] Tester le scan et la validation de signature +- [ ] Vérifier l'enregistrement en base de données +- [ ] Tester les cas d'erreur (date invalide, doublon, etc.) + +### Phase 4 : Déploiement +- [ ] Créer un checkpoint de la version testée +- [ ] Déployer sur le VPS +- [ ] Tester en production +- [ ] Valider avec l'utilisateur