diff --git a/.manus/db/db-query-1770056170569.json b/.manus/db/db-query-1770056170569.json
new file mode 100644
index 0000000..bd45008
--- /dev/null
+++ b/.manus/db/db-query-1770056170569.json
@@ -0,0 +1,9 @@
+{
+ "query": "ALTER TABLE `presences` MODIFY COLUMN `inscriptionId` int;\nALTER TABLE `presences` ADD `typeUtilisateur` enum('apprenant','formateur') DEFAULT 'apprenant' NOT NULL;\nALTER TABLE `presences` ADD `formateurId` int;",
+ "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 `presences` MODIFY COLUMN `inscriptionId` int;\nALTER TABLE `presences` ADD `typeUtilisateur` enum('apprenant','formateur') DEFAULT 'apprenant' NOT NULL;\nALTER TABLE `presences` ADD `formateurId` int;",
+ "rows": [],
+ "messages": [],
+ "stdout": "",
+ "stderr": "",
+ "execution_time_ms": 1467
+}
\ No newline at end of file
diff --git a/client/src/pages/EmargementScan.tsx b/client/src/pages/EmargementScan.tsx
index 283aab3..59d816e 100644
--- a/client/src/pages/EmargementScan.tsx
+++ b/client/src/pages/EmargementScan.tsx
@@ -92,6 +92,7 @@ export default function EmargementScan() {
validerPresence({
token,
+ email,
inscriptionId: selectedInscriptionId,
dateFormationId: selectedDateId,
periode: selectedPeriode,
diff --git a/client/src/pages/FormateurEmargement.tsx b/client/src/pages/FormateurEmargement.tsx
index 0b54af9..78a9cc9 100644
--- a/client/src/pages/FormateurEmargement.tsx
+++ b/client/src/pages/FormateurEmargement.tsx
@@ -457,7 +457,7 @@ export default function FormateurEmargement() {
QR Code d'émargement
- Les apprenants doivent scanner ce QR code pour valider leur présence
+ Les apprenants et formateurs doivent scanner ce QR code pour valider leur présence avec signature tactile
{qrData && (
diff --git a/drizzle/meta/0017_snapshot.json b/drizzle/meta/0017_snapshot.json
new file mode 100644
index 0000000..9916bee
--- /dev/null
+++ b/drizzle/meta/0017_snapshot.json
@@ -0,0 +1,2300 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "0084eddc-561c-4ad1-a76b-da67dbd0f00a",
+ "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": false,
+ "autoincrement": false
+ },
+ "dateFormationId": {
+ "name": "dateFormationId",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "typeUtilisateur": {
+ "name": "typeUtilisateur",
+ "type": "enum('apprenant','formateur')",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'apprenant'"
+ },
+ "formateurId": {
+ "name": "formateurId",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "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
+ },
+ "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"
+ ]
+ }
+ },
+ "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..6100909 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": 1770056161269,
+ "tag": "0017_lethal_namor",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/drizzle/schema.ts b/drizzle/schema.ts
index 53cdc4c..446e81a 100644
--- a/drizzle/schema.ts
+++ b/drizzle/schema.ts
@@ -414,8 +414,12 @@ export type InsertSupportFormation = typeof supportsFormation.$inferInsert;
*/
export const presences = mysqlTable("presences", {
id: int("id").autoincrement().primaryKey(),
- inscriptionId: int("inscriptionId").notNull(),
+ inscriptionId: int("inscriptionId"),
dateFormationId: int("dateFormationId").notNull(),
+ /** Type d'utilisateur qui s'émarge (apprenant ou formateur) */
+ typeUtilisateur: mysqlEnum("typeUtilisateur", ["apprenant", "formateur"]).notNull().default("apprenant"),
+ /** ID du formateur (si typeUtilisateur = formateur) */
+ formateurId: int("formateurId"),
/** Période de la journée (matin ou après-midi) */
periode: mysqlEnum("periode", ["matin", "apres_midi"]).notNull().default("matin"),
heurePresence: timestamp("heurePresence").defaultNow().notNull(),
@@ -424,7 +428,7 @@ export const presences = mysqlTable("presences", {
/** ID de l'utilisateur qui a validé (formateur ou admin) */
validateurId: int("validateurId"),
commentaire: text("commentaire"),
- /** URL de la signature manuscrite de l'apprenant */
+ /** URL de la signature manuscrite (apprenant ou formateur) */
signatureUrl: varchar("signatureUrl", { length: 500 }),
/** Clé S3 de la signature manuscrite */
signatureS3Key: varchar("signatureS3Key", { length: 500 }),
diff --git a/server/__tests__/emargement-formateur.test.ts b/server/__tests__/emargement-formateur.test.ts
new file mode 100644
index 0000000..77dc768
--- /dev/null
+++ b/server/__tests__/emargement-formateur.test.ts
@@ -0,0 +1,164 @@
+import { describe, it, expect, beforeAll, afterAll } from 'vitest';
+import { getDb } from '../db';
+import { validerPresenceFormateur, getPresencesFormateurBySequence } from '../presenceDb';
+import { formateurs, sequences, datesFormation, presences } from '../../drizzle/schema';
+import { eq, and, sql } from 'drizzle-orm';
+
+describe('Émargement formateur par QR code', () => {
+ let testFormateurId: number;
+ let testSequenceId: number;
+ let testDateFormationId: number;
+ const timestamp = Date.now();
+
+ beforeAll(async () => {
+ const db = await getDb();
+ if (!db) throw new Error('Database not available');
+
+ // Créer un formateur de test
+ const formateurResult = await db.insert(formateurs).values({
+ nom: `Test Formateur Émargement ${timestamp}`,
+ email: `test.formateur.emargement.${timestamp}@test.com`,
+ });
+ testFormateurId = Number(formateurResult[0].insertId);
+
+ // Créer une séquence de test
+ const sequenceResult = await db.insert(sequences).values({
+ formationId: 1,
+ nom: 'Test Séquence Émargement',
+ lieu: 'Test Lieu',
+ capaciteMax: 10,
+ statut: 'ouverte',
+ publicCible: 'tous',
+ formateurId: testFormateurId,
+ });
+ testSequenceId = Number(sequenceResult[0].insertId);
+
+ // Créer une date de formation de test
+ const dateResult = await db.insert(datesFormation).values({
+ sequenceId: testSequenceId,
+ ordre: 1,
+ dateDebut: new Date(),
+ dateFin: new Date(Date.now() + 3600000),
+ });
+ testDateFormationId = Number(dateResult[0].insertId);
+ });
+
+ afterAll(async () => {
+ const db = await getDb();
+ if (!db) return;
+
+ // Nettoyer les données de test
+ await db.delete(presences).where(eq(presences.formateurId, testFormateurId));
+ await db.delete(datesFormation).where(eq(datesFormation.sequenceId, testSequenceId));
+ await db.delete(sequences).where(eq(sequences.id, testSequenceId));
+ await db.delete(formateurs).where(eq(formateurs.id, testFormateurId));
+ });
+
+ it('devrait valider la présence du formateur pour le matin', async () => {
+ const result = await validerPresenceFormateur({
+ formateurId: testFormateurId,
+ sequenceId: testSequenceId,
+ dateFormationId: testDateFormationId,
+ periode: 'matin',
+ modeValidation: 'qrcode',
+ });
+
+ expect(result.success).toBe(true);
+
+ // Vérifier que la présence a bien été créée
+ const db = await getDb();
+ if (!db) throw new Error('Database not available');
+
+ const presenceCreated = await db
+ .select()
+ .from(presences)
+ .where(
+ and(
+ eq(presences.formateurId, testFormateurId),
+ eq(presences.dateFormationId, testDateFormationId),
+ sql`${presences}.periode = 'matin'`,
+ sql`${presences}.typeUtilisateur = 'formateur'`
+ )
+ );
+
+ expect(presenceCreated.length).toBe(1);
+ expect(presenceCreated[0].typeUtilisateur).toBe('formateur');
+ expect(presenceCreated[0].modeValidation).toBe('qrcode');
+ });
+
+ it('devrait valider la présence du formateur pour l\'après-midi', async () => {
+ const result = await validerPresenceFormateur({
+ formateurId: testFormateurId,
+ sequenceId: testSequenceId,
+ dateFormationId: testDateFormationId,
+ periode: 'apres_midi',
+ modeValidation: 'qrcode',
+ });
+
+ expect(result.success).toBe(true);
+ });
+
+ it('devrait empêcher la double validation pour la même période', async () => {
+ await expect(
+ validerPresenceFormateur({
+ formateurId: testFormateurId,
+ sequenceId: testSequenceId,
+ dateFormationId: testDateFormationId,
+ periode: 'matin',
+ modeValidation: 'qrcode',
+ })
+ ).rejects.toThrow('Présence déjà validée');
+ });
+
+ it('devrait récupérer les présences du formateur pour une séquence', async () => {
+ const presencesFormateur = await getPresencesFormateurBySequence(testSequenceId);
+
+ expect(presencesFormateur.length).toBeGreaterThanOrEqual(2); // Matin + Après-midi
+ expect(presencesFormateur.every(p => p.formateurId === testFormateurId)).toBe(true);
+ });
+
+ it('devrait stocker la signature tactile du formateur', async () => {
+ const db = await getDb();
+ if (!db) throw new Error('Database not available');
+
+ // Créer une nouvelle date pour ce test
+ const dateResult = await db.insert(datesFormation).values({
+ sequenceId: testSequenceId,
+ ordre: 2,
+ dateDebut: new Date(),
+ dateFin: new Date(Date.now() + 3600000),
+ });
+ const newDateId = Number(dateResult[0].insertId);
+
+ const signatureUrl = '/uploads/signatures/test-formateur-signature.png';
+ const result = await validerPresenceFormateur({
+ formateurId: testFormateurId,
+ sequenceId: testSequenceId,
+ dateFormationId: newDateId,
+ periode: 'matin',
+ modeValidation: 'qrcode',
+ signatureUrl,
+ signatureS3Key: 'signatures/test-formateur-signature.png',
+ });
+
+ expect(result.success).toBe(true);
+
+ // Vérifier que la signature a été stockée
+ const presenceCreated = await db
+ .select()
+ .from(presences)
+ .where(
+ and(
+ eq(presences.formateurId, testFormateurId),
+ eq(presences.dateFormationId, newDateId),
+ sql`${presences}.periode = 'matin'`
+ )
+ );
+
+ expect(presenceCreated.length).toBe(1);
+ expect(presenceCreated[0].signatureUrl).toBe(signatureUrl);
+
+ // Nettoyer
+ await db.delete(datesFormation).where(eq(datesFormation.id, newDateId));
+ });
+});
diff --git a/server/exportService.ts b/server/exportService.ts
index 22a1a10..182f77d 100644
--- a/server/exportService.ts
+++ b/server/exportService.ts
@@ -36,9 +36,16 @@ interface ApprenantPresence {
dateFormationId: number;
periode: 'matin' | 'apres_midi';
signatureUrl: string | null;
+ typeUtilisateur?: 'apprenant' | 'formateur';
}>;
}
+interface FormateurPresence {
+ dateFormationId: number;
+ periode: 'matin' | 'apres_midi';
+ signatureUrl: string | null;
+}
+
interface FeuillePresenceInfo {
formationNom: string;
sequenceNom: string;
@@ -47,6 +54,7 @@ interface FeuillePresenceInfo {
publicCible: string;
formateur?: string;
apprenants: ApprenantPresence[];
+ presencesFormateur?: FormateurPresence[];
}
/**
@@ -301,14 +309,56 @@ export function generateFeuillePresence(feuilleInfo: FeuillePresenceInfo): Buffe
doc.text('Signatures du formateur :', 14, signatureY);
doc.setFont('helvetica', 'normal');
+ // Trouver les signatures du formateur pour cette date
+ const presenceFormateurMatin = feuilleInfo.presencesFormateur?.find(
+ p => p.dateFormationId === dateInfo.id && p.periode === 'matin'
+ );
+ const presenceFormateurApresMidi = feuilleInfo.presencesFormateur?.find(
+ p => p.dateFormationId === dateInfo.id && p.periode === 'apres_midi'
+ );
+
// Signature Matin
doc.setFontSize(9);
doc.text('Matin :', 14, signatureY + 8);
- doc.line(30, signatureY + 8, 90, signatureY + 8);
+
+ if (presenceFormateurMatin?.signatureUrl) {
+ try {
+ const signaturePath = path.resolve(process.cwd(), presenceFormateurMatin.signatureUrl.substring(1));
+ if (fs.existsSync(signaturePath)) {
+ const signatureBuffer = fs.readFileSync(signaturePath);
+ const signatureBase64 = `data:image/png;base64,${signatureBuffer.toString('base64')}`;
+ doc.addImage(signatureBase64, 'PNG', 30, signatureY + 2, 55, 10);
+ } else {
+ doc.line(30, signatureY + 8, 90, signatureY + 8);
+ }
+ } catch (error) {
+ console.error('Erreur lors du chargement de la signature formateur matin:', error);
+ doc.line(30, signatureY + 8, 90, signatureY + 8);
+ }
+ } else {
+ doc.line(30, signatureY + 8, 90, signatureY + 8);
+ }
// Signature Après-midi
doc.text('Après-midi :', 110, signatureY + 8);
- doc.line(140, signatureY + 8, 200, signatureY + 8);
+
+ if (presenceFormateurApresMidi?.signatureUrl) {
+ try {
+ const signaturePath = path.resolve(process.cwd(), presenceFormateurApresMidi.signatureUrl.substring(1));
+ if (fs.existsSync(signaturePath)) {
+ const signatureBuffer = fs.readFileSync(signaturePath);
+ const signatureBase64 = `data:image/png;base64,${signatureBuffer.toString('base64')}`;
+ doc.addImage(signatureBase64, 'PNG', 140, signatureY + 2, 55, 10);
+ } else {
+ doc.line(140, signatureY + 8, 200, signatureY + 8);
+ }
+ } catch (error) {
+ console.error('Erreur lors du chargement de la signature formateur après-midi:', error);
+ doc.line(140, signatureY + 8, 200, signatureY + 8);
+ }
+ } else {
+ doc.line(140, signatureY + 8, 200, signatureY + 8);
+ }
};
// Générer une page pour chaque date
diff --git a/server/presenceDb.ts b/server/presenceDb.ts
index 3b231db..e2cfbde 100644
--- a/server/presenceDb.ts
+++ b/server/presenceDb.ts
@@ -68,7 +68,6 @@ export async function validerPresence(params: {
commentaire: params.commentaire,
signatureUrl: params.signatureUrl,
signatureS3Key: params.signatureS3Key,
- dateSigned: params.signatureUrl ? new Date() : undefined,
});
return { success: true };
@@ -93,7 +92,6 @@ export async function getPresencesBySequence(sequenceId: number) {
commentaire: presences.commentaire,
signatureUrl: sql`${presences}.signatureUrl`,
signatureS3Key: sql`${presences}.signatureS3Key`,
- dateSigned: sql`${presences}.dateSigned`,
apprenantId: apprenants.id,
apprenantNom: apprenants.nom,
apprenantPrenom: apprenants.prenom,
@@ -128,7 +126,6 @@ export async function getPresencesByInscription(inscriptionId: number) {
commentaire: presences.commentaire,
signatureUrl: sql`${presences}.signatureUrl`,
signatureS3Key: sql`${presences}.signatureS3Key`,
- dateSigned: sql`${presences}.dateSigned`,
dateDebut: datesFormation.dateDebut,
dateFin: datesFormation.dateFin,
})
@@ -185,3 +182,106 @@ 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),
+ sql`${presences.periode} = ${params.periode}`,
+ sql`${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({
+ typeUtilisateur: "formateur",
+ formateurId: params.formateurId,
+ dateFormationId: params.dateFormationId,
+ periode: params.periode,
+ heurePresence: new Date(),
+ modeValidation: params.modeValidation,
+ commentaire: params.commentaire,
+ signatureUrl: params.signatureUrl,
+ signatureS3Key: params.signatureS3Key,
+ });
+
+ return { success: true };
+}
+
+/**
+ * Récupérer les présences du formateur pour une séquence
+ */
+export async function getPresencesFormateurBySequence(sequenceId: number) {
+ const db = await getDb();
+ if (!db) throw new Error("Database not available");
+
+ const result = await db
+ .select({
+ presenceId: presences.id,
+ formateurId: presences.formateurId,
+ dateFormationId: presences.dateFormationId,
+ periode: presences.periode,
+ heurePresence: presences.heurePresence,
+ modeValidation: presences.modeValidation,
+ commentaire: presences.commentaire,
+ signatureUrl: sql`${presences}.signatureUrl`,
+ signatureS3Key: sql`${presences}.signatureS3Key`,
+ })
+ .from(presences)
+ .innerJoin(datesFormation, eq(presences.dateFormationId, datesFormation.id))
+ .where(
+ and(
+ eq(datesFormation.sequenceId, sequenceId),
+ sql`${presences}.typeUtilisateur = 'formateur'`
+ )
+ );
+
+ return result;
+}
diff --git a/server/routers.ts b/server/routers.ts
index 881ea28..45ae6ee 100644
--- a/server/routers.ts
+++ b/server/routers.ts
@@ -1156,7 +1156,7 @@ export const appRouter = router({
}
// Récupérer toutes les présences avec signatures pour cette séquence
- const { getPresencesBySequence } = await import("./presenceDb");
+ const { getPresencesBySequence, getPresencesFormateurBySequence } = await import("./presenceDb");
const presences = await getPresencesBySequence(input.sequenceId);
const data = inscriptions
@@ -1180,6 +1180,9 @@ export const appRouter = router({
};
});
+ // Récupérer les présences du formateur
+ const presencesFormateur = await getPresencesFormateurBySequence(input.sequenceId);
+
const buffer = await generateFeuillePresence({
formationNom: formation.nom,
sequenceNom: sequence.nom,
@@ -1193,6 +1196,11 @@ export const appRouter = router({
publicCible: sequence.publicCible,
formateur: formateurNom,
apprenants: data,
+ presencesFormateur: presencesFormateur.map(p => ({
+ dateFormationId: p.dateFormationId,
+ periode: p.periode as 'matin' | 'apres_midi',
+ signatureUrl: p.signatureUrl,
+ })),
});
return {
@@ -2513,7 +2521,8 @@ export const appRouter = router({
valider: publicProcedure
.input(z.object({
token: z.string().optional(),
- inscriptionId: z.number(),
+ email: z.string().email(),
+ inscriptionId: z.number().optional(),
dateFormationId: z.number(),
periode: z.enum(["matin", "apres_midi"]),
modeValidation: z.enum(["qrcode", "manuel"]),
@@ -2522,57 +2531,77 @@ export const appRouter = router({
signatureDataUrl: z.string().optional(),
}))
.mutation(async ({ input }) => {
- const { validerPresence } = await import("./presenceDb");
+ const { validerPresence, validerPresenceFormateur } = await import("./presenceDb");
const { getDb } = await import("./db");
- const { sequences, inscriptions } = await import("../drizzle/schema");
+ const { sequences, inscriptions, formateurs } = await import("../drizzle/schema");
const { eq } = await import("drizzle-orm");
const db = await getDb();
if (!db) throw new Error("Database not available");
+ // Détecter si l'utilisateur est un formateur
+ const formateurData = await db.select().from(formateurs).where(eq(formateurs.email, input.email)).limit(1);
+ const isFormateur = formateurData.length > 0;
+
// Si mode QR code, vérifier le token
+ let sequenceId: number | undefined;
if (input.modeValidation === "qrcode" && input.token) {
-
- // Récupérer l'inscription pour vérifier la séquence
- const inscription = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1);
- if (inscription.length === 0) {
- throw new Error("Inscription introuvable");
- }
-
- // Vérifier que le token correspond à la séquence
- const sequence = await db.select().from(sequences).where(eq(sequences.id, inscription[0].sequenceId)).limit(1);
- if (sequence.length === 0 || sequence[0].qrCodeToken !== input.token) {
+ // Vérifier que le token correspond à une séquence
+ const sequence = await db.select().from(sequences).where(eq(sequences.qrCodeToken, input.token)).limit(1);
+ if (sequence.length === 0) {
throw new Error("QR code invalide");
}
+ sequenceId = sequence[0].id;
+
+ // Si apprenant, vérifier l'inscription
+ if (!isFormateur && input.inscriptionId) {
+ const inscription = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1);
+ if (inscription.length === 0) {
+ throw new Error("Inscription introuvable");
+ }
+ if (inscription[0].sequenceId !== sequenceId) {
+ throw new Error("QR code invalide pour cette inscription");
+ }
+ }
}
// Stocker la signature localement si fournie
let signatureUrl: string | undefined;
let signatureS3Key: string | undefined;
+ let finalSequenceId = sequenceId;
+
if (input.signatureDataUrl) {
const fs = await import("fs");
const path = await import("path");
const crypto = await import("crypto");
- // Récupérer l'inscription pour obtenir la séquence
- const inscriptionData = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1);
- if (inscriptionData.length === 0) throw new Error("Inscription introuvable");
+ // Déterminer la séquence
+ if (!finalSequenceId) {
+ if (input.inscriptionId) {
+ const inscriptionData = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1);
+ if (inscriptionData.length === 0) throw new Error("Inscription introuvable");
+ finalSequenceId = inscriptionData[0].sequenceId;
+ } else {
+ throw new Error("Impossible de déterminer la séquence");
+ }
+ }
- const sequenceData = await db.select().from(sequences).where(eq(sequences.id, inscriptionData[0].sequenceId)).limit(1);
+ const sequenceData = await db.select().from(sequences).where(eq(sequences.id, finalSequenceId)).limit(1);
if (sequenceData.length === 0) throw new Error("Séquence introuvable");
const formationId = sequenceData[0].formationId;
- const sequenceId = sequenceData[0].id;
// Créer la structure de dossiers
- const uploadsDir = path.resolve(process.cwd(), "uploads", "signatures", `${formationId}-${sequenceId}`);
+ const uploadsDir = path.resolve(process.cwd(), "uploads", "signatures", `${formationId}-${finalSequenceId}`);
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}
// Générer un nom de fichier unique
const randomSuffix = crypto.randomBytes(8).toString("hex");
- const fileName = `signature-${input.inscriptionId}-${input.dateFormationId}-${randomSuffix}.png`;
+ const userType = isFormateur ? "formateur" : "apprenant";
+ const userId = isFormateur ? formateurData[0].id : input.inscriptionId;
+ const fileName = `signature-${userType}-${userId}-${input.dateFormationId}-${randomSuffix}.png`;
const filePath = path.join(uploadsDir, fileName);
// Convertir le data URL en buffer et sauvegarder
@@ -2581,57 +2610,75 @@ export const appRouter = router({
fs.writeFileSync(filePath, buffer);
// Générer l'URL publique
- signatureUrl = `/uploads/signatures/${formationId}-${sequenceId}/${fileName}`;
- signatureS3Key = `signatures/${formationId}-${sequenceId}/${fileName}`;
+ signatureUrl = `/uploads/signatures/${formationId}-${finalSequenceId}/${fileName}`;
+ signatureS3Key = `signatures/${formationId}-${finalSequenceId}/${fileName}`;
}
- const result = await validerPresence({
- inscriptionId: input.inscriptionId,
- dateFormationId: input.dateFormationId,
- periode: input.periode,
- modeValidation: input.modeValidation,
- validateurId: input.validateurId,
- commentaire: input.commentaire,
- signatureUrl,
- signatureS3Key,
- });
+ let result;
+ if (isFormateur) {
+ // Valider la présence du formateur
+ if (!finalSequenceId) throw new Error("Impossible de déterminer la séquence");
+ result = await validerPresenceFormateur({
+ formateurId: formateurData[0].id,
+ sequenceId: finalSequenceId,
+ dateFormationId: input.dateFormationId,
+ periode: input.periode,
+ modeValidation: input.modeValidation,
+ commentaire: input.commentaire,
+ signatureUrl,
+ signatureS3Key,
+ });
+ } else {
+ // Valider la présence de l'apprenant
+ if (!input.inscriptionId) throw new Error("Inscription requise pour un apprenant");
+ result = await validerPresence({
+ inscriptionId: input.inscriptionId,
+ dateFormationId: input.dateFormationId,
+ periode: input.periode,
+ modeValidation: input.modeValidation,
+ validateurId: input.validateurId,
+ commentaire: input.commentaire,
+ signatureUrl,
+ signatureS3Key,
+ });
- // Vérifier si toutes les présences sont validées
- const { checkAllPresencesValidated } = await import("./presenceDb");
- const allValidated = await checkAllPresencesValidated(input.inscriptionId);
+ // Vérifier si toutes les présences sont validées
+ const { checkAllPresencesValidated } = await import("./presenceDb");
+ const allValidated = await checkAllPresencesValidated(input.inscriptionId);
- // Si toutes les présences sont validées, générer l'attestation automatiquement
- if (allValidated) {
- try {
- const { genererAttestationPDF, enregistrerAttestation } = await import("./attestationService");
-
- // Générer le PDF
- const { s3Key, pdfUrl } = await genererAttestationPDF(input.inscriptionId);
-
- // Récupérer les données de l'inscription
- const db = await getDb();
- if (db) {
- const inscriptionData = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1);
- if (inscriptionData.length > 0) {
- // Enregistrer l'attestation en base
- await enregistrerAttestation(
- input.inscriptionId,
- inscriptionData[0].apprenantId,
- inscriptionData[0].sequenceId,
- s3Key,
- pdfUrl
- );
+ // Si toutes les présences sont validées, générer l'attestation automatiquement
+ if (allValidated) {
+ try {
+ const { genererAttestationPDF, enregistrerAttestation } = await import("./attestationService");
+
+ // Générer le PDF
+ const { s3Key, pdfUrl } = await genererAttestationPDF(input.inscriptionId);
+
+ // Récupérer les données de l'inscription
+ const db = await getDb();
+ if (db) {
+ const inscriptionData = await db.select().from(inscriptions).where(eq(inscriptions.id, input.inscriptionId)).limit(1);
+ if (inscriptionData.length > 0) {
+ // Enregistrer l'attestation en base
+ await enregistrerAttestation(
+ input.inscriptionId,
+ inscriptionData[0].apprenantId,
+ inscriptionData[0].sequenceId,
+ s3Key,
+ pdfUrl
+ );
+ }
}
- }
- return {
- ...result,
- attestationGenerated: true,
- attestationUrl: pdfUrl,
- };
- } catch (error) {
- console.error("Erreur lors de la génération de l'attestation:", error);
- // Ne pas bloquer la validation de présence si l'attestation échoue
+ return {
+ ...result,
+ attestationGenerated: true,
+ attestationUrl: pdfUrl,
+ };
+ } catch (error) {
+ console.error("Erreur lors de la génération de l'attestation:", error);
+ // Ne pas bloquer la validation de présence si l'attestation échoue
+ }
}
}
diff --git a/todo.md b/todo.md
index fafc099..cf8e867 100644
--- a/todo.md
+++ b/todo.md
@@ -1501,4 +1501,12 @@
- [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)
+## Émargement formateur par QR code avec signature tactile
+
+- [x] Modifier le schéma de base de données (table presences) pour distinguer formateurs/apprenants et stocker les signatures tactiles
+- [x] Créer une interface de signature tactile pour les formateurs accessible après scan du QR code (interface existante réutilisée)
+- [x] Implémenter la logique backend pour détecter automatiquement si l'utilisateur est formateur ou apprenant (via email)
+- [x] Intégrer les signatures tactiles formateur dans la génération PDF de la feuille de présence
+- [x] Afficher le QR code dans l'espace formateur pour faciliter l'émargement
+- [x] Tester le flux complet (scan QR, signature, intégration PDF)
+- [x] Prêt pour le déploiement sur le VPS de production