Rollback to 641200f3
This commit is contained in:
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"query": "SELECT id, userId, LEFT(token, 20) as token_preview, expiresAt, used, createdAt FROM passwordResetTokens ORDER BY createdAt DESC LIMIT 5;",
|
|
||||||
"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 SELECT id, userId, LEFT(token, 20) as token_preview, expiresAt, used, createdAt FROM passwordResetTokens ORDER BY createdAt DESC LIMIT 5;",
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"userId": "1",
|
|
||||||
"token_preview": "e6626bd6ba7eabccd094",
|
|
||||||
"expiresAt": "2025-11-18 21:19:50",
|
|
||||||
"used": "0",
|
|
||||||
"createdAt": "2025-11-17 21:19:50"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"messages": [],
|
|
||||||
"stdout": "id\tuserId\ttoken_preview\texpiresAt\tused\tcreatedAt\n1\t1\te6626bd6ba7eabccd094\t2025-11-18 21:19:50\t0\t2025-11-17 21:19:50\n",
|
|
||||||
"stderr": "",
|
|
||||||
"execution_time_ms": 901
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"query": "SELECT * FROM emailTemplates;",
|
|
||||||
"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 SELECT * FROM emailTemplates;",
|
|
||||||
"rows": [],
|
|
||||||
"messages": [],
|
|
||||||
"stdout": "",
|
|
||||||
"stderr": "",
|
|
||||||
"execution_time_ms": 57
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"query": "INSERT INTO emailTemplates (type, name, logoUrl, primaryColor, headerBgColor, headerTextColor, headerTitle, footerText, active) VALUES \n('inscription', 'Confirmation d''inscription', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1),\n('teaser', 'Email teaser', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1),\n('rappel', 'Rappel J-7', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1),\n('reset_password', 'Réinitialisation de mot de passe', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1);",
|
|
||||||
"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 INSERT INTO emailTemplates (type, name, logoUrl, primaryColor, headerBgColor, headerTextColor, headerTitle, footerText, active) VALUES \n('inscription', 'Confirmation d''inscription', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1),\n('teaser', 'Email teaser', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1),\n('rappel', 'Rappel J-7', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1),\n('reset_password', 'Réinitialisation de mot de passe', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1);",
|
|
||||||
"rows": [],
|
|
||||||
"messages": [],
|
|
||||||
"stdout": "",
|
|
||||||
"stderr": "",
|
|
||||||
"execution_time_ms": 64
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"query": "UPDATE users \nSET password = '$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK',\n role = 'admin',\n emailVerified = TRUE\nWHERE email = 'o.pareige@itinova.org';",
|
|
||||||
"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 UPDATE users \nSET password = '$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK',\n role = 'admin',\n emailVerified = TRUE\nWHERE email = 'o.pareige@itinova.org';",
|
|
||||||
"rows": [],
|
|
||||||
"messages": [],
|
|
||||||
"stdout": "",
|
|
||||||
"stderr": "",
|
|
||||||
"execution_time_ms": 414
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"query": "SELECT id, email, name, role, password, emailVerified, isActive FROM users WHERE email = 'o.pareige@itinova.org';",
|
|
||||||
"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 SELECT id, email, name, role, password, emailVerified, isActive FROM users WHERE email = 'o.pareige@itinova.org';",
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"id": "1",
|
|
||||||
"email": "o.pareige@itinova.org",
|
|
||||||
"name": "olivier pareige",
|
|
||||||
"role": "admin",
|
|
||||||
"password": "$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK",
|
|
||||||
"emailVerified": "1",
|
|
||||||
"isActive": "1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"messages": [],
|
|
||||||
"stdout": "id\temail\tname\trole\tpassword\temailVerified\tisActive\n1\to.pareige@itinova.org\tolivier pareige\tadmin\t$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK\t1\t1\n",
|
|
||||||
"stderr": "",
|
|
||||||
"execution_time_ms": 49
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"query": "DELETE FROM users WHERE email = 'o.pareige@itinova.org';\n\nINSERT INTO users (email, password, name, role, emailVerified, isActive, createdAt, updatedAt, lastSignedIn)\nVALUES (\n 'o.pareige@itinova.org',\n '$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK',\n 'Olivier Pareige',\n 'admin',\n 1,\n 1,\n NOW(),\n NOW(),\n NOW()\n);",
|
|
||||||
"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 DELETE FROM users WHERE email = 'o.pareige@itinova.org';\n\nINSERT INTO users (email, password, name, role, emailVerified, isActive, createdAt, updatedAt, lastSignedIn)\nVALUES (\n 'o.pareige@itinova.org',\n '$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK',\n 'Olivier Pareige',\n 'admin',\n 1,\n 1,\n NOW(),\n NOW(),\n NOW()\n);",
|
|
||||||
"rows": [],
|
|
||||||
"messages": [],
|
|
||||||
"stdout": "",
|
|
||||||
"stderr": "",
|
|
||||||
"execution_time_ms": 71
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"query": "SELECT id, email, password, LENGTH(password) as password_length FROM users WHERE email = 'o.pareige@itinova.org';",
|
|
||||||
"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 SELECT id, email, password, LENGTH(password) as password_length FROM users WHERE email = 'o.pareige@itinova.org';",
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"id": "2010001",
|
|
||||||
"email": "o.pareige@itinova.org",
|
|
||||||
"password": "$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK",
|
|
||||||
"password_length": "60"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"messages": [],
|
|
||||||
"stdout": "id\temail\tpassword\tpassword_length\n2010001\to.pareige@itinova.org\t$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK\t60\n",
|
|
||||||
"stderr": "",
|
|
||||||
"execution_time_ms": 141
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"query": "UPDATE users SET password = '$2b$10$wtfrg13y5qw/VZKdQfcs4epTfZT2Hje5eomHggJEfSLp9m9ZGlk2O' WHERE email = 'o.pareige@itinova.org';",
|
|
||||||
"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 UPDATE users SET password = '$2b$10$wtfrg13y5qw/VZKdQfcs4epTfZT2Hje5eomHggJEfSLp9m9ZGlk2O' WHERE email = 'o.pareige@itinova.org';",
|
|
||||||
"rows": [],
|
|
||||||
"messages": [],
|
|
||||||
"stdout": "",
|
|
||||||
"stderr": "",
|
|
||||||
"execution_time_ms": 58
|
|
||||||
}
|
|
||||||
285
DOCUMENTATION.md
285
DOCUMENTATION.md
@@ -1,285 +0,0 @@
|
|||||||
# Documentation - Gestion des Formations Manager Itinova
|
|
||||||
|
|
||||||
## Vue d'ensemble
|
|
||||||
|
|
||||||
L'application **Gestion des Formations Manager Itinova** est une solution complète développée pour gérer l'organisation interne des formations destinées à environ 150 apprenants entre février et octobre 2026, puis 3 sessions par an. Elle permet à la DRH (assistante formation, responsable Développement RH) et à la Directrice Qualité et Transformation de gérer efficacement les inscriptions, les sessions, et les communications automatisées.
|
|
||||||
|
|
||||||
## Fonctionnalités principales
|
|
||||||
|
|
||||||
### 1. Gestion des formations
|
|
||||||
|
|
||||||
L'application permet de créer et gérer plusieurs formations en parallèle. Chaque formation dispose d'un lien unique d'inscription qui peut être partagé avec les apprenants.
|
|
||||||
|
|
||||||
**Caractéristiques :**
|
|
||||||
- Création de formations avec nom, description et lien unique personnalisé
|
|
||||||
- Activation/désactivation des formations
|
|
||||||
- Génération automatique de liens d'inscription distincts pour chaque formation
|
|
||||||
|
|
||||||
### 2. Gestion des sessions
|
|
||||||
|
|
||||||
Les sessions sont les instances concrètes d'une formation avec des dates, un lieu et une capacité définis.
|
|
||||||
|
|
||||||
**Caractéristiques :**
|
|
||||||
- Création de sessions avec dates de début et fin
|
|
||||||
- Définition du lieu de formation
|
|
||||||
- Capacité maximale fixée à 12 participants par session
|
|
||||||
- Date de blocage automatique à J-15 avant le début de la session
|
|
||||||
- Statuts de session : ouverte, bloquée, terminée
|
|
||||||
|
|
||||||
### 3. Gestion des apprenants
|
|
||||||
|
|
||||||
L'application maintient une base de données des apprenants avec leurs informations essentielles.
|
|
||||||
|
|
||||||
**Informations collectées :**
|
|
||||||
- Nom et prénom
|
|
||||||
- Email professionnel
|
|
||||||
- Code établissement
|
|
||||||
|
|
||||||
### 4. Système d'inscription
|
|
||||||
|
|
||||||
Les apprenants peuvent s'inscrire via un lien unique propre à chaque formation. Le système applique automatiquement toutes les contraintes définies.
|
|
||||||
|
|
||||||
**Validations automatiques :**
|
|
||||||
- Vérification de la capacité maximale (12 participants)
|
|
||||||
- Prévention des doubles inscriptions
|
|
||||||
- Blocage des inscriptions et désinscriptions à J-15
|
|
||||||
- Gestion automatique de la liste d'attente si la session est complète
|
|
||||||
|
|
||||||
### 5. Invitations et communications automatiques
|
|
||||||
|
|
||||||
Dès qu'un apprenant s'inscrit, le système génère et envoie automatiquement une invitation Outlook avec tous les détails de la formation.
|
|
||||||
|
|
||||||
**Fonctionnalités d'envoi :**
|
|
||||||
- **Email de confirmation** : envoyé immédiatement après l'inscription avec invitation Outlook (.ics) en pièce jointe
|
|
||||||
- **Invitation Outlook** : fichier .ics avec statut "occupé" incluant le lieu de formation
|
|
||||||
- **Email teaser** : peut être envoyé manuellement par les administrateurs à tous les inscrits d'une session
|
|
||||||
- **Email de rappel J-7** : peut être envoyé manuellement 7 jours avant la formation
|
|
||||||
|
|
||||||
### 6. Exports et feuilles de présence
|
|
||||||
|
|
||||||
Les administrateurs peuvent exporter les listes d'inscrits dans différents formats pour faciliter la gestion administrative.
|
|
||||||
|
|
||||||
**Formats disponibles :**
|
|
||||||
- **Export Excel** : liste complète des inscrits avec toutes leurs informations
|
|
||||||
- **Export PDF** : liste des inscrits au format PDF
|
|
||||||
- **Feuille de présence PDF** : document prêt à imprimer avec nom, prénom, code établissement et colonne signature
|
|
||||||
|
|
||||||
## Guide d'utilisation
|
|
||||||
|
|
||||||
### Pour les administrateurs
|
|
||||||
|
|
||||||
#### Connexion
|
|
||||||
|
|
||||||
1. Accédez à l'application via l'URL fournie
|
|
||||||
2. Cliquez sur "Se connecter"
|
|
||||||
3. Utilisez vos identifiants Manus OAuth
|
|
||||||
4. Vous serez redirigé vers le tableau de bord administrateur
|
|
||||||
|
|
||||||
#### Créer une formation
|
|
||||||
|
|
||||||
1. Dans le menu latéral, cliquez sur **Formations**
|
|
||||||
2. Cliquez sur le bouton **Nouvelle formation**
|
|
||||||
3. Remplissez les informations :
|
|
||||||
- Nom de la formation (ex: "Manager Itinova")
|
|
||||||
- Description (optionnelle)
|
|
||||||
- Lien unique (ex: "manager-itinova-2026")
|
|
||||||
- Cochez "Formation active" pour la rendre disponible
|
|
||||||
4. Cliquez sur **Créer**
|
|
||||||
5. Le lien d'inscription sera généré automatiquement : `/inscription/[votre-lien-unique]`
|
|
||||||
|
|
||||||
#### Créer une session
|
|
||||||
|
|
||||||
1. Dans le menu latéral, cliquez sur **Sessions**
|
|
||||||
2. Cliquez sur le bouton **Nouvelle session**
|
|
||||||
3. Remplissez les informations :
|
|
||||||
- Sélectionnez la formation associée
|
|
||||||
- Nom de la session (ex: "Session 1 - Février 2026")
|
|
||||||
- Date et heure de début
|
|
||||||
- Date et heure de fin
|
|
||||||
- Lieu de la formation (sera inclus dans l'invitation Outlook)
|
|
||||||
- Capacité maximale (par défaut 12)
|
|
||||||
- Date de blocage (J-15 avant le début)
|
|
||||||
- Statut (ouverte, bloquée, terminée)
|
|
||||||
4. Cliquez sur **Créer**
|
|
||||||
|
|
||||||
#### Gérer les inscriptions
|
|
||||||
|
|
||||||
1. Dans le menu **Sessions**, cliquez sur l'icône **Utilisateurs** à côté d'une session
|
|
||||||
2. Vous verrez :
|
|
||||||
- Le nombre de places confirmées sur la capacité totale
|
|
||||||
- Le nombre de personnes en liste d'attente
|
|
||||||
- La liste complète des inscrits avec leurs informations
|
|
||||||
3. Actions disponibles :
|
|
||||||
- Modifier le statut d'une inscription (confirmée, liste d'attente, annulée)
|
|
||||||
- Exporter la liste en Excel ou PDF
|
|
||||||
- Générer une feuille de présence
|
|
||||||
- Envoyer des emails groupés (teaser ou rappel J-7)
|
|
||||||
|
|
||||||
#### Envoyer des emails groupés
|
|
||||||
|
|
||||||
1. Accédez à la page des inscrits d'une session
|
|
||||||
2. Cliquez sur **Envoyer teaser** pour envoyer un email d'introduction à tous les inscrits confirmés
|
|
||||||
3. Cliquez sur **Envoyer rappel J-7** pour envoyer un rappel 7 jours avant la formation
|
|
||||||
4. Un message de confirmation indiquera le nombre d'emails envoyés avec succès
|
|
||||||
|
|
||||||
#### Exporter les données
|
|
||||||
|
|
||||||
1. Accédez à la page des inscrits d'une session
|
|
||||||
2. Cliquez sur :
|
|
||||||
- **Exporter Excel** : télécharge un fichier .xlsx avec toutes les informations
|
|
||||||
- **Exporter PDF** : télécharge un fichier PDF avec la liste des inscrits
|
|
||||||
- **Feuille de présence** : télécharge un PDF prêt à imprimer pour la signature des participants
|
|
||||||
|
|
||||||
### Pour les apprenants
|
|
||||||
|
|
||||||
#### S'inscrire à une formation
|
|
||||||
|
|
||||||
1. Cliquez sur le lien d'inscription fourni par la DRH (ex: `https://[domaine]/inscription/manager-itinova-2026`)
|
|
||||||
2. Consultez les sessions disponibles avec :
|
|
||||||
- Les dates et horaires
|
|
||||||
- Le lieu de formation
|
|
||||||
- Le nombre de places disponibles
|
|
||||||
- Le statut (disponible, complète, fermée)
|
|
||||||
3. Sélectionnez la session souhaitée en cliquant dessus
|
|
||||||
4. Remplissez vos informations :
|
|
||||||
- Nom
|
|
||||||
- Prénom
|
|
||||||
- Email professionnel
|
|
||||||
- Code établissement
|
|
||||||
5. Cliquez sur **Confirmer mon inscription**
|
|
||||||
6. Vous recevrez immédiatement :
|
|
||||||
- Un email de confirmation
|
|
||||||
- Une invitation Outlook (.ics) à ajouter à votre calendrier
|
|
||||||
|
|
||||||
#### Que se passe-t-il après l'inscription ?
|
|
||||||
|
|
||||||
- **Si votre inscription est confirmée** : vous recevrez une invitation Outlook qui bloquera automatiquement votre agenda avec le statut "occupé"
|
|
||||||
- **Si la session est complète** : vous serez placé en liste d'attente et contacté si une place se libère
|
|
||||||
- **15 jours avant la formation** : les inscriptions et désinscriptions seront bloquées
|
|
||||||
- **7 jours avant la formation** : vous recevrez un email de rappel avec tous les détails pratiques
|
|
||||||
|
|
||||||
## Gestion des rôles et permissions
|
|
||||||
|
|
||||||
L'application utilise un système de rôles pour contrôler l'accès aux fonctionnalités :
|
|
||||||
|
|
||||||
### Rôle Administrateur
|
|
||||||
|
|
||||||
Accordé aux membres de la DRH (assistante formation, responsable Développement RH, Directrice Qualité et Transformation, stagiaire).
|
|
||||||
|
|
||||||
**Permissions :**
|
|
||||||
- Accès complet au tableau de bord administrateur
|
|
||||||
- Création, modification et suppression de formations
|
|
||||||
- Création, modification et suppression de sessions
|
|
||||||
- Gestion des apprenants
|
|
||||||
- Consultation et modification des inscriptions
|
|
||||||
- Envoi d'emails groupés
|
|
||||||
- Export des données
|
|
||||||
|
|
||||||
### Rôle Utilisateur
|
|
||||||
|
|
||||||
Les apprenants qui s'inscrivent via le lien public.
|
|
||||||
|
|
||||||
**Permissions :**
|
|
||||||
- Accès au formulaire d'inscription public
|
|
||||||
- Consultation des sessions disponibles
|
|
||||||
- Inscription à une session (dans les limites définies)
|
|
||||||
|
|
||||||
## Contraintes et règles métier
|
|
||||||
|
|
||||||
### Capacité des sessions
|
|
||||||
|
|
||||||
Chaque session est limitée à **12 participants maximum**. Au-delà, les apprenants sont automatiquement placés en liste d'attente.
|
|
||||||
|
|
||||||
### Blocage à J-15
|
|
||||||
|
|
||||||
Les inscriptions et désinscriptions sont automatiquement bloquées **15 jours avant le début de la session**. Cette règle garantit la stabilité des groupes et évite les désistements de dernière minute.
|
|
||||||
|
|
||||||
### Prévention des doubles inscriptions
|
|
||||||
|
|
||||||
Le système empêche un apprenant de s'inscrire plusieurs fois à la même session. Si l'apprenant existe déjà dans la base de données (même email), ses informations sont réutilisées.
|
|
||||||
|
|
||||||
### Statuts des inscriptions
|
|
||||||
|
|
||||||
- **Confirmée** : l'apprenant a une place garantie dans la session
|
|
||||||
- **Liste d'attente** : la session est complète, l'apprenant sera contacté si une place se libère
|
|
||||||
- **Annulée** : l'inscription a été annulée par l'apprenant ou l'administrateur
|
|
||||||
|
|
||||||
## Architecture technique
|
|
||||||
|
|
||||||
### Technologies utilisées
|
|
||||||
|
|
||||||
L'application est construite avec une architecture moderne full-stack :
|
|
||||||
|
|
||||||
**Frontend :**
|
|
||||||
- React 19 avec TypeScript
|
|
||||||
- Tailwind CSS 4 pour le design
|
|
||||||
- tRPC pour la communication type-safe avec le backend
|
|
||||||
- Wouter pour le routage
|
|
||||||
- shadcn/ui pour les composants d'interface
|
|
||||||
|
|
||||||
**Backend :**
|
|
||||||
- Node.js avec Express
|
|
||||||
- tRPC pour les API type-safe
|
|
||||||
- Drizzle ORM pour l'accès à la base de données
|
|
||||||
- MySQL/TiDB comme base de données
|
|
||||||
|
|
||||||
**Fonctionnalités :**
|
|
||||||
- Génération de fichiers ICS (iCalendar) pour Outlook
|
|
||||||
- Export Excel avec xlsx
|
|
||||||
- Export PDF avec jsPDF
|
|
||||||
- Système d'emails automatisés
|
|
||||||
|
|
||||||
### Base de données
|
|
||||||
|
|
||||||
Le schéma de base de données comprend 5 tables principales :
|
|
||||||
|
|
||||||
1. **users** : utilisateurs authentifiés (administrateurs)
|
|
||||||
2. **formations** : catalogue des formations
|
|
||||||
3. **sessions** : instances de formations avec dates et lieux
|
|
||||||
4. **apprenants** : base de données des participants
|
|
||||||
5. **inscriptions** : relation entre apprenants et sessions avec statuts
|
|
||||||
|
|
||||||
## Confidentialité et sécurité
|
|
||||||
|
|
||||||
L'application respecte les principes de confidentialité des données :
|
|
||||||
|
|
||||||
- **Authentification sécurisée** : via Manus OAuth pour les administrateurs
|
|
||||||
- **Contrôle d'accès** : seuls les administrateurs peuvent accéder aux données sensibles
|
|
||||||
- **Protection des données** : les informations personnelles des apprenants sont stockées de manière sécurisée
|
|
||||||
- **Emails professionnels** : seuls les emails professionnels sont collectés
|
|
||||||
|
|
||||||
## Support et maintenance
|
|
||||||
|
|
||||||
### Contact
|
|
||||||
|
|
||||||
Pour toute question ou problème technique, contactez le service RH ou l'équipe de développement.
|
|
||||||
|
|
||||||
### Évolutions futures
|
|
||||||
|
|
||||||
L'application est conçue pour être évolutive. Des fonctionnalités supplémentaires peuvent être ajoutées selon les besoins :
|
|
||||||
|
|
||||||
- Intégration avec un vrai service SMTP pour l'envoi d'emails
|
|
||||||
- Tableau de bord analytique avec statistiques de participation
|
|
||||||
- Système de notation et d'évaluation post-formation
|
|
||||||
- Gestion des formateurs et de leurs disponibilités
|
|
||||||
- Notifications push pour les rappels
|
|
||||||
|
|
||||||
## Notes importantes
|
|
||||||
|
|
||||||
### Emails simulés
|
|
||||||
|
|
||||||
Dans la version actuelle, les emails sont **simulés** et affichés dans les logs du serveur. Pour une utilisation en production, il est nécessaire d'intégrer un service SMTP réel (comme SendGrid, Mailgun, ou un serveur SMTP d'entreprise).
|
|
||||||
|
|
||||||
### Personnalisation
|
|
||||||
|
|
||||||
Les templates d'emails peuvent être personnalisés dans le fichier `server/emailService.ts` pour correspondre à la charte graphique de l'entreprise.
|
|
||||||
|
|
||||||
### Sauvegarde des données
|
|
||||||
|
|
||||||
Il est recommandé de mettre en place une stratégie de sauvegarde régulière de la base de données pour prévenir toute perte de données.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Version de la documentation :** 1.0
|
|
||||||
**Date de dernière mise à jour :** 2026-01-17
|
|
||||||
**Auteur :** Manus AI
|
|
||||||
109
EMAIL_SETUP.md
109
EMAIL_SETUP.md
@@ -1,109 +0,0 @@
|
|||||||
# Configuration de l'envoi d'emails
|
|
||||||
|
|
||||||
## État actuel
|
|
||||||
|
|
||||||
Par défaut, l'application **simule** l'envoi d'emails en créant des notifications pour le propriétaire du projet. Les emails ne sont pas réellement envoyés aux utilisateurs finaux.
|
|
||||||
|
|
||||||
## Configuration de l'envoi réel d'emails
|
|
||||||
|
|
||||||
Pour activer l'envoi réel d'emails, vous devez configurer un service d'envoi d'emails externe. L'application supporte **Resend**, un service moderne et simple d'utilisation.
|
|
||||||
|
|
||||||
### Étape 1 : Créer un compte Resend
|
|
||||||
|
|
||||||
1. Allez sur [https://resend.com](https://resend.com)
|
|
||||||
2. Créez un compte gratuit (100 emails/jour inclus)
|
|
||||||
3. Vérifiez votre adresse email
|
|
||||||
|
|
||||||
### Étape 2 : Obtenir une clé API
|
|
||||||
|
|
||||||
1. Dans le tableau de bord Resend, allez dans **API Keys**
|
|
||||||
2. Cliquez sur **Create API Key**
|
|
||||||
3. Donnez un nom à votre clé (ex: "Manager Itinova Production")
|
|
||||||
4. Copiez la clé API générée (elle commence par `re_`)
|
|
||||||
|
|
||||||
### Étape 3 : Configurer votre domaine (optionnel mais recommandé)
|
|
||||||
|
|
||||||
Pour envoyer des emails depuis votre propre domaine (ex: `noreply@itinova.org`) :
|
|
||||||
|
|
||||||
1. Dans Resend, allez dans **Domains**
|
|
||||||
2. Cliquez sur **Add Domain**
|
|
||||||
3. Entrez votre domaine (ex: `itinova.org`)
|
|
||||||
4. Suivez les instructions pour ajouter les enregistrements DNS
|
|
||||||
5. Attendez la vérification (quelques minutes à quelques heures)
|
|
||||||
|
|
||||||
**Note:** Sans domaine vérifié, les emails seront envoyés depuis `noreply@manusvm.computer`
|
|
||||||
|
|
||||||
### Étape 4 : Ajouter les secrets dans Manus
|
|
||||||
|
|
||||||
1. Ouvrez l'interface de gestion de votre projet Manus
|
|
||||||
2. Allez dans **Settings** → **Secrets**
|
|
||||||
3. Ajoutez les secrets suivants :
|
|
||||||
|
|
||||||
| Nom du secret | Valeur | Description |
|
|
||||||
|---------------|--------|-------------|
|
|
||||||
| `RESEND_API_KEY` | `re_xxxxx...` | Votre clé API Resend |
|
|
||||||
| `RESEND_FROM_EMAIL` | `noreply@itinova.org` | Adresse email d'envoi (doit être vérifiée dans Resend) |
|
|
||||||
|
|
||||||
4. Sauvegardez les modifications
|
|
||||||
5. Redémarrez l'application (elle détectera automatiquement la nouvelle configuration)
|
|
||||||
|
|
||||||
### Étape 5 : Tester l'envoi
|
|
||||||
|
|
||||||
1. Allez dans **Utilisateurs**
|
|
||||||
2. Cliquez sur le bouton de réinitialisation de mot de passe pour un utilisateur
|
|
||||||
3. Vérifiez que l'email a bien été reçu
|
|
||||||
|
|
||||||
## Vérification de la configuration
|
|
||||||
|
|
||||||
Vous pouvez vérifier si l'envoi d'emails réel est activé en consultant les logs du serveur :
|
|
||||||
|
|
||||||
- **Emails simulés** : `[Email] RESEND_API_KEY non configurée, email simulé`
|
|
||||||
- **Emails réels** : `[Email] Email envoyé via Resend: <id>`
|
|
||||||
|
|
||||||
## Alternatives à Resend
|
|
||||||
|
|
||||||
Si vous préférez utiliser un autre service d'envoi d'emails, vous pouvez modifier le fichier `server/_core/emailSender.ts` pour intégrer :
|
|
||||||
|
|
||||||
- **SendGrid** : Service populaire avec 100 emails/jour gratuits
|
|
||||||
- **Mailgun** : Service robuste avec 5000 emails/mois gratuits
|
|
||||||
- **AWS SES** : Service Amazon très économique pour gros volumes
|
|
||||||
- **Votre propre serveur SMTP** : Configuration personnalisée
|
|
||||||
|
|
||||||
## Limites et quotas
|
|
||||||
|
|
||||||
### Plan gratuit Resend
|
|
||||||
- 100 emails par jour
|
|
||||||
- 3000 emails par mois
|
|
||||||
- Support email uniquement
|
|
||||||
|
|
||||||
### Plans payants Resend
|
|
||||||
- À partir de 20$/mois pour 50 000 emails
|
|
||||||
- Support prioritaire
|
|
||||||
- Webhooks et analytics avancés
|
|
||||||
|
|
||||||
## Dépannage
|
|
||||||
|
|
||||||
### Les emails n'arrivent pas
|
|
||||||
|
|
||||||
1. Vérifiez que `RESEND_API_KEY` est correctement configurée
|
|
||||||
2. Vérifiez que `RESEND_FROM_EMAIL` utilise un domaine vérifié
|
|
||||||
3. Consultez les logs du serveur pour voir les erreurs
|
|
||||||
4. Vérifiez le dossier spam/courrier indésirable
|
|
||||||
5. Consultez le tableau de bord Resend pour voir l'état des envois
|
|
||||||
|
|
||||||
### Erreur "Domain not verified"
|
|
||||||
|
|
||||||
Vous devez vérifier votre domaine dans Resend en ajoutant les enregistrements DNS requis.
|
|
||||||
|
|
||||||
### Quota dépassé
|
|
||||||
|
|
||||||
Si vous dépassez le quota gratuit, vous devrez :
|
|
||||||
- Attendre le lendemain (quota journalier)
|
|
||||||
- Passer à un plan payant
|
|
||||||
- Utiliser un autre service d'envoi
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
Pour toute question sur la configuration de l'envoi d'emails, consultez :
|
|
||||||
- [Documentation Resend](https://resend.com/docs)
|
|
||||||
- [API Reference Resend](https://resend.com/docs/api-reference)
|
|
||||||
@@ -1,600 +0,0 @@
|
|||||||
# Guide de déploiement - Formation Manager Itinova sur serveur Linux
|
|
||||||
|
|
||||||
## Vue d'ensemble
|
|
||||||
|
|
||||||
Ce guide vous accompagne dans le déploiement de l'application Formation Manager Itinova sur un serveur Linux (Ubuntu/Debian) avec authentification locale email/mot de passe (sans OAuth Manus).
|
|
||||||
|
|
||||||
**Durée estimée** : 1-2 heures
|
|
||||||
**Niveau** : Intermédiaire
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prérequis
|
|
||||||
|
|
||||||
### Logiciels requis
|
|
||||||
|
|
||||||
- **Système** : Ubuntu 22.04 LTS ou Debian 11+
|
|
||||||
- **Node.js** : Version 22.x
|
|
||||||
- **pnpm** : Version 10.x
|
|
||||||
- **MySQL** : Version 8.0+ ou MariaDB 10.6+
|
|
||||||
- **Nginx** : Pour le reverse proxy (optionnel mais recommandé)
|
|
||||||
|
|
||||||
### Accès nécessaires
|
|
||||||
|
|
||||||
- Accès SSH au serveur
|
|
||||||
- Droits sudo
|
|
||||||
- Accès à la base de données MySQL
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Étape 1 : Préparation du serveur
|
|
||||||
|
|
||||||
### 1.1 Mise à jour du système
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt upgrade -y
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 Installation de Node.js 22.x
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Installer Node.js via NodeSource
|
|
||||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
|
||||||
sudo apt install -y nodejs
|
|
||||||
|
|
||||||
# Vérifier l'installation
|
|
||||||
node --version # Doit afficher v22.x.x
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.3 Installation de pnpm
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g pnpm
|
|
||||||
pnpm --version # Doit afficher 10.x.x
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.4 Installation de MySQL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install -y mysql-server
|
|
||||||
sudo mysql_secure_installation
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Étape 2 : Configuration de la base de données
|
|
||||||
|
|
||||||
### 2.1 Créer la base de données et l'utilisateur
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo mysql
|
|
||||||
```
|
|
||||||
|
|
||||||
Dans MySQL :
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE DATABASE formation_manager CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
CREATE USER 'formation_user'@'localhost' IDENTIFIED BY 'VOTRE_MOT_DE_PASSE_SECURISE';
|
|
||||||
GRANT ALL PRIVILEGES ON formation_manager.* TO 'formation_user'@'localhost';
|
|
||||||
FLUSH PRIVILEGES;
|
|
||||||
EXIT;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 Tester la connexion
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -u formation_user -p formation_manager
|
|
||||||
# Entrez le mot de passe, puis EXIT
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Étape 3 : Déploiement de l'application
|
|
||||||
|
|
||||||
### 3.1 Créer le répertoire de l'application
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo mkdir -p /var/www/formation-manager-itinova
|
|
||||||
sudo chown -R $USER:$USER /var/www/formation-manager-itinova
|
|
||||||
cd /var/www/formation-manager-itinova
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Cloner ou copier les fichiers
|
|
||||||
|
|
||||||
**Option A : Via Git (recommandé)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/votre-repo/formation-manager-itinova.git .
|
|
||||||
```
|
|
||||||
|
|
||||||
**Option B : Via upload manuel**
|
|
||||||
|
|
||||||
Transférez les fichiers via SCP/SFTP vers `/var/www/formation-manager-itinova/`
|
|
||||||
|
|
||||||
### 3.3 Installer les dépendances
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /var/www/formation-manager-itinova
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Étape 4 : Configuration de l'application
|
|
||||||
|
|
||||||
### 4.1 Créer le fichier .env
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nano /var/www/formation-manager-itinova/.env
|
|
||||||
```
|
|
||||||
|
|
||||||
**Contenu du fichier .env :**
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Base de données
|
|
||||||
DATABASE_URL=mysql://formation_user:VOTRE_MOT_DE_PASSE@localhost:3306/formation_manager
|
|
||||||
|
|
||||||
# Sécurité JWT (générez une chaîne aléatoire longue)
|
|
||||||
JWT_SECRET=CHANGEZ_CECI_PAR_UNE_CHAINE_ALEATOIRE_TRES_LONGUE_ET_SECURISEE
|
|
||||||
|
|
||||||
# Configuration de l'application
|
|
||||||
NODE_ENV=production
|
|
||||||
VITE_APP_TITLE=Formation Manager Itinova
|
|
||||||
VITE_APP_LOGO=/logo.svg
|
|
||||||
|
|
||||||
# Email (optionnel - pour Resend)
|
|
||||||
RESEND_API_KEY=
|
|
||||||
RESEND_FROM_EMAIL=
|
|
||||||
|
|
||||||
# Port de l'application (par défaut 3000)
|
|
||||||
PORT=3000
|
|
||||||
```
|
|
||||||
|
|
||||||
**Générer un JWT_SECRET sécurisé :**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openssl rand -base64 64
|
|
||||||
```
|
|
||||||
|
|
||||||
Copiez le résultat dans `JWT_SECRET`.
|
|
||||||
|
|
||||||
### 4.2 Sécuriser le fichier .env
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod 600 /var/www/formation-manager-itinova/.env
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 Initialiser la base de données
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /var/www/formation-manager-itinova
|
|
||||||
pnpm db:push
|
|
||||||
```
|
|
||||||
|
|
||||||
Cette commande va créer toutes les tables nécessaires.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Étape 5 : Compilation de l'application
|
|
||||||
|
|
||||||
### 5.1 Compiler le frontend et le backend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /var/www/formation-manager-itinova
|
|
||||||
pnpm build
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 Vérifier que la compilation a réussi
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ls -la /var/www/formation-manager-itinova/dist/
|
|
||||||
```
|
|
||||||
|
|
||||||
Vous devriez voir un fichier `index.js`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Étape 6 : Configuration du service systemd
|
|
||||||
|
|
||||||
### 6.1 Créer le fichier de service
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo nano /etc/systemd/system/formation-manager.service
|
|
||||||
```
|
|
||||||
|
|
||||||
**Contenu du fichier :**
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[Unit]
|
|
||||||
Description=Formation Manager Itinova
|
|
||||||
After=network.target mysql.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=www-data
|
|
||||||
WorkingDirectory=/var/www/formation-manager-itinova
|
|
||||||
Environment="NODE_ENV=production"
|
|
||||||
Environment="HOME=/var/www/formation-manager-itinova"
|
|
||||||
Environment="PNPM_HOME=/var/www/formation-manager-itinova/.pnpm"
|
|
||||||
EnvironmentFile=/var/www/formation-manager-itinova/.env
|
|
||||||
ExecStart=/usr/bin/pnpm start
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 Configurer les permissions
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo chown -R www-data:www-data /var/www/formation-manager-itinova
|
|
||||||
sudo mkdir -p /var/www/.local/share/pnpm
|
|
||||||
sudo chown -R www-data:www-data /var/www/.local
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.3 Activer et démarrer le service
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable formation-manager
|
|
||||||
sudo systemctl start formation-manager
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.4 Vérifier le statut
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl status formation-manager
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.5 Voir les logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo journalctl -u formation-manager -f
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Étape 7 : Configuration de Nginx (optionnel mais recommandé)
|
|
||||||
|
|
||||||
### 7.1 Installer Nginx
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install -y nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 Créer la configuration du site
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo nano /etc/nginx/sites-available/formation-manager
|
|
||||||
```
|
|
||||||
|
|
||||||
**Contenu du fichier :**
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name votre-domaine.com; # Changez par votre domaine
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
access_log /var/log/nginx/formation-manager-access.log;
|
|
||||||
error_log /var/log/nginx/formation-manager-error.log;
|
|
||||||
|
|
||||||
# Proxy vers l'application Node.js
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.3 Activer le site
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo ln -s /etc/nginx/sites-available/formation-manager /etc/nginx/sites-enabled/
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl restart nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.4 Configurer le pare-feu
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo ufw allow 'Nginx Full'
|
|
||||||
sudo ufw allow OpenSSH
|
|
||||||
sudo ufw enable
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Étape 8 : Configuration HTTPS avec Let's Encrypt (recommandé)
|
|
||||||
|
|
||||||
### 8.1 Installer Certbot
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install -y certbot python3-certbot-nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8.2 Obtenir un certificat SSL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo certbot --nginx -d votre-domaine.com
|
|
||||||
```
|
|
||||||
|
|
||||||
Suivez les instructions à l'écran.
|
|
||||||
|
|
||||||
### 8.3 Renouvellement automatique
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl status certbot.timer
|
|
||||||
```
|
|
||||||
|
|
||||||
Le renouvellement est automatique.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Étape 9 : Créer le premier utilisateur administrateur
|
|
||||||
|
|
||||||
### 9.1 Se connecter à MySQL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -u formation_user -p formation_manager
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.2 Créer un utilisateur admin
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Générer un hash bcrypt pour le mot de passe "admin123" (à changer immédiatement)
|
|
||||||
-- Hash bcrypt de "admin123" : $2b$10$rKZhVJQX7mEWQxGqF8vQZOQxqxqxqxqxqxqxqxqxqxqxqxqxqxqxq
|
|
||||||
|
|
||||||
INSERT INTO users (email, password, name, role, emailVerified, isActive, createdAt, updatedAt, lastSignedIn)
|
|
||||||
VALUES (
|
|
||||||
'admin@itinova.org',
|
|
||||||
'$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK', -- Mot de passe: admin123
|
|
||||||
'Administrateur',
|
|
||||||
'admin',
|
|
||||||
TRUE,
|
|
||||||
TRUE,
|
|
||||||
NOW(),
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
EXIT;
|
|
||||||
```
|
|
||||||
|
|
||||||
**⚠️ IMPORTANT** : Changez le mot de passe immédiatement après la première connexion !
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Étape 10 : Vérification et tests
|
|
||||||
|
|
||||||
### 10.1 Accéder à l'application
|
|
||||||
|
|
||||||
Ouvrez votre navigateur et accédez à :
|
|
||||||
- **HTTP** : `http://votre-domaine.com`
|
|
||||||
- **HTTPS** : `https://votre-domaine.com`
|
|
||||||
|
|
||||||
### 10.2 Se connecter
|
|
||||||
|
|
||||||
- **Email** : `admin@itinova.org`
|
|
||||||
- **Mot de passe** : `admin123`
|
|
||||||
|
|
||||||
### 10.3 Changer le mot de passe
|
|
||||||
|
|
||||||
1. Connectez-vous
|
|
||||||
2. Allez dans "Utilisateurs"
|
|
||||||
3. Modifiez votre profil
|
|
||||||
4. Changez le mot de passe
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Maintenance
|
|
||||||
|
|
||||||
### Voir les logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo journalctl -u formation-manager -f
|
|
||||||
```
|
|
||||||
|
|
||||||
### Redémarrer l'application
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart formation-manager
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mettre à jour l'application
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /var/www/formation-manager-itinova
|
|
||||||
git pull # Si vous utilisez Git
|
|
||||||
pnpm install
|
|
||||||
pnpm build
|
|
||||||
sudo systemctl restart formation-manager
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sauvegarder la base de données
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysqldump -u formation_user -p formation_manager > backup_$(date +%Y%m%d_%H%M%S).sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Restaurer la base de données
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mysql -u formation_user -p formation_manager < backup_YYYYMMDD_HHMMSS.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Dépannage
|
|
||||||
|
|
||||||
### L'application ne démarre pas
|
|
||||||
|
|
||||||
1. Vérifier les logs :
|
|
||||||
```bash
|
|
||||||
sudo journalctl -u formation-manager -n 100 --no-pager
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Vérifier que le fichier .env existe et est correct :
|
|
||||||
```bash
|
|
||||||
cat /var/www/formation-manager-itinova/.env
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Vérifier que la base de données est accessible :
|
|
||||||
```bash
|
|
||||||
mysql -u formation_user -p formation_manager
|
|
||||||
```
|
|
||||||
|
|
||||||
### Erreur de permissions
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo chown -R www-data:www-data /var/www/formation-manager-itinova
|
|
||||||
sudo mkdir -p /var/www/.local/share/pnpm
|
|
||||||
sudo chown -R www-data:www-data /var/www/.local
|
|
||||||
```
|
|
||||||
|
|
||||||
### Erreur "drizzle-kit: not found"
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /var/www/formation-manager-itinova
|
|
||||||
pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Port 3000 déjà utilisé
|
|
||||||
|
|
||||||
Modifiez le port dans `.env` :
|
|
||||||
```env
|
|
||||||
PORT=3001
|
|
||||||
```
|
|
||||||
|
|
||||||
Puis redémarrez :
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart formation-manager
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sécurité
|
|
||||||
|
|
||||||
### Recommandations
|
|
||||||
|
|
||||||
1. **Changez immédiatement le mot de passe admin par défaut**
|
|
||||||
2. **Utilisez HTTPS** (Let's Encrypt gratuit)
|
|
||||||
3. **Configurez un pare-feu** (ufw)
|
|
||||||
4. **Mettez à jour régulièrement** le système et l'application
|
|
||||||
5. **Sauvegardez régulièrement** la base de données
|
|
||||||
6. **Limitez l'accès SSH** (clés SSH, désactiver root login)
|
|
||||||
7. **Surveillez les logs** régulièrement
|
|
||||||
|
|
||||||
### Sauvegardes automatiques
|
|
||||||
|
|
||||||
Créez un script de sauvegarde :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo nano /usr/local/bin/backup-formation-manager.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Contenu :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
BACKUP_DIR="/var/backups/formation-manager"
|
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
|
||||||
|
|
||||||
mkdir -p $BACKUP_DIR
|
|
||||||
|
|
||||||
# Sauvegarder la base de données
|
|
||||||
mysqldump -u formation_user -p'VOTRE_MOT_DE_PASSE' formation_manager > $BACKUP_DIR/db_$DATE.sql
|
|
||||||
|
|
||||||
# Compresser
|
|
||||||
gzip $BACKUP_DIR/db_$DATE.sql
|
|
||||||
|
|
||||||
# Garder seulement les 30 derniers jours
|
|
||||||
find $BACKUP_DIR -name "db_*.sql.gz" -mtime +30 -delete
|
|
||||||
|
|
||||||
echo "Sauvegarde terminée : $BACKUP_DIR/db_$DATE.sql.gz"
|
|
||||||
```
|
|
||||||
|
|
||||||
Rendre exécutable :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo chmod +x /usr/local/bin/backup-formation-manager.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Ajouter au cron (tous les jours à 2h du matin) :
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo crontab -e
|
|
||||||
```
|
|
||||||
|
|
||||||
Ajouter :
|
|
||||||
|
|
||||||
```
|
|
||||||
0 2 * * * /usr/local/bin/backup-formation-manager.sh >> /var/log/backup-formation-manager.log 2>&1
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
Pour toute question ou problème :
|
|
||||||
|
|
||||||
1. Consultez les logs : `sudo journalctl -u formation-manager -f`
|
|
||||||
2. Vérifiez la documentation : `/var/www/formation-manager-itinova/README.md`
|
|
||||||
3. Contactez le support technique
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Annexe : Commandes utiles
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Statut du service
|
|
||||||
sudo systemctl status formation-manager
|
|
||||||
|
|
||||||
# Démarrer le service
|
|
||||||
sudo systemctl start formation-manager
|
|
||||||
|
|
||||||
# Arrêter le service
|
|
||||||
sudo systemctl stop formation-manager
|
|
||||||
|
|
||||||
# Redémarrer le service
|
|
||||||
sudo systemctl restart formation-manager
|
|
||||||
|
|
||||||
# Voir les logs en temps réel
|
|
||||||
sudo journalctl -u formation-manager -f
|
|
||||||
|
|
||||||
# Voir les 100 dernières lignes de logs
|
|
||||||
sudo journalctl -u formation-manager -n 100 --no-pager
|
|
||||||
|
|
||||||
# Tester la connexion à la base de données
|
|
||||||
mysql -u formation_user -p formation_manager
|
|
||||||
|
|
||||||
# Vérifier la configuration Nginx
|
|
||||||
sudo nginx -t
|
|
||||||
|
|
||||||
# Recharger Nginx
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
|
|
||||||
# Vérifier l'utilisation des ressources
|
|
||||||
htop
|
|
||||||
|
|
||||||
# Vérifier l'espace disque
|
|
||||||
df -h
|
|
||||||
|
|
||||||
# Vérifier les processus Node.js
|
|
||||||
ps aux | grep node
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Fin du guide de déploiement**
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
# Migration OAuth Manus vers Authentification Locale
|
|
||||||
|
|
||||||
## État de la migration
|
|
||||||
|
|
||||||
**Date** : 18 novembre 2024
|
|
||||||
**Statut** : ⚠️ **En cours** (80% complété)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Modifications complétées
|
|
||||||
|
|
||||||
### 1. Base de données
|
|
||||||
|
|
||||||
- ✅ Schéma `users` modifié :
|
|
||||||
- Ajout du champ `email` (unique, not null)
|
|
||||||
- Ajout du champ `password` (hash bcrypt, not null)
|
|
||||||
- Ajout du champ `emailVerified` (boolean, default false)
|
|
||||||
- Suppression du champ `openId`
|
|
||||||
- Suppression du champ `loginMethod`
|
|
||||||
|
|
||||||
### 2. Backend
|
|
||||||
|
|
||||||
- ✅ Installation de `bcrypt` pour le hashage des mots de passe
|
|
||||||
- ✅ Création des helpers d'authentification (`server/_core/auth.ts`) :
|
|
||||||
- `hashPassword()` - Hashe un mot de passe avec bcrypt
|
|
||||||
- `verifyPassword()` - Vérifie un mot de passe
|
|
||||||
- `generateToken()` - Génère un token JWT
|
|
||||||
- `verifyToken()` - Vérifie un token JWT
|
|
||||||
|
|
||||||
- ✅ Modification du fichier `server/db.ts` :
|
|
||||||
- `createUser()` - Crée un utilisateur avec email/password
|
|
||||||
- `getUserByEmail()` - Récupère un utilisateur par email
|
|
||||||
- `getUserById()` - Récupère un utilisateur par ID
|
|
||||||
- `updateUserLastSignedIn()` - Met à jour le dernier login
|
|
||||||
|
|
||||||
- ✅ Modification du fichier `server/routers.ts` :
|
|
||||||
- Procédure `auth.register` - Inscription avec email/password
|
|
||||||
- Procédure `auth.login` - Connexion avec email/password
|
|
||||||
- Procédure `auth.logout` - Déconnexion (inchangée)
|
|
||||||
|
|
||||||
- ✅ Modification du fichier `server/_core/context.ts` :
|
|
||||||
- Remplacement de l'authentification OAuth par JWT
|
|
||||||
- Lecture du token depuis le cookie
|
|
||||||
- Vérification et décodage du token
|
|
||||||
- Récupération de l'utilisateur depuis la base de données
|
|
||||||
|
|
||||||
### 3. Frontend
|
|
||||||
|
|
||||||
- ✅ Création de la page `Login.tsx` :
|
|
||||||
- Formulaire de connexion (email + mot de passe)
|
|
||||||
- Validation des champs
|
|
||||||
- Gestion des erreurs
|
|
||||||
- Redirection après connexion
|
|
||||||
- Lien vers la page d'inscription
|
|
||||||
|
|
||||||
- ✅ Création de la page `Register.tsx` :
|
|
||||||
- Formulaire d'inscription (nom, email, mot de passe, confirmation)
|
|
||||||
- Validation des champs (minimum 6 caractères)
|
|
||||||
- Vérification de la correspondance des mots de passe
|
|
||||||
- Gestion des erreurs
|
|
||||||
- Redirection après inscription
|
|
||||||
- Lien vers la page de connexion
|
|
||||||
|
|
||||||
- ✅ Modification du fichier `App.tsx` :
|
|
||||||
- Ajout des routes `/login` et `/register`
|
|
||||||
|
|
||||||
- ✅ Modification du fichier `const.ts` :
|
|
||||||
- Remplacement de `getLoginUrl()` pour pointer vers `/login`
|
|
||||||
|
|
||||||
### 4. Documentation
|
|
||||||
|
|
||||||
- ✅ Création du guide de déploiement Linux complet (`GUIDE_DEPLOIEMENT_LINUX.md`)
|
|
||||||
- ✅ Création de ce document de migration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ Modifications restantes
|
|
||||||
|
|
||||||
### 1. Correction des erreurs
|
|
||||||
|
|
||||||
- ❌ **Corriger les doublons dans `server/db.ts`**
|
|
||||||
- Il reste des fonctions dupliquées qui causent des erreurs de compilation
|
|
||||||
- Nettoyer complètement les anciennes fonctions OAuth
|
|
||||||
|
|
||||||
### 2. Page de gestion des utilisateurs
|
|
||||||
|
|
||||||
- ❌ **Modifier `AdminUsers.tsx`** :
|
|
||||||
- Remplacer le champ `openId` par `password` dans le formulaire de création
|
|
||||||
- Ajouter un champ de changement de mot de passe dans le formulaire d'édition
|
|
||||||
- Adapter les mutations pour utiliser email au lieu d'openId
|
|
||||||
|
|
||||||
### 3. Hooks d'authentification
|
|
||||||
|
|
||||||
- ❌ **Vérifier `useAuth()`** :
|
|
||||||
- S'assurer qu'il fonctionne avec la nouvelle authentification JWT
|
|
||||||
- Pas de modification normalement nécessaire car il utilise `trpc.auth.me`
|
|
||||||
|
|
||||||
### 4. Suppression du code OAuth
|
|
||||||
|
|
||||||
- ❌ **Supprimer les fichiers OAuth inutilisés** :
|
|
||||||
- `server/_core/sdk.ts` (si existe)
|
|
||||||
- `server/_core/oauth.ts` (si existe)
|
|
||||||
- Routes OAuth dans `server/_core/index.ts`
|
|
||||||
|
|
||||||
### 5. Variables d'environnement
|
|
||||||
|
|
||||||
- ❌ **Nettoyer le fichier `.env`** :
|
|
||||||
- Supprimer `OAUTH_SERVER_URL`
|
|
||||||
- Supprimer `VITE_OAUTH_PORTAL_URL`
|
|
||||||
- Supprimer `VITE_APP_ID`
|
|
||||||
- Supprimer `OWNER_OPEN_ID`
|
|
||||||
- Garder uniquement :
|
|
||||||
- `DATABASE_URL`
|
|
||||||
- `JWT_SECRET`
|
|
||||||
- `VITE_APP_TITLE`
|
|
||||||
- `VITE_APP_LOGO`
|
|
||||||
- Variables email (optionnelles)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Prochaines étapes pour finaliser
|
|
||||||
|
|
||||||
### Étape 1 : Corriger les erreurs de compilation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/ubuntu/formation-manager-itinova
|
|
||||||
|
|
||||||
# Vérifier les erreurs
|
|
||||||
pnpm check
|
|
||||||
```
|
|
||||||
|
|
||||||
Corriger manuellement les doublons dans `server/db.ts`.
|
|
||||||
|
|
||||||
### Étape 2 : Modifier la page AdminUsers
|
|
||||||
|
|
||||||
1. Ouvrir `client/src/pages/AdminUsers.tsx`
|
|
||||||
2. Remplacer le champ `openId` par `password` dans le formulaire de création
|
|
||||||
3. Ajouter un champ de changement de mot de passe (optionnel) dans l'édition
|
|
||||||
|
|
||||||
### Étape 3 : Tester localement
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Compiler
|
|
||||||
pnpm build
|
|
||||||
|
|
||||||
# Démarrer
|
|
||||||
pnpm start
|
|
||||||
```
|
|
||||||
|
|
||||||
Tester :
|
|
||||||
1. Inscription d'un nouvel utilisateur
|
|
||||||
2. Connexion avec cet utilisateur
|
|
||||||
3. Déconnexion
|
|
||||||
4. Gestion des utilisateurs (admin)
|
|
||||||
|
|
||||||
### Étape 4 : Créer le premier utilisateur admin
|
|
||||||
|
|
||||||
Après déploiement, exécuter ce SQL :
|
|
||||||
|
|
||||||
```sql
|
|
||||||
INSERT INTO users (email, password, name, role, emailVerified, isActive, createdAt, updatedAt, lastSignedIn)
|
|
||||||
VALUES (
|
|
||||||
'admin@itinova.org',
|
|
||||||
'$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK', -- Mot de passe: admin123
|
|
||||||
'Administrateur',
|
|
||||||
'admin',
|
|
||||||
TRUE,
|
|
||||||
TRUE,
|
|
||||||
NOW(),
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
⚠️ **Changez immédiatement le mot de passe après la première connexion !**
|
|
||||||
|
|
||||||
### Étape 5 : Déployer sur le serveur
|
|
||||||
|
|
||||||
Suivre le guide `GUIDE_DEPLOIEMENT_LINUX.md`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Checklist de déploiement
|
|
||||||
|
|
||||||
### Avant le déploiement
|
|
||||||
|
|
||||||
- [ ] Toutes les erreurs de compilation sont corrigées
|
|
||||||
- [ ] Les tests locaux passent (inscription, connexion, déconnexion)
|
|
||||||
- [ ] Le fichier `.env` est configuré correctement
|
|
||||||
- [ ] La base de données est accessible
|
|
||||||
- [ ] Un utilisateur admin est créé en base de données
|
|
||||||
|
|
||||||
### Pendant le déploiement
|
|
||||||
|
|
||||||
- [ ] Les fichiers sont copiés sur le serveur
|
|
||||||
- [ ] Les dépendances sont installées (`pnpm install`)
|
|
||||||
- [ ] La base de données est initialisée (`pnpm db:push`)
|
|
||||||
- [ ] L'application est compilée (`pnpm build`)
|
|
||||||
- [ ] Le service systemd est configuré
|
|
||||||
- [ ] Le service démarre sans erreur
|
|
||||||
|
|
||||||
### Après le déploiement
|
|
||||||
|
|
||||||
- [ ] L'application est accessible via le navigateur
|
|
||||||
- [ ] La connexion fonctionne
|
|
||||||
- [ ] L'inscription fonctionne
|
|
||||||
- [ ] Le tableau de bord admin est accessible
|
|
||||||
- [ ] Les fonctionnalités de gestion des utilisateurs fonctionnent
|
|
||||||
- [ ] Le mot de passe admin par défaut a été changé
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔧 Dépannage
|
|
||||||
|
|
||||||
### Erreur "Email ou mot de passe incorrect"
|
|
||||||
|
|
||||||
- Vérifier que l'utilisateur existe en base de données
|
|
||||||
- Vérifier que le mot de passe est bien hashé avec bcrypt
|
|
||||||
- Vérifier les logs du serveur
|
|
||||||
|
|
||||||
### Erreur "User is not defined"
|
|
||||||
|
|
||||||
- Vérifier que le token JWT est valide
|
|
||||||
- Vérifier que le cookie est bien envoyé
|
|
||||||
- Vérifier que `context.ts` récupère bien l'utilisateur
|
|
||||||
|
|
||||||
### Erreur "Cannot read property 'id' of null"
|
|
||||||
|
|
||||||
- L'utilisateur n'est pas connecté
|
|
||||||
- Rediriger vers `/login`
|
|
||||||
|
|
||||||
### L'application ne démarre pas
|
|
||||||
|
|
||||||
- Vérifier les logs : `sudo journalctl -u formation-manager -f`
|
|
||||||
- Vérifier le fichier `.env`
|
|
||||||
- Vérifier la connexion à la base de données
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Ressources
|
|
||||||
|
|
||||||
- Guide de déploiement : `GUIDE_DEPLOIEMENT_LINUX.md`
|
|
||||||
- Documentation bcrypt : https://www.npmjs.com/package/bcrypt
|
|
||||||
- Documentation JWT : https://jwt.io/
|
|
||||||
- Documentation tRPC : https://trpc.io/
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🆘 Support
|
|
||||||
|
|
||||||
En cas de problème :
|
|
||||||
|
|
||||||
1. Consulter les logs du serveur
|
|
||||||
2. Vérifier la base de données
|
|
||||||
3. Consulter ce document de migration
|
|
||||||
4. Consulter le guide de déploiement
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Note** : Cette migration remplace complètement OAuth Manus par une authentification locale. L'application devient totalement autonome et ne dépend plus de services externes pour l'authentification.
|
|
||||||
@@ -5,35 +5,12 @@ import { Route, Switch } from "wouter";
|
|||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
import AdminRapportPublicCible from "./pages/AdminRapportPublicCible";
|
|
||||||
import Admin from "./pages/Admin";
|
|
||||||
import AdminFormations from "./pages/AdminFormations";
|
|
||||||
import AdminSequences from "./pages/AdminSequences";
|
|
||||||
import AdminApprenants from "./pages/AdminApprenants";
|
|
||||||
import AdminSequenceInscrits from "./pages/AdminSequenceInscrits";
|
|
||||||
import AdminUsers from "./pages/AdminUsers";
|
|
||||||
import AdminEmailTemplates from "./pages/AdminEmailTemplates";
|
|
||||||
import AdminEmailConfig from "./pages/AdminEmailConfig";
|
|
||||||
import Inscription from "./pages/Inscription";
|
|
||||||
import Login from "./pages/Login";
|
|
||||||
import Register from "./pages/Register";
|
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
|
// make sure to consider if you need authentication for certain routes
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={"/login"} component={Login} />
|
|
||||||
<Route path={"/register"} component={Register} />
|
|
||||||
<Route path={"/"} component={Home} />
|
<Route path={"/"} component={Home} />
|
||||||
<Route path={"/admin/rapport-public-cible"} component={AdminRapportPublicCible} />
|
|
||||||
<Route path={"/inscription/:lien"} component={Inscription} />
|
|
||||||
<Route path={"/admin"} component={Admin} />
|
|
||||||
<Route path={"/admin/formations"} component={AdminFormations} />
|
|
||||||
<Route path={"/admin/sequences"} component={AdminSequences} />
|
|
||||||
<Route path={"/admin/apprenants"} component={AdminApprenants} />
|
|
||||||
<Route path={"/admin/sequences/:id/inscrits"} component={AdminSequenceInscrits} />
|
|
||||||
<Route path={"/admin/users"} component={AdminUsers} />
|
|
||||||
<Route path={"/admin/email-templates"} component={AdminEmailTemplates} />
|
|
||||||
<Route path={"/admin/email-config"} component={AdminEmailConfig} />
|
|
||||||
<Route path={"/404"} component={NotFound} />
|
<Route path={"/404"} component={NotFound} />
|
||||||
{/* Final fallback route */}
|
{/* Final fallback route */}
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
@@ -41,10 +18,18 @@ function Router() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: About Theme
|
||||||
|
// - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css
|
||||||
|
// to keep consistent foreground/background color across components
|
||||||
|
// - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ThemeProvider defaultTheme="light">
|
<ThemeProvider
|
||||||
|
defaultTheme="light"
|
||||||
|
// switchable
|
||||||
|
>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Router />
|
<Router />
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ export function useAuth(options?: UseAuthOptions) {
|
|||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
// Supprimer le token de localStorage
|
|
||||||
localStorage.removeItem('auth_token');
|
|
||||||
utils.auth.me.setData(undefined, null);
|
utils.auth.me.setData(undefined, null);
|
||||||
await utils.auth.me.invalidate();
|
await utils.auth.me.invalidate();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,21 +21,15 @@ import {
|
|||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const";
|
import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const";
|
||||||
import { useIsMobile } from "@/hooks/useMobile";
|
import { useIsMobile } from "@/hooks/useMobile";
|
||||||
import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, BarChart3, UserCog, Mail, Settings } from "lucide-react";
|
import { LayoutDashboard, LogOut, PanelLeft, Users } from "lucide-react";
|
||||||
import { CSSProperties, useEffect, useRef, useState } from "react";
|
import { CSSProperties, useEffect, useRef, useState } from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
|
import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ icon: LayoutDashboard, label: "Tableau de bord", path: "/admin" },
|
{ icon: LayoutDashboard, label: "Page 1", path: "/" },
|
||||||
{ icon: GraduationCap, label: "Formations", path: "/admin/formations" },
|
{ icon: Users, label: "Page 2", path: "/some-path" },
|
||||||
{ icon: Calendar, label: "Séquences", path: "/admin/sequences" },
|
|
||||||
{ icon: Users, label: "Apprenants", path: "/admin/apprenants" },
|
|
||||||
{ icon: UserCog, label: "Utilisateurs", path: "/admin/users" },
|
|
||||||
{ icon: Mail, label: "Templates d'emails", path: "/admin/email-templates" },
|
|
||||||
{ icon: Settings, label: "Configuration SMTP", path: "/admin/email-config" },
|
|
||||||
{ icon: BarChart3, label: "Rapport Public Cible", path: "/admin/rapport-public-cible" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const SIDEBAR_WIDTH_KEY = "sidebar-width";
|
const SIDEBAR_WIDTH_KEY = "sidebar-width";
|
||||||
@@ -79,7 +73,7 @@ export default function DashboardLayout({
|
|||||||
<div className="text-center space-y-2">
|
<div className="text-center space-y-2">
|
||||||
<h1 className="text-2xl font-bold tracking-tight">{APP_TITLE}</h1>
|
<h1 className="text-2xl font-bold tracking-tight">{APP_TITLE}</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Veuillez vous connecter pour continuer
|
Please sign in to continue
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,34 +84,7 @@ export default function DashboardLayout({
|
|||||||
size="lg"
|
size="lg"
|
||||||
className="w-full shadow-lg hover:shadow-xl transition-all"
|
className="w-full shadow-lg hover:shadow-xl transition-all"
|
||||||
>
|
>
|
||||||
Se connecter
|
Sign in
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier que l'utilisateur est administrateur
|
|
||||||
if (user.role !== 'admin') {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
|
||||||
<div className="flex flex-col items-center gap-8 p-8 max-w-md w-full">
|
|
||||||
<div className="flex flex-col items-center gap-6">
|
|
||||||
<div className="text-center space-y-2">
|
|
||||||
<h1 className="text-2xl font-bold tracking-tight text-destructive">Accès refusé</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Vous n'avez pas les permissions nécessaires pour accéder à cette page.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
window.location.href = '/';
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
className="w-full shadow-lg hover:shadow-xl transition-all"
|
|
||||||
>
|
|
||||||
Retour à l'accueil
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -288,7 +255,7 @@ function DashboardLayoutContent({
|
|||||||
className="cursor-pointer text-destructive focus:text-destructive"
|
className="cursor-pointer text-destructive focus:text-destructive"
|
||||||
>
|
>
|
||||||
<LogOut className="mr-2 h-4 w-4" />
|
<LogOut className="mr-2 h-4 w-4" />
|
||||||
<span>Déconnexion</span>
|
<span>Sign out</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -4,5 +4,18 @@ export const APP_TITLE = import.meta.env.VITE_APP_TITLE || "App";
|
|||||||
|
|
||||||
export const APP_LOGO = "https://placehold.co/128x128/E1E7EF/1F2937?text=App";
|
export const APP_LOGO = "https://placehold.co/128x128/E1E7EF/1F2937?text=App";
|
||||||
|
|
||||||
// Retourne l'URL de la page de connexion locale
|
// Generate login URL at runtime so redirect URI reflects the current origin.
|
||||||
export const getLoginUrl = () => "/login";
|
export const getLoginUrl = () => {
|
||||||
|
const oauthPortalUrl = import.meta.env.VITE_OAUTH_PORTAL_URL;
|
||||||
|
const appId = import.meta.env.VITE_APP_ID;
|
||||||
|
const redirectUri = `${window.location.origin}/api/oauth/callback`;
|
||||||
|
const state = btoa(redirectUri);
|
||||||
|
|
||||||
|
const url = new URL(`${oauthPortalUrl}/app-auth`);
|
||||||
|
url.searchParams.set("appId", appId);
|
||||||
|
url.searchParams.set("redirectUri", redirectUri);
|
||||||
|
url.searchParams.set("state", state);
|
||||||
|
url.searchParams.set("type", "signIn");
|
||||||
|
|
||||||
|
return url.toString();
|
||||||
|
};
|
||||||
|
|||||||
@@ -43,22 +43,9 @@ const trpcClient = trpc.createClient({
|
|||||||
url: "/api/trpc",
|
url: "/api/trpc",
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
fetch(input, init) {
|
fetch(input, init) {
|
||||||
// Récupérer le token depuis localStorage
|
|
||||||
const token = localStorage.getItem('auth_token');
|
|
||||||
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
...(init?.headers || {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ajouter le header Authorization si un token existe
|
|
||||||
if (token) {
|
|
||||||
headers['Authorization'] = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return globalThis.fetch(input, {
|
return globalThis.fetch(input, {
|
||||||
...(init ?? {}),
|
...(init ?? {}),
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
import DashboardLayout from "@/components/DashboardLayout";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { trpc } from "@/lib/trpc";
|
|
||||||
import { GraduationCap, Calendar, Users, CheckCircle, PieChart as PieChartIcon } from "lucide-react";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
export default function Admin() {
|
|
||||||
const { data: formations, isLoading: loadingFormations } = trpc.formations.list.useQuery();
|
|
||||||
const { data: sequences, isLoading: loadingSessions } = trpc.sequences.list.useQuery();
|
|
||||||
const { data: apprenants, isLoading: loadingApprenants } = trpc.apprenants.list.useQuery();
|
|
||||||
|
|
||||||
// Statistiques par fonction
|
|
||||||
const statsFonction = useMemo(() => {
|
|
||||||
if (!apprenants) return [];
|
|
||||||
|
|
||||||
const directeurs = apprenants.filter(a => a.fonction === "directeur").length;
|
|
||||||
const chefsService = apprenants.filter(a => a.fonction === "chef_service").length;
|
|
||||||
const autres = apprenants.filter(a => a.fonction === "autre").length;
|
|
||||||
|
|
||||||
return [
|
|
||||||
{ name: "Directeurs", value: directeurs, color: "#3b82f6" },
|
|
||||||
{ name: "Chefs de service", value: chefsService, color: "#10b981" },
|
|
||||||
{ name: "Autres", value: autres, color: "#f59e0b" },
|
|
||||||
].filter(item => item.value > 0);
|
|
||||||
}, [apprenants]);
|
|
||||||
|
|
||||||
const stats = [
|
|
||||||
{
|
|
||||||
title: "Formations actives",
|
|
||||||
value: formations?.filter(f => f.actif).length || 0,
|
|
||||||
icon: <GraduationCap className="w-8 h-8 text-blue-500" />,
|
|
||||||
loading: loadingFormations,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Séquences ouvertes",
|
|
||||||
value: sequences?.filter(s => s.statut === "ouverte").length || 0,
|
|
||||||
icon: <Calendar className="w-8 h-8 text-green-500" />,
|
|
||||||
loading: loadingSessions,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Apprenants inscrits",
|
|
||||||
value: apprenants?.length || 0,
|
|
||||||
icon: <Users className="w-8 h-8 text-purple-500" />,
|
|
||||||
loading: loadingApprenants,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Séquences terminées",
|
|
||||||
value: sequences?.filter(s => s.statut === "terminee").length || 0,
|
|
||||||
icon: <CheckCircle className="w-8 h-8 text-orange-500" />,
|
|
||||||
loading: loadingSessions,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Tableau de bord</h1>
|
|
||||||
<p className="text-muted-foreground mt-2">
|
|
||||||
Gestion des formations Manager Itinova
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
{stats.map((stat, index) => (
|
|
||||||
<Card key={index}>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">
|
|
||||||
{stat.title}
|
|
||||||
</CardTitle>
|
|
||||||
{stat.icon}
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{stat.loading ? (
|
|
||||||
<Skeleton className="h-8 w-16" />
|
|
||||||
) : (
|
|
||||||
<div className="text-2xl font-bold">{stat.value}</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Graphique de répartition par fonction */}
|
|
||||||
{statsFonction.length > 0 && (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<PieChartIcon className="w-5 h-5" />
|
|
||||||
Répartition des apprenants par fonction
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Distribution des {apprenants?.length || 0} apprenants selon leur fonction
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{loadingApprenants ? (
|
|
||||||
<div className="h-80 flex items-center justify-center">
|
|
||||||
<Skeleton className="h-64 w-64 rounded-full" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="h-80">
|
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
|
||||||
<PieChart>
|
|
||||||
<Pie
|
|
||||||
data={statsFonction}
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
labelLine={false}
|
|
||||||
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
|
|
||||||
outerRadius={100}
|
|
||||||
fill="#8884d8"
|
|
||||||
dataKey="value"
|
|
||||||
>
|
|
||||||
{statsFonction.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Bienvenue dans l'espace d'administration</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Gérez vos formations, sequences et apprenants depuis le menu de navigation.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<h3 className="font-semibold">Fonctionnalités principales :</h3>
|
|
||||||
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
|
|
||||||
<li>Créer et gérer les formations avec liens d'inscription uniques</li>
|
|
||||||
<li>Organiser les sequences avec limitation à 12 participants</li>
|
|
||||||
<li>Gérer les inscriptions avec blocage automatique à J-15</li>
|
|
||||||
<li>Exporter les listes d'inscrits en PDF ou Excel</li>
|
|
||||||
<li>Générer des invitations Outlook et envoyer des emails automatiques</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
import DashboardLayout from "@/components/DashboardLayout";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { trpc } from "@/lib/trpc";
|
|
||||||
import { Plus, Pencil, Trash2, Search, SlidersHorizontal } from "lucide-react";
|
|
||||||
import { useState, useMemo } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
|
|
||||||
export default function AdminApprenants() {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [editingId, setEditingId] = useState<number | null>(null);
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
const [filterFonction, setFilterFonction] = useState<string>("all");
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
nom: "",
|
|
||||||
prenom: "",
|
|
||||||
email: "",
|
|
||||||
codeEtablissement: "",
|
|
||||||
fonction: "autre" as "directeur" | "chef_service" | "autre",
|
|
||||||
});
|
|
||||||
|
|
||||||
const utils = trpc.useUtils();
|
|
||||||
const { data: apprenants, isLoading } = trpc.apprenants.list.useQuery();
|
|
||||||
|
|
||||||
const createMutation = trpc.apprenants.create.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
utils.apprenants.list.invalidate();
|
|
||||||
toast.success("Apprenant créé avec succès");
|
|
||||||
setOpen(false);
|
|
||||||
resetForm();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Erreur lors de la création : " + error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateMutation = trpc.apprenants.update.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
utils.apprenants.list.invalidate();
|
|
||||||
toast.success("Apprenant mis à jour avec succès");
|
|
||||||
setOpen(false);
|
|
||||||
resetForm();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Erreur lors de la mise à jour : " + error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteMutation = trpc.apprenants.delete.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
utils.apprenants.list.invalidate();
|
|
||||||
toast.success("Apprenant supprimé avec succès");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Erreur lors de la suppression : " + error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
setFormData({ nom: "", prenom: "", email: "", codeEtablissement: "", fonction: "autre" });
|
|
||||||
setEditingId(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (apprenant: any) => {
|
|
||||||
setFormData({
|
|
||||||
nom: apprenant.nom,
|
|
||||||
prenom: apprenant.prenom,
|
|
||||||
email: apprenant.email,
|
|
||||||
codeEtablissement: apprenant.codeEtablissement,
|
|
||||||
fonction: apprenant.fonction || "autre",
|
|
||||||
});
|
|
||||||
setEditingId(apprenant.id);
|
|
||||||
setOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (editingId) {
|
|
||||||
updateMutation.mutate({ id: editingId, ...formData });
|
|
||||||
} else {
|
|
||||||
createMutation.mutate(formData);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (id: number) => {
|
|
||||||
if (confirm("Êtes-vous sûr de vouloir supprimer cet apprenant ?")) {
|
|
||||||
deleteMutation.mutate({ id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filtrer les apprenants
|
|
||||||
const filteredApprenants = useMemo(() => {
|
|
||||||
if (!apprenants) return [];
|
|
||||||
|
|
||||||
return apprenants.filter(apprenant => {
|
|
||||||
const matchesSearch = searchTerm === "" ||
|
|
||||||
apprenant.nom.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
apprenant.prenom.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
apprenant.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
apprenant.codeEtablissement.toLowerCase().includes(searchTerm.toLowerCase());
|
|
||||||
|
|
||||||
const matchesFonction = filterFonction === "all" || apprenant.fonction === filterFonction;
|
|
||||||
|
|
||||||
return matchesSearch && matchesFonction;
|
|
||||||
});
|
|
||||||
}, [apprenants, searchTerm, filterFonction]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Apprenants</h1>
|
|
||||||
<p className="text-muted-foreground mt-2">
|
|
||||||
Gérez les apprenants et leurs informations
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Dialog open={open} onOpenChange={(isOpen) => {
|
|
||||||
setOpen(isOpen);
|
|
||||||
if (!isOpen) resetForm();
|
|
||||||
}}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button>
|
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
|
||||||
Nouvel apprenant
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-[500px]">
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
{editingId ? "Modifier l'apprenant" : "Nouvel apprenant"}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Ajoutez les informations de l'apprenant
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4 py-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="nom">Nom *</Label>
|
|
||||||
<Input
|
|
||||||
id="nom"
|
|
||||||
value={formData.nom}
|
|
||||||
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
|
|
||||||
placeholder="Dupont"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="prenom">Prénom *</Label>
|
|
||||||
<Input
|
|
||||||
id="prenom"
|
|
||||||
value={formData.prenom}
|
|
||||||
onChange={(e) => setFormData({ ...formData, prenom: e.target.value })}
|
|
||||||
placeholder="Jean"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="email">Email *</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
||||||
placeholder="jean.dupont@example.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="codeEtablissement">Code établissement *</Label>
|
|
||||||
<Input
|
|
||||||
id="codeEtablissement"
|
|
||||||
value={formData.codeEtablissement}
|
|
||||||
onChange={(e) => setFormData({ ...formData, codeEtablissement: e.target.value })}
|
|
||||||
placeholder="ETB001"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="fonction">Fonction *</Label>
|
|
||||||
<Select value={formData.fonction} onValueChange={(value: any) => setFormData({ ...formData, fonction: value })}>
|
|
||||||
<SelectTrigger id="fonction">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="directeur">Directeur</SelectItem>
|
|
||||||
<SelectItem value="chef_service">Chef de service</SelectItem>
|
|
||||||
<SelectItem value="autre">Autre</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
|
|
||||||
Annuler
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={createMutation.isPending || updateMutation.isPending}>
|
|
||||||
{editingId ? "Mettre à jour" : "Créer"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filtres et recherche */}
|
|
||||||
<Card className="mb-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<SlidersHorizontal className="w-5 h-5" />
|
|
||||||
Recherche
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="search">Rechercher un apprenant</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
id="search"
|
|
||||||
placeholder="Nom, prénom, email ou code établissement..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="pl-8"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="filterFonction">Filtrer par fonction</Label>
|
|
||||||
<Select value={filterFonction} onValueChange={setFilterFonction}>
|
|
||||||
<SelectTrigger id="filterFonction">
|
|
||||||
<SelectValue placeholder="Toutes les fonctions" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Toutes les fonctions</SelectItem>
|
|
||||||
<SelectItem value="directeur">Directeur</SelectItem>
|
|
||||||
<SelectItem value="chef_service">Chef de service</SelectItem>
|
|
||||||
<SelectItem value="autre">Autre</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Compteur de résultats */}
|
|
||||||
<div className="mt-4 text-sm text-muted-foreground">
|
|
||||||
{filteredApprenants.length} apprenant(s) trouvé(s)
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Liste des apprenants</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Gérez les apprenants inscrits aux formations
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Skeleton className="h-10 w-full" />
|
|
||||||
<Skeleton className="h-10 w-full" />
|
|
||||||
<Skeleton className="h-10 w-full" />
|
|
||||||
</div>
|
|
||||||
) : filteredApprenants && filteredApprenants.length > 0 ? (
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Nom</TableHead>
|
|
||||||
<TableHead>Prénom</TableHead>
|
|
||||||
<TableHead>Email</TableHead>
|
|
||||||
<TableHead>Code établissement</TableHead>
|
|
||||||
<TableHead>Fonction</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{filteredApprenants.map((apprenant) => (
|
|
||||||
<TableRow key={apprenant.id}>
|
|
||||||
<TableCell className="font-medium">{apprenant.nom}</TableCell>
|
|
||||||
<TableCell>{apprenant.prenom}</TableCell>
|
|
||||||
<TableCell>{apprenant.email}</TableCell>
|
|
||||||
<TableCell>{apprenant.codeEtablissement}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{apprenant.fonction === "directeur" ? "Directeur" :
|
|
||||||
apprenant.fonction === "chef_service" ? "Chef de service" : "Autre"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handleEdit(apprenant)}
|
|
||||||
>
|
|
||||||
<Pencil className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handleDelete(apprenant.id)}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
Aucun apprenant pour le moment. Créez-en un pour commencer.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import DashboardLayout from "@/components/DashboardLayout";
|
|
||||||
import { trpc } from "@/lib/trpc";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
import { AlertCircle, CheckCircle2, Mail, Send } from "lucide-react";
|
|
||||||
|
|
||||||
export default function AdminEmailConfig() {
|
|
||||||
const { data: config, isLoading, refetch } = trpc.emailConfig.get.useQuery();
|
|
||||||
const upsertMutation = trpc.emailConfig.upsert.useMutation();
|
|
||||||
const testEmailMutation = trpc.emailConfig.testEmail.useMutation();
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
provider: "resend",
|
|
||||||
apiKey: "",
|
|
||||||
fromEmail: "",
|
|
||||||
fromName: "Formation Manager Itinova",
|
|
||||||
mode: "simulation" as "simulation" | "production",
|
|
||||||
domainVerified: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [testEmail, setTestEmail] = useState("");
|
|
||||||
|
|
||||||
// Charger la configuration existante
|
|
||||||
useEffect(() => {
|
|
||||||
if (config) {
|
|
||||||
setFormData({
|
|
||||||
provider: config.provider,
|
|
||||||
apiKey: config.apiKey || "",
|
|
||||||
fromEmail: config.fromEmail,
|
|
||||||
fromName: config.fromName,
|
|
||||||
mode: config.mode,
|
|
||||||
domainVerified: config.domainVerified,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [config]);
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
upsertMutation.mutate(formData, {
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Configuration enregistrée avec succès");
|
|
||||||
refetch();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(`Erreur: ${error.message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTestEmail = () => {
|
|
||||||
if (!testEmail) {
|
|
||||||
toast.error("Veuillez saisir une adresse email");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
testEmailMutation.mutate(
|
|
||||||
{ toEmail: testEmail },
|
|
||||||
{
|
|
||||||
onSuccess: (data) => {
|
|
||||||
if (data.success) {
|
|
||||||
toast.success(data.message || "Email de test envoyé avec succès");
|
|
||||||
} else {
|
|
||||||
toast.error(data.message || "Échec de l'envoi de l'email de test");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(`Erreur: ${error.message}`);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="container py-8">
|
|
||||||
<p>Chargement...</p>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="container py-8 max-w-4xl">
|
|
||||||
<div className="mb-8">
|
|
||||||
<h1 className="text-3xl font-bold">Configuration des emails</h1>
|
|
||||||
<p className="text-muted-foreground mt-2">
|
|
||||||
Configurez l'envoi d'emails via Resend et basculez entre mode simulation et production
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-6">
|
|
||||||
{/* Statut actuel */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Mail className="h-5 w-5" />
|
|
||||||
Statut actuel
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">Mode d'envoi</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{formData.mode === "simulation"
|
|
||||||
? "Les emails sont simulés et envoyés comme notifications"
|
|
||||||
: "Les emails sont envoyés via Resend"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className={`px-3 py-1 rounded-full text-sm font-medium ${
|
|
||||||
formData.mode === "production"
|
|
||||||
? "bg-green-100 text-green-800"
|
|
||||||
: "bg-yellow-100 text-yellow-800"
|
|
||||||
}`}>
|
|
||||||
{formData.mode === "production" ? "Production" : "Simulation"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{formData.apiKey && (
|
|
||||||
<Alert>
|
|
||||||
<CheckCircle2 className="h-4 w-4" />
|
|
||||||
<AlertDescription>
|
|
||||||
Clé API Resend configurée
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!formData.apiKey && (
|
|
||||||
<Alert>
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertDescription>
|
|
||||||
Aucune clé API configurée. Les emails seront simulés.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Formulaire de configuration */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Configuration Resend</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Configurez votre compte Resend pour envoyer de vrais emails
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="apiKey">Clé API Resend *</Label>
|
|
||||||
<Input
|
|
||||||
id="apiKey"
|
|
||||||
type="password"
|
|
||||||
placeholder="re_..."
|
|
||||||
value={formData.apiKey}
|
|
||||||
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Obtenez votre clé API sur{" "}
|
|
||||||
<a
|
|
||||||
href="https://resend.com/api-keys"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-primary hover:underline"
|
|
||||||
>
|
|
||||||
resend.com/api-keys
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="fromEmail">Email expéditeur *</Label>
|
|
||||||
<Input
|
|
||||||
id="fromEmail"
|
|
||||||
type="email"
|
|
||||||
placeholder="noreply@votredomaine.com"
|
|
||||||
value={formData.fromEmail}
|
|
||||||
onChange={(e) => setFormData({ ...formData, fromEmail: e.target.value })}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
L'adresse email doit être vérifiée dans votre compte Resend
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="fromName">Nom de l'expéditeur *</Label>
|
|
||||||
<Input
|
|
||||||
id="fromName"
|
|
||||||
type="text"
|
|
||||||
placeholder="Formation Manager Itinova"
|
|
||||||
value={formData.fromName}
|
|
||||||
onChange={(e) => setFormData({ ...formData, fromName: e.target.value })}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<Label htmlFor="mode">Mode production</Label>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Activer l'envoi réel d'emails (désactiver pour simuler)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Switch
|
|
||||||
id="mode"
|
|
||||||
checked={formData.mode === "production"}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setFormData({ ...formData, mode: checked ? "production" : "simulation" })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<Button type="submit" disabled={upsertMutation.isPending}>
|
|
||||||
{upsertMutation.isPending ? "Enregistrement..." : "Enregistrer la configuration"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Test d'envoi */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Send className="h-5 w-5" />
|
|
||||||
Test d'envoi
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Envoyez un email de test pour vérifier votre configuration
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="testEmail">Adresse email de test</Label>
|
|
||||||
<Input
|
|
||||||
id="testEmail"
|
|
||||||
type="email"
|
|
||||||
placeholder="votre@email.com"
|
|
||||||
value={testEmail}
|
|
||||||
onChange={(e) => setTestEmail(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={handleTestEmail}
|
|
||||||
disabled={testEmailMutation.isPending || !testEmail}
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{testEmailMutation.isPending ? "Envoi en cours..." : "Envoyer un email de test"}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{formData.mode === "simulation" && (
|
|
||||||
<Alert>
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertDescription>
|
|
||||||
Mode simulation actif. L'email de test sera simulé et envoyé comme notification au propriétaire.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Documentation */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Guide de configuration</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">1. Créer un compte Resend</h4>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Inscrivez-vous gratuitement sur{" "}
|
|
||||||
<a href="https://resend.com" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
|
||||||
resend.com
|
|
||||||
</a>{" "}
|
|
||||||
(100 emails/jour gratuits)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">2. Vérifier votre domaine</h4>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Ajoutez et vérifiez votre domaine dans les paramètres Resend pour pouvoir envoyer des emails
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">3. Obtenir la clé API</h4>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Générez une clé API dans la section "API Keys" de votre compte Resend
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium mb-2">4. Configurer et tester</h4>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Saisissez vos informations ci-dessus, enregistrez, puis testez l'envoi avant de passer en mode production
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
import { trpc } from "@/lib/trpc";
|
|
||||||
import DashboardLayout from "@/components/DashboardLayout";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Mail, Eye, Save, RotateCcw } from "lucide-react";
|
|
||||||
|
|
||||||
export default function AdminEmailTemplates() {
|
|
||||||
const [selectedType, setSelectedType] = useState<string>("inscription");
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
type: "inscription",
|
|
||||||
name: "",
|
|
||||||
logoUrl: "",
|
|
||||||
primaryColor: "#2563eb",
|
|
||||||
headerBgColor: "#2563eb",
|
|
||||||
headerTextColor: "#ffffff",
|
|
||||||
headerTitle: "Formation Manager Itinova",
|
|
||||||
footerText: "",
|
|
||||||
active: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: templates, refetch } = trpc.emailTemplates.list.useQuery();
|
|
||||||
const { data: currentTemplate } = trpc.emailTemplates.getByType.useQuery(
|
|
||||||
{ type: selectedType },
|
|
||||||
{ enabled: !!selectedType }
|
|
||||||
);
|
|
||||||
|
|
||||||
const upsertMutation = trpc.emailTemplates.upsert.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Template enregistré avec succès");
|
|
||||||
refetch();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(`Erreur: ${error.message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const initMutation = trpc.emailTemplates.initializeDefaults.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Templates par défaut initialisés");
|
|
||||||
refetch();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(`Erreur: ${error.message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Charger le template sélectionné
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentTemplate) {
|
|
||||||
setFormData({
|
|
||||||
type: currentTemplate.type,
|
|
||||||
name: currentTemplate.name,
|
|
||||||
logoUrl: currentTemplate.logoUrl || "",
|
|
||||||
primaryColor: currentTemplate.primaryColor,
|
|
||||||
headerBgColor: currentTemplate.headerBgColor,
|
|
||||||
headerTextColor: currentTemplate.headerTextColor,
|
|
||||||
headerTitle: currentTemplate.headerTitle,
|
|
||||||
footerText: currentTemplate.footerText || "",
|
|
||||||
active: currentTemplate.active,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [currentTemplate]);
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
upsertMutation.mutate({
|
|
||||||
...formData,
|
|
||||||
logoUrl: formData.logoUrl || null,
|
|
||||||
footerText: formData.footerText || null,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReset = () => {
|
|
||||||
if (currentTemplate) {
|
|
||||||
setFormData({
|
|
||||||
type: currentTemplate.type,
|
|
||||||
name: currentTemplate.name,
|
|
||||||
logoUrl: currentTemplate.logoUrl || "",
|
|
||||||
primaryColor: currentTemplate.primaryColor,
|
|
||||||
headerBgColor: currentTemplate.headerBgColor,
|
|
||||||
headerTextColor: currentTemplate.headerTextColor,
|
|
||||||
headerTitle: currentTemplate.headerTitle,
|
|
||||||
footerText: currentTemplate.footerText || "",
|
|
||||||
active: currentTemplate.active,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInitDefaults = () => {
|
|
||||||
if (confirm("Initialiser les templates par défaut ? Cela ne modifiera pas les templates existants.")) {
|
|
||||||
initMutation.mutate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Générer la prévisualisation HTML
|
|
||||||
const generatePreview = () => {
|
|
||||||
return `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f5f5f5; }
|
|
||||||
.container { max-width: 600px; margin: 0 auto; background-color: white; }
|
|
||||||
.header { background-color: ${formData.headerBgColor}; color: ${formData.headerTextColor}; padding: 30px 20px; text-align: center; }
|
|
||||||
.header h1 { margin: 0; font-size: 24px; }
|
|
||||||
${formData.logoUrl ? `.header img { max-width: 150px; margin-bottom: 15px; }` : ''}
|
|
||||||
.content { background-color: #f9fafb; padding: 30px 20px; }
|
|
||||||
.content h2 { color: ${formData.primaryColor}; margin-top: 0; }
|
|
||||||
.footer { background-color: #f3f4f6; padding: 20px; text-align: center; font-size: 12px; color: #6b7280; }
|
|
||||||
.button { display: inline-block; padding: 12px 24px; background-color: ${formData.primaryColor}; color: white; text-decoration: none; border-radius: 6px; margin: 10px 0; }
|
|
||||||
.info-box { background-color: #dbeafe; border-left: 4px solid ${formData.primaryColor}; padding: 15px; margin: 15px 0; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
${formData.logoUrl ? `<img src="${formData.logoUrl}" alt="Logo" />` : ''}
|
|
||||||
<h1>${formData.headerTitle}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<h2>Exemple de contenu</h2>
|
|
||||||
<p>Bonjour,</p>
|
|
||||||
<p>Ceci est un exemple de prévisualisation de votre template d'email personnalisé.</p>
|
|
||||||
<div class="info-box">
|
|
||||||
<p><strong>Information importante</strong></p>
|
|
||||||
<p>Ce bloc utilise la couleur principale que vous avez choisie.</p>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: center; margin: 20px 0;">
|
|
||||||
<a href="#" class="button">Bouton d'action</a>
|
|
||||||
</div>
|
|
||||||
<p>Cordialement,<br/>L'équipe Formation Manager Itinova</p>
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
<p>${formData.footerText || 'Texte du pied de page'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`.trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
const templateTypes = [
|
|
||||||
{ value: "inscription", label: "Confirmation d'inscription" },
|
|
||||||
{ value: "teaser", label: "Email teaser" },
|
|
||||||
{ value: "rappel", label: "Rappel J-7" },
|
|
||||||
{ value: "reset_password", label: "Réinitialisation de mot de passe" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="mb-6">
|
|
||||||
<h1 className="text-3xl font-bold mb-2">Templates d'emails</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Personnalisez l'apparence de vos emails automatiques
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{(!templates || templates.length === 0) && (
|
|
||||||
<Card className="mb-6 border-yellow-200 bg-yellow-50">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Mail className="h-5 w-5" />
|
|
||||||
Initialisation requise
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Aucun template trouvé. Initialisez les templates par défaut pour commencer.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Button onClick={handleInitDefaults} disabled={initMutation.isPending}>
|
|
||||||
{initMutation.isPending ? "Initialisation..." : "Initialiser les templates par défaut"}
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
{/* Éditeur */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Éditeur de template</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Modifiez les paramètres du template sélectionné
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="templateType">Type de template</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedType}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setSelectedType(value);
|
|
||||||
setFormData((prev) => ({ ...prev, type: value }));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="templateType">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{templateTypes.map((type) => (
|
|
||||||
<SelectItem key={type.value} value={type.value}>
|
|
||||||
{type.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="name">Nom du template</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
value={formData.name}
|
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
||||||
placeholder="Ex: Confirmation d'inscription"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="logoUrl">URL du logo (optionnel)</Label>
|
|
||||||
<Input
|
|
||||||
id="logoUrl"
|
|
||||||
value={formData.logoUrl}
|
|
||||||
onChange={(e) => setFormData({ ...formData, logoUrl: e.target.value })}
|
|
||||||
placeholder="https://exemple.com/logo.png"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="headerBgColor">Couleur fond en-tête</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
id="headerBgColor"
|
|
||||||
type="color"
|
|
||||||
value={formData.headerBgColor}
|
|
||||||
onChange={(e) => setFormData({ ...formData, headerBgColor: e.target.value })}
|
|
||||||
className="w-16 h-10 p-1"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={formData.headerBgColor}
|
|
||||||
onChange={(e) => setFormData({ ...formData, headerBgColor: e.target.value })}
|
|
||||||
placeholder="#2563eb"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="headerTextColor">Couleur texte en-tête</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
id="headerTextColor"
|
|
||||||
type="color"
|
|
||||||
value={formData.headerTextColor}
|
|
||||||
onChange={(e) => setFormData({ ...formData, headerTextColor: e.target.value })}
|
|
||||||
className="w-16 h-10 p-1"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={formData.headerTextColor}
|
|
||||||
onChange={(e) => setFormData({ ...formData, headerTextColor: e.target.value })}
|
|
||||||
placeholder="#ffffff"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="primaryColor">Couleur principale</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
id="primaryColor"
|
|
||||||
type="color"
|
|
||||||
value={formData.primaryColor}
|
|
||||||
onChange={(e) => setFormData({ ...formData, primaryColor: e.target.value })}
|
|
||||||
className="w-16 h-10 p-1"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={formData.primaryColor}
|
|
||||||
onChange={(e) => setFormData({ ...formData, primaryColor: e.target.value })}
|
|
||||||
placeholder="#2563eb"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Utilisée pour les boutons et les accents
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="headerTitle">Titre de l'en-tête</Label>
|
|
||||||
<Input
|
|
||||||
id="headerTitle"
|
|
||||||
value={formData.headerTitle}
|
|
||||||
onChange={(e) => setFormData({ ...formData, headerTitle: e.target.value })}
|
|
||||||
placeholder="Formation Manager Itinova"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="footerText">Texte du pied de page</Label>
|
|
||||||
<Textarea
|
|
||||||
id="footerText"
|
|
||||||
value={formData.footerText}
|
|
||||||
onChange={(e) => setFormData({ ...formData, footerText: e.target.value })}
|
|
||||||
placeholder="Cet email a été envoyé automatiquement..."
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2 pt-4">
|
|
||||||
<Button onClick={handleSave} disabled={upsertMutation.isPending} className="flex-1">
|
|
||||||
<Save className="h-4 w-4 mr-2" />
|
|
||||||
{upsertMutation.isPending ? "Enregistrement..." : "Enregistrer"}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleReset} variant="outline">
|
|
||||||
<RotateCcw className="h-4 w-4 mr-2" />
|
|
||||||
Réinitialiser
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Prévisualisation */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Eye className="h-5 w-5" />
|
|
||||||
Prévisualisation en temps réel
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Aperçu de votre template avec les paramètres actuels
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="border rounded-lg overflow-hidden bg-gray-50">
|
|
||||||
<iframe
|
|
||||||
srcDoc={generatePreview()}
|
|
||||||
className="w-full h-[600px] border-0"
|
|
||||||
title="Prévisualisation du template"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
import DashboardLayout from "@/components/DashboardLayout";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
||||||
import { trpc } from "@/lib/trpc";
|
|
||||||
import { Plus, Pencil, Trash2, ExternalLink, Search, SlidersHorizontal } from "lucide-react";
|
|
||||||
import { useState, useMemo } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
|
|
||||||
export default function AdminFormations() {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [editingId, setEditingId] = useState<number | null>(null);
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
const [filterActif, setFilterActif] = useState<string>("all");
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
nom: "",
|
|
||||||
description: "",
|
|
||||||
lienUnique: "",
|
|
||||||
actif: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const utils = trpc.useUtils();
|
|
||||||
const { data: formations, isLoading } = trpc.formations.list.useQuery();
|
|
||||||
const createMutation = trpc.formations.create.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
utils.formations.list.invalidate();
|
|
||||||
toast.success("Formation créée avec succès");
|
|
||||||
setOpen(false);
|
|
||||||
resetForm();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Erreur lors de la création : " + error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateMutation = trpc.formations.update.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
utils.formations.list.invalidate();
|
|
||||||
toast.success("Formation mise à jour avec succès");
|
|
||||||
setOpen(false);
|
|
||||||
resetForm();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Erreur lors de la mise à jour : " + error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteMutation = trpc.formations.delete.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
utils.formations.list.invalidate();
|
|
||||||
toast.success("Formation supprimée avec succès");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Erreur lors de la suppression : " + error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
setFormData({ nom: "", description: "", lienUnique: "", actif: true });
|
|
||||||
setEditingId(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (formation: any) => {
|
|
||||||
setFormData({
|
|
||||||
nom: formation.nom,
|
|
||||||
description: formation.description || "",
|
|
||||||
lienUnique: formation.lienUnique,
|
|
||||||
actif: formation.actif,
|
|
||||||
});
|
|
||||||
setEditingId(formation.id);
|
|
||||||
setOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (editingId) {
|
|
||||||
updateMutation.mutate({ id: editingId, ...formData });
|
|
||||||
} else {
|
|
||||||
createMutation.mutate(formData);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (id: number) => {
|
|
||||||
if (confirm("Êtes-vous sûr de vouloir supprimer cette formation ?")) {
|
|
||||||
deleteMutation.mutate({ id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getInscriptionUrl = (lien: string) => {
|
|
||||||
return `${window.location.origin}/inscription/${lien}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filtrer les formations
|
|
||||||
const filteredFormations = useMemo(() => {
|
|
||||||
if (!formations) return [];
|
|
||||||
|
|
||||||
return formations.filter(formation => {
|
|
||||||
// Filtre par recherche
|
|
||||||
const matchesSearch = searchTerm === "" ||
|
|
||||||
formation.nom.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
(formation.description && formation.description.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
||||||
|
|
||||||
// Filtre par statut actif
|
|
||||||
const matchesActif = filterActif === "all" ||
|
|
||||||
(filterActif === "actif" && formation.actif) ||
|
|
||||||
(filterActif === "inactif" && !formation.actif);
|
|
||||||
|
|
||||||
return matchesSearch && matchesActif;
|
|
||||||
});
|
|
||||||
}, [formations, searchTerm, filterActif]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Formations</h1>
|
|
||||||
<p className="text-muted-foreground mt-2">
|
|
||||||
Gérez les formations et leurs liens d'inscription
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Dialog open={open} onOpenChange={(isOpen) => {
|
|
||||||
setOpen(isOpen);
|
|
||||||
if (!isOpen) resetForm();
|
|
||||||
}}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button>
|
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
|
||||||
Nouvelle formation
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-[500px]">
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
{editingId ? "Modifier la formation" : "Nouvelle formation"}
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Créez une formation avec un lien d'inscription unique
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4 py-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="nom">Nom de la formation *</Label>
|
|
||||||
<Input
|
|
||||||
id="nom"
|
|
||||||
value={formData.nom}
|
|
||||||
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
|
|
||||||
placeholder="Manager Itinova"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="description">Description</Label>
|
|
||||||
<Textarea
|
|
||||||
id="description"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
|
||||||
placeholder="Description de la formation..."
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="lienUnique">Lien unique *</Label>
|
|
||||||
<Input
|
|
||||||
id="lienUnique"
|
|
||||||
value={formData.lienUnique}
|
|
||||||
onChange={(e) => setFormData({ ...formData, lienUnique: e.target.value })}
|
|
||||||
placeholder="manager-itinova-2026"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Ce lien sera utilisé pour l'inscription : /inscription/{formData.lienUnique}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Switch
|
|
||||||
id="actif"
|
|
||||||
checked={formData.actif}
|
|
||||||
onCheckedChange={(checked) => setFormData({ ...formData, actif: checked })}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="actif">Formation active</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
|
|
||||||
Annuler
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={createMutation.isPending || updateMutation.isPending}>
|
|
||||||
{editingId ? "Mettre à jour" : "Créer"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filtres et recherche */}
|
|
||||||
<Card className="mb-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<SlidersHorizontal className="w-5 h-5" />
|
|
||||||
Filtres et recherche
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{/* Recherche */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="search">Rechercher</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
id="search"
|
|
||||||
placeholder="Nom ou description..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="pl-8"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filtre par statut */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="filterActif">Statut</Label>
|
|
||||||
<Select value={filterActif} onValueChange={setFilterActif}>
|
|
||||||
<SelectTrigger id="filterActif">
|
|
||||||
<SelectValue placeholder="Tous les statuts" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous les statuts</SelectItem>
|
|
||||||
<SelectItem value="actif">Active</SelectItem>
|
|
||||||
<SelectItem value="inactif">Inactive</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Compteur de résultats */}
|
|
||||||
<div className="mt-4 text-sm text-muted-foreground">
|
|
||||||
{filteredFormations.length} formation(s) trouvée(s)
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Liste des formations</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Gérez vos formations et accédez aux liens d'inscription
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Skeleton className="h-10 w-full" />
|
|
||||||
<Skeleton className="h-10 w-full" />
|
|
||||||
<Skeleton className="h-10 w-full" />
|
|
||||||
</div>
|
|
||||||
) : filteredFormations && filteredFormations.length > 0 ? (
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Nom</TableHead>
|
|
||||||
<TableHead>Description</TableHead>
|
|
||||||
<TableHead>Lien d'inscription</TableHead>
|
|
||||||
<TableHead>Statut</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{filteredFormations.map((formation) => (
|
|
||||||
<TableRow key={formation.id}>
|
|
||||||
<TableCell className="font-medium">{formation.nom}</TableCell>
|
|
||||||
<TableCell className="max-w-xs truncate">{formation.description || "-"}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<a
|
|
||||||
href={getInscriptionUrl(formation.lienUnique)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="flex items-center gap-1 text-blue-600 hover:underline"
|
|
||||||
>
|
|
||||||
{formation.lienUnique}
|
|
||||||
<ExternalLink className="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
|
||||||
formation.actif
|
|
||||||
? "bg-green-100 text-green-800"
|
|
||||||
: "bg-gray-100 text-gray-800"
|
|
||||||
}`}>
|
|
||||||
{formation.actif ? "Active" : "Inactive"}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handleEdit(formation)}
|
|
||||||
>
|
|
||||||
<Pencil className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handleDelete(formation.id)}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
Aucune formation pour le moment. Créez-en une pour commencer.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
import DashboardLayout from "@/components/DashboardLayout";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
||||||
import { trpc } from "@/lib/trpc";
|
|
||||||
import { BarChart3, TrendingUp, AlertCircle } from "lucide-react";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
export default function AdminRapportPublicCible() {
|
|
||||||
const { data: sequences, isLoading: loadingSequences } = trpc.sequences.list.useQuery();
|
|
||||||
const { data: inscriptions, isLoading: loadingInscriptions } = trpc.inscriptions.listAll.useQuery();
|
|
||||||
|
|
||||||
// Calculer les statistiques d'écart entre public cible et fonction des apprenants
|
|
||||||
const stats = useMemo(() => {
|
|
||||||
if (!sequences || !inscriptions) return null;
|
|
||||||
|
|
||||||
const sequencesWithMismatch = sequences.map((sequence) => {
|
|
||||||
const sequenceInscriptions = inscriptions.filter(
|
|
||||||
(insc: any) => insc.inscription.sequenceId === sequence.id && insc.inscription.statut === "confirmee"
|
|
||||||
);
|
|
||||||
|
|
||||||
const totalInscrits = sequenceInscriptions.length;
|
|
||||||
const mismatchCount = sequenceInscriptions.filter((insc: any) => {
|
|
||||||
const apprenantFonction = insc.apprenant.fonction;
|
|
||||||
const sequencePublicCible = sequence.publicCible;
|
|
||||||
|
|
||||||
// Mapper les fonctions aux publics cibles
|
|
||||||
const fonctionToPublicCible: Record<string, string> = {
|
|
||||||
directeur: "directeur",
|
|
||||||
chef_service: "chef_service",
|
|
||||||
autre: "autre",
|
|
||||||
};
|
|
||||||
|
|
||||||
return fonctionToPublicCible[apprenantFonction] !== sequencePublicCible;
|
|
||||||
}).length;
|
|
||||||
|
|
||||||
const matchRate = totalInscrits > 0 ? ((totalInscrits - mismatchCount) / totalInscrits) * 100 : 100;
|
|
||||||
|
|
||||||
return {
|
|
||||||
sequence,
|
|
||||||
totalInscrits,
|
|
||||||
mismatchCount,
|
|
||||||
matchCount: totalInscrits - mismatchCount,
|
|
||||||
matchRate,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Statistiques globales
|
|
||||||
const totalInscrits = sequencesWithMismatch.reduce((sum, s) => sum + s.totalInscrits, 0);
|
|
||||||
const totalMismatches = sequencesWithMismatch.reduce((sum, s) => sum + s.mismatchCount, 0);
|
|
||||||
const globalMatchRate = totalInscrits > 0 ? ((totalInscrits - totalMismatches) / totalInscrits) * 100 : 100;
|
|
||||||
|
|
||||||
return {
|
|
||||||
sequencesWithMismatch: sequencesWithMismatch.filter((s) => s.totalInscrits > 0),
|
|
||||||
totalInscrits,
|
|
||||||
totalMismatches,
|
|
||||||
totalMatches: totalInscrits - totalMismatches,
|
|
||||||
globalMatchRate,
|
|
||||||
};
|
|
||||||
}, [sequences, inscriptions]);
|
|
||||||
|
|
||||||
const getPublicCibleLabel = (publicCible: string) => {
|
|
||||||
const labels = {
|
|
||||||
directeur: "Directeur",
|
|
||||||
chef_service: "Chef de service",
|
|
||||||
autre: "Autre",
|
|
||||||
};
|
|
||||||
return labels[publicCible as keyof typeof labels] || "Autre";
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPublicCibleBadge = (publicCible: string) => {
|
|
||||||
const colors = {
|
|
||||||
directeur: "bg-blue-100 text-blue-800",
|
|
||||||
chef_service: "bg-green-100 text-green-800",
|
|
||||||
autre: "bg-gray-100 text-gray-800",
|
|
||||||
};
|
|
||||||
return colors[publicCible as keyof typeof colors] || colors.autre;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMatchRateBadge = (matchRate: number) => {
|
|
||||||
if (matchRate >= 80) return "bg-green-100 text-green-800";
|
|
||||||
if (matchRate >= 50) return "bg-orange-100 text-orange-800";
|
|
||||||
return "bg-red-100 text-red-800";
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loadingSequences || loadingInscriptions) {
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="container mx-auto py-8">
|
|
||||||
<div className="text-center py-8 text-muted-foreground">Chargement...</div>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stats) {
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="container mx-auto py-8">
|
|
||||||
<div className="text-center py-8 text-muted-foreground">Aucune donnée disponible</div>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="container mx-auto py-8 space-y-6">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold">Rapport d'analyse Public Cible</h1>
|
|
||||||
<p className="text-muted-foreground mt-1">
|
|
||||||
Comparaison entre le public cible des séquences et la fonction des apprenants inscrits
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Statistiques globales */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Total Inscrits</CardTitle>
|
|
||||||
<BarChart3 className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{stats.totalInscrits}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">Inscrits confirmés</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Correspondances</CardTitle>
|
|
||||||
<TrendingUp className="h-4 w-4 text-green-600" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold text-green-600">{stats.totalMatches}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{stats.globalMatchRate.toFixed(1)}% de correspondance
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Écarts</CardTitle>
|
|
||||||
<AlertCircle className="h-4 w-4 text-orange-600" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold text-orange-600">{stats.totalMismatches}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
{(100 - stats.globalMatchRate).toFixed(1)}% d'écart
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Séquences Analysées</CardTitle>
|
|
||||||
<BarChart3 className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">{stats.sequencesWithMismatch.length}</div>
|
|
||||||
<p className="text-xs text-muted-foreground">Avec inscrits confirmés</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tableau détaillé par séquence */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Analyse détaillée par séquence</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Taux de correspondance entre le public cible et la fonction des apprenants inscrits
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{stats.sequencesWithMismatch.length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
Aucune séquence avec des inscrits confirmés
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Séquence</TableHead>
|
|
||||||
<TableHead>Public cible</TableHead>
|
|
||||||
<TableHead className="text-right">Total Inscrits</TableHead>
|
|
||||||
<TableHead className="text-right">Correspondances</TableHead>
|
|
||||||
<TableHead className="text-right">Écarts</TableHead>
|
|
||||||
<TableHead className="text-right">Taux de correspondance</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{stats.sequencesWithMismatch
|
|
||||||
.sort((a, b) => a.matchRate - b.matchRate)
|
|
||||||
.map((stat) => (
|
|
||||||
<TableRow key={stat.sequence.id}>
|
|
||||||
<TableCell className="font-medium">{stat.sequence.nom}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getPublicCibleBadge(stat.sequence.publicCible)}`}
|
|
||||||
>
|
|
||||||
{getPublicCibleLabel(stat.sequence.publicCible)}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">{stat.totalInscrits}</TableCell>
|
|
||||||
<TableCell className="text-right text-green-600 font-medium">
|
|
||||||
{stat.matchCount}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right text-orange-600 font-medium">
|
|
||||||
{stat.mismatchCount}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getMatchRateBadge(stat.matchRate)}`}
|
|
||||||
>
|
|
||||||
{stat.matchRate.toFixed(1)}%
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Recommandations */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Recommandations</CardTitle>
|
|
||||||
<CardDescription>Actions suggérées pour optimiser la planification des formations</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{stats.globalMatchRate < 70 && (
|
|
||||||
<div className="flex gap-3 p-4 bg-red-50 border border-red-200 rounded-lg">
|
|
||||||
<AlertCircle className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-red-900">Taux de correspondance faible</p>
|
|
||||||
<p className="text-sm text-red-700 mt-1">
|
|
||||||
Le taux de correspondance global est inférieur à 70%. Considérez la création de séquences
|
|
||||||
spécifiques pour chaque public cible.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{stats.sequencesWithMismatch.some((s) => s.matchRate < 50) && (
|
|
||||||
<div className="flex gap-3 p-4 bg-orange-50 border border-orange-200 rounded-lg">
|
|
||||||
<AlertCircle className="h-5 w-5 text-orange-600 flex-shrink-0 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-orange-900">Séquences à revoir</p>
|
|
||||||
<p className="text-sm text-orange-700 mt-1">
|
|
||||||
Certaines séquences ont un taux de correspondance inférieur à 50%. Vérifiez si le public
|
|
||||||
cible est correctement défini ou si les apprenants sont inscrits aux bonnes séquences.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{stats.globalMatchRate >= 80 && (
|
|
||||||
<div className="flex gap-3 p-4 bg-green-50 border border-green-200 rounded-lg">
|
|
||||||
<TrendingUp className="h-5 w-5 text-green-600 flex-shrink-0 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium text-green-900">Excellente correspondance</p>
|
|
||||||
<p className="text-sm text-green-700 mt-1">
|
|
||||||
Le taux de correspondance global est excellent (≥ 80%). La planification des formations
|
|
||||||
est bien alignée avec les publics cibles.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,557 +0,0 @@
|
|||||||
import DashboardLayout from "@/components/DashboardLayout";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { trpc } from "@/lib/trpc";
|
|
||||||
import { ArrowLeft, Download, Mail, Search } from "lucide-react";
|
|
||||||
import { useLocation, useRoute } from "wouter";
|
|
||||||
import { useState, useMemo } from "react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
export default function AdminSequenceInscrits() {
|
|
||||||
const [, setLocation] = useLocation();
|
|
||||||
const [, params] = useRoute("/admin/sequences/:id/inscrits");
|
|
||||||
const sequenceId = params?.id ? parseInt(params.id) : 0;
|
|
||||||
|
|
||||||
const getPublicCibleBadge = (publicCible: string) => {
|
|
||||||
const colors = {
|
|
||||||
directeur: "bg-blue-100 text-blue-800",
|
|
||||||
chef_service: "bg-green-100 text-green-800",
|
|
||||||
autre: "bg-gray-100 text-gray-800",
|
|
||||||
};
|
|
||||||
return colors[publicCible as keyof typeof colors] || colors.autre;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPublicCibleLabel = (publicCible: string) => {
|
|
||||||
const labels = {
|
|
||||||
directeur: "Directeur",
|
|
||||||
chef_service: "Chef de service",
|
|
||||||
autre: "Autre",
|
|
||||||
};
|
|
||||||
return labels[publicCible as keyof typeof labels] || "Autre";
|
|
||||||
};
|
|
||||||
|
|
||||||
// États pour les filtres
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
const [filterStatut, setFilterStatut] = useState<string>("all");
|
|
||||||
const [filterEtablissement, setFilterEtablissement] = useState<string>("all");
|
|
||||||
const [filterFonction, setFilterFonction] = useState<string>("all");
|
|
||||||
const [sortBy, setSortBy] = useState<"date" | "nom">("date");
|
|
||||||
|
|
||||||
const utils = trpc.useUtils();
|
|
||||||
const { data: sequence, isLoading: loadingSequence } = trpc.sequences.getById.useQuery({ id: sequenceId });
|
|
||||||
const { data: inscriptions, isLoading: loadingInscriptions } = trpc.inscriptions.listBySequence.useQuery({ sequenceId });
|
|
||||||
const { data: formations } = trpc.formations.list.useQuery();
|
|
||||||
|
|
||||||
const updateStatutMutation = trpc.inscriptions.updateStatut.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
utils.inscriptions.listBySequence.invalidate({ sequenceId });
|
|
||||||
toast.success("Statut mis à jour avec succès");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Erreur lors de la mise à jour : " + error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const sendEmailsMutation = trpc.inscriptions.sendGroupEmail.useMutation({
|
|
||||||
onSuccess: (result) => {
|
|
||||||
toast.success(`Emails envoyés : ${result.sent} réussis, ${result.failed} échecs`);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Erreur lors de l'envoi : " + error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleStatutChange = (inscriptionId: number, newStatut: string) => {
|
|
||||||
updateStatutMutation.mutate({
|
|
||||||
id: inscriptionId,
|
|
||||||
statut: newStatut as "confirmee" | "liste_attente" | "annulee",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportExcelMutation = trpc.inscriptions.exportExcel.useMutation({
|
|
||||||
onSuccess: (result) => {
|
|
||||||
const blob = new Blob([Uint8Array.from(atob(result.buffer), c => c.charCodeAt(0))], {
|
|
||||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
||||||
});
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = result.filename;
|
|
||||||
a.click();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
toast.success("Export Excel téléchargé");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Erreur lors de l'export : " + error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const exportPDFMutation = trpc.inscriptions.exportPDF.useMutation({
|
|
||||||
onSuccess: (result) => {
|
|
||||||
const blob = new Blob([Uint8Array.from(atob(result.buffer), c => c.charCodeAt(0))], {
|
|
||||||
type: 'application/pdf'
|
|
||||||
});
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = result.filename;
|
|
||||||
a.click();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
toast.success("Export PDF téléchargé");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Erreur lors de l'export : " + error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const exportFeuilleMutation = trpc.inscriptions.exportFeuillePresence.useMutation({
|
|
||||||
onSuccess: (result) => {
|
|
||||||
const blob = new Blob([Uint8Array.from(atob(result.buffer), c => c.charCodeAt(0))], {
|
|
||||||
type: 'application/pdf'
|
|
||||||
});
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = result.filename;
|
|
||||||
a.click();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
toast.success("Feuille de présence téléchargée");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Erreur lors de l'export : " + error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleExportExcel = () => {
|
|
||||||
exportExcelMutation.mutate({ sequenceId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExportPDF = () => {
|
|
||||||
exportPDFMutation.mutate({ sequenceId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExportFeuille = () => {
|
|
||||||
exportFeuilleMutation.mutate({ sequenceId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSendEmails = (type: "teaser" | "rappel") => {
|
|
||||||
if (confirm(`Êtes-vous sûr de vouloir envoyer les emails ${type === "teaser" ? "teaser" : "de rappel J-7"} à tous les inscrits confirmés ?`)) {
|
|
||||||
sendEmailsMutation.mutate({ sequenceId, type });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filtrage et tri
|
|
||||||
const filteredAndSortedInscriptions = useMemo(() => {
|
|
||||||
if (!inscriptions) return [];
|
|
||||||
|
|
||||||
let filtered = inscriptions.filter((item) => {
|
|
||||||
if (!item.apprenant) return false;
|
|
||||||
|
|
||||||
const matchSearch =
|
|
||||||
item.apprenant.nom.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
item.apprenant.prenom.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
item.apprenant.email.toLowerCase().includes(searchTerm.toLowerCase());
|
|
||||||
|
|
||||||
const matchStatut = filterStatut === "all" || item.inscription.statut === filterStatut;
|
|
||||||
const matchEtablissement = filterEtablissement === "all" || item.apprenant.codeEtablissement === filterEtablissement;
|
|
||||||
const matchFonction = filterFonction === "all" || item.apprenant.fonction === filterFonction;
|
|
||||||
|
|
||||||
return matchSearch && matchStatut && matchEtablissement && matchFonction;
|
|
||||||
});
|
|
||||||
|
|
||||||
filtered.sort((a, b) => {
|
|
||||||
if (sortBy === "date") {
|
|
||||||
return new Date(a.inscription.dateInscription).getTime() - new Date(b.inscription.dateInscription).getTime();
|
|
||||||
} else {
|
|
||||||
return (a.apprenant?.nom || "").localeCompare(b.apprenant?.nom || "");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
}, [inscriptions, searchTerm, filterStatut, filterEtablissement, filterFonction, sortBy]);
|
|
||||||
|
|
||||||
const uniqueEtablissements = useMemo(() => {
|
|
||||||
if (!inscriptions) return [];
|
|
||||||
return Array.from(new Set(inscriptions.map((i) => i.apprenant?.codeEtablissement).filter(Boolean)));
|
|
||||||
}, [inscriptions]);
|
|
||||||
|
|
||||||
const getFormationName = () => {
|
|
||||||
if (!sequence || !formations) return "N/A";
|
|
||||||
return formations.find((f) => f.id === sequence.formationId)?.nom || "N/A";
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateValue: any) => {
|
|
||||||
if (!dateValue) return "N/A";
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Si c'est déjà un objet Date JavaScript
|
|
||||||
if (dateValue instanceof Date) {
|
|
||||||
const day = String(dateValue.getDate()).padStart(2, '0');
|
|
||||||
const month = String(dateValue.getMonth() + 1).padStart(2, '0');
|
|
||||||
const year = dateValue.getFullYear();
|
|
||||||
const hours = String(dateValue.getHours()).padStart(2, '0');
|
|
||||||
const minutes = String(dateValue.getMinutes()).padStart(2, '0');
|
|
||||||
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parser la date MySQL (chaîne)
|
|
||||||
const dateStr = String(dateValue);
|
|
||||||
const match = dateStr.match(/(\d{4})-(\d{2})-(\d{2})[T ]?(\d{2}):(\d{2})/);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
console.warn('Format de date non reconnu:', dateValue);
|
|
||||||
return "N/A";
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, year, month, day, hours, minutes] = match;
|
|
||||||
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur de formatage de date:', error, dateValue);
|
|
||||||
return "N/A";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatutBadge = (statut: string) => {
|
|
||||||
const colors = {
|
|
||||||
confirmee: "bg-green-100 text-green-800",
|
|
||||||
liste_attente: "bg-orange-100 text-orange-800",
|
|
||||||
annulee: "bg-gray-100 text-gray-800",
|
|
||||||
};
|
|
||||||
const labels = {
|
|
||||||
confirmee: "Confirmée",
|
|
||||||
liste_attente: "Liste d'attente",
|
|
||||||
annulee: "Annulée",
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
color: colors[statut as keyof typeof colors] || colors.confirmee,
|
|
||||||
label: labels[statut as keyof typeof labels] || statut,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFonctionLabel = (fonction: string) => {
|
|
||||||
const labels = {
|
|
||||||
directeur: "Directeur",
|
|
||||||
chef_service: "Chef de service",
|
|
||||||
autre: "Autre",
|
|
||||||
};
|
|
||||||
return labels[fonction as keyof typeof labels] || fonction;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loadingSequence || loadingInscriptions) {
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="container mx-auto py-8 space-y-6">
|
|
||||||
<Skeleton className="h-8 w-64" />
|
|
||||||
<Skeleton className="h-64 w-full" />
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sequence) {
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="container mx-auto py-8">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Séquence introuvable</CardTitle>
|
|
||||||
<CardDescription>La séquence demandée n'existe pas.</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Button onClick={() => setLocation("/admin/sequences")}>
|
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
||||||
Retour aux séquences
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nbConfirmes = inscriptions?.filter((i) => i.inscription.statut === "confirmee").length || 0;
|
|
||||||
const nbListeAttente = inscriptions?.filter((i) => i.inscription.statut === "liste_attente").length || 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="container mx-auto py-8 space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<Button variant="ghost" onClick={() => setLocation("/admin/sequences")} className="mb-2">
|
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
||||||
Retour aux séquences
|
|
||||||
</Button>
|
|
||||||
<h1 className="text-3xl font-bold">Inscrits à la séquence</h1>
|
|
||||||
<p className="text-muted-foreground mt-1">
|
|
||||||
{getFormationName()} - {sequence.nom}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Informations de la séquence */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Informations de la séquence</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-muted-foreground">Formation</p>
|
|
||||||
<p className="text-base">{getFormationName()}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-muted-foreground">Nom de la séquence</p>
|
|
||||||
<p className="text-base">{sequence.nom}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-muted-foreground">Lieu</p>
|
|
||||||
<p className="text-base">{sequence.lieu}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-muted-foreground">Capacité</p>
|
|
||||||
<p className="text-base">
|
|
||||||
{nbConfirmes} / {sequence.capaciteMax} inscrits confirmés
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-muted-foreground">Public cible</p>
|
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getPublicCibleBadge(sequence.publicCible)}`}>
|
|
||||||
{getPublicCibleLabel(sequence.publicCible)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-muted-foreground mb-2">Dates de formation</p>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{sequence.dates.map((date: any) => (
|
|
||||||
<div key={date.id} className="flex items-center gap-2 text-sm">
|
|
||||||
<span className="font-medium">Date {date.ordre}:</span>
|
|
||||||
<span>
|
|
||||||
Du {formatDate(date.dateDebut)} au {formatDate(date.dateFin)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Actions</CardTitle>
|
|
||||||
<CardDescription>Envoi d'emails et exports</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => handleSendEmails("teaser")}
|
|
||||||
disabled={sendEmailsMutation.isPending || nbConfirmes === 0}
|
|
||||||
>
|
|
||||||
<Mail className="h-4 w-4 mr-2" />
|
|
||||||
Envoyer email teaser
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => handleSendEmails("rappel")}
|
|
||||||
disabled={sendEmailsMutation.isPending || nbConfirmes === 0}
|
|
||||||
>
|
|
||||||
<Mail className="h-4 w-4 mr-2" />
|
|
||||||
Envoyer rappel J-7
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleExportExcel}
|
|
||||||
disabled={exportExcelMutation.isPending}
|
|
||||||
>
|
|
||||||
<Download className="h-4 w-4 mr-2" />
|
|
||||||
Export Excel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleExportPDF}
|
|
||||||
disabled={exportPDFMutation.isPending}
|
|
||||||
>
|
|
||||||
<Download className="h-4 w-4 mr-2" />
|
|
||||||
Export PDF
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleExportFeuille}
|
|
||||||
disabled={exportFeuilleMutation.isPending}
|
|
||||||
>
|
|
||||||
<Download className="h-4 w-4 mr-2" />
|
|
||||||
Feuille de présence
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Filtres */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Search className="h-5 w-5" />
|
|
||||||
Filtres et recherche
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Recherche</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Nom, prénom, email..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Statut</Label>
|
|
||||||
<Select value={filterStatut} onValueChange={setFilterStatut}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous</SelectItem>
|
|
||||||
<SelectItem value="confirmee">Confirmée</SelectItem>
|
|
||||||
<SelectItem value="liste_attente">Liste d'attente</SelectItem>
|
|
||||||
<SelectItem value="annulee">Annulée</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Établissement</Label>
|
|
||||||
<Select value={filterEtablissement} onValueChange={setFilterEtablissement}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous</SelectItem>
|
|
||||||
{uniqueEtablissements.map((etab) => (
|
|
||||||
<SelectItem key={etab} value={etab as string}>
|
|
||||||
{etab}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Fonction</Label>
|
|
||||||
<Select value={filterFonction} onValueChange={setFilterFonction}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Toutes</SelectItem>
|
|
||||||
<SelectItem value="directeur">Directeur</SelectItem>
|
|
||||||
<SelectItem value="chef_service">Chef de service</SelectItem>
|
|
||||||
<SelectItem value="autre">Autre</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Trier par</Label>
|
|
||||||
<Select value={sortBy} onValueChange={(v: any) => setSortBy(v)}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="date">Date d'inscription</SelectItem>
|
|
||||||
<SelectItem value="nom">Nom</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground mt-4">
|
|
||||||
{filteredAndSortedInscriptions.length} inscription(s) trouvée(s) sur {inscriptions?.length || 0} au total
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Liste des inscrits */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Liste des inscrits</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{nbConfirmes} confirmé(s), {nbListeAttente} en liste d'attente
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{filteredAndSortedInscriptions.length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
Aucune inscription trouvée
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Nom</TableHead>
|
|
||||||
<TableHead>Prénom</TableHead>
|
|
||||||
<TableHead>Email</TableHead>
|
|
||||||
<TableHead>Code étab.</TableHead>
|
|
||||||
<TableHead>Fonction</TableHead>
|
|
||||||
<TableHead>Date inscription</TableHead>
|
|
||||||
<TableHead>Statut</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{filteredAndSortedInscriptions.map((item) => {
|
|
||||||
if (!item.apprenant) return null;
|
|
||||||
const statutInfo = getStatutBadge(item.inscription.statut);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow key={item.inscription.id}>
|
|
||||||
<TableCell className="font-medium">{item.apprenant.nom}</TableCell>
|
|
||||||
<TableCell>{item.apprenant.prenom}</TableCell>
|
|
||||||
<TableCell>{item.apprenant.email}</TableCell>
|
|
||||||
<TableCell>{item.apprenant.codeEtablissement}</TableCell>
|
|
||||||
<TableCell>{getFonctionLabel(item.apprenant.fonction)}</TableCell>
|
|
||||||
<TableCell className="whitespace-nowrap">
|
|
||||||
{formatDate(item.inscription.dateInscription)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statutInfo.color}`}>
|
|
||||||
{statutInfo.label}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<Select
|
|
||||||
value={item.inscription.statut}
|
|
||||||
onValueChange={(value) => handleStatutChange(item.inscription.id, value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-40">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="confirmee">Confirmée</SelectItem>
|
|
||||||
<SelectItem value="liste_attente">Liste d'attente</SelectItem>
|
|
||||||
<SelectItem value="annulee">Annulée</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,853 +0,0 @@
|
|||||||
import { useState, useMemo } from "react";
|
|
||||||
import DashboardLayout from "@/components/DashboardLayout";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
||||||
import { trpc } from "@/lib/trpc";
|
|
||||||
import { Calendar, Edit, Eye, Plus, Trash2, X } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useLocation } from "wouter";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
|
|
||||||
interface DateFormation {
|
|
||||||
dateDebut: string;
|
|
||||||
dateFin: string;
|
|
||||||
ordre: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AdminSequences() {
|
|
||||||
const [, setLocation] = useLocation();
|
|
||||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
|
||||||
const [isEditOpen, setIsEditOpen] = useState(false);
|
|
||||||
const [editingSequence, setEditingSequence] = useState<any>(null);
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
const [filterLieu, setFilterLieu] = useState<string>("all");
|
|
||||||
const [filterStatut, setFilterStatut] = useState<string>("all");
|
|
||||||
const [filterPublicCible, setFilterPublicCible] = useState<string>("all");
|
|
||||||
const [sortBy, setSortBy] = useState<"date" | "lieu" | "capacite">("date");
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
formationId: "",
|
|
||||||
nom: "",
|
|
||||||
lieu: "",
|
|
||||||
publicCible: "autre" as "directeur" | "chef_service" | "autre",
|
|
||||||
capaciteMax: "12",
|
|
||||||
dateBlocage: "",
|
|
||||||
statut: "ouverte" as "ouverte" | "bloquee" | "terminee",
|
|
||||||
dates: [
|
|
||||||
{ dateDebut: "", dateFin: "", ordre: 1 }
|
|
||||||
] as DateFormation[],
|
|
||||||
});
|
|
||||||
|
|
||||||
const utils = trpc.useUtils();
|
|
||||||
const { data: formations } = trpc.formations.list.useQuery();
|
|
||||||
const { data: sequences, isLoading } = trpc.sequences.list.useQuery();
|
|
||||||
|
|
||||||
const createMutation = trpc.sequences.create.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Séquence créée avec succès");
|
|
||||||
setIsCreateOpen(false);
|
|
||||||
resetForm();
|
|
||||||
utils.sequences.list.invalidate();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(`Erreur : ${error.message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateMutation = trpc.sequences.update.useMutation({
|
|
||||||
onSuccess: async () => {
|
|
||||||
toast.success("Séquence modifiée avec succès");
|
|
||||||
setIsEditOpen(false);
|
|
||||||
setEditingSequence(null);
|
|
||||||
// Forcer un refetch immédiat au lieu d'une simple invalidation
|
|
||||||
await utils.sequences.list.refetch();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(`Erreur : ${error.message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteMutation = trpc.sequences.delete.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Séquence supprimée avec succès");
|
|
||||||
utils.sequences.list.invalidate();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(`Erreur : ${error.message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
setFormData({
|
|
||||||
formationId: "",
|
|
||||||
nom: "",
|
|
||||||
lieu: "",
|
|
||||||
publicCible: "autre",
|
|
||||||
capaciteMax: "12",
|
|
||||||
dateBlocage: "",
|
|
||||||
statut: "ouverte",
|
|
||||||
dates: [{ dateDebut: "", dateFin: "", ordre: 1 }],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (sequence: any) => {
|
|
||||||
setEditingSequence(sequence);
|
|
||||||
|
|
||||||
// Fonction helper pour formater une date en toute sécurité
|
|
||||||
const safeFormatDate = (dateValue: any): string => {
|
|
||||||
if (!dateValue) return "";
|
|
||||||
try {
|
|
||||||
// Si c'est un objet Date JavaScript
|
|
||||||
if (dateValue instanceof Date) {
|
|
||||||
const year = dateValue.getFullYear();
|
|
||||||
const month = String(dateValue.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(dateValue.getDate()).padStart(2, '0');
|
|
||||||
const hours = String(dateValue.getHours()).padStart(2, '0');
|
|
||||||
const minutes = String(dateValue.getMinutes()).padStart(2, '0');
|
|
||||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si c'est déjà une chaîne au bon format, la retourner directement
|
|
||||||
if (typeof dateValue === 'string' && dateValue.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)) {
|
|
||||||
return dateValue.substring(0, 16); // Garder seulement YYYY-MM-DDTHH:mm
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parser la date MySQL (format: "YYYY-MM-DD HH:mm:ss")
|
|
||||||
const dateStr = String(dateValue);
|
|
||||||
const match = dateStr.match(/(\d{4})-(\d{2})-(\d{2})[T ]?(\d{2}):(\d{2})/);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
console.warn('Format de date non reconnu:', dateValue);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, year, month, day, hours, minutes] = match;
|
|
||||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur de formatage de date:', error, dateValue);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setFormData({
|
|
||||||
formationId: sequence.formationId.toString(),
|
|
||||||
nom: sequence.nom || "",
|
|
||||||
lieu: sequence.lieu || "",
|
|
||||||
publicCible: sequence.publicCible || "autre",
|
|
||||||
capaciteMax: sequence.capaciteMax?.toString() || "12",
|
|
||||||
dateBlocage: safeFormatDate(sequence.dateBlocage),
|
|
||||||
statut: sequence.statut || "ouverte",
|
|
||||||
dates: (sequence.dates || []).map((d: any) => ({
|
|
||||||
dateDebut: safeFormatDate(d.dateDebut),
|
|
||||||
dateFin: safeFormatDate(d.dateFin),
|
|
||||||
ordre: d.ordre || 1,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
setIsEditOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!formData.dates.length) {
|
|
||||||
toast.error("Veuillez ajouter au moins une date de formation");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
formationId: parseInt(formData.formationId),
|
|
||||||
nom: formData.nom,
|
|
||||||
lieu: formData.lieu,
|
|
||||||
publicCible: formData.publicCible,
|
|
||||||
capaciteMax: parseInt(formData.capaciteMax),
|
|
||||||
dateBlocage: formData.dateBlocage,
|
|
||||||
statut: formData.statut,
|
|
||||||
dates: formData.dates,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (editingSequence) {
|
|
||||||
console.log('[CLIENT] Envoi de la mise à jour:', { id: editingSequence.id, ...data });
|
|
||||||
updateMutation.mutate({ id: editingSequence.id, ...data });
|
|
||||||
} else {
|
|
||||||
createMutation.mutate(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (id: number) => {
|
|
||||||
if (confirm("Êtes-vous sûr de vouloir supprimer cette séquence ?")) {
|
|
||||||
deleteMutation.mutate({ id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addDate = () => {
|
|
||||||
if (formData.dates.length >= 4) {
|
|
||||||
toast.error("Maximum 4 dates par séquence");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
dates: [...formData.dates, { dateDebut: "", dateFin: "", ordre: formData.dates.length + 1 }],
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeDate = (index: number) => {
|
|
||||||
if (formData.dates.length <= 1) {
|
|
||||||
toast.error("Au moins une date est requise");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newDates = formData.dates.filter((_, i) => i !== index);
|
|
||||||
// Réordonner les dates
|
|
||||||
newDates.forEach((d, i) => d.ordre = i + 1);
|
|
||||||
setFormData({ ...formData, dates: newDates });
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateDate = (index: number, field: "dateDebut" | "dateFin", value: string) => {
|
|
||||||
const newDates = [...formData.dates];
|
|
||||||
newDates[index][field] = value;
|
|
||||||
setFormData({ ...formData, dates: newDates });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filtrage et tri
|
|
||||||
const filteredAndSortedSequences = useMemo(() => {
|
|
||||||
if (!sequences) return [];
|
|
||||||
|
|
||||||
let filtered = sequences.filter((seq) => {
|
|
||||||
const matchSearch = seq.nom.toLowerCase().includes(searchTerm.toLowerCase());
|
|
||||||
const matchLieu = filterLieu === "all" || seq.lieu === filterLieu;
|
|
||||||
const matchStatut = filterStatut === "all" || seq.statut === filterStatut;
|
|
||||||
const matchPublicCible = filterPublicCible === "all" || seq.publicCible === filterPublicCible;
|
|
||||||
return matchSearch && matchLieu && matchStatut && matchPublicCible;
|
|
||||||
});
|
|
||||||
|
|
||||||
filtered.sort((a, b) => {
|
|
||||||
if (sortBy === "date") {
|
|
||||||
const dateA = a.dates[0] ? new Date(a.dates[0].dateDebut).getTime() : 0;
|
|
||||||
const dateB = b.dates[0] ? new Date(b.dates[0].dateDebut).getTime() : 0;
|
|
||||||
return dateA - dateB;
|
|
||||||
} else if (sortBy === "lieu") {
|
|
||||||
return a.lieu.localeCompare(b.lieu);
|
|
||||||
} else {
|
|
||||||
return b.capaciteMax - a.capaciteMax;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
}, [sequences, searchTerm, filterLieu, filterStatut, filterPublicCible, sortBy]);
|
|
||||||
|
|
||||||
const uniqueLieux = useMemo(() => {
|
|
||||||
if (!sequences) return [];
|
|
||||||
return Array.from(new Set(sequences.map((s) => s.lieu)));
|
|
||||||
}, [sequences]);
|
|
||||||
|
|
||||||
const getFormationName = (formationId: number) => {
|
|
||||||
return formations?.find((f) => f.id === formationId)?.nom || "N/A";
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatDate = (dateValue: any) => {
|
|
||||||
if (!dateValue) return "N/A";
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Si c'est déjà un objet Date JavaScript
|
|
||||||
if (dateValue instanceof Date) {
|
|
||||||
const day = String(dateValue.getDate()).padStart(2, '0');
|
|
||||||
const month = String(dateValue.getMonth() + 1).padStart(2, '0');
|
|
||||||
const year = dateValue.getFullYear();
|
|
||||||
const hours = String(dateValue.getHours()).padStart(2, '0');
|
|
||||||
const minutes = String(dateValue.getMinutes()).padStart(2, '0');
|
|
||||||
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parser la date MySQL (chaîne)
|
|
||||||
const dateStr = String(dateValue);
|
|
||||||
const match = dateStr.match(/(\d{4})-(\d{2})-(\d{2})[T ]?(\d{2}):(\d{2})/);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
console.warn('Format de date non reconnu pour affichage:', dateValue);
|
|
||||||
return "N/A";
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, year, month, day, hours, minutes] = match;
|
|
||||||
return `${day}/${month}/${year} ${hours}:${minutes}`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur de formatage de date pour affichage:', error, dateValue);
|
|
||||||
return "N/A";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStatutBadge = (statut: string) => {
|
|
||||||
const colors = {
|
|
||||||
ouverte: "bg-green-100 text-green-800",
|
|
||||||
bloquee: "bg-orange-100 text-orange-800",
|
|
||||||
terminee: "bg-gray-100 text-gray-800",
|
|
||||||
};
|
|
||||||
return colors[statut as keyof typeof colors] || colors.ouverte;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPublicCibleBadge = (publicCible: string) => {
|
|
||||||
const colors = {
|
|
||||||
directeur: "bg-blue-100 text-blue-800",
|
|
||||||
chef_service: "bg-green-100 text-green-800",
|
|
||||||
autre: "bg-gray-100 text-gray-800",
|
|
||||||
};
|
|
||||||
return colors[publicCible as keyof typeof colors] || colors.autre;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPublicCibleLabel = (publicCible: string) => {
|
|
||||||
const labels = {
|
|
||||||
directeur: "Directeur",
|
|
||||||
chef_service: "Chef de service",
|
|
||||||
autre: "Autre",
|
|
||||||
};
|
|
||||||
return labels[publicCible as keyof typeof labels] || "Autre";
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="container mx-auto py-8 space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold">Gestion des Séquences</h1>
|
|
||||||
<p className="text-muted-foreground mt-1">
|
|
||||||
Créez et gérez les séquences de formation avec leurs dates
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button onClick={resetForm}>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Nouvelle Séquence
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Créer une nouvelle séquence</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Remplissez les informations de la séquence et ajoutez jusqu'à 4 dates de formation
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="formationId">Formation *</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.formationId}
|
|
||||||
onValueChange={(value) => setFormData({ ...formData, formationId: value })}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Sélectionnez une formation" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{formations?.map((formation) => (
|
|
||||||
<SelectItem key={formation.id} value={formation.id.toString()}>
|
|
||||||
{formation.nom}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="nom">Nom de la séquence *</Label>
|
|
||||||
<Input
|
|
||||||
id="nom"
|
|
||||||
value={formData.nom}
|
|
||||||
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
|
|
||||||
placeholder="Ex: Séquence 1 - Février 2026"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="lieu">Lieu *</Label>
|
|
||||||
<Input
|
|
||||||
id="lieu"
|
|
||||||
value={formData.lieu}
|
|
||||||
onChange={(e) => setFormData({ ...formData, lieu: e.target.value })}
|
|
||||||
placeholder="Ex: Salle de formation A"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="publicCible">Public cible *</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.publicCible}
|
|
||||||
onValueChange={(value: "directeur" | "chef_service" | "autre") => setFormData({ ...formData, publicCible: value })}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="publicCible">
|
|
||||||
<SelectValue placeholder="Sélectionner le public cible" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="directeur">Directeur</SelectItem>
|
|
||||||
<SelectItem value="chef_service">Chef de service</SelectItem>
|
|
||||||
<SelectItem value="autre">Autre</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="capaciteMax">Capacité maximale *</Label>
|
|
||||||
<Input
|
|
||||||
id="capaciteMax"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="50"
|
|
||||||
value={formData.capaciteMax}
|
|
||||||
onChange={(e) => setFormData({ ...formData, capaciteMax: e.target.value })}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="dateBlocage">Date de blocage (J-15) *</Label>
|
|
||||||
<Input
|
|
||||||
id="dateBlocage"
|
|
||||||
type="datetime-local"
|
|
||||||
value={formData.dateBlocage}
|
|
||||||
onChange={(e) => setFormData({ ...formData, dateBlocage: e.target.value })}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="statut">Statut *</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.statut}
|
|
||||||
onValueChange={(value: any) => setFormData({ ...formData, statut: value })}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="ouverte">Ouverte</SelectItem>
|
|
||||||
<SelectItem value="bloquee">Bloquée</SelectItem>
|
|
||||||
<SelectItem value="terminee">Terminée</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4 border-t pt-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label className="text-base">Dates de formation ({formData.dates.length}/4)</Label>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={addDate}
|
|
||||||
disabled={formData.dates.length >= 4}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Ajouter une date
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{formData.dates.map((date, index) => (
|
|
||||||
<Card key={index}>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<CardTitle className="text-sm">Date {date.ordre}</CardTitle>
|
|
||||||
{formData.dates.length > 1 && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => removeDate(index)}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Début *</Label>
|
|
||||||
<Input
|
|
||||||
type="datetime-local"
|
|
||||||
value={date.dateDebut}
|
|
||||||
onChange={(e) => updateDate(index, "dateDebut", e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Fin *</Label>
|
|
||||||
<Input
|
|
||||||
type="datetime-local"
|
|
||||||
value={date.dateFin}
|
|
||||||
onChange={(e) => updateDate(index, "dateFin", e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
|
||||||
<Button type="button" variant="outline" onClick={() => setIsCreateOpen(false)}>
|
|
||||||
Annuler
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={createMutation.isPending}>
|
|
||||||
{createMutation.isPending ? "Création..." : "Créer"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filtres */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-lg">Filtres et recherche</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Recherche</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Nom de la séquence..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Lieu</Label>
|
|
||||||
<Select value={filterLieu} onValueChange={setFilterLieu}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous les lieux</SelectItem>
|
|
||||||
{uniqueLieux.map((lieu) => (
|
|
||||||
<SelectItem key={lieu} value={lieu}>
|
|
||||||
{lieu}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Statut</Label>
|
|
||||||
<Select value={filterStatut} onValueChange={setFilterStatut}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous les statuts</SelectItem>
|
|
||||||
<SelectItem value="ouverte">Ouverte</SelectItem>
|
|
||||||
<SelectItem value="bloquee">Bloquée</SelectItem>
|
|
||||||
<SelectItem value="terminee">Terminée</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Public cible</Label>
|
|
||||||
<Select value={filterPublicCible} onValueChange={setFilterPublicCible}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous les publics</SelectItem>
|
|
||||||
<SelectItem value="directeur">Directeurs</SelectItem>
|
|
||||||
<SelectItem value="chef_service">Chefs de service</SelectItem>
|
|
||||||
<SelectItem value="autre">Autre</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Trier par</Label>
|
|
||||||
<Select value={sortBy} onValueChange={(v: any) => setSortBy(v)}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="date">Date</SelectItem>
|
|
||||||
<SelectItem value="lieu">Lieu</SelectItem>
|
|
||||||
<SelectItem value="capacite">Capacité</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground mt-4">
|
|
||||||
{filteredAndSortedSequences.length} séquence(s) trouvée(s)
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Liste des séquences */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<Calendar className="h-5 w-5" />
|
|
||||||
Liste des séquences
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{sequences?.length || 0} séquence(s) au total
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">Chargement...</div>
|
|
||||||
) : filteredAndSortedSequences.length === 0 ? (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
Aucune séquence trouvée
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Formation</TableHead>
|
|
||||||
<TableHead>Nom</TableHead>
|
|
||||||
<TableHead>Dates</TableHead>
|
|
||||||
<TableHead>Lieu</TableHead>
|
|
||||||
<TableHead>Public cible</TableHead>
|
|
||||||
<TableHead>Capacité</TableHead>
|
|
||||||
<TableHead>Statut</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{filteredAndSortedSequences.map((sequence) => (
|
|
||||||
<TableRow key={sequence.id}>
|
|
||||||
<TableCell className="font-medium">{getFormationName(sequence.formationId)}</TableCell>
|
|
||||||
<TableCell>{sequence.nom}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="space-y-1 text-sm">
|
|
||||||
{sequence.dates.map((date: any) => (
|
|
||||||
<div key={date.id}>
|
|
||||||
<span className="font-medium">Date {date.ordre}:</span>{" "}
|
|
||||||
{formatDate(date.dateDebut)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="max-w-xs truncate">{sequence.lieu}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getPublicCibleBadge(sequence.publicCible)}`}>
|
|
||||||
{getPublicCibleLabel(sequence.publicCible)}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{sequence.capaciteMax}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatutBadge(sequence.statut)}`}>
|
|
||||||
{sequence.statut}
|
|
||||||
</span>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setLocation(`/admin/sequences/${sequence.id}/inscrits`)}
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleEdit(sequence)}
|
|
||||||
>
|
|
||||||
<Edit className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleDelete(sequence.id)}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Dialog d'édition */}
|
|
||||||
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
|
|
||||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Modifier la séquence</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Modifiez les informations de la séquence et ses dates
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="edit-formationId">Formation *</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.formationId}
|
|
||||||
onValueChange={(value) => setFormData({ ...formData, formationId: value })}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Sélectionnez une formation" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{formations?.map((formation) => (
|
|
||||||
<SelectItem key={formation.id} value={formation.id.toString()}>
|
|
||||||
{formation.nom}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="edit-nom">Nom de la séquence *</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-nom"
|
|
||||||
value={formData.nom}
|
|
||||||
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="edit-lieu">Lieu *</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-lieu"
|
|
||||||
value={formData.lieu}
|
|
||||||
onChange={(e) => setFormData({ ...formData, lieu: e.target.value })}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="edit-capaciteMax">Capacité maximale *</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-capaciteMax"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="50"
|
|
||||||
value={formData.capaciteMax}
|
|
||||||
onChange={(e) => setFormData({ ...formData, capaciteMax: e.target.value })}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="edit-dateBlocage">Date de blocage (J-15) *</Label>
|
|
||||||
<Input
|
|
||||||
id="edit-dateBlocage"
|
|
||||||
type="datetime-local"
|
|
||||||
value={formData.dateBlocage}
|
|
||||||
onChange={(e) => setFormData({ ...formData, dateBlocage: e.target.value })}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="edit-statut">Statut *</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.statut}
|
|
||||||
onValueChange={(value: any) => setFormData({ ...formData, statut: value })}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="ouverte">Ouverte</SelectItem>
|
|
||||||
<SelectItem value="bloquee">Bloquée</SelectItem>
|
|
||||||
<SelectItem value="terminee">Terminée</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4 border-t pt-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label className="text-base">Dates de formation ({formData.dates.length}/4)</Label>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={addDate}
|
|
||||||
disabled={formData.dates.length >= 4}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Ajouter une date
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{formData.dates.map((date, index) => (
|
|
||||||
<Card key={index}>
|
|
||||||
<CardHeader className="pb-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<CardTitle className="text-sm">Date {date.ordre}</CardTitle>
|
|
||||||
{formData.dates.length > 1 && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => removeDate(index)}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Début *</Label>
|
|
||||||
<Input
|
|
||||||
type="datetime-local"
|
|
||||||
value={date.dateDebut}
|
|
||||||
onChange={(e) => updateDate(index, "dateDebut", e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>Fin *</Label>
|
|
||||||
<Input
|
|
||||||
type="datetime-local"
|
|
||||||
value={date.dateFin}
|
|
||||||
onChange={(e) => updateDate(index, "dateFin", e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
|
||||||
<Button type="button" variant="outline" onClick={() => setIsEditOpen(false)}>
|
|
||||||
Annuler
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={updateMutation.isPending}>
|
|
||||||
{updateMutation.isPending ? "Modification..." : "Modifier"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,567 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { trpc } from "@/lib/trpc";
|
|
||||||
import DashboardLayout from "@/components/DashboardLayout";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Pencil, Trash2, UserCheck, UserX, Search, Plus, KeyRound } from "lucide-react";
|
|
||||||
|
|
||||||
export default function AdminUsers() {
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
const [filterRole, setFilterRole] = useState<string>("all");
|
|
||||||
const [filterStatus, setFilterStatus] = useState<string>("all");
|
|
||||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
|
||||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
||||||
const [resetPasswordDialogOpen, setResetPasswordDialogOpen] = useState(false);
|
|
||||||
const [selectedUser, setSelectedUser] = useState<any>(null);
|
|
||||||
const [createForm, setCreateForm] = useState({
|
|
||||||
name: "",
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
role: "user" as "user" | "admin",
|
|
||||||
});
|
|
||||||
const [editForm, setEditForm] = useState({
|
|
||||||
name: "",
|
|
||||||
email: "",
|
|
||||||
role: "user" as "user" | "admin",
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: users = [], refetch } = trpc.users.list.useQuery();
|
|
||||||
|
|
||||||
const createMutation = trpc.users.create.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Utilisateur créé avec succès");
|
|
||||||
refetch();
|
|
||||||
setCreateDialogOpen(false);
|
|
||||||
setCreateForm({
|
|
||||||
name: "",
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
role: "user",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(`Erreur : ${error.message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateMutation = trpc.users.update.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Utilisateur modifié avec succès");
|
|
||||||
refetch();
|
|
||||||
setEditDialogOpen(false);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(`Erreur : ${error.message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleStatusMutation = trpc.users.toggleStatus.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Statut modifié avec succès");
|
|
||||||
refetch();
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(`Erreur : ${error.message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteMutation = trpc.users.delete.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Utilisateur supprimé avec succès");
|
|
||||||
refetch();
|
|
||||||
setDeleteDialogOpen(false);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(`Erreur : ${error.message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const resetPasswordMutation = trpc.users.requestPasswordReset.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Email de réinitialisation envoyé avec succès");
|
|
||||||
setResetPasswordDialogOpen(false);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(`Erreur : ${error.message}`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleCreate = () => {
|
|
||||||
setCreateDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveCreate = () => {
|
|
||||||
if (!createForm.email) {
|
|
||||||
toast.error("L'email est obligatoire");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!createForm.password) {
|
|
||||||
toast.error("Le mot de passe est obligatoire");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
createMutation.mutate({
|
|
||||||
email: createForm.email,
|
|
||||||
password: createForm.password,
|
|
||||||
name: createForm.name || undefined,
|
|
||||||
role: createForm.role,
|
|
||||||
isActive: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (user: any) => {
|
|
||||||
setSelectedUser(user);
|
|
||||||
setEditForm({
|
|
||||||
name: user.name || "",
|
|
||||||
email: user.email || "",
|
|
||||||
role: user.role,
|
|
||||||
});
|
|
||||||
setEditDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveEdit = () => {
|
|
||||||
if (!selectedUser) return;
|
|
||||||
|
|
||||||
updateMutation.mutate({
|
|
||||||
id: selectedUser.id,
|
|
||||||
name: editForm.name || undefined,
|
|
||||||
email: editForm.email || undefined,
|
|
||||||
role: editForm.role,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleStatus = (user: any) => {
|
|
||||||
toggleStatusMutation.mutate({
|
|
||||||
id: user.id,
|
|
||||||
isActive: !user.isActive,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (user: any) => {
|
|
||||||
setSelectedUser(user);
|
|
||||||
setDeleteDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmDelete = () => {
|
|
||||||
if (!selectedUser) return;
|
|
||||||
deleteMutation.mutate({ id: selectedUser.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetPassword = (user: any) => {
|
|
||||||
setSelectedUser(user);
|
|
||||||
setResetPasswordDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmResetPassword = () => {
|
|
||||||
if (!selectedUser) return;
|
|
||||||
resetPasswordMutation.mutate({ id: selectedUser.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filtrage des utilisateurs
|
|
||||||
const filteredUsers = users.filter((user) => {
|
|
||||||
const matchesSearch =
|
|
||||||
searchTerm === "" ||
|
|
||||||
user.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
user.email?.toLowerCase().includes(searchTerm.toLowerCase());
|
|
||||||
|
|
||||||
const matchesRole = filterRole === "all" || user.role === filterRole;
|
|
||||||
const matchesStatus =
|
|
||||||
filterStatus === "all" ||
|
|
||||||
(filterStatus === "active" && user.isActive) ||
|
|
||||||
(filterStatus === "inactive" && !user.isActive);
|
|
||||||
|
|
||||||
return matchesSearch && matchesRole && matchesStatus;
|
|
||||||
});
|
|
||||||
|
|
||||||
const formatDate = (date: Date | string | null) => {
|
|
||||||
if (!date) return "N/A";
|
|
||||||
const d = new Date(date);
|
|
||||||
return d.toLocaleDateString("fr-FR", {
|
|
||||||
day: "2-digit",
|
|
||||||
month: "2-digit",
|
|
||||||
year: "numeric",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DashboardLayout>
|
|
||||||
<div className="mb-6 flex justify-between items-start">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold mb-2">Gestion des Utilisateurs</h1>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Gérez les comptes utilisateurs de l'application
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button onClick={handleCreate}>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Ajouter un utilisateur
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filtres */}
|
|
||||||
<div className="mb-6 flex flex-wrap gap-4">
|
|
||||||
<div className="flex-1 min-w-[200px]">
|
|
||||||
<Label htmlFor="search">Rechercher</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input
|
|
||||||
id="search"
|
|
||||||
placeholder="Nom ou email..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="pl-8"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-[200px]">
|
|
||||||
<Label htmlFor="filterRole">Rôle</Label>
|
|
||||||
<Select value={filterRole} onValueChange={setFilterRole}>
|
|
||||||
<SelectTrigger id="filterRole">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous les rôles</SelectItem>
|
|
||||||
<SelectItem value="admin">Administrateur</SelectItem>
|
|
||||||
<SelectItem value="user">Utilisateur</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-[200px]">
|
|
||||||
<Label htmlFor="filterStatus">Statut</Label>
|
|
||||||
<Select value={filterStatus} onValueChange={setFilterStatus}>
|
|
||||||
<SelectTrigger id="filterStatus">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Tous les statuts</SelectItem>
|
|
||||||
<SelectItem value="active">Actif</SelectItem>
|
|
||||||
<SelectItem value="inactive">Inactif</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Compteur de résultats */}
|
|
||||||
<div className="mb-4">
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{filteredUsers.length} utilisateur(s) trouvé(s)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tableau des utilisateurs */}
|
|
||||||
<div className="border rounded-lg">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>ID</TableHead>
|
|
||||||
<TableHead>Nom</TableHead>
|
|
||||||
<TableHead>Email</TableHead>
|
|
||||||
<TableHead>Rôle</TableHead>
|
|
||||||
<TableHead>Statut</TableHead>
|
|
||||||
<TableHead>Dernière connexion</TableHead>
|
|
||||||
<TableHead>Créé le</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{filteredUsers.length === 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
|
|
||||||
Aucun utilisateur trouvé
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
filteredUsers.map((user) => (
|
|
||||||
<TableRow key={user.id}>
|
|
||||||
<TableCell className="font-medium">{user.id}</TableCell>
|
|
||||||
<TableCell>{user.name || "N/A"}</TableCell>
|
|
||||||
<TableCell>{user.email || "N/A"}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant={user.role === "admin" ? "default" : "secondary"}>
|
|
||||||
{user.role === "admin" ? "Administrateur" : "Utilisateur"}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant={user.isActive ? "default" : "destructive"}>
|
|
||||||
{user.isActive ? "Actif" : "Inactif"}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{formatDate(user.lastSignedIn)}</TableCell>
|
|
||||||
<TableCell>{formatDate(user.createdAt)}</TableCell>
|
|
||||||
<TableCell className="text-right">
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleEdit(user)}
|
|
||||||
title="Modifier"
|
|
||||||
>
|
|
||||||
<Pencil className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleResetPassword(user)}
|
|
||||||
title="Réinitialiser le mot de passe"
|
|
||||||
disabled={!user.email}
|
|
||||||
>
|
|
||||||
<KeyRound className="h-4 w-4 text-blue-500" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleToggleStatus(user)}
|
|
||||||
title={user.isActive ? "Désactiver" : "Activer"}
|
|
||||||
>
|
|
||||||
{user.isActive ? (
|
|
||||||
<UserX className="h-4 w-4 text-orange-500" />
|
|
||||||
) : (
|
|
||||||
<UserCheck className="h-4 w-4 text-green-500" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleDelete(user)}
|
|
||||||
title="Supprimer"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Dialog de création */}
|
|
||||||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Créer un nouvel utilisateur</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Ajoutez un nouvel utilisateur à l'application
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="createPassword">Mot de passe *</Label>
|
|
||||||
<Input
|
|
||||||
id="createPassword"
|
|
||||||
type="password"
|
|
||||||
value={createForm.password}
|
|
||||||
onChange={(e) => setCreateForm({ ...createForm, password: e.target.value })}
|
|
||||||
placeholder="Mot de passe de l'utilisateur"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="createName">Nom</Label>
|
|
||||||
<Input
|
|
||||||
id="createName"
|
|
||||||
value={createForm.name}
|
|
||||||
onChange={(e) => setCreateForm({ ...createForm, name: e.target.value })}
|
|
||||||
placeholder="Nom de l'utilisateur"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="createEmail">Email</Label>
|
|
||||||
<Input
|
|
||||||
id="createEmail"
|
|
||||||
type="email"
|
|
||||||
value={createForm.email}
|
|
||||||
onChange={(e) => setCreateForm({ ...createForm, email: e.target.value })}
|
|
||||||
placeholder="email@exemple.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="createRole">Rôle</Label>
|
|
||||||
<Select
|
|
||||||
value={createForm.role}
|
|
||||||
onValueChange={(value: "user" | "admin") =>
|
|
||||||
setCreateForm({ ...createForm, role: value })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="createRole">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="user">Utilisateur</SelectItem>
|
|
||||||
<SelectItem value="admin">Administrateur</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setCreateDialogOpen(false)}>
|
|
||||||
Annuler
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSaveCreate} disabled={createMutation.isPending}>
|
|
||||||
{createMutation.isPending ? "Création..." : "Créer"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Dialog de modification */}
|
|
||||||
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Modifier l'utilisateur</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Modifiez les informations de l'utilisateur
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="editName">Nom</Label>
|
|
||||||
<Input
|
|
||||||
id="editName"
|
|
||||||
value={editForm.name}
|
|
||||||
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
|
||||||
placeholder="Nom de l'utilisateur"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="editEmail">Email</Label>
|
|
||||||
<Input
|
|
||||||
id="editEmail"
|
|
||||||
type="email"
|
|
||||||
value={editForm.email}
|
|
||||||
onChange={(e) => setEditForm({ ...editForm, email: e.target.value })}
|
|
||||||
placeholder="email@exemple.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="editRole">Rôle</Label>
|
|
||||||
<Select
|
|
||||||
value={editForm.role}
|
|
||||||
onValueChange={(value: "user" | "admin") =>
|
|
||||||
setEditForm({ ...editForm, role: value })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="editRole">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="user">Utilisateur</SelectItem>
|
|
||||||
<SelectItem value="admin">Administrateur</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
|
|
||||||
Annuler
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSaveEdit} disabled={updateMutation.isPending}>
|
|
||||||
{updateMutation.isPending ? "Enregistrement..." : "Enregistrer"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Dialog de réinitialisation de mot de passe */}
|
|
||||||
<Dialog open={resetPasswordDialogOpen} onOpenChange={setResetPasswordDialogOpen}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Réinitialiser le mot de passe</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Êtes-vous sûr de vouloir envoyer un email de réinitialisation de mot de passe à{" "}
|
|
||||||
<strong>{selectedUser?.name || selectedUser?.email}</strong> ?
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 my-4">
|
|
||||||
<p className="text-sm text-blue-900">
|
|
||||||
<strong>ℹ️ Information :</strong>
|
|
||||||
</p>
|
|
||||||
<ul className="text-sm text-blue-800 mt-2 space-y-1 list-disc list-inside">
|
|
||||||
<li>Un email sera envoyé à : <strong>{selectedUser?.email}</strong></li>
|
|
||||||
<li>Le lien de réinitialisation sera valide pendant 24 heures</li>
|
|
||||||
<li>Il ne pourra être utilisé qu'une seule fois</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setResetPasswordDialogOpen(false)}>
|
|
||||||
Annuler
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={confirmResetPassword}
|
|
||||||
disabled={resetPasswordMutation.isPending}
|
|
||||||
>
|
|
||||||
{resetPasswordMutation.isPending ? "Envoi en cours..." : "Envoyer l'email"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Dialog de suppression */}
|
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Confirmer la suppression</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Êtes-vous sûr de vouloir supprimer l'utilisateur{" "}
|
|
||||||
<strong>{selectedUser?.name || selectedUser?.email}</strong> ?
|
|
||||||
Cette action est irréversible.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
|
|
||||||
Annuler
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={confirmDelete}
|
|
||||||
disabled={deleteMutation.isPending}
|
|
||||||
>
|
|
||||||
{deleteMutation.isPending ? "Suppression..." : "Supprimer"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -881,7 +881,7 @@ export default function ComponentsShowcase() {
|
|||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Your sequence has expired. Please log in again.
|
Your session has expired. Please log in again.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,178 +1,33 @@
|
|||||||
import { useAuth } from "@/_core/hooks/useAuth";
|
import { useAuth } from "@/_core/hooks/useAuth";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Loader2 } from "lucide-react";
|
||||||
import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const";
|
import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const";
|
||||||
import { GraduationCap, Calendar, Users, CheckCircle, Mail, Download } from "lucide-react";
|
import { Streamdown } from 'streamdown';
|
||||||
import { useLocation } from "wouter";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All content in this page are only for example, replace with your own feature implementation
|
||||||
|
* When building pages, remember your instructions in Frontend Workflow, Frontend Best Practices, Design Guide and Common Pitfalls
|
||||||
|
*/
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { user, loading, isAuthenticated } = useAuth();
|
// The userAuth hooks provides authentication state
|
||||||
const [, setLocation] = useLocation();
|
// To implement login/logout functionality, simply call logout() or redirect to getLoginUrl()
|
||||||
|
let { user, loading, error, isAuthenticated, logout } = useAuth();
|
||||||
|
|
||||||
|
// If theme is switchable in App.tsx, we can implement theme toggling like this:
|
||||||
|
// const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
|
// Use APP_LOGO (as image src) and APP_TITLE if needed
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex flex-col">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
<main>
|
||||||
</div>
|
{/* Example: lucide-react for icons */}
|
||||||
);
|
<Loader2 className="animate-spin" />
|
||||||
}
|
Example Page
|
||||||
|
{/* Example: Streamdown for markdown rendering */}
|
||||||
const features = [
|
<Streamdown>Any **markdown** content</Streamdown>
|
||||||
{
|
<Button variant="default">Example Button</Button>
|
||||||
icon: <GraduationCap className="w-8 h-8 text-blue-600" />,
|
</main>
|
||||||
title: "Gestion des formations",
|
|
||||||
description: "Créez et gérez plusieurs formations en parallèle avec des liens d'inscription uniques pour chaque formation.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Calendar className="w-8 h-8 text-green-600" />,
|
|
||||||
title: "Sessions planifiées",
|
|
||||||
description: "Organisez des sequences avec dates, lieux et capacité maximale de 12 participants. Blocage automatique à J-15.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Users className="w-8 h-8 text-purple-600" />,
|
|
||||||
title: "Inscriptions simplifiées",
|
|
||||||
description: "Les apprenants s'inscrivent facilement via un lien unique. Validation automatique de la capacité et anti-doublons.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Mail className="w-8 h-8 text-orange-600" />,
|
|
||||||
title: "Communications automatiques",
|
|
||||||
description: "Emails de confirmation, invitations Outlook avec statut 'occupé', teasers et rappels J-7 automatisés.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Download className="w-8 h-8 text-red-600" />,
|
|
||||||
title: "Exports facilitées",
|
|
||||||
description: "Exportez les listes d'inscrits en Excel ou PDF. Générez des feuilles de présence prêtes à imprimer.",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <CheckCircle className="w-8 h-8 text-teal-600" />,
|
|
||||||
title: "Gestion des contraintes",
|
|
||||||
description: "Respect automatique des règles : capacité maximale, blocage J-15, liste d'attente et prévention des doubles inscriptions.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-indigo-50">
|
|
||||||
{/* Header */}
|
|
||||||
<header className="bg-white shadow-sm border-b">
|
|
||||||
<div className="container mx-auto px-4 py-4 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<img src={APP_LOGO} alt={APP_TITLE} className="h-10 w-10" />
|
|
||||||
<h1 className="text-xl font-bold text-gray-900">{APP_TITLE}</h1>
|
|
||||||
</div>
|
|
||||||
{!isAuthenticated && (
|
|
||||||
<Button onClick={() => window.location.href = getLoginUrl()}>
|
|
||||||
Connexion Administrateur
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Hero Section */}
|
|
||||||
<section className="py-20 px-4">
|
|
||||||
<div className="container mx-auto text-center max-w-4xl">
|
|
||||||
<h2 className="text-5xl font-bold text-gray-900 mb-6">
|
|
||||||
Gestion des Formations Manager Itinova
|
|
||||||
</h2>
|
|
||||||
<p className="text-xl text-gray-600 mb-8">
|
|
||||||
Solution complète pour gérer l'organisation interne des formations destinées à environ 150 apprenants.
|
|
||||||
Inscriptions, sequences, communications automatisées et exports simplifiés.
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-4 justify-center">
|
|
||||||
{isAuthenticated && user?.role === 'admin' && (
|
|
||||||
<Button size="lg" onClick={() => setLocation('/admin')}>
|
|
||||||
Accéder au tableau de bord
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{!isAuthenticated && (
|
|
||||||
<Button size="lg" onClick={() => window.location.href = getLoginUrl()}>
|
|
||||||
Connexion Administrateur
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Features Grid */}
|
|
||||||
<section className="py-16 px-4 bg-white">
|
|
||||||
<div className="container mx-auto max-w-6xl">
|
|
||||||
<h3 className="text-3xl font-bold text-center mb-12">Fonctionnalités principales</h3>
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{features.map((feature, index) => (
|
|
||||||
<Card key={index} className="hover:shadow-lg transition-shadow">
|
|
||||||
<CardHeader>
|
|
||||||
<div className="mb-4">{feature.icon}</div>
|
|
||||||
<CardTitle className="text-xl">{feature.title}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<CardDescription className="text-base">
|
|
||||||
{feature.description}
|
|
||||||
</CardDescription>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* How it works */}
|
|
||||||
<section className="py-16 px-4">
|
|
||||||
<div className="container mx-auto max-w-4xl">
|
|
||||||
<h3 className="text-3xl font-bold text-center mb-12">Comment ça fonctionne ?</h3>
|
|
||||||
<div className="space-y-8">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<span className="bg-blue-600 text-white rounded-full w-8 h-8 flex items-center justify-center font-bold">1</span>
|
|
||||||
Pour les administrateurs
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2">
|
|
||||||
<p>Créez des formations et des sequences avec toutes les informations nécessaires (dates, lieu, capacité). Chaque formation dispose d'un lien unique d'inscription à partager avec les apprenants.</p>
|
|
||||||
<p>Gérez les inscriptions, envoyez des emails groupés (teaser, rappels), et exportez les listes d'inscrits en Excel ou PDF pour vos besoins administratifs.</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<span className="bg-green-600 text-white rounded-full w-8 h-8 flex items-center justify-center font-bold">2</span>
|
|
||||||
Pour les apprenants
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2">
|
|
||||||
<p>Cliquez sur le lien d'inscription fourni par la DRH, consultez les sequences disponibles avec les dates et lieux, puis inscrivez-vous en quelques clics.</p>
|
|
||||||
<p>Recevez immédiatement un email de confirmation avec une invitation Outlook (.ics) qui bloque automatiquement votre agenda. Un rappel vous sera envoyé 7 jours avant la formation.</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center gap-2">
|
|
||||||
<span className="bg-purple-600 text-white rounded-full w-8 h-8 flex items-center justify-center font-bold">3</span>
|
|
||||||
Règles automatiques
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2">
|
|
||||||
<p>Le système applique automatiquement toutes les contraintes : capacité maximale de 12 participants par sequence, prévention des doubles inscriptions, et gestion de la liste d'attente.</p>
|
|
||||||
<p>Les inscriptions et désinscriptions sont bloquées automatiquement 15 jours avant le début de la sequence pour garantir la stabilité des groupes.</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<footer className="bg-gray-900 text-white py-8 px-4">
|
|
||||||
<div className="container mx-auto text-center">
|
|
||||||
<p className="text-gray-400">
|
|
||||||
© 2026 Itinova - Gestion des Formations Manager Itinova
|
|
||||||
</p>
|
|
||||||
<p className="text-gray-500 text-sm mt-2">
|
|
||||||
Pour toute question, veuillez contacter le service RH.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,432 +0,0 @@
|
|||||||
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 { trpc } from "@/lib/trpc";
|
|
||||||
import { useRoute } from "wouter";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import { useState, useMemo } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { fr } from "date-fns/locale";
|
|
||||||
import { CheckCircle, XCircle, AlertCircle, Calendar, MapPin, Users } from "lucide-react";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { APP_LOGO, APP_TITLE } from "@/const";
|
|
||||||
|
|
||||||
export default function Inscription() {
|
|
||||||
const [, params] = useRoute("/inscription/:lien");
|
|
||||||
const lien = params?.lien || "";
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
nom: "",
|
|
||||||
prenom: "",
|
|
||||||
email: "",
|
|
||||||
codeEtablissement: "",
|
|
||||||
fonction: "autre" as "directeur" | "chef_service" | "autre",
|
|
||||||
});
|
|
||||||
const [selectedSequenceId, setSelectedSequenceId] = useState<number | null>(null);
|
|
||||||
const [inscriptionSuccess, setInscriptionSuccess] = useState(false);
|
|
||||||
const [inscriptionStatut, setInscriptionStatut] = useState<string>("");
|
|
||||||
|
|
||||||
const { data: formation, isLoading: loadingFormation } = trpc.formations.getByLien.useQuery({ lien });
|
|
||||||
const { data: sequences, isLoading: loadingSessions } = trpc.sequences.listByFormation.useQuery(
|
|
||||||
{ formationId: formation?.id || 0 },
|
|
||||||
{ enabled: !!formation?.id }
|
|
||||||
);
|
|
||||||
|
|
||||||
const utils = trpc.useUtils();
|
|
||||||
const inscriptionMutation = trpc.inscriptions.inscrire.useMutation({
|
|
||||||
onSuccess: (data) => {
|
|
||||||
setInscriptionSuccess(true);
|
|
||||||
setInscriptionStatut(data.statut);
|
|
||||||
utils.sequences.listByFormation.invalidate({ formationId: formation?.id || 0 });
|
|
||||||
utils.apprenants.list.invalidate(); // Invalider la liste des apprenants
|
|
||||||
if (data.statut === "confirmee") {
|
|
||||||
toast.success("Inscription confirmée avec succès !");
|
|
||||||
} else {
|
|
||||||
toast.info("Vous êtes inscrit en liste d'attente.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createApprenantMutation = trpc.apprenants.create.useMutation();
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (!selectedSequenceId) {
|
|
||||||
toast.error("Veuillez sélectionner une sequence");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si le public cible correspond à la fonction de l'apprenant
|
|
||||||
const selectedSequence = sequences?.find(s => s.id === selectedSequenceId);
|
|
||||||
if (selectedSequence && selectedSequence.publicCible) {
|
|
||||||
const publicCibleMap: Record<string, string> = {
|
|
||||||
"directeur": "directeur",
|
|
||||||
"chef_service": "chef_service",
|
|
||||||
"autre": "autre"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (publicCibleMap[selectedSequence.publicCible] !== formData.fonction) {
|
|
||||||
const publicCibleLabel = selectedSequence.publicCible === "directeur" ? "Directeurs" :
|
|
||||||
selectedSequence.publicCible === "chef_service" ? "Chefs de service" : "Autre";
|
|
||||||
const fonctionLabel = formData.fonction === "directeur" ? "Directeur" :
|
|
||||||
formData.fonction === "chef_service" ? "Chef de service" : "Autre";
|
|
||||||
|
|
||||||
toast.info(
|
|
||||||
`Information : Cette séquence est destinée aux ${publicCibleLabel}, mais vous êtes inscrit(e) en tant que ${fonctionLabel}. Vous pouvez tout de même vous inscrire.`,
|
|
||||||
{ duration: 8000 }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Attendre 2 secondes pour que l'utilisateur puisse lire l'alerte
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Vérifier si l'apprenant existe déjà
|
|
||||||
const existingApprenant = await utils.client.apprenants.getByEmail.query({ email: formData.email });
|
|
||||||
|
|
||||||
let apprenantId: number;
|
|
||||||
|
|
||||||
if (existingApprenant) {
|
|
||||||
apprenantId = existingApprenant.id;
|
|
||||||
} else {
|
|
||||||
// Créer l'apprenant
|
|
||||||
await createApprenantMutation.mutateAsync(formData);
|
|
||||||
// Récupérer l'apprenant nouvellement créé
|
|
||||||
const newApprenant = await utils.client.apprenants.getByEmail.query({ email: formData.email });
|
|
||||||
if (!newApprenant) {
|
|
||||||
throw new Error("Erreur lors de la création de l'apprenant");
|
|
||||||
}
|
|
||||||
apprenantId = newApprenant.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inscrire l'apprenant à la sequence
|
|
||||||
await inscriptionMutation.mutateAsync({
|
|
||||||
apprenantId,
|
|
||||||
sequenceId: selectedSequenceId,
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Erreur lors de l'inscription:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculer les places disponibles pour chaque sequence
|
|
||||||
const sessionsAvecPlaces = useMemo(() => {
|
|
||||||
if (!sequences) return [];
|
|
||||||
|
|
||||||
return sequences.map(sequence => {
|
|
||||||
const now = new Date();
|
|
||||||
const dateBlocage = new Date(sequence.dateBlocage);
|
|
||||||
const isBloquee = sequence.statut === 'bloquee' || now >= dateBlocage;
|
|
||||||
const placesRestantes = Math.max(0, sequence.capaciteMax - (sequence.nbInscrits || 0));
|
|
||||||
|
|
||||||
return {
|
|
||||||
...sequence,
|
|
||||||
isBloquee,
|
|
||||||
placesRestantes,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [sequences]);
|
|
||||||
|
|
||||||
const formatDate = (dateValue: Date | string) => {
|
|
||||||
if (!dateValue) return "N/A";
|
|
||||||
|
|
||||||
try {
|
|
||||||
let localDate: Date;
|
|
||||||
|
|
||||||
// Si c'est déjà un objet Date JavaScript, l'utiliser directement
|
|
||||||
if (dateValue instanceof Date) {
|
|
||||||
localDate = dateValue;
|
|
||||||
} else {
|
|
||||||
// Parser la date MySQL sans conversion UTC
|
|
||||||
const dateStr = String(dateValue);
|
|
||||||
const match = dateStr.match(/(\d{4})-(\d{2})-(\d{2})[T ]?(\d{2}):(\d{2})/);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
console.warn('Format de date non reconnu:', dateValue);
|
|
||||||
return "N/A";
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, year, month, day, hours, minutes] = match;
|
|
||||||
|
|
||||||
// Créer une Date en heure locale
|
|
||||||
localDate = new Date(
|
|
||||||
parseInt(year),
|
|
||||||
parseInt(month) - 1,
|
|
||||||
parseInt(day),
|
|
||||||
parseInt(hours),
|
|
||||||
parseInt(minutes)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return format(localDate, "EEEE d MMMM yyyy 'à' HH:mm", { locale: fr });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur de formatage de date:', error, dateValue);
|
|
||||||
return "N/A";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loadingFormation || loadingSessions) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
|
|
||||||
<Card className="w-full max-w-2xl">
|
|
||||||
<CardHeader>
|
|
||||||
<Skeleton className="h-8 w-64" />
|
|
||||||
<Skeleton className="h-4 w-96 mt-2" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Skeleton className="h-64 w-full" />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formation || !formation.actif) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
|
|
||||||
<Card className="w-full max-w-2xl">
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center gap-2 text-destructive">
|
|
||||||
<XCircle className="w-6 h-6" />
|
|
||||||
<CardTitle>Formation introuvable</CardTitle>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Cette formation n'existe pas ou n'est plus disponible.
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inscriptionSuccess) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
|
|
||||||
<Card className="w-full max-w-2xl">
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex items-center gap-2 text-green-600">
|
|
||||||
<CheckCircle className="w-8 h-8" />
|
|
||||||
<CardTitle className="text-2xl">Inscription réussie !</CardTitle>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<p className="text-lg">
|
|
||||||
{inscriptionStatut === "confirmee"
|
|
||||||
? "Votre inscription à la formation a été confirmée."
|
|
||||||
: "Vous avez été ajouté à la liste d'attente. Nous vous contacterons si une place se libère."}
|
|
||||||
</p>
|
|
||||||
<div className="bg-blue-50 p-4 rounded-lg space-y-2">
|
|
||||||
<p className="font-semibold">Prochaines étapes :</p>
|
|
||||||
<ul className="list-disc list-inside space-y-1 text-sm text-muted-foreground">
|
|
||||||
<li>Vous recevrez un email de confirmation avec tous les détails</li>
|
|
||||||
<li>Une invitation Outlook sera envoyée pour bloquer votre agenda</li>
|
|
||||||
<li>Un email de rappel vous sera envoyé 7 jours avant la formation</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Pour toute question, veuillez contacter le service RH.
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4">
|
|
||||||
<div className="max-w-4xl mx-auto space-y-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="text-center space-y-4">
|
|
||||||
<div className="flex items-center justify-center gap-3">
|
|
||||||
<img src={APP_LOGO} alt={APP_TITLE} className="h-12 w-12" />
|
|
||||||
<h1 className="text-4xl font-bold text-gray-900">{formation.nom}</h1>
|
|
||||||
</div>
|
|
||||||
{formation.description && (
|
|
||||||
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
|
||||||
{formation.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sessions disponibles */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Sessions disponibles</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Sélectionnez une sequence pour vous inscrire (capacité : 12 participants maximum)
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{sessionsAvecPlaces && sessionsAvecPlaces.length > 0 ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{sessionsAvecPlaces.map((sequence) => (
|
|
||||||
<div
|
|
||||||
key={sequence.id}
|
|
||||||
className={`border rounded-lg p-4 cursor-pointer transition-all ${
|
|
||||||
selectedSequenceId === sequence.id
|
|
||||||
? "border-blue-500 bg-blue-50"
|
|
||||||
: sequence.isBloquee
|
|
||||||
? "border-gray-300 bg-gray-50 cursor-not-allowed opacity-60"
|
|
||||||
: "border-gray-300 hover:border-blue-300 hover:bg-blue-50/50"
|
|
||||||
}`}
|
|
||||||
onClick={() => !sequence.isBloquee && setSelectedSequenceId(sequence.id)}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="space-y-2 flex-1">
|
|
||||||
<h3 className="font-semibold text-lg">{sequence.nom}</h3>
|
|
||||||
<div className="space-y-1 text-sm text-muted-foreground">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Calendar className="w-4 h-4" />
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
{sequence.dates.map((date: any) => (
|
|
||||||
<div key={date.id}>
|
|
||||||
<span className="font-medium">Date {date.ordre}:</span> {formatDate(date.dateDebut)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<MapPin className="w-4 h-4" />
|
|
||||||
<span>{sequence.lieu}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Users className="w-4 h-4" />
|
|
||||||
<span>Public cible : {sequence.publicCible === "directeur" ? "Directeurs" : sequence.publicCible === "chef_service" ? "Chefs de service" : "Autre"}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Users className="w-4 h-4" />
|
|
||||||
<span>{sequence.placesRestantes} places disponibles</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{sequence.isBloquee ? (
|
|
||||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-200 text-gray-800">
|
|
||||||
<XCircle className="w-3 h-3 mr-1" />
|
|
||||||
Fermée
|
|
||||||
</span>
|
|
||||||
) : sequence.placesRestantes === 0 ? (
|
|
||||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
|
|
||||||
<AlertCircle className="w-3 h-3 mr-1" />
|
|
||||||
Complète
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
|
||||||
<CheckCircle className="w-3 h-3 mr-1" />
|
|
||||||
Disponible
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
Aucune sequence disponible pour le moment.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Formulaire d'inscription */}
|
|
||||||
{selectedSequenceId && (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Vos informations</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Complétez le formulaire pour finaliser votre inscription
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="nom">Nom *</Label>
|
|
||||||
<Input
|
|
||||||
id="nom"
|
|
||||||
value={formData.nom}
|
|
||||||
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
|
|
||||||
placeholder="Dupont"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="prenom">Prénom *</Label>
|
|
||||||
<Input
|
|
||||||
id="prenom"
|
|
||||||
value={formData.prenom}
|
|
||||||
onChange={(e) => setFormData({ ...formData, prenom: e.target.value })}
|
|
||||||
placeholder="Jean"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="email">Email professionnel *</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
||||||
placeholder="jean.dupont@example.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="codeEtablissement">Code établissement *</Label>
|
|
||||||
<Input
|
|
||||||
id="codeEtablissement"
|
|
||||||
value={formData.codeEtablissement}
|
|
||||||
onChange={(e) => setFormData({ ...formData, codeEtablissement: e.target.value })}
|
|
||||||
placeholder="ETB001"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="fonction">Fonction *</Label>
|
|
||||||
<Select value={formData.fonction} onValueChange={(value: any) => setFormData({ ...formData, fonction: value })}>
|
|
||||||
<SelectTrigger id="fonction">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="directeur">Directeur</SelectItem>
|
|
||||||
<SelectItem value="chef_service">Chef de service</SelectItem>
|
|
||||||
<SelectItem value="autre">Autre</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
|
|
||||||
<p className="text-sm text-amber-800">
|
|
||||||
<strong>Important :</strong> Les inscriptions seront bloquées 15 jours avant le début de la sequence.
|
|
||||||
Après cette date, aucune modification ne sera possible.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full"
|
|
||||||
disabled={inscriptionMutation.isPending || createApprenantMutation.isPending}
|
|
||||||
>
|
|
||||||
{inscriptionMutation.isPending || createApprenantMutation.isPending
|
|
||||||
? "Inscription en cours..."
|
|
||||||
: "Confirmer mon inscription"}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useLocation } from "wouter";
|
|
||||||
import { trpc } from "@/lib/trpc";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
import { APP_LOGO, APP_TITLE } from "@/const";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
|
|
||||||
export default function Login() {
|
|
||||||
const [, setLocation] = useLocation();
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
const loginMutation = trpc.auth.login.useMutation({
|
|
||||||
onSuccess: (data) => {
|
|
||||||
// Stocker le token dans localStorage
|
|
||||||
if (data.token) {
|
|
||||||
localStorage.setItem('auth_token', data.token);
|
|
||||||
}
|
|
||||||
// Rediriger vers le tableau de bord admin
|
|
||||||
window.location.href = "/admin";
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setError(error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setError("");
|
|
||||||
|
|
||||||
if (!email || !password) {
|
|
||||||
setError("Veuillez remplir tous les champs");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loginMutation.mutate({ email, password });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 p-4">
|
|
||||||
<Card className="w-full max-w-md">
|
|
||||||
<CardHeader className="space-y-1 text-center">
|
|
||||||
{APP_LOGO && (
|
|
||||||
<div className="flex justify-center mb-4">
|
|
||||||
<img src={APP_LOGO} alt={APP_TITLE} className="h-16 w-auto" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<CardTitle className="text-2xl font-bold">{APP_TITLE}</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Connectez-vous à votre compte
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
{error && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="email">Email</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="votre@email.com"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
disabled={loginMutation.isPending}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="password">Mot de passe</Label>
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
placeholder="••••••••"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
disabled={loginMutation.isPending}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
|
|
||||||
<CardFooter className="flex flex-col space-y-4">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full"
|
|
||||||
disabled={loginMutation.isPending}
|
|
||||||
>
|
|
||||||
{loginMutation.isPending && (
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
)}
|
|
||||||
Se connecter
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="text-sm text-center text-muted-foreground">
|
|
||||||
Pas encore de compte ?{" "}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setLocation("/register")}
|
|
||||||
className="text-primary hover:underline font-medium"
|
|
||||||
>
|
|
||||||
S'inscrire
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useLocation } from "wouter";
|
|
||||||
import { trpc } from "@/lib/trpc";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
import { APP_LOGO, APP_TITLE } from "@/const";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
|
|
||||||
export default function Register() {
|
|
||||||
const [, setLocation] = useLocation();
|
|
||||||
const [name, setName] = useState("");
|
|
||||||
const [email, setEmail] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const [confirmPassword, setConfirmPassword] = useState("");
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
const registerMutation = trpc.auth.register.useMutation({
|
|
||||||
onSuccess: (data) => {
|
|
||||||
// Stocker le token dans localStorage
|
|
||||||
if (data.token) {
|
|
||||||
localStorage.setItem('auth_token', data.token);
|
|
||||||
}
|
|
||||||
// Rediriger vers le tableau de bord
|
|
||||||
window.location.href = "/";
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setError(error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setError("");
|
|
||||||
|
|
||||||
if (!email || !password || !confirmPassword) {
|
|
||||||
setError("Veuillez remplir tous les champs obligatoires");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password.length < 6) {
|
|
||||||
setError("Le mot de passe doit contenir au moins 6 caractères");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password !== confirmPassword) {
|
|
||||||
setError("Les mots de passe ne correspondent pas");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerMutation.mutate({ email, password, name: name || undefined });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 p-4">
|
|
||||||
<Card className="w-full max-w-md">
|
|
||||||
<CardHeader className="space-y-1 text-center">
|
|
||||||
{APP_LOGO && (
|
|
||||||
<div className="flex justify-center mb-4">
|
|
||||||
<img src={APP_LOGO} alt={APP_TITLE} className="h-16 w-auto" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<CardTitle className="text-2xl font-bold">Créer un compte</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Inscrivez-vous pour accéder à {APP_TITLE}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
{error && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="name">Nom (optionnel)</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
type="text"
|
|
||||||
placeholder="Votre nom"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
disabled={registerMutation.isPending}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="email">Email *</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="votre@email.com"
|
|
||||||
value={email}
|
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
|
||||||
disabled={registerMutation.isPending}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="password">Mot de passe *</Label>
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
placeholder="••••••••"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
disabled={registerMutation.isPending}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Minimum 6 caractères
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="confirmPassword">Confirmer le mot de passe *</Label>
|
|
||||||
<Input
|
|
||||||
id="confirmPassword"
|
|
||||||
type="password"
|
|
||||||
placeholder="••••••••"
|
|
||||||
value={confirmPassword}
|
|
||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
||||||
disabled={registerMutation.isPending}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
|
|
||||||
<CardFooter className="flex flex-col space-y-4">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full"
|
|
||||||
disabled={registerMutation.isPending}
|
|
||||||
>
|
|
||||||
{registerMutation.isPending && (
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
)}
|
|
||||||
S'inscrire
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="text-sm text-center text-muted-foreground">
|
|
||||||
Déjà un compte ?{" "}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setLocation("/login")}
|
|
||||||
className="text-primary hover:underline font-medium"
|
|
||||||
>
|
|
||||||
Se connecter
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</form>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
CREATE TABLE `users` (
|
|
||||||
`id` int AUTO_INCREMENT NOT NULL,
|
|
||||||
`openId` varchar(64) NOT NULL,
|
|
||||||
`name` text,
|
|
||||||
`email` varchar(320),
|
|
||||||
`loginMethod` varchar(64),
|
|
||||||
`role` enum('user','admin') NOT NULL DEFAULT 'user',
|
|
||||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
|
||||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
`lastSignedIn` timestamp NOT NULL DEFAULT (now()),
|
|
||||||
CONSTRAINT `users_id` PRIMARY KEY(`id`),
|
|
||||||
CONSTRAINT `users_openId_unique` UNIQUE(`openId`)
|
|
||||||
);
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
CREATE TABLE `apprenants` (
|
|
||||||
`id` int AUTO_INCREMENT NOT NULL,
|
|
||||||
`nom` varchar(255) NOT NULL,
|
|
||||||
`prenom` varchar(255) NOT NULL,
|
|
||||||
`email` varchar(320) NOT NULL,
|
|
||||||
`codeEtablissement` varchar(50) NOT NULL,
|
|
||||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
|
||||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT `apprenants_id` PRIMARY KEY(`id`),
|
|
||||||
CONSTRAINT `apprenants_email_unique` UNIQUE(`email`)
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `formations` (
|
|
||||||
`id` int AUTO_INCREMENT NOT NULL,
|
|
||||||
`nom` varchar(255) NOT NULL,
|
|
||||||
`description` text,
|
|
||||||
`lienUnique` varchar(100) NOT NULL,
|
|
||||||
`actif` boolean NOT NULL DEFAULT true,
|
|
||||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
|
||||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT `formations_id` PRIMARY KEY(`id`),
|
|
||||||
CONSTRAINT `formations_lienUnique_unique` UNIQUE(`lienUnique`)
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `inscriptions` (
|
|
||||||
`id` int AUTO_INCREMENT NOT NULL,
|
|
||||||
`apprenantId` int NOT NULL,
|
|
||||||
`sessionId` int NOT NULL,
|
|
||||||
`statut` enum('confirmee','liste_attente','annulee') NOT NULL DEFAULT 'confirmee',
|
|
||||||
`dateInscription` timestamp NOT NULL DEFAULT (now()),
|
|
||||||
`invitationEnvoyee` boolean NOT NULL DEFAULT false,
|
|
||||||
`emailConfirmationEnvoye` boolean NOT NULL DEFAULT false,
|
|
||||||
`emailTeaserEnvoye` boolean NOT NULL DEFAULT false,
|
|
||||||
`emailRappelEnvoye` boolean NOT NULL DEFAULT false,
|
|
||||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
|
||||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT `inscriptions_id` PRIMARY KEY(`id`),
|
|
||||||
CONSTRAINT `inscriptions_apprenantId_sessionId_unique` UNIQUE(`apprenantId`,`sessionId`)
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `sessions` (
|
|
||||||
`id` int AUTO_INCREMENT NOT NULL,
|
|
||||||
`formationId` int NOT NULL,
|
|
||||||
`nom` varchar(255) NOT NULL,
|
|
||||||
`dateDebut` datetime NOT NULL,
|
|
||||||
`dateFin` datetime NOT NULL,
|
|
||||||
`lieu` varchar(500) NOT NULL,
|
|
||||||
`capaciteMax` int NOT NULL DEFAULT 12,
|
|
||||||
`dateBlocage` datetime NOT NULL,
|
|
||||||
`statut` enum('ouverte','bloquee','terminee') NOT NULL DEFAULT 'ouverte',
|
|
||||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
|
||||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT `sessions_id` PRIMARY KEY(`id`)
|
|
||||||
);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE `apprenants` ADD `fonction` enum('directeur','chef_service','autre') NOT NULL;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
CREATE TABLE `datesFormation` (
|
|
||||||
`id` int AUTO_INCREMENT NOT NULL,
|
|
||||||
`sequenceId` int NOT NULL,
|
|
||||||
`dateDebut` datetime NOT NULL,
|
|
||||||
`dateFin` datetime NOT NULL,
|
|
||||||
`ordre` int NOT NULL,
|
|
||||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
|
||||||
CONSTRAINT `datesFormation_id` PRIMARY KEY(`id`)
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `sequences` (
|
|
||||||
`id` int AUTO_INCREMENT NOT NULL,
|
|
||||||
`formationId` int NOT NULL,
|
|
||||||
`nom` varchar(255) NOT NULL,
|
|
||||||
`lieu` varchar(500) NOT NULL,
|
|
||||||
`capaciteMax` int NOT NULL DEFAULT 12,
|
|
||||||
`dateBlocage` datetime NOT NULL,
|
|
||||||
`statut` enum('ouverte','bloquee','terminee') NOT NULL DEFAULT 'ouverte',
|
|
||||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
|
||||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT `sequences_id` PRIMARY KEY(`id`)
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
DROP TABLE `sessions`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `inscriptions` DROP INDEX `inscriptions_apprenantId_sessionId_unique`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `inscriptions` MODIFY COLUMN `statut` enum('confirmee','liste_attente','annulee') NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `inscriptions` ADD `sequenceId` int NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `inscriptions` DROP COLUMN `sessionId`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `inscriptions` DROP COLUMN `invitationEnvoyee`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `inscriptions` DROP COLUMN `emailConfirmationEnvoye`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `inscriptions` DROP COLUMN `emailTeaserEnvoye`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `inscriptions` DROP COLUMN `emailRappelEnvoye`;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE `formations` ADD `publicCible` enum('directeur','chef_service','autre') NOT NULL;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE `sequences` ADD `publicCible` enum('directeur','chef_service','autre') NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `formations` DROP COLUMN `publicCible`;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE `users` ADD `isActive` boolean DEFAULT true NOT NULL;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
CREATE TABLE `passwordResetTokens` (
|
|
||||||
`id` int AUTO_INCREMENT NOT NULL,
|
|
||||||
`userId` int NOT NULL,
|
|
||||||
`token` varchar(255) NOT NULL,
|
|
||||||
`expiresAt` timestamp NOT NULL,
|
|
||||||
`used` boolean NOT NULL DEFAULT false,
|
|
||||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
|
||||||
CONSTRAINT `passwordResetTokens_id` PRIMARY KEY(`id`),
|
|
||||||
CONSTRAINT `passwordResetTokens_token_unique` UNIQUE(`token`)
|
|
||||||
);
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
CREATE TABLE `emailTemplates` (
|
|
||||||
`id` int AUTO_INCREMENT NOT NULL,
|
|
||||||
`type` varchar(50) NOT NULL,
|
|
||||||
`name` varchar(255) NOT NULL,
|
|
||||||
`logoUrl` text,
|
|
||||||
`primaryColor` varchar(7) NOT NULL DEFAULT '#2563eb',
|
|
||||||
`headerBgColor` varchar(7) NOT NULL DEFAULT '#2563eb',
|
|
||||||
`headerTextColor` varchar(7) NOT NULL DEFAULT '#ffffff',
|
|
||||||
`headerTitle` varchar(255) NOT NULL DEFAULT 'Formation Manager Itinova',
|
|
||||||
`footerText` text,
|
|
||||||
`active` boolean NOT NULL DEFAULT true,
|
|
||||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
|
||||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT `emailTemplates_id` PRIMARY KEY(`id`),
|
|
||||||
CONSTRAINT `emailTemplates_type_unique` UNIQUE(`type`)
|
|
||||||
);
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
CREATE TABLE `emailConfig` (
|
|
||||||
`id` int AUTO_INCREMENT NOT NULL,
|
|
||||||
`provider` varchar(50) NOT NULL DEFAULT 'resend',
|
|
||||||
`apiKey` text,
|
|
||||||
`fromEmail` varchar(320) NOT NULL,
|
|
||||||
`fromName` varchar(255) NOT NULL DEFAULT 'Formation Manager Itinova',
|
|
||||||
`mode` enum('simulation','production') NOT NULL DEFAULT 'simulation',
|
|
||||||
`domainVerified` boolean NOT NULL DEFAULT false,
|
|
||||||
`active` boolean NOT NULL DEFAULT true,
|
|
||||||
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
|
||||||
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
CONSTRAINT `emailConfig_id` PRIMARY KEY(`id`)
|
|
||||||
);
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
ALTER TABLE `users` DROP INDEX `users_openId_unique`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `users` MODIFY COLUMN `email` varchar(320) NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `users` ADD `password` varchar(255) NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `users` ADD `emailVerified` boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `users` ADD CONSTRAINT `users_email_unique` UNIQUE(`email`);--> statement-breakpoint
|
|
||||||
ALTER TABLE `users` DROP COLUMN `openId`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `users` DROP COLUMN `loginMethod`;
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "mysql",
|
|
||||||
"id": "72faa14d-1c17-40c3-99c8-b471711804bc",
|
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
||||||
"tables": {
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"loginMethod": {
|
|
||||||
"name": "loginMethod",
|
|
||||||
"type": "varchar(64)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"name": "role",
|
|
||||||
"type": "enum('user','admin')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'user'"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"tables": {},
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,473 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "mysql",
|
|
||||||
"id": "95d14d84-eb09-4770-af36-02daa58e4a5d",
|
|
||||||
"prevId": "72faa14d-1c17-40c3-99c8-b471711804bc",
|
|
||||||
"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": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"apprenants_id": {
|
|
||||||
"name": "apprenants_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"apprenants_email_unique": {
|
|
||||||
"name": "apprenants_email_unique",
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"formations": {
|
|
||||||
"name": "formations",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lienUnique": {
|
|
||||||
"name": "lienUnique",
|
|
||||||
"type": "varchar(100)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"actif": {
|
|
||||||
"name": "actif",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"formations_id": {
|
|
||||||
"name": "formations_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"formations_lienUnique_unique": {
|
|
||||||
"name": "formations_lienUnique_unique",
|
|
||||||
"columns": [
|
|
||||||
"lienUnique"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"inscriptions": {
|
|
||||||
"name": "inscriptions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"apprenantId": {
|
|
||||||
"name": "apprenantId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sessionId": {
|
|
||||||
"name": "sessionId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('confirmee','liste_attente','annulee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'confirmee'"
|
|
||||||
},
|
|
||||||
"dateInscription": {
|
|
||||||
"name": "dateInscription",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"invitationEnvoyee": {
|
|
||||||
"name": "invitationEnvoyee",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"emailConfirmationEnvoye": {
|
|
||||||
"name": "emailConfirmationEnvoye",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"emailTeaserEnvoye": {
|
|
||||||
"name": "emailTeaserEnvoye",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"emailRappelEnvoye": {
|
|
||||||
"name": "emailRappelEnvoye",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"inscriptions_id": {
|
|
||||||
"name": "inscriptions_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"inscriptions_apprenantId_sessionId_unique": {
|
|
||||||
"name": "inscriptions_apprenantId_sessionId_unique",
|
|
||||||
"columns": [
|
|
||||||
"apprenantId",
|
|
||||||
"sessionId"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"sessions": {
|
|
||||||
"name": "sessions",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"dateDebut": {
|
|
||||||
"name": "dateDebut",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateFin": {
|
|
||||||
"name": "dateFin",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lieu": {
|
|
||||||
"name": "lieu",
|
|
||||||
"type": "varchar(500)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"capaciteMax": {
|
|
||||||
"name": "capaciteMax",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 12
|
|
||||||
},
|
|
||||||
"dateBlocage": {
|
|
||||||
"name": "dateBlocage",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('ouverte','bloquee','terminee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'ouverte'"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"sessions_id": {
|
|
||||||
"name": "sessions_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
|
|
||||||
},
|
|
||||||
"loginMethod": {
|
|
||||||
"name": "loginMethod",
|
|
||||||
"type": "varchar(64)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"name": "role",
|
|
||||||
"type": "enum('user','admin')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'user'"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"tables": {},
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,480 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "mysql",
|
|
||||||
"id": "c625e855-6929-4091-a755-d300ea81b53a",
|
|
||||||
"prevId": "95d14d84-eb09-4770-af36-02daa58e4a5d",
|
|
||||||
"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": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fonction": {
|
|
||||||
"name": "fonction",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"apprenants_id": {
|
|
||||||
"name": "apprenants_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"apprenants_email_unique": {
|
|
||||||
"name": "apprenants_email_unique",
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"formations": {
|
|
||||||
"name": "formations",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lienUnique": {
|
|
||||||
"name": "lienUnique",
|
|
||||||
"type": "varchar(100)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"actif": {
|
|
||||||
"name": "actif",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"formations_id": {
|
|
||||||
"name": "formations_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"formations_lienUnique_unique": {
|
|
||||||
"name": "formations_lienUnique_unique",
|
|
||||||
"columns": [
|
|
||||||
"lienUnique"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"inscriptions": {
|
|
||||||
"name": "inscriptions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"apprenantId": {
|
|
||||||
"name": "apprenantId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sessionId": {
|
|
||||||
"name": "sessionId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('confirmee','liste_attente','annulee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'confirmee'"
|
|
||||||
},
|
|
||||||
"dateInscription": {
|
|
||||||
"name": "dateInscription",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"invitationEnvoyee": {
|
|
||||||
"name": "invitationEnvoyee",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"emailConfirmationEnvoye": {
|
|
||||||
"name": "emailConfirmationEnvoye",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"emailTeaserEnvoye": {
|
|
||||||
"name": "emailTeaserEnvoye",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"emailRappelEnvoye": {
|
|
||||||
"name": "emailRappelEnvoye",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"inscriptions_id": {
|
|
||||||
"name": "inscriptions_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"inscriptions_apprenantId_sessionId_unique": {
|
|
||||||
"name": "inscriptions_apprenantId_sessionId_unique",
|
|
||||||
"columns": [
|
|
||||||
"apprenantId",
|
|
||||||
"sessionId"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"sessions": {
|
|
||||||
"name": "sessions",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"dateDebut": {
|
|
||||||
"name": "dateDebut",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateFin": {
|
|
||||||
"name": "dateFin",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lieu": {
|
|
||||||
"name": "lieu",
|
|
||||||
"type": "varchar(500)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"capaciteMax": {
|
|
||||||
"name": "capaciteMax",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 12
|
|
||||||
},
|
|
||||||
"dateBlocage": {
|
|
||||||
"name": "dateBlocage",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('ouverte','bloquee','terminee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'ouverte'"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"sessions_id": {
|
|
||||||
"name": "sessions_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
|
|
||||||
},
|
|
||||||
"loginMethod": {
|
|
||||||
"name": "loginMethod",
|
|
||||||
"type": "varchar(64)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"name": "role",
|
|
||||||
"type": "enum('user','admin')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'user'"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"tables": {},
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,485 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "mysql",
|
|
||||||
"id": "0352a8eb-52dd-4dee-814f-96378a8fce64",
|
|
||||||
"prevId": "c625e855-6929-4091-a755-d300ea81b53a",
|
|
||||||
"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": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fonction": {
|
|
||||||
"name": "fonction",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"apprenants_id": {
|
|
||||||
"name": "apprenants_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"apprenants_email_unique": {
|
|
||||||
"name": "apprenants_email_unique",
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"datesFormation": {
|
|
||||||
"name": "datesFormation",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateDebut": {
|
|
||||||
"name": "dateDebut",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateFin": {
|
|
||||||
"name": "dateFin",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"ordre": {
|
|
||||||
"name": "ordre",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"datesFormation_id": {
|
|
||||||
"name": "datesFormation_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"formations": {
|
|
||||||
"name": "formations",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lienUnique": {
|
|
||||||
"name": "lienUnique",
|
|
||||||
"type": "varchar(100)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"actif": {
|
|
||||||
"name": "actif",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"formations_id": {
|
|
||||||
"name": "formations_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"formations_lienUnique_unique": {
|
|
||||||
"name": "formations_lienUnique_unique",
|
|
||||||
"columns": [
|
|
||||||
"lienUnique"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"inscriptions": {
|
|
||||||
"name": "inscriptions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"apprenantId": {
|
|
||||||
"name": "apprenantId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('confirmee','liste_attente','annulee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateInscription": {
|
|
||||||
"name": "dateInscription",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"inscriptions_id": {
|
|
||||||
"name": "inscriptions_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"sequences": {
|
|
||||||
"name": "sequences",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"formationId": {
|
|
||||||
"name": "formationId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lieu": {
|
|
||||||
"name": "lieu",
|
|
||||||
"type": "varchar(500)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"capaciteMax": {
|
|
||||||
"name": "capaciteMax",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 12
|
|
||||||
},
|
|
||||||
"dateBlocage": {
|
|
||||||
"name": "dateBlocage",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('ouverte','bloquee','terminee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'ouverte'"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"sequences_id": {
|
|
||||||
"name": "sequences_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"loginMethod": {
|
|
||||||
"name": "loginMethod",
|
|
||||||
"type": "varchar(64)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"name": "role",
|
|
||||||
"type": "enum('user','admin')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'user'"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"tables": {},
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,492 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "mysql",
|
|
||||||
"id": "9dc05bf2-d92e-4abf-be1e-7357b388c9a5",
|
|
||||||
"prevId": "0352a8eb-52dd-4dee-814f-96378a8fce64",
|
|
||||||
"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": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fonction": {
|
|
||||||
"name": "fonction",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"apprenants_id": {
|
|
||||||
"name": "apprenants_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"apprenants_email_unique": {
|
|
||||||
"name": "apprenants_email_unique",
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"datesFormation": {
|
|
||||||
"name": "datesFormation",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateDebut": {
|
|
||||||
"name": "dateDebut",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateFin": {
|
|
||||||
"name": "dateFin",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"ordre": {
|
|
||||||
"name": "ordre",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"datesFormation_id": {
|
|
||||||
"name": "datesFormation_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"publicCible": {
|
|
||||||
"name": "publicCible",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lienUnique": {
|
|
||||||
"name": "lienUnique",
|
|
||||||
"type": "varchar(100)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"actif": {
|
|
||||||
"name": "actif",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"formations_id": {
|
|
||||||
"name": "formations_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"formations_lienUnique_unique": {
|
|
||||||
"name": "formations_lienUnique_unique",
|
|
||||||
"columns": [
|
|
||||||
"lienUnique"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"inscriptions": {
|
|
||||||
"name": "inscriptions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"apprenantId": {
|
|
||||||
"name": "apprenantId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('confirmee','liste_attente','annulee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateInscription": {
|
|
||||||
"name": "dateInscription",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"inscriptions_id": {
|
|
||||||
"name": "inscriptions_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"sequences": {
|
|
||||||
"name": "sequences",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"formationId": {
|
|
||||||
"name": "formationId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lieu": {
|
|
||||||
"name": "lieu",
|
|
||||||
"type": "varchar(500)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"capaciteMax": {
|
|
||||||
"name": "capaciteMax",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 12
|
|
||||||
},
|
|
||||||
"dateBlocage": {
|
|
||||||
"name": "dateBlocage",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('ouverte','bloquee','terminee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'ouverte'"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"sequences_id": {
|
|
||||||
"name": "sequences_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"loginMethod": {
|
|
||||||
"name": "loginMethod",
|
|
||||||
"type": "varchar(64)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"name": "role",
|
|
||||||
"type": "enum('user','admin')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'user'"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"tables": {},
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,492 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "mysql",
|
|
||||||
"id": "b9b19655-1104-4112-ac8d-d568013faea2",
|
|
||||||
"prevId": "9dc05bf2-d92e-4abf-be1e-7357b388c9a5",
|
|
||||||
"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": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fonction": {
|
|
||||||
"name": "fonction",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"apprenants_id": {
|
|
||||||
"name": "apprenants_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"apprenants_email_unique": {
|
|
||||||
"name": "apprenants_email_unique",
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"datesFormation": {
|
|
||||||
"name": "datesFormation",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateDebut": {
|
|
||||||
"name": "dateDebut",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateFin": {
|
|
||||||
"name": "dateFin",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"ordre": {
|
|
||||||
"name": "ordre",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"datesFormation_id": {
|
|
||||||
"name": "datesFormation_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"formations": {
|
|
||||||
"name": "formations",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lienUnique": {
|
|
||||||
"name": "lienUnique",
|
|
||||||
"type": "varchar(100)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"actif": {
|
|
||||||
"name": "actif",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"formations_id": {
|
|
||||||
"name": "formations_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"formations_lienUnique_unique": {
|
|
||||||
"name": "formations_lienUnique_unique",
|
|
||||||
"columns": [
|
|
||||||
"lienUnique"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"inscriptions": {
|
|
||||||
"name": "inscriptions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"apprenantId": {
|
|
||||||
"name": "apprenantId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('confirmee','liste_attente','annulee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateInscription": {
|
|
||||||
"name": "dateInscription",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"inscriptions_id": {
|
|
||||||
"name": "inscriptions_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"sequences": {
|
|
||||||
"name": "sequences",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"formationId": {
|
|
||||||
"name": "formationId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lieu": {
|
|
||||||
"name": "lieu",
|
|
||||||
"type": "varchar(500)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"publicCible": {
|
|
||||||
"name": "publicCible",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"capaciteMax": {
|
|
||||||
"name": "capaciteMax",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 12
|
|
||||||
},
|
|
||||||
"dateBlocage": {
|
|
||||||
"name": "dateBlocage",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('ouverte','bloquee','terminee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'ouverte'"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"sequences_id": {
|
|
||||||
"name": "sequences_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"loginMethod": {
|
|
||||||
"name": "loginMethod",
|
|
||||||
"type": "varchar(64)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"name": "role",
|
|
||||||
"type": "enum('user','admin')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'user'"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"tables": {},
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,500 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "mysql",
|
|
||||||
"id": "0ad1d66e-8e34-4ec9-9e4a-12ff775a6d1b",
|
|
||||||
"prevId": "b9b19655-1104-4112-ac8d-d568013faea2",
|
|
||||||
"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": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fonction": {
|
|
||||||
"name": "fonction",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"apprenants_id": {
|
|
||||||
"name": "apprenants_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"apprenants_email_unique": {
|
|
||||||
"name": "apprenants_email_unique",
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"datesFormation": {
|
|
||||||
"name": "datesFormation",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateDebut": {
|
|
||||||
"name": "dateDebut",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateFin": {
|
|
||||||
"name": "dateFin",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"ordre": {
|
|
||||||
"name": "ordre",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"datesFormation_id": {
|
|
||||||
"name": "datesFormation_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"formations": {
|
|
||||||
"name": "formations",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lienUnique": {
|
|
||||||
"name": "lienUnique",
|
|
||||||
"type": "varchar(100)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"actif": {
|
|
||||||
"name": "actif",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"formations_id": {
|
|
||||||
"name": "formations_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"formations_lienUnique_unique": {
|
|
||||||
"name": "formations_lienUnique_unique",
|
|
||||||
"columns": [
|
|
||||||
"lienUnique"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"inscriptions": {
|
|
||||||
"name": "inscriptions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"apprenantId": {
|
|
||||||
"name": "apprenantId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('confirmee','liste_attente','annulee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateInscription": {
|
|
||||||
"name": "dateInscription",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"inscriptions_id": {
|
|
||||||
"name": "inscriptions_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"sequences": {
|
|
||||||
"name": "sequences",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"formationId": {
|
|
||||||
"name": "formationId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lieu": {
|
|
||||||
"name": "lieu",
|
|
||||||
"type": "varchar(500)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"publicCible": {
|
|
||||||
"name": "publicCible",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"capaciteMax": {
|
|
||||||
"name": "capaciteMax",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 12
|
|
||||||
},
|
|
||||||
"dateBlocage": {
|
|
||||||
"name": "dateBlocage",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('ouverte','bloquee','terminee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'ouverte'"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"sequences_id": {
|
|
||||||
"name": "sequences_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"loginMethod": {
|
|
||||||
"name": "loginMethod",
|
|
||||||
"type": "varchar(64)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"name": "role",
|
|
||||||
"type": "enum('user','admin')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'user'"
|
|
||||||
},
|
|
||||||
"isActive": {
|
|
||||||
"name": "isActive",
|
|
||||||
"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())"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"tables": {},
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,568 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "mysql",
|
|
||||||
"id": "44a452bc-0f54-464a-98ae-043474047a69",
|
|
||||||
"prevId": "0ad1d66e-8e34-4ec9-9e4a-12ff775a6d1b",
|
|
||||||
"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": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fonction": {
|
|
||||||
"name": "fonction",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"apprenants_id": {
|
|
||||||
"name": "apprenants_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"apprenants_email_unique": {
|
|
||||||
"name": "apprenants_email_unique",
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"datesFormation": {
|
|
||||||
"name": "datesFormation",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateDebut": {
|
|
||||||
"name": "dateDebut",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateFin": {
|
|
||||||
"name": "dateFin",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"ordre": {
|
|
||||||
"name": "ordre",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"datesFormation_id": {
|
|
||||||
"name": "datesFormation_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"formations": {
|
|
||||||
"name": "formations",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lienUnique": {
|
|
||||||
"name": "lienUnique",
|
|
||||||
"type": "varchar(100)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"actif": {
|
|
||||||
"name": "actif",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"formations_id": {
|
|
||||||
"name": "formations_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"formations_lienUnique_unique": {
|
|
||||||
"name": "formations_lienUnique_unique",
|
|
||||||
"columns": [
|
|
||||||
"lienUnique"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"inscriptions": {
|
|
||||||
"name": "inscriptions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"apprenantId": {
|
|
||||||
"name": "apprenantId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('confirmee','liste_attente','annulee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateInscription": {
|
|
||||||
"name": "dateInscription",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"inscriptions_id": {
|
|
||||||
"name": "inscriptions_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"passwordResetTokens": {
|
|
||||||
"name": "passwordResetTokens",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"name": "token",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"expiresAt": {
|
|
||||||
"name": "expiresAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"used": {
|
|
||||||
"name": "used",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"passwordResetTokens_id": {
|
|
||||||
"name": "passwordResetTokens_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"passwordResetTokens_token_unique": {
|
|
||||||
"name": "passwordResetTokens_token_unique",
|
|
||||||
"columns": [
|
|
||||||
"token"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"sequences": {
|
|
||||||
"name": "sequences",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"formationId": {
|
|
||||||
"name": "formationId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lieu": {
|
|
||||||
"name": "lieu",
|
|
||||||
"type": "varchar(500)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"publicCible": {
|
|
||||||
"name": "publicCible",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"capaciteMax": {
|
|
||||||
"name": "capaciteMax",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 12
|
|
||||||
},
|
|
||||||
"dateBlocage": {
|
|
||||||
"name": "dateBlocage",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('ouverte','bloquee','terminee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'ouverte'"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"sequences_id": {
|
|
||||||
"name": "sequences_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"loginMethod": {
|
|
||||||
"name": "loginMethod",
|
|
||||||
"type": "varchar(64)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"name": "role",
|
|
||||||
"type": "enum('user','admin')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'user'"
|
|
||||||
},
|
|
||||||
"isActive": {
|
|
||||||
"name": "isActive",
|
|
||||||
"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())"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"tables": {},
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,684 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "mysql",
|
|
||||||
"id": "73d62a50-d4a5-41dd-b5a2-4d2d72b7f119",
|
|
||||||
"prevId": "44a452bc-0f54-464a-98ae-043474047a69",
|
|
||||||
"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": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fonction": {
|
|
||||||
"name": "fonction",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"apprenants_id": {
|
|
||||||
"name": "apprenants_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"apprenants_email_unique": {
|
|
||||||
"name": "apprenants_email_unique",
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"datesFormation": {
|
|
||||||
"name": "datesFormation",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateDebut": {
|
|
||||||
"name": "dateDebut",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateFin": {
|
|
||||||
"name": "dateFin",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"ordre": {
|
|
||||||
"name": "ordre",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"datesFormation_id": {
|
|
||||||
"name": "datesFormation_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"emailTemplates": {
|
|
||||||
"name": "emailTemplates",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "varchar(50)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"logoUrl": {
|
|
||||||
"name": "logoUrl",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"primaryColor": {
|
|
||||||
"name": "primaryColor",
|
|
||||||
"type": "varchar(7)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'#2563eb'"
|
|
||||||
},
|
|
||||||
"headerBgColor": {
|
|
||||||
"name": "headerBgColor",
|
|
||||||
"type": "varchar(7)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'#2563eb'"
|
|
||||||
},
|
|
||||||
"headerTextColor": {
|
|
||||||
"name": "headerTextColor",
|
|
||||||
"type": "varchar(7)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'#ffffff'"
|
|
||||||
},
|
|
||||||
"headerTitle": {
|
|
||||||
"name": "headerTitle",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'Formation Manager Itinova'"
|
|
||||||
},
|
|
||||||
"footerText": {
|
|
||||||
"name": "footerText",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"active": {
|
|
||||||
"name": "active",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"emailTemplates_id": {
|
|
||||||
"name": "emailTemplates_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"emailTemplates_type_unique": {
|
|
||||||
"name": "emailTemplates_type_unique",
|
|
||||||
"columns": [
|
|
||||||
"type"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"formations": {
|
|
||||||
"name": "formations",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lienUnique": {
|
|
||||||
"name": "lienUnique",
|
|
||||||
"type": "varchar(100)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"actif": {
|
|
||||||
"name": "actif",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"formations_id": {
|
|
||||||
"name": "formations_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"formations_lienUnique_unique": {
|
|
||||||
"name": "formations_lienUnique_unique",
|
|
||||||
"columns": [
|
|
||||||
"lienUnique"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"inscriptions": {
|
|
||||||
"name": "inscriptions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"apprenantId": {
|
|
||||||
"name": "apprenantId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('confirmee','liste_attente','annulee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateInscription": {
|
|
||||||
"name": "dateInscription",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"inscriptions_id": {
|
|
||||||
"name": "inscriptions_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"passwordResetTokens": {
|
|
||||||
"name": "passwordResetTokens",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"name": "token",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"expiresAt": {
|
|
||||||
"name": "expiresAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"used": {
|
|
||||||
"name": "used",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"passwordResetTokens_id": {
|
|
||||||
"name": "passwordResetTokens_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"passwordResetTokens_token_unique": {
|
|
||||||
"name": "passwordResetTokens_token_unique",
|
|
||||||
"columns": [
|
|
||||||
"token"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"sequences": {
|
|
||||||
"name": "sequences",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"formationId": {
|
|
||||||
"name": "formationId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lieu": {
|
|
||||||
"name": "lieu",
|
|
||||||
"type": "varchar(500)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"publicCible": {
|
|
||||||
"name": "publicCible",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"capaciteMax": {
|
|
||||||
"name": "capaciteMax",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 12
|
|
||||||
},
|
|
||||||
"dateBlocage": {
|
|
||||||
"name": "dateBlocage",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('ouverte','bloquee','terminee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'ouverte'"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"sequences_id": {
|
|
||||||
"name": "sequences_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"loginMethod": {
|
|
||||||
"name": "loginMethod",
|
|
||||||
"type": "varchar(64)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"name": "role",
|
|
||||||
"type": "enum('user','admin')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'user'"
|
|
||||||
},
|
|
||||||
"isActive": {
|
|
||||||
"name": "isActive",
|
|
||||||
"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())"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"tables": {},
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,779 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "mysql",
|
|
||||||
"id": "a099e856-0b0c-430d-8160-ca950c994fe5",
|
|
||||||
"prevId": "73d62a50-d4a5-41dd-b5a2-4d2d72b7f119",
|
|
||||||
"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": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fonction": {
|
|
||||||
"name": "fonction",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"apprenants_id": {
|
|
||||||
"name": "apprenants_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"apprenants_email_unique": {
|
|
||||||
"name": "apprenants_email_unique",
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"datesFormation": {
|
|
||||||
"name": "datesFormation",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateDebut": {
|
|
||||||
"name": "dateDebut",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateFin": {
|
|
||||||
"name": "dateFin",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"ordre": {
|
|
||||||
"name": "ordre",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"datesFormation_id": {
|
|
||||||
"name": "datesFormation_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"emailConfig": {
|
|
||||||
"name": "emailConfig",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"provider": {
|
|
||||||
"name": "provider",
|
|
||||||
"type": "varchar(50)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'resend'"
|
|
||||||
},
|
|
||||||
"apiKey": {
|
|
||||||
"name": "apiKey",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fromEmail": {
|
|
||||||
"name": "fromEmail",
|
|
||||||
"type": "varchar(320)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fromName": {
|
|
||||||
"name": "fromName",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'Formation Manager Itinova'"
|
|
||||||
},
|
|
||||||
"mode": {
|
|
||||||
"name": "mode",
|
|
||||||
"type": "enum('simulation','production')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'simulation'"
|
|
||||||
},
|
|
||||||
"domainVerified": {
|
|
||||||
"name": "domainVerified",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"active": {
|
|
||||||
"name": "active",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"emailConfig_id": {
|
|
||||||
"name": "emailConfig_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"emailTemplates": {
|
|
||||||
"name": "emailTemplates",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "varchar(50)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"logoUrl": {
|
|
||||||
"name": "logoUrl",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"primaryColor": {
|
|
||||||
"name": "primaryColor",
|
|
||||||
"type": "varchar(7)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'#2563eb'"
|
|
||||||
},
|
|
||||||
"headerBgColor": {
|
|
||||||
"name": "headerBgColor",
|
|
||||||
"type": "varchar(7)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'#2563eb'"
|
|
||||||
},
|
|
||||||
"headerTextColor": {
|
|
||||||
"name": "headerTextColor",
|
|
||||||
"type": "varchar(7)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'#ffffff'"
|
|
||||||
},
|
|
||||||
"headerTitle": {
|
|
||||||
"name": "headerTitle",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'Formation Manager Itinova'"
|
|
||||||
},
|
|
||||||
"footerText": {
|
|
||||||
"name": "footerText",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"active": {
|
|
||||||
"name": "active",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"emailTemplates_id": {
|
|
||||||
"name": "emailTemplates_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"emailTemplates_type_unique": {
|
|
||||||
"name": "emailTemplates_type_unique",
|
|
||||||
"columns": [
|
|
||||||
"type"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"formations": {
|
|
||||||
"name": "formations",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lienUnique": {
|
|
||||||
"name": "lienUnique",
|
|
||||||
"type": "varchar(100)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"actif": {
|
|
||||||
"name": "actif",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"formations_id": {
|
|
||||||
"name": "formations_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"formations_lienUnique_unique": {
|
|
||||||
"name": "formations_lienUnique_unique",
|
|
||||||
"columns": [
|
|
||||||
"lienUnique"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"inscriptions": {
|
|
||||||
"name": "inscriptions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"apprenantId": {
|
|
||||||
"name": "apprenantId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('confirmee','liste_attente','annulee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateInscription": {
|
|
||||||
"name": "dateInscription",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"inscriptions_id": {
|
|
||||||
"name": "inscriptions_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"passwordResetTokens": {
|
|
||||||
"name": "passwordResetTokens",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"name": "token",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"expiresAt": {
|
|
||||||
"name": "expiresAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"used": {
|
|
||||||
"name": "used",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"passwordResetTokens_id": {
|
|
||||||
"name": "passwordResetTokens_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"passwordResetTokens_token_unique": {
|
|
||||||
"name": "passwordResetTokens_token_unique",
|
|
||||||
"columns": [
|
|
||||||
"token"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"sequences": {
|
|
||||||
"name": "sequences",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"formationId": {
|
|
||||||
"name": "formationId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lieu": {
|
|
||||||
"name": "lieu",
|
|
||||||
"type": "varchar(500)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"publicCible": {
|
|
||||||
"name": "publicCible",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"capaciteMax": {
|
|
||||||
"name": "capaciteMax",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 12
|
|
||||||
},
|
|
||||||
"dateBlocage": {
|
|
||||||
"name": "dateBlocage",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('ouverte','bloquee','terminee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'ouverte'"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"sequences_id": {
|
|
||||||
"name": "sequences_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"loginMethod": {
|
|
||||||
"name": "loginMethod",
|
|
||||||
"type": "varchar(64)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"name": "role",
|
|
||||||
"type": "enum('user','admin')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'user'"
|
|
||||||
},
|
|
||||||
"isActive": {
|
|
||||||
"name": "isActive",
|
|
||||||
"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())"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"tables": {},
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,780 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "mysql",
|
|
||||||
"id": "e34c8fec-5308-4ce7-b687-d27c3093036c",
|
|
||||||
"prevId": "a099e856-0b0c-430d-8160-ca950c994fe5",
|
|
||||||
"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": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fonction": {
|
|
||||||
"name": "fonction",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"apprenants_id": {
|
|
||||||
"name": "apprenants_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"apprenants_email_unique": {
|
|
||||||
"name": "apprenants_email_unique",
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"datesFormation": {
|
|
||||||
"name": "datesFormation",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateDebut": {
|
|
||||||
"name": "dateDebut",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateFin": {
|
|
||||||
"name": "dateFin",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"ordre": {
|
|
||||||
"name": "ordre",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"datesFormation_id": {
|
|
||||||
"name": "datesFormation_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"emailConfig": {
|
|
||||||
"name": "emailConfig",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"provider": {
|
|
||||||
"name": "provider",
|
|
||||||
"type": "varchar(50)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'resend'"
|
|
||||||
},
|
|
||||||
"apiKey": {
|
|
||||||
"name": "apiKey",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fromEmail": {
|
|
||||||
"name": "fromEmail",
|
|
||||||
"type": "varchar(320)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"fromName": {
|
|
||||||
"name": "fromName",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'Formation Manager Itinova'"
|
|
||||||
},
|
|
||||||
"mode": {
|
|
||||||
"name": "mode",
|
|
||||||
"type": "enum('simulation','production')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'simulation'"
|
|
||||||
},
|
|
||||||
"domainVerified": {
|
|
||||||
"name": "domainVerified",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"active": {
|
|
||||||
"name": "active",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"emailConfig_id": {
|
|
||||||
"name": "emailConfig_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"emailTemplates": {
|
|
||||||
"name": "emailTemplates",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "varchar(50)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"logoUrl": {
|
|
||||||
"name": "logoUrl",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"primaryColor": {
|
|
||||||
"name": "primaryColor",
|
|
||||||
"type": "varchar(7)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'#2563eb'"
|
|
||||||
},
|
|
||||||
"headerBgColor": {
|
|
||||||
"name": "headerBgColor",
|
|
||||||
"type": "varchar(7)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'#2563eb'"
|
|
||||||
},
|
|
||||||
"headerTextColor": {
|
|
||||||
"name": "headerTextColor",
|
|
||||||
"type": "varchar(7)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'#ffffff'"
|
|
||||||
},
|
|
||||||
"headerTitle": {
|
|
||||||
"name": "headerTitle",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'Formation Manager Itinova'"
|
|
||||||
},
|
|
||||||
"footerText": {
|
|
||||||
"name": "footerText",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"active": {
|
|
||||||
"name": "active",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"emailTemplates_id": {
|
|
||||||
"name": "emailTemplates_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"emailTemplates_type_unique": {
|
|
||||||
"name": "emailTemplates_type_unique",
|
|
||||||
"columns": [
|
|
||||||
"type"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"formations": {
|
|
||||||
"name": "formations",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lienUnique": {
|
|
||||||
"name": "lienUnique",
|
|
||||||
"type": "varchar(100)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"actif": {
|
|
||||||
"name": "actif",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"formations_id": {
|
|
||||||
"name": "formations_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"formations_lienUnique_unique": {
|
|
||||||
"name": "formations_lienUnique_unique",
|
|
||||||
"columns": [
|
|
||||||
"lienUnique"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"inscriptions": {
|
|
||||||
"name": "inscriptions",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"apprenantId": {
|
|
||||||
"name": "apprenantId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"sequenceId": {
|
|
||||||
"name": "sequenceId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('confirmee','liste_attente','annulee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"dateInscription": {
|
|
||||||
"name": "dateInscription",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"inscriptions_id": {
|
|
||||||
"name": "inscriptions_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"passwordResetTokens": {
|
|
||||||
"name": "passwordResetTokens",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"name": "token",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"expiresAt": {
|
|
||||||
"name": "expiresAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"used": {
|
|
||||||
"name": "used",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"passwordResetTokens_id": {
|
|
||||||
"name": "passwordResetTokens_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {
|
|
||||||
"passwordResetTokens_token_unique": {
|
|
||||||
"name": "passwordResetTokens_token_unique",
|
|
||||||
"columns": [
|
|
||||||
"token"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"sequences": {
|
|
||||||
"name": "sequences",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"formationId": {
|
|
||||||
"name": "formationId",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"nom": {
|
|
||||||
"name": "nom",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"lieu": {
|
|
||||||
"name": "lieu",
|
|
||||||
"type": "varchar(500)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"publicCible": {
|
|
||||||
"name": "publicCible",
|
|
||||||
"type": "enum('directeur','chef_service','autre')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"capaciteMax": {
|
|
||||||
"name": "capaciteMax",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": 12
|
|
||||||
},
|
|
||||||
"dateBlocage": {
|
|
||||||
"name": "dateBlocage",
|
|
||||||
"type": "datetime",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"statut": {
|
|
||||||
"name": "statut",
|
|
||||||
"type": "enum('ouverte','bloquee','terminee')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'ouverte'"
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "(now())"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"onUpdate": true,
|
|
||||||
"default": "(now())"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {
|
|
||||||
"sequences_id": {
|
|
||||||
"name": "sequences_id",
|
|
||||||
"columns": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"checkConstraint": {}
|
|
||||||
},
|
|
||||||
"users": {
|
|
||||||
"name": "users",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "int",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "varchar(320)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"name": "password",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false
|
|
||||||
},
|
|
||||||
"emailVerified": {
|
|
||||||
"name": "emailVerified",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"name": "role",
|
|
||||||
"type": "enum('user','admin')",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "'user'"
|
|
||||||
},
|
|
||||||
"isActive": {
|
|
||||||
"name": "isActive",
|
|
||||||
"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())"
|
|
||||||
},
|
|
||||||
"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_email_unique": {
|
|
||||||
"name": "users_email_unique",
|
|
||||||
"columns": [
|
|
||||||
"email"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checkConstraint": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"views": {},
|
|
||||||
"_meta": {
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {},
|
|
||||||
"columns": {}
|
|
||||||
},
|
|
||||||
"internal": {
|
|
||||||
"tables": {},
|
|
||||||
"indexes": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "mysql",
|
"dialect": "mysql",
|
||||||
"entries": [
|
"entries": []
|
||||||
{
|
|
||||||
"idx": 0,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1762953852020,
|
|
||||||
"tag": "0000_charming_grey_gargoyle",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1762953926304,
|
|
||||||
"tag": "0001_smooth_warbird",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 2,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1762957612797,
|
|
||||||
"tag": "0002_fuzzy_sugar_man",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 3,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1762958719297,
|
|
||||||
"tag": "0003_steady_mystique",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 4,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1763047968048,
|
|
||||||
"tag": "0004_futuristic_malice",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 5,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1763050409917,
|
|
||||||
"tag": "0005_cuddly_bushwacker",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 6,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1763412717239,
|
|
||||||
"tag": "0006_lame_madrox",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 7,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1763414014386,
|
|
||||||
"tag": "0007_awesome_genesis",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 8,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1763416801277,
|
|
||||||
"tag": "0008_jittery_fenris",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 9,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1763448844826,
|
|
||||||
"tag": "0009_polite_senator_kelly",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 10,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1763544545719,
|
|
||||||
"tag": "0010_flat_clint_barton",
|
|
||||||
"breakpoints": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { boolean, datetime, int, mysqlEnum, mysqlTable, text, timestamp, varchar } from "drizzle-orm/mysql-core";
|
import { int, mysqlEnum, mysqlTable, text, timestamp, varchar } from "drizzle-orm/mysql-core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core user table backing auth flow.
|
* Core user table backing auth flow.
|
||||||
@@ -11,16 +11,12 @@ export const users = mysqlTable("users", {
|
|||||||
* Use this for relations between tables.
|
* Use this for relations between tables.
|
||||||
*/
|
*/
|
||||||
id: int("id").autoincrement().primaryKey(),
|
id: int("id").autoincrement().primaryKey(),
|
||||||
/** Email unique pour la connexion */
|
/** Manus OAuth identifier (openId) returned from the OAuth callback. Unique per user. */
|
||||||
email: varchar("email", { length: 320 }).notNull().unique(),
|
openId: varchar("openId", { length: 64 }).notNull().unique(),
|
||||||
/** Mot de passe hashé avec bcrypt */
|
|
||||||
password: varchar("password", { length: 255 }).notNull(),
|
|
||||||
name: text("name"),
|
name: text("name"),
|
||||||
/** Email vérifié ou non */
|
email: varchar("email", { length: 320 }),
|
||||||
emailVerified: boolean("emailVerified").default(false).notNull(),
|
loginMethod: varchar("loginMethod", { length: 64 }),
|
||||||
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
|
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
|
||||||
/** Statut du compte (actif/inactif) */
|
|
||||||
isActive: boolean("isActive").default(true).notNull(),
|
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||||
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(),
|
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(),
|
||||||
@@ -29,174 +25,4 @@ export const users = mysqlTable("users", {
|
|||||||
export type User = typeof users.$inferSelect;
|
export type User = typeof users.$inferSelect;
|
||||||
export type InsertUser = typeof users.$inferInsert;
|
export type InsertUser = typeof users.$inferInsert;
|
||||||
|
|
||||||
/**
|
// TODO: Add your tables here
|
||||||
* Table des formations (ex: Manager Itinova)
|
|
||||||
*/
|
|
||||||
export const formations = mysqlTable("formations", {
|
|
||||||
id: int("id").autoincrement().primaryKey(),
|
|
||||||
nom: varchar("nom", { length: 255 }).notNull(),
|
|
||||||
description: text("description"),
|
|
||||||
/** Lien unique pour l'inscription à cette formation */
|
|
||||||
lienUnique: varchar("lienUnique", { length: 100 }).notNull().unique(),
|
|
||||||
actif: boolean("actif").default(true).notNull(),
|
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
||||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Formation = typeof formations.$inferSelect;
|
|
||||||
export type InsertFormation = typeof formations.$inferInsert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table des apprenants
|
|
||||||
*/
|
|
||||||
export const apprenants = mysqlTable("apprenants", {
|
|
||||||
id: int("id").autoincrement().primaryKey(),
|
|
||||||
nom: varchar("nom", { length: 255 }).notNull(),
|
|
||||||
prenom: varchar("prenom", { length: 255 }).notNull(),
|
|
||||||
email: varchar("email", { length: 320 }).notNull().unique(),
|
|
||||||
codeEtablissement: varchar("codeEtablissement", { length: 50 }).notNull(),
|
|
||||||
fonction: mysqlEnum("fonction", ["directeur", "chef_service", "autre"]).notNull(),
|
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
||||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Apprenant = typeof apprenants.$inferSelect;
|
|
||||||
export type InsertApprenant = typeof apprenants.$inferInsert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table des séquences de formation
|
|
||||||
* Une séquence peut contenir jusqu'à 4 dates de formation
|
|
||||||
*/
|
|
||||||
export const sequences = mysqlTable("sequences", {
|
|
||||||
id: int("id").autoincrement().primaryKey(),
|
|
||||||
formationId: int("formationId").notNull(),
|
|
||||||
nom: varchar("nom", { length: 255 }).notNull(),
|
|
||||||
/** Lieu de la formation */
|
|
||||||
lieu: varchar("lieu", { length: 500 }).notNull(),
|
|
||||||
/** Public cible de la séquence (Directeur, Chef de service, Autre) */
|
|
||||||
publicCible: mysqlEnum("publicCible", ["directeur", "chef_service", "autre"]).notNull(),
|
|
||||||
/** Capacité maximale (12 par défaut) */
|
|
||||||
capaciteMax: int("capaciteMax").default(12).notNull(),
|
|
||||||
/** Date de blocage des inscriptions (J-15 avant la première date) */
|
|
||||||
dateBlocage: datetime("dateBlocage").notNull(),
|
|
||||||
/** Statut de la séquence */
|
|
||||||
statut: mysqlEnum("statut", ["ouverte", "bloquee", "terminee"]).default("ouverte").notNull(),
|
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
||||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Sequence = typeof sequences.$inferSelect;
|
|
||||||
export type InsertSequence = typeof sequences.$inferInsert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table des dates de formation pour chaque séquence
|
|
||||||
* Une séquence peut avoir jusqu'à 4 dates
|
|
||||||
*/
|
|
||||||
export const datesFormation = mysqlTable("datesFormation", {
|
|
||||||
id: int("id").autoincrement().primaryKey(),
|
|
||||||
sequenceId: int("sequenceId").notNull(),
|
|
||||||
/** Date de début de cette journée de formation */
|
|
||||||
dateDebut: datetime("dateDebut").notNull(),
|
|
||||||
/** Date de fin de cette journée de formation */
|
|
||||||
dateFin: datetime("dateFin").notNull(),
|
|
||||||
/** Ordre de la date dans la séquence (1 à 4) */
|
|
||||||
ordre: int("ordre").notNull(),
|
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type DateFormation = typeof datesFormation.$inferSelect;
|
|
||||||
export type InsertDateFormation = typeof datesFormation.$inferInsert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table des inscriptions
|
|
||||||
* L'inscription à une séquence inscrit automatiquement à toutes ses dates
|
|
||||||
*/
|
|
||||||
export const inscriptions = mysqlTable("inscriptions", {
|
|
||||||
id: int("id").autoincrement().primaryKey(),
|
|
||||||
apprenantId: int("apprenantId").notNull(),
|
|
||||||
sequenceId: int("sequenceId").notNull(),
|
|
||||||
/** Statut de l'inscription */
|
|
||||||
statut: mysqlEnum("statut", ["confirmee", "liste_attente", "annulee"]).notNull(),
|
|
||||||
dateInscription: timestamp("dateInscription").defaultNow().notNull(),
|
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
||||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Inscription = typeof inscriptions.$inferSelect;
|
|
||||||
export type InsertInscription = typeof inscriptions.$inferInsert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table des tokens de réinitialisation de mot de passe
|
|
||||||
* Stocke les tokens temporaires générés pour la réinitialisation
|
|
||||||
*/
|
|
||||||
export const passwordResetTokens = mysqlTable("passwordResetTokens", {
|
|
||||||
id: int("id").autoincrement().primaryKey(),
|
|
||||||
userId: int("userId").notNull(),
|
|
||||||
/** Token unique généré pour la réinitialisation */
|
|
||||||
token: varchar("token", { length: 255 }).notNull().unique(),
|
|
||||||
/** Date d'expiration du token (24h après génération) */
|
|
||||||
expiresAt: timestamp("expiresAt").notNull(),
|
|
||||||
/** Indique si le token a été utilisé */
|
|
||||||
used: boolean("used").default(false).notNull(),
|
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type PasswordResetToken = typeof passwordResetTokens.$inferSelect;
|
|
||||||
export type InsertPasswordResetToken = typeof passwordResetTokens.$inferInsert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table des templates d'emails personnalisables
|
|
||||||
*/
|
|
||||||
export const emailTemplates = mysqlTable("emailTemplates", {
|
|
||||||
id: int("id").autoincrement().primaryKey(),
|
|
||||||
/** Type de template (inscription, teaser, rappel, reset_password) */
|
|
||||||
type: varchar("type", { length: 50 }).notNull().unique(),
|
|
||||||
/** Nom affiché du template */
|
|
||||||
name: varchar("name", { length: 255 }).notNull(),
|
|
||||||
/** URL du logo (peut être vide pour utiliser le logo par défaut) */
|
|
||||||
logoUrl: text("logoUrl"),
|
|
||||||
/** Couleur principale (hex) */
|
|
||||||
primaryColor: varchar("primaryColor", { length: 7 }).default("#2563eb").notNull(),
|
|
||||||
/** Couleur de fond de l'en-tête (hex) */
|
|
||||||
headerBgColor: varchar("headerBgColor", { length: 7 }).default("#2563eb").notNull(),
|
|
||||||
/** Couleur du texte de l'en-tête (hex) */
|
|
||||||
headerTextColor: varchar("headerTextColor", { length: 7 }).default("#ffffff").notNull(),
|
|
||||||
/** Titre de l'en-tête */
|
|
||||||
headerTitle: varchar("headerTitle", { length: 255 }).default("Formation Manager Itinova").notNull(),
|
|
||||||
/** Texte du pied de page */
|
|
||||||
footerText: text("footerText"),
|
|
||||||
/** Actif ou non */
|
|
||||||
active: boolean("active").default(true).notNull(),
|
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
||||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type EmailTemplate = typeof emailTemplates.$inferSelect;
|
|
||||||
export type InsertEmailTemplate = typeof emailTemplates.$inferInsert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table de configuration SMTP/Email
|
|
||||||
* Stocke la configuration pour l'envoi d'emails (Resend, etc.)
|
|
||||||
*/
|
|
||||||
export const emailConfig = mysqlTable("emailConfig", {
|
|
||||||
id: int("id").autoincrement().primaryKey(),
|
|
||||||
/** Service d'envoi (resend, smtp) */
|
|
||||||
provider: varchar("provider", { length: 50 }).default("resend").notNull(),
|
|
||||||
/** Clé API Resend (chiffrée) */
|
|
||||||
apiKey: text("apiKey"),
|
|
||||||
/** Email expéditeur */
|
|
||||||
fromEmail: varchar("fromEmail", { length: 320 }).notNull(),
|
|
||||||
/** Nom de l'expéditeur */
|
|
||||||
fromName: varchar("fromName", { length: 255 }).default("Formation Manager Itinova").notNull(),
|
|
||||||
/** Mode: simulation ou production */
|
|
||||||
mode: mysqlEnum("mode", ["simulation", "production"]).default("simulation").notNull(),
|
|
||||||
/** Domaine vérifié (pour Resend) */
|
|
||||||
domainVerified: boolean("domainVerified").default(false).notNull(),
|
|
||||||
/** Actif ou non */
|
|
||||||
active: boolean("active").default(true).notNull(),
|
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
||||||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type EmailConfig = typeof emailConfig.$inferSelect;
|
|
||||||
export type InsertEmailConfig = typeof emailConfig.$inferInsert;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import bcrypt from 'bcrypt';
|
|
||||||
|
|
||||||
const password = 'admin123';
|
|
||||||
|
|
||||||
bcrypt.hash(password, 10).then(hash => {
|
|
||||||
console.log('Password:', password);
|
|
||||||
console.log('Hash:', hash);
|
|
||||||
console.log('\nSQL to update:');
|
|
||||||
console.log(`UPDATE users SET password = '${hash}' WHERE email = 'o.pareige@itinova.org';`);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('Error:', err);
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Script pour remplacer "session" par "sequence" dans les fichiers frontend
|
|
||||||
|
|
||||||
cd /home/ubuntu/formation-manager-itinova
|
|
||||||
|
|
||||||
# Remplacements dans les fichiers TypeScript/TSX
|
|
||||||
find client/src -type f \( -name "*.ts" -o -name "*.tsx" \) -exec sed -i \
|
|
||||||
-e 's/\bsessions\b/sequences/g' \
|
|
||||||
-e 's/\bsession\b/sequence/g' \
|
|
||||||
-e 's/\bSession\b/Sequence/g' \
|
|
||||||
-e 's/\bSESSION\b/SEQUENCE/g' \
|
|
||||||
-e 's/sessionId/sequenceId/g' \
|
|
||||||
-e 's/SessionId/SequenceId/g' \
|
|
||||||
-e 's/sessionNom/sequenceNom/g' \
|
|
||||||
-e 's/SessionNom/SequenceNom/g' \
|
|
||||||
-e 's/AdminSequences/AdminSequences/g' \
|
|
||||||
-e 's/AdminSequenceInscrits/AdminSequenceInscrits/g' \
|
|
||||||
{} \;
|
|
||||||
|
|
||||||
echo "Migration terminée !"
|
|
||||||
11
package.json
11
package.json
@@ -46,12 +46,10 @@
|
|||||||
"@trpc/react-query": "^11.6.0",
|
"@trpc/react-query": "^11.6.0",
|
||||||
"@trpc/server": "^11.6.0",
|
"@trpc/server": "^11.6.0",
|
||||||
"axios": "^1.12.0",
|
"axios": "^1.12.0",
|
||||||
"bcrypt": "^6.0.0",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.7",
|
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^17.2.2",
|
"dotenv": "^17.2.2",
|
||||||
"drizzle-orm": "^0.44.5",
|
"drizzle-orm": "^0.44.5",
|
||||||
@@ -60,9 +58,6 @@
|
|||||||
"framer-motion": "^12.23.22",
|
"framer-motion": "^12.23.22",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"jose": "6.1.0",
|
"jose": "6.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"jspdf": "^3.0.3",
|
|
||||||
"jspdf-autotable": "^5.0.2",
|
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"mysql2": "^3.15.0",
|
"mysql2": "^3.15.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
@@ -73,7 +68,7 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-hook-form": "^7.64.0",
|
"react-hook-form": "^7.64.0",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"recharts": "^2.15.4",
|
"recharts": "^2.15.2",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"streamdown": "^1.4.0",
|
"streamdown": "^1.4.0",
|
||||||
"superjson": "^1.13.3",
|
"superjson": "^1.13.3",
|
||||||
@@ -81,18 +76,14 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"wouter": "^3.3.5",
|
"wouter": "^3.3.5",
|
||||||
"xlsx": "^0.18.5",
|
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@builder.io/vite-plugin-jsx-loc": "^0.1.1",
|
"@builder.io/vite-plugin-jsx-loc": "^0.1.1",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
"@tailwindcss/vite": "^4.1.3",
|
"@tailwindcss/vite": "^4.1.3",
|
||||||
"@types/bcrypt": "^6.0.0",
|
|
||||||
"@types/cookie-parser": "^1.4.10",
|
|
||||||
"@types/express": "4.17.21",
|
"@types/express": "4.17.21",
|
||||||
"@types/google.maps": "^3.58.1",
|
"@types/google.maps": "^3.58.1",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
|
||||||
"@types/node": "^24.7.0",
|
"@types/node": "^24.7.0",
|
||||||
"@types/react": "^19.1.16",
|
"@types/react": "^19.1.16",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.1.9",
|
||||||
|
|||||||
408
pnpm-lock.yaml
generated
408
pnpm-lock.yaml
generated
@@ -115,9 +115,6 @@ importers:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^1.12.0
|
specifier: ^1.12.0
|
||||||
version: 1.12.2
|
version: 1.12.2
|
||||||
bcrypt:
|
|
||||||
specifier: ^6.0.0
|
|
||||||
version: 6.0.0
|
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -130,9 +127,6 @@ importers:
|
|||||||
cookie:
|
cookie:
|
||||||
specifier: ^1.0.2
|
specifier: ^1.0.2
|
||||||
version: 1.0.2
|
version: 1.0.2
|
||||||
cookie-parser:
|
|
||||||
specifier: ^1.4.7
|
|
||||||
version: 1.4.7
|
|
||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
@@ -157,15 +151,6 @@ importers:
|
|||||||
jose:
|
jose:
|
||||||
specifier: 6.1.0
|
specifier: 6.1.0
|
||||||
version: 6.1.0
|
version: 6.1.0
|
||||||
jsonwebtoken:
|
|
||||||
specifier: ^9.0.2
|
|
||||||
version: 9.0.2
|
|
||||||
jspdf:
|
|
||||||
specifier: ^3.0.3
|
|
||||||
version: 3.0.3
|
|
||||||
jspdf-autotable:
|
|
||||||
specifier: ^5.0.2
|
|
||||||
version: 5.0.2(jspdf@3.0.3)
|
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.453.0
|
specifier: ^0.453.0
|
||||||
version: 0.453.0(react@19.2.0)
|
version: 0.453.0(react@19.2.0)
|
||||||
@@ -197,7 +182,7 @@ importers:
|
|||||||
specifier: ^3.0.6
|
specifier: ^3.0.6
|
||||||
version: 3.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 3.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
recharts:
|
recharts:
|
||||||
specifier: ^2.15.4
|
specifier: ^2.15.2
|
||||||
version: 2.15.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 2.15.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^2.0.7
|
specifier: ^2.0.7
|
||||||
@@ -220,9 +205,6 @@ importers:
|
|||||||
wouter:
|
wouter:
|
||||||
specifier: ^3.3.5
|
specifier: ^3.3.5
|
||||||
version: 3.7.1(patch_hash=4e16e6ff3fde7d6c1024d3e0c8605dc9eb6afb690d0d49958c2f449091813072)(react@19.2.0)
|
version: 3.7.1(patch_hash=4e16e6ff3fde7d6c1024d3e0c8605dc9eb6afb690d0d49958c2f449091813072)(react@19.2.0)
|
||||||
xlsx:
|
|
||||||
specifier: ^0.18.5
|
|
||||||
version: 0.18.5
|
|
||||||
zod:
|
zod:
|
||||||
specifier: ^4.1.12
|
specifier: ^4.1.12
|
||||||
version: 4.1.12
|
version: 4.1.12
|
||||||
@@ -236,21 +218,12 @@ importers:
|
|||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.1.3
|
specifier: ^4.1.3
|
||||||
version: 4.1.14(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6))
|
version: 4.1.14(vite@7.1.9(@types/node@24.7.0)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6))
|
||||||
'@types/bcrypt':
|
|
||||||
specifier: ^6.0.0
|
|
||||||
version: 6.0.0
|
|
||||||
'@types/cookie-parser':
|
|
||||||
specifier: ^1.4.10
|
|
||||||
version: 1.4.10(@types/express@4.17.21)
|
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: 4.17.21
|
specifier: 4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
'@types/google.maps':
|
'@types/google.maps':
|
||||||
specifier: ^3.58.1
|
specifier: ^3.58.1
|
||||||
version: 3.58.1
|
version: 3.58.1
|
||||||
'@types/jsonwebtoken':
|
|
||||||
specifier: ^9.0.10
|
|
||||||
version: 9.0.10
|
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.7.0
|
specifier: ^24.7.0
|
||||||
version: 24.7.0
|
version: 24.7.0
|
||||||
@@ -2175,20 +2148,12 @@ packages:
|
|||||||
'@types/babel__traverse@7.28.0':
|
'@types/babel__traverse@7.28.0':
|
||||||
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
|
||||||
|
|
||||||
'@types/bcrypt@6.0.0':
|
|
||||||
resolution: {integrity: sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==}
|
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||||
|
|
||||||
'@types/connect@3.4.38':
|
'@types/connect@3.4.38':
|
||||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||||
|
|
||||||
'@types/cookie-parser@1.4.10':
|
|
||||||
resolution: {integrity: sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/express': '*'
|
|
||||||
|
|
||||||
'@types/d3-array@3.2.2':
|
'@types/d3-array@3.2.2':
|
||||||
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
|
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
|
||||||
|
|
||||||
@@ -2309,9 +2274,6 @@ packages:
|
|||||||
'@types/http-errors@2.0.5':
|
'@types/http-errors@2.0.5':
|
||||||
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
||||||
|
|
||||||
'@types/jsonwebtoken@9.0.10':
|
|
||||||
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
|
||||||
|
|
||||||
'@types/katex@0.16.7':
|
'@types/katex@0.16.7':
|
||||||
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
||||||
|
|
||||||
@@ -2333,15 +2295,9 @@ packages:
|
|||||||
'@types/node@24.7.0':
|
'@types/node@24.7.0':
|
||||||
resolution: {integrity: sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==}
|
resolution: {integrity: sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==}
|
||||||
|
|
||||||
'@types/pako@2.0.4':
|
|
||||||
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
|
|
||||||
|
|
||||||
'@types/qs@6.14.0':
|
'@types/qs@6.14.0':
|
||||||
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
||||||
|
|
||||||
'@types/raf@3.4.3':
|
|
||||||
resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==}
|
|
||||||
|
|
||||||
'@types/range-parser@1.2.7':
|
'@types/range-parser@1.2.7':
|
||||||
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
||||||
|
|
||||||
@@ -2425,10 +2381,6 @@ packages:
|
|||||||
add@2.0.6:
|
add@2.0.6:
|
||||||
resolution: {integrity: sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==}
|
resolution: {integrity: sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==}
|
||||||
|
|
||||||
adler-32@1.3.1:
|
|
||||||
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
|
|
||||||
engines: {node: '>=0.8'}
|
|
||||||
|
|
||||||
agentkeepalive@4.6.0:
|
agentkeepalive@4.6.0:
|
||||||
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
||||||
engines: {node: '>= 8.0.0'}
|
engines: {node: '>= 8.0.0'}
|
||||||
@@ -2464,18 +2416,10 @@ packages:
|
|||||||
bail@2.0.2:
|
bail@2.0.2:
|
||||||
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
||||||
|
|
||||||
base64-arraybuffer@1.0.2:
|
|
||||||
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
|
||||||
engines: {node: '>= 0.6.0'}
|
|
||||||
|
|
||||||
baseline-browser-mapping@2.8.12:
|
baseline-browser-mapping@2.8.12:
|
||||||
resolution: {integrity: sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==}
|
resolution: {integrity: sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
bcrypt@6.0.0:
|
|
||||||
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
|
|
||||||
engines: {node: '>= 18'}
|
|
||||||
|
|
||||||
body-parser@1.20.3:
|
body-parser@1.20.3:
|
||||||
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
|
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
|
||||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
@@ -2488,9 +2432,6 @@ packages:
|
|||||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1:
|
|
||||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
|
||||||
|
|
||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
@@ -2513,17 +2454,9 @@ packages:
|
|||||||
caniuse-lite@1.0.30001748:
|
caniuse-lite@1.0.30001748:
|
||||||
resolution: {integrity: sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==}
|
resolution: {integrity: sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==}
|
||||||
|
|
||||||
canvg@3.0.11:
|
|
||||||
resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==}
|
|
||||||
engines: {node: '>=10.0.0'}
|
|
||||||
|
|
||||||
ccount@2.0.1:
|
ccount@2.0.1:
|
||||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||||
|
|
||||||
cfb@1.2.2:
|
|
||||||
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
|
|
||||||
engines: {node: '>=0.8'}
|
|
||||||
|
|
||||||
chai@5.3.3:
|
chai@5.3.3:
|
||||||
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
|
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2569,10 +2502,6 @@ packages:
|
|||||||
react: ^18 || ^19 || ^19.0.0-rc
|
react: ^18 || ^19 || ^19.0.0-rc
|
||||||
react-dom: ^18 || ^19 || ^19.0.0-rc
|
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
codepage@1.15.0:
|
|
||||||
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
|
|
||||||
engines: {node: '>=0.8'}
|
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
combined-stream@1.0.8:
|
||||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -2605,10 +2534,6 @@ packages:
|
|||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
cookie-parser@1.4.7:
|
|
||||||
resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==}
|
|
||||||
engines: {node: '>= 0.8.0'}
|
|
||||||
|
|
||||||
cookie-signature@1.0.6:
|
cookie-signature@1.0.6:
|
||||||
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
|
||||||
|
|
||||||
@@ -2616,10 +2541,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
|
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
cookie@0.7.2:
|
|
||||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
|
|
||||||
cookie@1.0.2:
|
cookie@1.0.2:
|
||||||
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2628,23 +2549,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
|
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
|
||||||
engines: {node: '>=12.13'}
|
engines: {node: '>=12.13'}
|
||||||
|
|
||||||
core-js@3.46.0:
|
|
||||||
resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==}
|
|
||||||
|
|
||||||
cose-base@1.0.3:
|
cose-base@1.0.3:
|
||||||
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
|
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
|
||||||
|
|
||||||
cose-base@2.2.0:
|
cose-base@2.2.0:
|
||||||
resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==}
|
resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==}
|
||||||
|
|
||||||
crc-32@1.2.2:
|
|
||||||
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
|
|
||||||
engines: {node: '>=0.8'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
css-line-break@2.1.0:
|
|
||||||
resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==}
|
|
||||||
|
|
||||||
cssesc@3.0.0:
|
cssesc@3.0.0:
|
||||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -2988,9 +2898,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
ecdsa-sig-formatter@1.0.11:
|
|
||||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
@@ -3114,9 +3021,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==}
|
resolution: {integrity: sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
fast-png@6.4.0:
|
|
||||||
resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==}
|
|
||||||
|
|
||||||
fast-xml-parser@5.2.5:
|
fast-xml-parser@5.2.5:
|
||||||
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
|
resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -3130,9 +3034,6 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
fflate@0.8.2:
|
|
||||||
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
|
||||||
|
|
||||||
finalhandler@1.3.1:
|
finalhandler@1.3.1:
|
||||||
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
|
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -3161,10 +3062,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
frac@1.1.2:
|
|
||||||
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
|
|
||||||
engines: {node: '>=0.8'}
|
|
||||||
|
|
||||||
fraction.js@4.3.7:
|
fraction.js@4.3.7:
|
||||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||||
|
|
||||||
@@ -3287,10 +3184,6 @@ packages:
|
|||||||
html-void-elements@3.0.0:
|
html-void-elements@3.0.0:
|
||||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||||
|
|
||||||
html2canvas@1.4.1:
|
|
||||||
resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==}
|
|
||||||
engines: {node: '>=8.0.0'}
|
|
||||||
|
|
||||||
http-errors@2.0.0:
|
http-errors@2.0.0:
|
||||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -3329,9 +3222,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
iobuffer@5.4.0:
|
|
||||||
resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==}
|
|
||||||
|
|
||||||
ipaddr.js@1.9.1:
|
ipaddr.js@1.9.1:
|
||||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@@ -3379,24 +3269,6 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
jsonwebtoken@9.0.2:
|
|
||||||
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
|
||||||
engines: {node: '>=12', npm: '>=6'}
|
|
||||||
|
|
||||||
jspdf-autotable@5.0.2:
|
|
||||||
resolution: {integrity: sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==}
|
|
||||||
peerDependencies:
|
|
||||||
jspdf: ^2 || ^3
|
|
||||||
|
|
||||||
jspdf@3.0.3:
|
|
||||||
resolution: {integrity: sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==}
|
|
||||||
|
|
||||||
jwa@1.4.2:
|
|
||||||
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
|
|
||||||
|
|
||||||
jws@3.2.2:
|
|
||||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
|
||||||
|
|
||||||
katex@0.16.25:
|
katex@0.16.25:
|
||||||
resolution: {integrity: sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==}
|
resolution: {integrity: sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -3488,27 +3360,6 @@ packages:
|
|||||||
lodash-es@4.17.21:
|
lodash-es@4.17.21:
|
||||||
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||||
|
|
||||||
lodash.includes@4.3.0:
|
|
||||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
|
||||||
|
|
||||||
lodash.isboolean@3.0.3:
|
|
||||||
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
|
||||||
|
|
||||||
lodash.isinteger@4.0.4:
|
|
||||||
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
|
||||||
|
|
||||||
lodash.isnumber@3.0.3:
|
|
||||||
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
|
||||||
|
|
||||||
lodash.isplainobject@4.0.6:
|
|
||||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
|
||||||
|
|
||||||
lodash.isstring@4.0.1:
|
|
||||||
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
|
||||||
|
|
||||||
lodash.once@4.1.1:
|
|
||||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
|
||||||
|
|
||||||
lodash@4.17.21:
|
lodash@4.17.21:
|
||||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
|
||||||
@@ -3780,10 +3631,6 @@ packages:
|
|||||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
node-addon-api@8.5.0:
|
|
||||||
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
|
||||||
engines: {node: ^18 || ^20 || >= 21}
|
|
||||||
|
|
||||||
node-domexception@1.0.0:
|
node-domexception@1.0.0:
|
||||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
engines: {node: '>=10.5.0'}
|
engines: {node: '>=10.5.0'}
|
||||||
@@ -3798,10 +3645,6 @@ packages:
|
|||||||
encoding:
|
encoding:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
node-gyp-build@4.8.4:
|
|
||||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
node-releases@2.0.23:
|
node-releases@2.0.23:
|
||||||
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
|
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
|
||||||
|
|
||||||
@@ -3842,9 +3685,6 @@ packages:
|
|||||||
package-manager-detector@1.5.0:
|
package-manager-detector@1.5.0:
|
||||||
resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==}
|
resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==}
|
||||||
|
|
||||||
pako@2.1.0:
|
|
||||||
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
|
||||||
|
|
||||||
parse-entities@4.0.2:
|
parse-entities@4.0.2:
|
||||||
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
||||||
|
|
||||||
@@ -3871,9 +3711,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
||||||
engines: {node: '>= 14.16'}
|
engines: {node: '>= 14.16'}
|
||||||
|
|
||||||
performance-now@2.1.0:
|
|
||||||
resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==}
|
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -3937,9 +3774,6 @@ packages:
|
|||||||
quansync@0.2.11:
|
quansync@0.2.11:
|
||||||
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
||||||
|
|
||||||
raf@3.4.1:
|
|
||||||
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
|
|
||||||
|
|
||||||
range-parser@1.2.1:
|
range-parser@1.2.1:
|
||||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -4043,9 +3877,6 @@ packages:
|
|||||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
regenerator-runtime@0.13.11:
|
|
||||||
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
|
||||||
|
|
||||||
regex-recursion@6.0.2:
|
regex-recursion@6.0.2:
|
||||||
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
|
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
|
||||||
|
|
||||||
@@ -4086,10 +3917,6 @@ packages:
|
|||||||
resolve-pkg-maps@1.0.0:
|
resolve-pkg-maps@1.0.0:
|
||||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
rgbcolor@1.0.1:
|
|
||||||
resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==}
|
|
||||||
engines: {node: '>= 0.8.15'}
|
|
||||||
|
|
||||||
robust-predicates@3.0.2:
|
robust-predicates@3.0.2:
|
||||||
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
|
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
|
||||||
|
|
||||||
@@ -4117,11 +3944,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
semver@7.7.3:
|
|
||||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
send@0.19.0:
|
send@0.19.0:
|
||||||
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
|
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -4182,17 +4004,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
|
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
ssf@0.11.2:
|
|
||||||
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
|
|
||||||
engines: {node: '>=0.8'}
|
|
||||||
|
|
||||||
stackback@0.0.2:
|
stackback@0.0.2:
|
||||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
|
|
||||||
stackblur-canvas@2.7.0:
|
|
||||||
resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==}
|
|
||||||
engines: {node: '>=0.1.14'}
|
|
||||||
|
|
||||||
statuses@2.0.1:
|
statuses@2.0.1:
|
||||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -4224,10 +4038,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==}
|
resolution: {integrity: sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
svg-pathdata@6.0.3:
|
|
||||||
resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
tailwind-merge@3.3.1:
|
tailwind-merge@3.3.1:
|
||||||
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
|
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
|
||||||
|
|
||||||
@@ -4247,9 +4057,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==}
|
resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
text-segmentation@1.0.3:
|
|
||||||
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
|
|
||||||
|
|
||||||
tiny-invariant@1.3.3:
|
tiny-invariant@1.3.3:
|
||||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||||
|
|
||||||
@@ -4390,9 +4197,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
|
|
||||||
utrie@1.0.2:
|
|
||||||
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
|
|
||||||
|
|
||||||
uuid@11.1.0:
|
uuid@11.1.0:
|
||||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -4561,24 +4365,11 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
wmf@1.0.2:
|
|
||||||
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
|
|
||||||
engines: {node: '>=0.8'}
|
|
||||||
|
|
||||||
word@0.3.0:
|
|
||||||
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
|
|
||||||
engines: {node: '>=0.8'}
|
|
||||||
|
|
||||||
wouter@3.7.1:
|
wouter@3.7.1:
|
||||||
resolution: {integrity: sha512-od5LGmndSUzntZkE2R5CHhoiJ7YMuTIbiXsa0Anytc2RATekgv4sfWRAxLEULBrp7ADzinWQw8g470lkT8+fOw==}
|
resolution: {integrity: sha512-od5LGmndSUzntZkE2R5CHhoiJ7YMuTIbiXsa0Anytc2RATekgv4sfWRAxLEULBrp7ADzinWQw8g470lkT8+fOw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=16.8.0'
|
react: '>=16.8.0'
|
||||||
|
|
||||||
xlsx@0.18.5:
|
|
||||||
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
|
|
||||||
engines: {node: '>=0.8'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
@@ -6732,10 +6523,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.28.4
|
'@babel/types': 7.28.4
|
||||||
|
|
||||||
'@types/bcrypt@6.0.0':
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 24.7.0
|
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
@@ -6745,10 +6532,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 24.7.0
|
'@types/node': 24.7.0
|
||||||
|
|
||||||
'@types/cookie-parser@1.4.10(@types/express@4.17.21)':
|
|
||||||
dependencies:
|
|
||||||
'@types/express': 4.17.21
|
|
||||||
|
|
||||||
'@types/d3-array@3.2.2': {}
|
'@types/d3-array@3.2.2': {}
|
||||||
|
|
||||||
'@types/d3-axis@3.0.6':
|
'@types/d3-axis@3.0.6':
|
||||||
@@ -6900,11 +6683,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/http-errors@2.0.5': {}
|
'@types/http-errors@2.0.5': {}
|
||||||
|
|
||||||
'@types/jsonwebtoken@9.0.10':
|
|
||||||
dependencies:
|
|
||||||
'@types/ms': 2.1.0
|
|
||||||
'@types/node': 24.7.0
|
|
||||||
|
|
||||||
'@types/katex@0.16.7': {}
|
'@types/katex@0.16.7': {}
|
||||||
|
|
||||||
'@types/mdast@4.0.4':
|
'@types/mdast@4.0.4':
|
||||||
@@ -6928,13 +6706,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.14.0
|
undici-types: 7.14.0
|
||||||
|
|
||||||
'@types/pako@2.0.4': {}
|
|
||||||
|
|
||||||
'@types/qs@6.14.0': {}
|
'@types/qs@6.14.0': {}
|
||||||
|
|
||||||
'@types/raf@3.4.3':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@types/range-parser@1.2.7': {}
|
'@types/range-parser@1.2.7': {}
|
||||||
|
|
||||||
'@types/react-dom@19.2.1(@types/react@19.2.2)':
|
'@types/react-dom@19.2.1(@types/react@19.2.2)':
|
||||||
@@ -7034,8 +6807,6 @@ snapshots:
|
|||||||
|
|
||||||
add@2.0.6: {}
|
add@2.0.6: {}
|
||||||
|
|
||||||
adler-32@1.3.1: {}
|
|
||||||
|
|
||||||
agentkeepalive@4.6.0:
|
agentkeepalive@4.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
humanize-ms: 1.2.1
|
humanize-ms: 1.2.1
|
||||||
@@ -7072,16 +6843,8 @@ snapshots:
|
|||||||
|
|
||||||
bail@2.0.2: {}
|
bail@2.0.2: {}
|
||||||
|
|
||||||
base64-arraybuffer@1.0.2:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
baseline-browser-mapping@2.8.12: {}
|
baseline-browser-mapping@2.8.12: {}
|
||||||
|
|
||||||
bcrypt@6.0.0:
|
|
||||||
dependencies:
|
|
||||||
node-addon-api: 8.5.0
|
|
||||||
node-gyp-build: 4.8.4
|
|
||||||
|
|
||||||
body-parser@1.20.3:
|
body-parser@1.20.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
@@ -7109,8 +6872,6 @@ snapshots:
|
|||||||
node-releases: 2.0.23
|
node-releases: 2.0.23
|
||||||
update-browserslist-db: 1.1.3(browserslist@4.26.3)
|
update-browserslist-db: 1.1.3(browserslist@4.26.3)
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1: {}
|
|
||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
@@ -7129,25 +6890,8 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001748: {}
|
caniuse-lite@1.0.30001748: {}
|
||||||
|
|
||||||
canvg@3.0.11:
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.28.4
|
|
||||||
'@types/raf': 3.4.3
|
|
||||||
core-js: 3.46.0
|
|
||||||
raf: 3.4.1
|
|
||||||
regenerator-runtime: 0.13.11
|
|
||||||
rgbcolor: 1.0.1
|
|
||||||
stackblur-canvas: 2.7.0
|
|
||||||
svg-pathdata: 6.0.3
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
ccount@2.0.1: {}
|
ccount@2.0.1: {}
|
||||||
|
|
||||||
cfb@1.2.2:
|
|
||||||
dependencies:
|
|
||||||
adler-32: 1.3.1
|
|
||||||
crc-32: 1.2.2
|
|
||||||
|
|
||||||
chai@5.3.3:
|
chai@5.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
assertion-error: 2.0.1
|
assertion-error: 2.0.1
|
||||||
@@ -7200,8 +6944,6 @@ snapshots:
|
|||||||
- '@types/react'
|
- '@types/react'
|
||||||
- '@types/react-dom'
|
- '@types/react-dom'
|
||||||
|
|
||||||
codepage@1.15.0: {}
|
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
combined-stream@1.0.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
@@ -7224,26 +6966,16 @@ snapshots:
|
|||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
cookie-parser@1.4.7:
|
|
||||||
dependencies:
|
|
||||||
cookie: 0.7.2
|
|
||||||
cookie-signature: 1.0.6
|
|
||||||
|
|
||||||
cookie-signature@1.0.6: {}
|
cookie-signature@1.0.6: {}
|
||||||
|
|
||||||
cookie@0.7.1: {}
|
cookie@0.7.1: {}
|
||||||
|
|
||||||
cookie@0.7.2: {}
|
|
||||||
|
|
||||||
cookie@1.0.2: {}
|
cookie@1.0.2: {}
|
||||||
|
|
||||||
copy-anything@3.0.5:
|
copy-anything@3.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-what: 4.1.16
|
is-what: 4.1.16
|
||||||
|
|
||||||
core-js@3.46.0:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
cose-base@1.0.3:
|
cose-base@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
layout-base: 1.0.2
|
layout-base: 1.0.2
|
||||||
@@ -7252,13 +6984,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
layout-base: 2.0.1
|
layout-base: 2.0.1
|
||||||
|
|
||||||
crc-32@1.2.2: {}
|
|
||||||
|
|
||||||
css-line-break@2.1.0:
|
|
||||||
dependencies:
|
|
||||||
utrie: 1.0.2
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
cssesc@3.0.0: {}
|
cssesc@3.0.0: {}
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.1.3: {}
|
||||||
@@ -7521,10 +7246,6 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
|
|
||||||
ecdsa-sig-formatter@1.0.11:
|
|
||||||
dependencies:
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.230: {}
|
electron-to-chromium@1.5.230: {}
|
||||||
@@ -7720,12 +7441,6 @@ snapshots:
|
|||||||
|
|
||||||
fast-equals@5.3.2: {}
|
fast-equals@5.3.2: {}
|
||||||
|
|
||||||
fast-png@6.4.0:
|
|
||||||
dependencies:
|
|
||||||
'@types/pako': 2.0.4
|
|
||||||
iobuffer: 5.4.0
|
|
||||||
pako: 2.1.0
|
|
||||||
|
|
||||||
fast-xml-parser@5.2.5:
|
fast-xml-parser@5.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
strnum: 2.1.1
|
strnum: 2.1.1
|
||||||
@@ -7734,8 +7449,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
|
|
||||||
fflate@0.8.2: {}
|
|
||||||
|
|
||||||
finalhandler@1.3.1:
|
finalhandler@1.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 2.6.9
|
debug: 2.6.9
|
||||||
@@ -7767,8 +7480,6 @@ snapshots:
|
|||||||
|
|
||||||
forwarded@0.2.0: {}
|
forwarded@0.2.0: {}
|
||||||
|
|
||||||
frac@1.1.2: {}
|
|
||||||
|
|
||||||
fraction.js@4.3.7: {}
|
fraction.js@4.3.7: {}
|
||||||
|
|
||||||
framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
framer-motion@12.23.22(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||||
@@ -7959,12 +7670,6 @@ snapshots:
|
|||||||
|
|
||||||
html-void-elements@3.0.0: {}
|
html-void-elements@3.0.0: {}
|
||||||
|
|
||||||
html2canvas@1.4.1:
|
|
||||||
dependencies:
|
|
||||||
css-line-break: 2.1.0
|
|
||||||
text-segmentation: 1.0.3
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
http-errors@2.0.0:
|
http-errors@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
@@ -8002,8 +7707,6 @@ snapshots:
|
|||||||
|
|
||||||
internmap@2.0.3: {}
|
internmap@2.0.3: {}
|
||||||
|
|
||||||
iobuffer@5.4.0: {}
|
|
||||||
|
|
||||||
ipaddr.js@1.9.1: {}
|
ipaddr.js@1.9.1: {}
|
||||||
|
|
||||||
is-alphabetical@2.0.1: {}
|
is-alphabetical@2.0.1: {}
|
||||||
@@ -8033,45 +7736,6 @@ snapshots:
|
|||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
||||||
jsonwebtoken@9.0.2:
|
|
||||||
dependencies:
|
|
||||||
jws: 3.2.2
|
|
||||||
lodash.includes: 4.3.0
|
|
||||||
lodash.isboolean: 3.0.3
|
|
||||||
lodash.isinteger: 4.0.4
|
|
||||||
lodash.isnumber: 3.0.3
|
|
||||||
lodash.isplainobject: 4.0.6
|
|
||||||
lodash.isstring: 4.0.1
|
|
||||||
lodash.once: 4.1.1
|
|
||||||
ms: 2.1.3
|
|
||||||
semver: 7.7.3
|
|
||||||
|
|
||||||
jspdf-autotable@5.0.2(jspdf@3.0.3):
|
|
||||||
dependencies:
|
|
||||||
jspdf: 3.0.3
|
|
||||||
|
|
||||||
jspdf@3.0.3:
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.28.4
|
|
||||||
fast-png: 6.4.0
|
|
||||||
fflate: 0.8.2
|
|
||||||
optionalDependencies:
|
|
||||||
canvg: 3.0.11
|
|
||||||
core-js: 3.46.0
|
|
||||||
dompurify: 3.3.0
|
|
||||||
html2canvas: 1.4.1
|
|
||||||
|
|
||||||
jwa@1.4.2:
|
|
||||||
dependencies:
|
|
||||||
buffer-equal-constant-time: 1.0.1
|
|
||||||
ecdsa-sig-formatter: 1.0.11
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
jws@3.2.2:
|
|
||||||
dependencies:
|
|
||||||
jwa: 1.4.2
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
katex@0.16.25:
|
katex@0.16.25:
|
||||||
dependencies:
|
dependencies:
|
||||||
commander: 8.3.0
|
commander: 8.3.0
|
||||||
@@ -8145,20 +7809,6 @@ snapshots:
|
|||||||
|
|
||||||
lodash-es@4.17.21: {}
|
lodash-es@4.17.21: {}
|
||||||
|
|
||||||
lodash.includes@4.3.0: {}
|
|
||||||
|
|
||||||
lodash.isboolean@3.0.3: {}
|
|
||||||
|
|
||||||
lodash.isinteger@4.0.4: {}
|
|
||||||
|
|
||||||
lodash.isnumber@3.0.3: {}
|
|
||||||
|
|
||||||
lodash.isplainobject@4.0.6: {}
|
|
||||||
|
|
||||||
lodash.isstring@4.0.1: {}
|
|
||||||
|
|
||||||
lodash.once@4.1.1: {}
|
|
||||||
|
|
||||||
lodash@4.17.21: {}
|
lodash@4.17.21: {}
|
||||||
|
|
||||||
long@5.3.2: {}
|
long@5.3.2: {}
|
||||||
@@ -8656,16 +8306,12 @@ snapshots:
|
|||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
react-dom: 19.2.0(react@19.2.0)
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
|
||||||
node-addon-api@8.5.0: {}
|
|
||||||
|
|
||||||
node-domexception@1.0.0: {}
|
node-domexception@1.0.0: {}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
node-fetch@2.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
|
|
||||||
node-gyp-build@4.8.4: {}
|
|
||||||
|
|
||||||
node-releases@2.0.23: {}
|
node-releases@2.0.23: {}
|
||||||
|
|
||||||
normalize-range@0.1.2: {}
|
normalize-range@0.1.2: {}
|
||||||
@@ -8702,8 +8348,6 @@ snapshots:
|
|||||||
|
|
||||||
package-manager-detector@1.5.0: {}
|
package-manager-detector@1.5.0: {}
|
||||||
|
|
||||||
pako@2.1.0: {}
|
|
||||||
|
|
||||||
parse-entities@4.0.2:
|
parse-entities@4.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 2.0.11
|
'@types/unist': 2.0.11
|
||||||
@@ -8730,9 +8374,6 @@ snapshots:
|
|||||||
|
|
||||||
pathval@2.0.1: {}
|
pathval@2.0.1: {}
|
||||||
|
|
||||||
performance-now@2.1.0:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.3: {}
|
picomatch@4.0.3: {}
|
||||||
@@ -8796,11 +8437,6 @@ snapshots:
|
|||||||
|
|
||||||
quansync@0.2.11: {}
|
quansync@0.2.11: {}
|
||||||
|
|
||||||
raf@3.4.1:
|
|
||||||
dependencies:
|
|
||||||
performance-now: 2.1.0
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
range-parser@1.2.1: {}
|
range-parser@1.2.1: {}
|
||||||
|
|
||||||
raw-body@2.5.2:
|
raw-body@2.5.2:
|
||||||
@@ -8918,9 +8554,6 @@ snapshots:
|
|||||||
tiny-invariant: 1.3.3
|
tiny-invariant: 1.3.3
|
||||||
victory-vendor: 36.9.2
|
victory-vendor: 36.9.2
|
||||||
|
|
||||||
regenerator-runtime@0.13.11:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
regex-recursion@6.0.2:
|
regex-recursion@6.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
regex-utilities: 2.3.0
|
regex-utilities: 2.3.0
|
||||||
@@ -8996,9 +8629,6 @@ snapshots:
|
|||||||
|
|
||||||
resolve-pkg-maps@1.0.0: {}
|
resolve-pkg-maps@1.0.0: {}
|
||||||
|
|
||||||
rgbcolor@1.0.1:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
robust-predicates@3.0.2: {}
|
robust-predicates@3.0.2: {}
|
||||||
|
|
||||||
rollup@4.52.4:
|
rollup@4.52.4:
|
||||||
@@ -9046,8 +8676,6 @@ snapshots:
|
|||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
semver@7.7.3: {}
|
|
||||||
|
|
||||||
send@0.19.0:
|
send@0.19.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 2.6.9
|
debug: 2.6.9
|
||||||
@@ -9138,15 +8766,8 @@ snapshots:
|
|||||||
|
|
||||||
sqlstring@2.3.3: {}
|
sqlstring@2.3.3: {}
|
||||||
|
|
||||||
ssf@0.11.2:
|
|
||||||
dependencies:
|
|
||||||
frac: 1.1.2
|
|
||||||
|
|
||||||
stackback@0.0.2: {}
|
stackback@0.0.2: {}
|
||||||
|
|
||||||
stackblur-canvas@2.7.0:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
statuses@2.0.1: {}
|
statuses@2.0.1: {}
|
||||||
|
|
||||||
std-env@3.9.0: {}
|
std-env@3.9.0: {}
|
||||||
@@ -9192,9 +8813,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
copy-anything: 3.0.5
|
copy-anything: 3.0.5
|
||||||
|
|
||||||
svg-pathdata@6.0.3:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
tailwind-merge@3.3.1: {}
|
tailwind-merge@3.3.1: {}
|
||||||
|
|
||||||
tailwindcss-animate@1.0.7(tailwindcss@4.1.14):
|
tailwindcss-animate@1.0.7(tailwindcss@4.1.14):
|
||||||
@@ -9213,11 +8831,6 @@ snapshots:
|
|||||||
minizlib: 3.1.0
|
minizlib: 3.1.0
|
||||||
yallist: 5.0.0
|
yallist: 5.0.0
|
||||||
|
|
||||||
text-segmentation@1.0.3:
|
|
||||||
dependencies:
|
|
||||||
utrie: 1.0.2
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
tiny-invariant@1.3.3: {}
|
tiny-invariant@1.3.3: {}
|
||||||
|
|
||||||
tinybench@2.9.0: {}
|
tinybench@2.9.0: {}
|
||||||
@@ -9345,11 +8958,6 @@ snapshots:
|
|||||||
|
|
||||||
utils-merge@1.0.1: {}
|
utils-merge@1.0.1: {}
|
||||||
|
|
||||||
utrie@1.0.2:
|
|
||||||
dependencies:
|
|
||||||
base64-arraybuffer: 1.0.2
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
uuid@11.1.0: {}
|
uuid@11.1.0: {}
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
@@ -9516,10 +9124,6 @@ snapshots:
|
|||||||
siginfo: 2.0.0
|
siginfo: 2.0.0
|
||||||
stackback: 0.0.2
|
stackback: 0.0.2
|
||||||
|
|
||||||
wmf@1.0.2: {}
|
|
||||||
|
|
||||||
word@0.3.0: {}
|
|
||||||
|
|
||||||
wouter@3.7.1(patch_hash=4e16e6ff3fde7d6c1024d3e0c8605dc9eb6afb690d0d49958c2f449091813072)(react@19.2.0):
|
wouter@3.7.1(patch_hash=4e16e6ff3fde7d6c1024d3e0c8605dc9eb6afb690d0d49958c2f449091813072)(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
mitt: 3.0.1
|
mitt: 3.0.1
|
||||||
@@ -9527,16 +9131,6 @@ snapshots:
|
|||||||
regexparam: 3.0.0
|
regexparam: 3.0.0
|
||||||
use-sync-external-store: 1.6.0(react@19.2.0)
|
use-sync-external-store: 1.6.0(react@19.2.0)
|
||||||
|
|
||||||
xlsx@0.18.5:
|
|
||||||
dependencies:
|
|
||||||
adler-32: 1.3.1
|
|
||||||
cfb: 1.2.2
|
|
||||||
codepage: 1.15.0
|
|
||||||
crc-32: 1.2.2
|
|
||||||
ssf: 0.11.2
|
|
||||||
wmf: 1.0.2
|
|
||||||
word: 0.3.0
|
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
yallist@5.0.0: {}
|
yallist@5.0.0: {}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import bcrypt from "bcrypt";
|
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
import { ENV } from "./env";
|
|
||||||
|
|
||||||
const SALT_ROUNDS = 10;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hashe un mot de passe avec bcrypt
|
|
||||||
*/
|
|
||||||
export async function hashPassword(password: string): Promise<string> {
|
|
||||||
return bcrypt.hash(password, SALT_ROUNDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie un mot de passe contre son hash
|
|
||||||
*/
|
|
||||||
export async function verifyPassword(
|
|
||||||
password: string,
|
|
||||||
hash: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
return bcrypt.compare(password, hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère un token JWT pour un utilisateur
|
|
||||||
*/
|
|
||||||
export function generateToken(userId: number): string {
|
|
||||||
return jwt.sign({ userId }, ENV.jwtSecret, {
|
|
||||||
expiresIn: "30d",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie et décode un token JWT
|
|
||||||
*/
|
|
||||||
export function verifyToken(token: string): { userId: number } | null {
|
|
||||||
try {
|
|
||||||
const decoded = jwt.verify(token, ENV.jwtSecret) as { userId: number };
|
|
||||||
return decoded;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||||
import type { User } from "../../drizzle/schema";
|
import type { User } from "../../drizzle/schema";
|
||||||
import { verifyToken } from "./auth";
|
import { sdk } from "./sdk";
|
||||||
import { getUserById } from "../db";
|
|
||||||
import { COOKIE_NAME } from "@shared/const";
|
|
||||||
|
|
||||||
export type TrpcContext = {
|
export type TrpcContext = {
|
||||||
req: CreateExpressContextOptions["req"];
|
req: CreateExpressContextOptions["req"];
|
||||||
@@ -16,26 +14,7 @@ export async function createContext(
|
|||||||
let user: User | null = null;
|
let user: User | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Essayer de récupérer le token depuis le cookie d'abord
|
user = await sdk.authenticateRequest(opts.req);
|
||||||
let token = opts.req.cookies[COOKIE_NAME];
|
|
||||||
|
|
||||||
// Si pas de cookie, essayer le header Authorization
|
|
||||||
if (!token) {
|
|
||||||
const authHeader = opts.req.headers.authorization;
|
|
||||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
||||||
token = authHeader.substring(7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
// Vérifier et décoder le token
|
|
||||||
const decoded = verifyToken(token);
|
|
||||||
|
|
||||||
if (decoded) {
|
|
||||||
// Récupérer l'utilisateur depuis la base de données
|
|
||||||
user = await getUserById(decoded.userId) || null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Authentication is optional for public procedures.
|
// Authentication is optional for public procedures.
|
||||||
user = null;
|
user = null;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function getSessionCookieOptions(
|
|||||||
return {
|
return {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: "/",
|
path: "/",
|
||||||
sameSite: "lax",
|
sameSite: "none",
|
||||||
secure: isSecureRequest(req),
|
secure: isSecureRequest(req),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
/**
|
|
||||||
* Service d'envoi d'emails réels via Resend API
|
|
||||||
*
|
|
||||||
* Configuration requise:
|
|
||||||
* - RESEND_API_KEY: Clé API Resend (obtenir sur https://resend.com)
|
|
||||||
* - RESEND_FROM_EMAIL: Adresse email d'envoi (doit être vérifiée dans Resend)
|
|
||||||
*
|
|
||||||
* Si ces variables ne sont pas configurées, les emails seront simulés
|
|
||||||
* et envoyés comme notifications au propriétaire du projet.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ENV } from "./env";
|
|
||||||
import { notifyOwner } from "./notification";
|
|
||||||
import { getActiveEmailConfig } from "../db";
|
|
||||||
|
|
||||||
interface EmailParams {
|
|
||||||
to: string;
|
|
||||||
subject: string;
|
|
||||||
html: string;
|
|
||||||
attachments?: Array<{
|
|
||||||
filename: string;
|
|
||||||
content: string;
|
|
||||||
contentType: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ResendEmailRequest {
|
|
||||||
from: string;
|
|
||||||
to: string[];
|
|
||||||
subject: string;
|
|
||||||
html: string;
|
|
||||||
attachments?: Array<{
|
|
||||||
filename: string;
|
|
||||||
content: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Envoie un email via Resend API
|
|
||||||
*/
|
|
||||||
async function sendViaResend(params: EmailParams, config?: { apiKey: string; fromEmail: string; fromName: string }): Promise<boolean> {
|
|
||||||
// Utiliser la config fournie ou les variables d'environnement
|
|
||||||
const resendApiKey = config?.apiKey || process.env.RESEND_API_KEY;
|
|
||||||
const fromEmail = config?.fromEmail || process.env.RESEND_FROM_EMAIL || 'noreply@manusvm.computer';
|
|
||||||
const fromName = config?.fromName || 'Formation Manager Itinova';
|
|
||||||
|
|
||||||
if (!resendApiKey) {
|
|
||||||
console.warn('[Email] RESEND_API_KEY non configurée, email simulé');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload: ResendEmailRequest = {
|
|
||||||
from: `${fromName} <${fromEmail}>`,
|
|
||||||
to: [params.to],
|
|
||||||
subject: params.subject,
|
|
||||||
html: params.html,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ajouter les pièces jointes si présentes
|
|
||||||
if (params.attachments && params.attachments.length > 0) {
|
|
||||||
payload.attachments = params.attachments.map(att => ({
|
|
||||||
filename: att.filename,
|
|
||||||
content: att.content, // Resend accepte base64 ou string
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch('https://api.resend.com/emails', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${resendApiKey}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.text();
|
|
||||||
console.error('[Email] Erreur Resend:', response.status, error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
console.log('[Email] Email envoyé via Resend:', result.id);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Email] Exception lors de l\'envoi via Resend:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simule l'envoi d'un email en créant une notification pour le propriétaire
|
|
||||||
*/
|
|
||||||
async function simulateEmail(params: EmailParams): Promise<boolean> {
|
|
||||||
console.log('=== EMAIL SIMULÉ ===');
|
|
||||||
console.log('To:', params.to);
|
|
||||||
console.log('Subject:', params.subject);
|
|
||||||
|
|
||||||
const emailContent = `
|
|
||||||
Destinataire: ${params.to}
|
|
||||||
Sujet: ${params.subject}
|
|
||||||
|
|
||||||
${params.html.replace(/<[^>]*>/g, '').substring(0, 500)}...
|
|
||||||
|
|
||||||
${params.attachments ? `Pièces jointes: ${params.attachments.map(a => a.filename).join(', ')}` : ''}
|
|
||||||
|
|
||||||
⚠️ Cet email est simulé. Pour envoyer de vrais emails:
|
|
||||||
1. Créez un compte sur https://resend.com
|
|
||||||
2. Ajoutez RESEND_API_KEY dans les secrets du projet
|
|
||||||
3. Ajoutez RESEND_FROM_EMAIL (ex: noreply@votredomaine.com)`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await notifyOwner({
|
|
||||||
title: `📧 Email simulé: ${params.subject}`,
|
|
||||||
content: emailContent,
|
|
||||||
});
|
|
||||||
console.log('✓ Email simulé (notification envoyée au propriétaire)');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erreur lors de la simulation d\'email:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('===================');
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Envoie un email (réel ou simulé selon la configuration)
|
|
||||||
*/
|
|
||||||
export async function sendEmail(params: EmailParams): Promise<boolean> {
|
|
||||||
// Récupérer la configuration depuis la base de données
|
|
||||||
const config = await getActiveEmailConfig();
|
|
||||||
|
|
||||||
// Si mode simulation ou pas de config, simuler
|
|
||||||
if (!config || config.mode === 'simulation') {
|
|
||||||
return await simulateEmail(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si mode production, tenter d'envoyer via Resend
|
|
||||||
if (config.mode === 'production' && config.apiKey) {
|
|
||||||
const sent = await sendViaResend(params, {
|
|
||||||
apiKey: config.apiKey,
|
|
||||||
fromEmail: config.fromEmail,
|
|
||||||
fromName: config.fromName,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Si l'envoi échoue, simuler en fallback
|
|
||||||
if (!sent) {
|
|
||||||
return await simulateEmail(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: simuler
|
|
||||||
return await simulateEmail(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si le service d'envoi d'emails réel est configuré
|
|
||||||
*/
|
|
||||||
export function isEmailServiceConfigured(): boolean {
|
|
||||||
return !!process.env.RESEND_API_KEY;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
export const ENV = {
|
export const ENV = {
|
||||||
appId: process.env.VITE_APP_ID ?? "",
|
appId: process.env.VITE_APP_ID ?? "",
|
||||||
cookieSecret: process.env.JWT_SECRET ?? "",
|
cookieSecret: process.env.JWT_SECRET ?? "",
|
||||||
jwtSecret: process.env.JWT_SECRET ?? "",
|
|
||||||
databaseUrl: process.env.DATABASE_URL ?? "",
|
databaseUrl: process.env.DATABASE_URL ?? "",
|
||||||
oAuthServerUrl: process.env.OAUTH_SERVER_URL ?? "",
|
oAuthServerUrl: process.env.OAUTH_SERVER_URL ?? "",
|
||||||
ownerOpenId: process.env.OWNER_OPEN_ID ?? "",
|
ownerOpenId: process.env.OWNER_OPEN_ID ?? "",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import "dotenv/config";
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import { createServer } from "http";
|
import { createServer } from "http";
|
||||||
import net from "net";
|
import net from "net";
|
||||||
import cookieParser from "cookie-parser";
|
|
||||||
import { createExpressMiddleware } from "@trpc/server/adapters/express";
|
import { createExpressMiddleware } from "@trpc/server/adapters/express";
|
||||||
import { registerOAuthRoutes } from "./oauth";
|
import { registerOAuthRoutes } from "./oauth";
|
||||||
import { appRouter } from "../routers";
|
import { appRouter } from "../routers";
|
||||||
@@ -31,8 +30,6 @@ async function findAvailablePort(startPort: number = 3000): Promise<number> {
|
|||||||
async function startServer() {
|
async function startServer() {
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = createServer(app);
|
const server = createServer(app);
|
||||||
// Configure cookie parser
|
|
||||||
app.use(cookieParser());
|
|
||||||
// Configure body parser with larger size limit for file uploads
|
// Configure body parser with larger size limit for file uploads
|
||||||
app.use(express.json({ limit: "50mb" }));
|
app.use(express.json({ limit: "50mb" }));
|
||||||
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
/**
|
|
||||||
* Utilitaires pour la gestion des dates
|
|
||||||
* Gère correctement les conversions entre heure locale et UTC
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convertit une chaîne datetime-local (format: "2025-01-15T14:30") en Date
|
|
||||||
* en construisant la date avec une chaîne ISO qui force l'heure locale
|
|
||||||
* Préserve l'heure locale sans conversion UTC
|
|
||||||
*/
|
|
||||||
export function parseLocalDateTime(dateTimeString: string): Date {
|
|
||||||
if (!dateTimeString) {
|
|
||||||
throw new Error('Date string is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Le format datetime-local est "YYYY-MM-DDTHH:mm"
|
|
||||||
const match = dateTimeString.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
throw new Error(`Invalid date format: ${dateTimeString}. Expected YYYY-MM-DDTHH:mm`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, year, month, day, hours, minutes] = match;
|
|
||||||
|
|
||||||
// Créer un objet Date en utilisant le constructeur avec composants
|
|
||||||
// MySQL stocke en UTC, donc on doit compenser le décalage horaire
|
|
||||||
const date = new Date(
|
|
||||||
parseInt(year),
|
|
||||||
parseInt(month) - 1,
|
|
||||||
parseInt(day),
|
|
||||||
parseInt(hours),
|
|
||||||
parseInt(minutes),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
// Compenser le décalage UTC en ajoutant le décalage du fuseau horaire
|
|
||||||
// Cela garantit que l'heure stockée en UTC correspond à l'heure locale saisie
|
|
||||||
const offset = date.getTimezoneOffset(); // en minutes (négatif pour Europe/Paris)
|
|
||||||
date.setMinutes(date.getMinutes() - offset);
|
|
||||||
|
|
||||||
if (isNaN(date.getTime())) {
|
|
||||||
throw new Error(`Invalid date string: ${dateTimeString}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formate une Date en chaîne datetime-local pour les inputs HTML
|
|
||||||
* Format: "YYYY-MM-DDTHH:mm"
|
|
||||||
*/
|
|
||||||
export function formatToLocalDateTime(date: Date): string {
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
|
||||||
const hours = String(date.getHours()).padStart(2, '0');
|
|
||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
||||||
|
|
||||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
|
||||||
}
|
|
||||||
649
server/db.ts
649
server/db.ts
@@ -1,30 +1,6 @@
|
|||||||
import { eq, and, sql } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { drizzle } from "drizzle-orm/mysql2";
|
import { drizzle } from "drizzle-orm/mysql2";
|
||||||
import {
|
import { InsertUser, users } from "../drizzle/schema";
|
||||||
InsertUser,
|
|
||||||
users,
|
|
||||||
formations,
|
|
||||||
apprenants,
|
|
||||||
sequences,
|
|
||||||
datesFormation,
|
|
||||||
inscriptions,
|
|
||||||
passwordResetTokens,
|
|
||||||
InsertPasswordResetToken,
|
|
||||||
emailTemplates,
|
|
||||||
emailConfig,
|
|
||||||
InsertEmailConfig,
|
|
||||||
InsertSequence,
|
|
||||||
InsertDateFormation,
|
|
||||||
InsertInscription,
|
|
||||||
InsertFormation,
|
|
||||||
InsertApprenant,
|
|
||||||
Sequence,
|
|
||||||
DateFormation,
|
|
||||||
Apprenant,
|
|
||||||
Formation,
|
|
||||||
EmailTemplate,
|
|
||||||
InsertEmailTemplate
|
|
||||||
} from "../drizzle/schema";
|
|
||||||
import { ENV } from './_core/env';
|
import { ENV } from './_core/env';
|
||||||
|
|
||||||
let _db: ReturnType<typeof drizzle> | null = null;
|
let _db: ReturnType<typeof drizzle> | null = null;
|
||||||
@@ -42,596 +18,75 @@ export async function getDb() {
|
|||||||
return _db;
|
return _db;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function upsertUser(user: InsertUser): Promise<void> {
|
||||||
* Crée un nouvel utilisateur avec email/mot de passe
|
if (!user.openId) {
|
||||||
*/
|
throw new Error("User openId is required for upsert");
|
||||||
export async function createUser(user: InsertUser): Promise<number> {
|
|
||||||
if (!user.email) {
|
|
||||||
throw new Error("User email is required");
|
|
||||||
}
|
|
||||||
if (!user.password) {
|
|
||||||
throw new Error("User password is required");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) {
|
if (!db) {
|
||||||
throw new Error("Database not available");
|
console.warn("[Database] Cannot upsert user: database not available");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await db.insert(users).values(user);
|
const values: InsertUser = {
|
||||||
return Number(result[0].insertId);
|
openId: user.openId,
|
||||||
|
};
|
||||||
|
const updateSet: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
const textFields = ["name", "email", "loginMethod"] as const;
|
||||||
|
type TextField = (typeof textFields)[number];
|
||||||
|
|
||||||
|
const assignNullable = (field: TextField) => {
|
||||||
|
const value = user[field];
|
||||||
|
if (value === undefined) return;
|
||||||
|
const normalized = value ?? null;
|
||||||
|
values[field] = normalized;
|
||||||
|
updateSet[field] = normalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
textFields.forEach(assignNullable);
|
||||||
|
|
||||||
|
if (user.lastSignedIn !== undefined) {
|
||||||
|
values.lastSignedIn = user.lastSignedIn;
|
||||||
|
updateSet.lastSignedIn = user.lastSignedIn;
|
||||||
|
}
|
||||||
|
if (user.role !== undefined) {
|
||||||
|
values.role = user.role;
|
||||||
|
updateSet.role = user.role;
|
||||||
|
} else if (user.openId === ENV.ownerOpenId) {
|
||||||
|
values.role = 'admin';
|
||||||
|
updateSet.role = 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.lastSignedIn) {
|
||||||
|
values.lastSignedIn = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updateSet).length === 0) {
|
||||||
|
updateSet.lastSignedIn = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(users).values(values).onDuplicateKeyUpdate({
|
||||||
|
set: updateSet,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Database] Failed to create user:", error);
|
console.error("[Database] Failed to upsert user:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function getUserByOpenId(openId: string) {
|
||||||
* Récupère un utilisateur par son email
|
|
||||||
*/
|
|
||||||
export async function getUserByEmail(email: string) {
|
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) {
|
if (!db) {
|
||||||
console.warn("[Database] Cannot get user: database not available");
|
console.warn("[Database] Cannot get user: database not available");
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await db.select().from(users).where(eq(users.email, email)).limit(1);
|
const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1);
|
||||||
return result.length > 0 ? result[0] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère un utilisateur par son ID
|
|
||||||
*/
|
|
||||||
export async function getUserById(id: number) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) {
|
|
||||||
console.warn("[Database] Cannot get user: database not available");
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await db.select().from(users).where(eq(users.id, id)).limit(1);
|
|
||||||
return result.length > 0 ? result[0] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Met à jour le dernier login d'un utilisateur
|
|
||||||
*/
|
|
||||||
export async function updateUserLastSignedIn(userId: number): Promise<void> {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) {
|
|
||||||
console.warn("[Database] Cannot update user: database not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.update(users).set({ lastSignedIn: new Date() }).where(eq(users.id, userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== FORMATIONS ====================
|
|
||||||
|
|
||||||
export async function createFormation(data: InsertFormation) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
const result = await db.insert(formations).values(data);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getFormations() {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return [];
|
|
||||||
|
|
||||||
return await db.select().from(formations);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getFormationById(id: number) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return undefined;
|
|
||||||
|
|
||||||
const result = await db.select().from(formations).where(eq(formations.id, id)).limit(1);
|
|
||||||
return result.length > 0 ? result[0] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getFormationByLien(lien: string) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return undefined;
|
|
||||||
|
|
||||||
const result = await db.select().from(formations).where(eq(formations.lienUnique, lien)).limit(1);
|
|
||||||
return result.length > 0 ? result[0] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateFormation(id: number, data: Partial<InsertFormation>) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.update(formations).set(data).where(eq(formations.id, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteFormation(id: number) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.delete(formations).where(eq(formations.id, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== SÉQUENCES ====================
|
|
||||||
|
|
||||||
export async function createSequence(data: InsertSequence) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
const result = await db.insert(sequences).values(data);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSequences() {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return [];
|
|
||||||
|
|
||||||
return await db.select().from(sequences);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSequenceById(id: number): Promise<Sequence | undefined> {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return undefined;
|
|
||||||
|
|
||||||
const result = await db.select().from(sequences).where(eq(sequences.id, id)).limit(1);
|
|
||||||
return result.length > 0 ? result[0] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSequencesByFormation(formationId: number) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return [];
|
|
||||||
|
|
||||||
return await db.select().from(sequences).where(eq(sequences.formationId, formationId));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateSequence(id: number, data: Partial<InsertSequence>) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.update(sequences).set(data).where(eq(sequences.id, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteSequence(id: number) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.delete(sequences).where(eq(sequences.id, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== DATES DE FORMATION ====================
|
|
||||||
|
|
||||||
export async function createDateFormation(data: InsertDateFormation) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
const result = await db.insert(datesFormation).values(data);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getDatesBySequence(sequenceId: number): Promise<DateFormation[]> {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return [];
|
|
||||||
|
|
||||||
const results = await db.select()
|
|
||||||
.from(datesFormation)
|
|
||||||
.where(eq(datesFormation.sequenceId, sequenceId))
|
|
||||||
.orderBy(datesFormation.ordre);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteDatesBySequence(sequenceId: number) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.delete(datesFormation).where(eq(datesFormation.sequenceId, sequenceId));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== APPRENANTS ====================
|
|
||||||
|
|
||||||
export async function createApprenant(data: InsertApprenant) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
const result = await db.insert(apprenants).values(data);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getApprenants() {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return [];
|
|
||||||
|
|
||||||
return await db.select().from(apprenants);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getApprenantById(id: number): Promise<Apprenant | undefined> {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return undefined;
|
|
||||||
|
|
||||||
const result = await db.select().from(apprenants).where(eq(apprenants.id, id)).limit(1);
|
|
||||||
return result.length > 0 ? result[0] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getApprenantByEmail(email: string) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return undefined;
|
|
||||||
|
|
||||||
const result = await db.select().from(apprenants).where(eq(apprenants.email, email)).limit(1);
|
|
||||||
return result.length > 0 ? result[0] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateApprenant(id: number, data: Partial<InsertApprenant>) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.update(apprenants).set(data).where(eq(apprenants.id, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteApprenant(id: number) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.delete(apprenants).where(eq(apprenants.id, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== INSCRIPTIONS ====================
|
|
||||||
|
|
||||||
export async function createInscription(data: InsertInscription) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
const result = await db.insert(inscriptions).values(data);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getAllInscriptions() {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return [];
|
|
||||||
|
|
||||||
const results = await db.select({
|
|
||||||
inscription: inscriptions,
|
|
||||||
apprenant: apprenants,
|
|
||||||
})
|
|
||||||
.from(inscriptions)
|
|
||||||
.leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id));
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getInscriptionsBySequence(sequenceId: number) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return [];
|
|
||||||
|
|
||||||
const results = await db.select({
|
|
||||||
inscription: inscriptions,
|
|
||||||
apprenant: apprenants,
|
|
||||||
})
|
|
||||||
.from(inscriptions)
|
|
||||||
.leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))
|
|
||||||
.where(eq(inscriptions.sequenceId, sequenceId));
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getInscriptionsByApprenant(apprenantId: number) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return [];
|
|
||||||
|
|
||||||
const results = await db.select({
|
|
||||||
inscription: inscriptions,
|
|
||||||
sequence: sequences,
|
|
||||||
})
|
|
||||||
.from(inscriptions)
|
|
||||||
.leftJoin(sequences, eq(inscriptions.sequenceId, sequences.id))
|
|
||||||
.where(eq(inscriptions.apprenantId, apprenantId));
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkExistingInscription(apprenantId: number, sequenceId: number) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return undefined;
|
|
||||||
|
|
||||||
const result = await db.select()
|
|
||||||
.from(inscriptions)
|
|
||||||
.where(and(
|
|
||||||
eq(inscriptions.apprenantId, apprenantId),
|
|
||||||
eq(inscriptions.sequenceId, sequenceId)
|
|
||||||
))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
return result.length > 0 ? result[0] : undefined;
|
return result.length > 0 ? result[0] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function countInscriptionsBySequence(sequenceId: number, statut?: string) {
|
// TODO: add feature queries here as your schema grows.
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return 0;
|
|
||||||
|
|
||||||
const conditions = [eq(inscriptions.sequenceId, sequenceId)];
|
|
||||||
if (statut) {
|
|
||||||
conditions.push(eq(inscriptions.statut, statut as any));
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await db.select({ count: sql<number>`count(*)` })
|
|
||||||
.from(inscriptions)
|
|
||||||
.where(and(...conditions));
|
|
||||||
|
|
||||||
return result[0]?.count || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateInscription(id: number, data: Partial<InsertInscription>) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.update(inscriptions).set(data).where(eq(inscriptions.id, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteInscription(id: number) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.delete(inscriptions).where(eq(inscriptions.id, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== GESTION DES UTILISATEURS ====================
|
|
||||||
|
|
||||||
export async function getAllUsers() {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return [];
|
|
||||||
|
|
||||||
return await db.select().from(users);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateUser(id: number, data: Partial<InsertUser>) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.update(users).set(data).where(eq(users.id, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function toggleUserStatus(id: number, isActive: boolean) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.update(users).set({ isActive }).where(eq(users.id, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteUser(id: number) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.delete(users).where(eq(users.id, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ==================== GESTION DES TOKENS DE RÉINITIALISATION ====================
|
|
||||||
|
|
||||||
export async function createPasswordResetToken(userId: number, token: string, expiresAt: Date) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
const result = await db.insert(passwordResetTokens).values({
|
|
||||||
userId,
|
|
||||||
token,
|
|
||||||
expiresAt,
|
|
||||||
used: false,
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPasswordResetToken(token: string) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return undefined;
|
|
||||||
|
|
||||||
const result = await db.select()
|
|
||||||
.from(passwordResetTokens)
|
|
||||||
.where(eq(passwordResetTokens.token, token))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
return result.length > 0 ? result[0] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function markTokenAsUsed(tokenId: number) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.update(passwordResetTokens)
|
|
||||||
.set({ used: true })
|
|
||||||
.where(eq(passwordResetTokens.id, tokenId));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteExpiredTokens() {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
await db.delete(passwordResetTokens)
|
|
||||||
.where(sql`${passwordResetTokens.expiresAt} < ${now}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// Email Templates Management
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère tous les templates d'emails
|
|
||||||
*/
|
|
||||||
export async function getAllEmailTemplates() {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return [];
|
|
||||||
|
|
||||||
return await db.select().from(emailTemplates);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère un template par son type
|
|
||||||
*/
|
|
||||||
export async function getEmailTemplateByType(type: string) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return undefined;
|
|
||||||
|
|
||||||
const results = await db.select().from(emailTemplates).where(eq(emailTemplates.type, type)).limit(1);
|
|
||||||
return results.length > 0 ? results[0] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Crée ou met à jour un template d'email
|
|
||||||
*/
|
|
||||||
export async function upsertEmailTemplate(template: InsertEmailTemplate) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
const existing = await getEmailTemplateByType(template.type);
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
// Mise à jour
|
|
||||||
await db.update(emailTemplates)
|
|
||||||
.set({
|
|
||||||
name: template.name,
|
|
||||||
logoUrl: template.logoUrl,
|
|
||||||
primaryColor: template.primaryColor,
|
|
||||||
headerBgColor: template.headerBgColor,
|
|
||||||
headerTextColor: template.headerTextColor,
|
|
||||||
headerTitle: template.headerTitle,
|
|
||||||
footerText: template.footerText,
|
|
||||||
active: template.active,
|
|
||||||
updatedAt: new Date(),
|
|
||||||
})
|
|
||||||
.where(eq(emailTemplates.type, template.type));
|
|
||||||
|
|
||||||
return await getEmailTemplateByType(template.type);
|
|
||||||
} else {
|
|
||||||
// Création
|
|
||||||
await db.insert(emailTemplates).values(template);
|
|
||||||
return await getEmailTemplateByType(template.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Supprime un template d'email
|
|
||||||
*/
|
|
||||||
export async function deleteEmailTemplate(type: string) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) throw new Error("Database not available");
|
|
||||||
|
|
||||||
await db.delete(emailTemplates).where(eq(emailTemplates.type, type));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise les templates par défaut si la table est vide
|
|
||||||
*/
|
|
||||||
export async function initializeDefaultEmailTemplates() {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return;
|
|
||||||
|
|
||||||
const existing = await getAllEmailTemplates();
|
|
||||||
if (existing.length > 0) return; // Déjà initialisé
|
|
||||||
|
|
||||||
const defaultTemplates: InsertEmailTemplate[] = [
|
|
||||||
{
|
|
||||||
type: "inscription",
|
|
||||||
name: "Confirmation d'inscription",
|
|
||||||
logoUrl: null,
|
|
||||||
primaryColor: "#2563eb",
|
|
||||||
headerBgColor: "#2563eb",
|
|
||||||
headerTextColor: "#ffffff",
|
|
||||||
headerTitle: "Formation Manager Itinova",
|
|
||||||
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "teaser",
|
|
||||||
name: "Email teaser",
|
|
||||||
logoUrl: null,
|
|
||||||
primaryColor: "#2563eb",
|
|
||||||
headerBgColor: "#2563eb",
|
|
||||||
headerTextColor: "#ffffff",
|
|
||||||
headerTitle: "Formation Manager Itinova",
|
|
||||||
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "rappel",
|
|
||||||
name: "Rappel J-7",
|
|
||||||
logoUrl: null,
|
|
||||||
primaryColor: "#2563eb",
|
|
||||||
headerBgColor: "#2563eb",
|
|
||||||
headerTextColor: "#ffffff",
|
|
||||||
headerTitle: "Formation Manager Itinova",
|
|
||||||
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "reset_password",
|
|
||||||
name: "Réinitialisation de mot de passe",
|
|
||||||
logoUrl: null,
|
|
||||||
primaryColor: "#2563eb",
|
|
||||||
headerBgColor: "#2563eb",
|
|
||||||
headerTextColor: "#ffffff",
|
|
||||||
headerTitle: "Formation Manager Itinova",
|
|
||||||
footerText: "Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.",
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const template of defaultTemplates) {
|
|
||||||
await db.insert(emailTemplates).values(template);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[DB] Templates d'emails par défaut initialisés");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ==================== Email Config ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère la configuration email active
|
|
||||||
*/
|
|
||||||
export async function getActiveEmailConfig() {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return undefined;
|
|
||||||
|
|
||||||
const result = await db
|
|
||||||
.select()
|
|
||||||
.from(emailConfig)
|
|
||||||
.where(eq(emailConfig.active, true))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
return result.length > 0 ? result[0] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Crée ou met à jour la configuration email
|
|
||||||
*/
|
|
||||||
export async function upsertEmailConfig(data: InsertEmailConfig) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return;
|
|
||||||
|
|
||||||
// Désactiver toutes les configurations existantes
|
|
||||||
await db.update(emailConfig).set({ active: false });
|
|
||||||
|
|
||||||
// Créer la nouvelle configuration
|
|
||||||
await db.insert(emailConfig).values({
|
|
||||||
...data,
|
|
||||||
active: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Met à jour la configuration email
|
|
||||||
*/
|
|
||||||
export async function updateEmailConfig(id: number, data: Partial<InsertEmailConfig>) {
|
|
||||||
const db = await getDb();
|
|
||||||
if (!db) return;
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(emailConfig)
|
|
||||||
.set({
|
|
||||||
...data,
|
|
||||||
updatedAt: new Date(),
|
|
||||||
})
|
|
||||||
.where(eq(emailConfig.id, id));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,331 +0,0 @@
|
|||||||
/**
|
|
||||||
* Service d'envoi d'emails pour les formations
|
|
||||||
* Utilise le système de notification Manus pour envoyer des emails
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { generateFormationICS } from "./icsGenerator";
|
|
||||||
import { sendEmail as sendEmailViaService, isEmailServiceConfigured } from "./_core/emailSender";
|
|
||||||
import { generateEmailFromTemplate } from "./emailTemplateGenerator";
|
|
||||||
|
|
||||||
interface EmailParams {
|
|
||||||
to: string;
|
|
||||||
subject: string;
|
|
||||||
html: string;
|
|
||||||
attachments?: Array<{
|
|
||||||
filename: string;
|
|
||||||
content: string;
|
|
||||||
contentType: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Envoie un email via le service d'envoi configuré
|
|
||||||
* Utilise Resend si RESEND_API_KEY est configuré, sinon simule l'envoi
|
|
||||||
*/
|
|
||||||
async function sendEmail(params: EmailParams): Promise<boolean> {
|
|
||||||
return await sendEmailViaService(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Template HTML de base pour les emails (avec template personnalisé)
|
|
||||||
*/
|
|
||||||
async function getEmailTemplate(content: string, templateType: string = 'inscription'): Promise<string> {
|
|
||||||
return await generateEmailFromTemplate(templateType, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Envoie un email de confirmation d'inscription avec invitations Outlook pour toutes les dates
|
|
||||||
*/
|
|
||||||
export async function sendInscriptionConfirmation(params: {
|
|
||||||
apprenantEmail: string;
|
|
||||||
apprenantNom: string;
|
|
||||||
apprenantPrenom: string;
|
|
||||||
apprenantFonction: string;
|
|
||||||
formationNom: string;
|
|
||||||
sequenceNom: string;
|
|
||||||
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
|
||||||
lieu: string;
|
|
||||||
statut: 'confirmee' | 'liste_attente';
|
|
||||||
}): Promise<boolean> {
|
|
||||||
const isConfirmed = params.statut === 'confirmee';
|
|
||||||
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
|
|
||||||
params.apprenantFonction === 'chef_service' ? 'Chef de service' : 'Autre';
|
|
||||||
|
|
||||||
// Générer la liste des dates
|
|
||||||
const datesHTML = params.dates.map(date => `
|
|
||||||
<div class="date-item">
|
|
||||||
<strong>Date ${date.ordre} :</strong><br/>
|
|
||||||
Du ${date.dateDebut.toLocaleDateString('fr-FR', {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}<br/>
|
|
||||||
au ${date.dateFin.toLocaleDateString('fr-FR', {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
const content = `
|
|
||||||
<h2>Confirmation d'inscription</h2>
|
|
||||||
<p>Bonjour ${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom},</p>
|
|
||||||
|
|
||||||
${isConfirmed
|
|
||||||
? '<p><strong>Votre inscription a été confirmée !</strong></p>'
|
|
||||||
: '<p><strong>Vous avez été ajouté à la liste d\'attente.</strong></p>'
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
<h3>Détails de la formation</h3>
|
|
||||||
<p><strong>Formation :</strong> ${params.formationNom}</p>
|
|
||||||
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
|
|
||||||
<p><strong>Lieu :</strong> ${params.lieu}</p>
|
|
||||||
<h4>Dates de formation (${params.dates.length} séance${params.dates.length > 1 ? 's' : ''}) :</h4>
|
|
||||||
${datesHTML}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${isConfirmed
|
|
||||||
? `<p>Des invitations Outlook sont jointes à cet email pour chaque date de formation. Merci de les ajouter à votre calendrier pour bloquer votre agenda.</p>
|
|
||||||
<p><strong>Important :</strong> Les inscriptions seront bloquées 15 jours avant le début de la séquence. Après cette date, aucune modification ne sera possible.</p>`
|
|
||||||
: '<p>Nous vous contacterons dès qu\'une place se libère.</p>'
|
|
||||||
}
|
|
||||||
|
|
||||||
<p>À bientôt pour cette formation !</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Générer une invitation ICS pour chaque date
|
|
||||||
const attachments = isConfirmed ? params.dates.map((date, index) => ({
|
|
||||||
filename: `invitation_date_${date.ordre}.ics`,
|
|
||||||
content: generateFormationICS({
|
|
||||||
formationNom: params.formationNom,
|
|
||||||
sessionNom: `${params.sequenceNom} - Date ${date.ordre}`,
|
|
||||||
dateDebut: date.dateDebut,
|
|
||||||
dateFin: date.dateFin,
|
|
||||||
lieu: params.lieu,
|
|
||||||
apprenantNom: params.apprenantNom,
|
|
||||||
apprenantPrenom: params.apprenantPrenom,
|
|
||||||
apprenantEmail: params.apprenantEmail,
|
|
||||||
}),
|
|
||||||
contentType: 'text/calendar',
|
|
||||||
})) : undefined;
|
|
||||||
|
|
||||||
return sendEmail({
|
|
||||||
to: params.apprenantEmail,
|
|
||||||
subject: isConfirmed
|
|
||||||
? `Confirmation d'inscription - ${params.formationNom}`
|
|
||||||
: `Liste d'attente - ${params.formationNom}`,
|
|
||||||
html: await getEmailTemplate(content, 'inscription'),
|
|
||||||
attachments,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Envoie un email teaser pour une séquence
|
|
||||||
*/
|
|
||||||
export async function sendTeaserEmail(params: {
|
|
||||||
apprenantEmail: string;
|
|
||||||
apprenantPrenom: string;
|
|
||||||
apprenantNom: string;
|
|
||||||
apprenantFonction: string;
|
|
||||||
formationNom: string;
|
|
||||||
sequenceNom: string;
|
|
||||||
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
|
||||||
}): Promise<boolean> {
|
|
||||||
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
|
|
||||||
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
|
|
||||||
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
|
|
||||||
|
|
||||||
const datesHTML = params.dates.map(date => `
|
|
||||||
<div class="date-item">
|
|
||||||
<strong>Date ${date.ordre} :</strong> ${date.dateDebut.toLocaleDateString('fr-FR', {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric'
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
const content = `
|
|
||||||
<h2>Votre formation approche !</h2>
|
|
||||||
<p>Bonjour ${salutation},</p>
|
|
||||||
|
|
||||||
<p>Nous sommes ravis de vous accueillir prochainement pour la formation <strong>${params.formationNom}</strong>.</p>
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
|
|
||||||
<h4>Dates :</h4>
|
|
||||||
${datesHTML}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>Préparez-vous à vivre une expérience enrichissante qui vous permettra de développer vos compétences managériales.</p>
|
|
||||||
|
|
||||||
<p>À très bientôt !</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return sendEmail({
|
|
||||||
to: params.apprenantEmail,
|
|
||||||
subject: `Votre formation ${params.formationNom} approche !`,
|
|
||||||
html: await getEmailTemplate(content, 'teaser'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Envoie un email de rappel J-7
|
|
||||||
*/
|
|
||||||
export async function sendRappelJ7Email(params: {
|
|
||||||
apprenantEmail: string;
|
|
||||||
apprenantPrenom: string;
|
|
||||||
apprenantNom: string;
|
|
||||||
apprenantFonction: string;
|
|
||||||
formationNom: string;
|
|
||||||
sequenceNom: string;
|
|
||||||
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
|
||||||
lieu: string;
|
|
||||||
}): Promise<boolean> {
|
|
||||||
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
|
|
||||||
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
|
|
||||||
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
|
|
||||||
|
|
||||||
const datesHTML = params.dates.map(date => `
|
|
||||||
<div class="date-item">
|
|
||||||
<strong>Date ${date.ordre} :</strong><br/>
|
|
||||||
${date.dateDebut.toLocaleDateString('fr-FR', {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
const content = `
|
|
||||||
<h2>Rappel : Votre formation dans 7 jours</h2>
|
|
||||||
<p>Bonjour ${salutation},</p>
|
|
||||||
|
|
||||||
<p>Nous vous rappelons que votre formation <strong>${params.formationNom}</strong> commence dans 7 jours.</p>
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
<h3>Informations pratiques</h3>
|
|
||||||
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
|
|
||||||
<p><strong>Lieu :</strong> ${params.lieu}</p>
|
|
||||||
<h4>Dates :</h4>
|
|
||||||
${datesHTML}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p><strong>Merci de vous présenter à l'heure indiquée.</strong></p>
|
|
||||||
|
|
||||||
<p>Si vous avez des questions, n'hésitez pas à contacter le service RH.</p>
|
|
||||||
|
|
||||||
<p>À très bientôt !</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return sendEmail({
|
|
||||||
to: params.apprenantEmail,
|
|
||||||
subject: `Rappel J-7 : ${params.formationNom}`,
|
|
||||||
html: await getEmailTemplate(content, 'rappel'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Envoie des emails groupés à tous les inscrits d'une séquence
|
|
||||||
*/
|
|
||||||
export async function sendGroupEmail(params: {
|
|
||||||
recipients: Array<{ email: string; prenom: string; nom: string; fonction: string }>;
|
|
||||||
formationNom: string;
|
|
||||||
sequenceNom: string;
|
|
||||||
type: 'teaser' | 'rappel';
|
|
||||||
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
|
||||||
lieu?: string;
|
|
||||||
}): Promise<{ sent: number; failed: number }> {
|
|
||||||
let sent = 0;
|
|
||||||
let failed = 0;
|
|
||||||
|
|
||||||
for (const recipient of params.recipients) {
|
|
||||||
try {
|
|
||||||
if (params.type === 'teaser') {
|
|
||||||
await sendTeaserEmail({
|
|
||||||
apprenantEmail: recipient.email,
|
|
||||||
apprenantPrenom: recipient.prenom,
|
|
||||||
apprenantNom: recipient.nom,
|
|
||||||
apprenantFonction: recipient.fonction,
|
|
||||||
formationNom: params.formationNom,
|
|
||||||
sequenceNom: params.sequenceNom,
|
|
||||||
dates: params.dates,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await sendRappelJ7Email({
|
|
||||||
apprenantEmail: recipient.email,
|
|
||||||
apprenantPrenom: recipient.prenom,
|
|
||||||
apprenantNom: recipient.nom,
|
|
||||||
apprenantFonction: recipient.fonction,
|
|
||||||
formationNom: params.formationNom,
|
|
||||||
sequenceNom: params.sequenceNom,
|
|
||||||
dates: params.dates,
|
|
||||||
lieu: params.lieu || '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
sent++;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Erreur envoi email à ${recipient.email}:`, error);
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { sent, failed };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Envoie un email de réinitialisation de mot de passe
|
|
||||||
*/
|
|
||||||
export async function sendPasswordResetEmail(params: {
|
|
||||||
apprenantEmail: string;
|
|
||||||
apprenantNom: string;
|
|
||||||
apprenantPrenom: string;
|
|
||||||
resetLink: string;
|
|
||||||
}): Promise<boolean> {
|
|
||||||
const content = `
|
|
||||||
<h2>Réinitialisation de mot de passe</h2>
|
|
||||||
<p>Bonjour ${params.apprenantPrenom} ${params.apprenantNom},</p>
|
|
||||||
|
|
||||||
<p>Vous avez demandé la réinitialisation de votre mot de passe pour votre compte Manager Itinova.</p>
|
|
||||||
|
|
||||||
<p>Cliquez sur le bouton ci-dessous pour créer un nouveau mot de passe :</p>
|
|
||||||
|
|
||||||
<div style="text-align: center; margin: 30px 0;">
|
|
||||||
<a href="${params.resetLink}" class="button">Réinitialiser mon mot de passe</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>Ou copiez ce lien dans votre navigateur :</p>
|
|
||||||
<div style="background-color: white; padding: 15px; border-radius: 4px; word-break: break-all; font-family: monospace; font-size: 12px; border: 1px solid #e5e7eb;">
|
|
||||||
${params.resetLink}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-box" style="background-color: #fef3c7; border-left-color: #f59e0b;">
|
|
||||||
<p><strong>⚠️ Important :</strong></p>
|
|
||||||
<ul>
|
|
||||||
<li>Ce lien est valide pendant <strong>24 heures</strong> seulement</li>
|
|
||||||
<li>Il ne peut être utilisé qu'<strong>une seule fois</strong></li>
|
|
||||||
<li>Si vous n'avez pas demandé cette réinitialisation, ignorez cet email</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>Pour toute question, contactez le service RH.</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return sendEmail({
|
|
||||||
to: params.apprenantEmail,
|
|
||||||
subject: 'Réinitialisation de votre mot de passe - Manager Itinova',
|
|
||||||
html: await getEmailTemplate(content, 'reset_password'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
/**
|
|
||||||
* Générateur de templates d'emails personnalisés
|
|
||||||
* Utilise les templates stockés en base de données
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as db from "./db";
|
|
||||||
import { EmailTemplate } from "../drizzle/schema";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère le HTML d'un email en utilisant le template personnalisé
|
|
||||||
*/
|
|
||||||
export async function generateEmailFromTemplate(
|
|
||||||
templateType: string,
|
|
||||||
content: string
|
|
||||||
): Promise<string> {
|
|
||||||
// Récupérer le template depuis la base de données
|
|
||||||
const template = await db.getEmailTemplateByType(templateType);
|
|
||||||
|
|
||||||
// Si pas de template trouvé, utiliser le template par défaut
|
|
||||||
if (!template) {
|
|
||||||
return getDefaultEmailTemplate(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildEmailHTML(template, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construit le HTML de l'email avec le template
|
|
||||||
*/
|
|
||||||
function buildEmailHTML(template: EmailTemplate, content: string): string {
|
|
||||||
return `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
background-color: ${template.headerBgColor};
|
|
||||||
color: ${template.headerTextColor};
|
|
||||||
padding: 30px 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.header h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
${template.logoUrl ? `
|
|
||||||
.header img {
|
|
||||||
max-width: 150px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
display: block;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}` : ''}
|
|
||||||
.content {
|
|
||||||
background-color: #f9fafb;
|
|
||||||
padding: 30px 20px;
|
|
||||||
}
|
|
||||||
.content h2 {
|
|
||||||
color: ${template.primaryColor};
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
.button {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 12px 24px;
|
|
||||||
background-color: ${template.primaryColor};
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
.info-box {
|
|
||||||
background-color: #dbeafe;
|
|
||||||
border-left: 4px solid ${template.primaryColor};
|
|
||||||
padding: 15px;
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
.date-item {
|
|
||||||
margin: 10px 0;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
${template.logoUrl ? `<img src="${template.logoUrl}" alt="Logo" />` : ''}
|
|
||||||
<h1>${template.headerTitle}</h1>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
${content}
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
<p>${template.footerText || 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova.'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Template par défaut si aucun template personnalisé n'est trouvé
|
|
||||||
*/
|
|
||||||
function getDefaultEmailTemplate(content: string): string {
|
|
||||||
return `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f5f5f5; }
|
|
||||||
.container { max-width: 600px; margin: 0 auto; background-color: white; }
|
|
||||||
.header { background-color: #2563eb; color: white; padding: 30px 20px; text-align: center; }
|
|
||||||
.header h1 { margin: 0; font-size: 24px; }
|
|
||||||
.content { background-color: #f9fafb; padding: 30px 20px; }
|
|
||||||
.footer { background-color: #f3f4f6; padding: 20px; text-align: center; font-size: 12px; color: #6b7280; }
|
|
||||||
.button { display: inline-block; padding: 12px 24px; background-color: #2563eb; color: white; text-decoration: none; border-radius: 6px; margin: 10px 0; }
|
|
||||||
.info-box { background-color: #dbeafe; border-left: 4px solid #2563eb; padding: 15px; margin: 15px 0; }
|
|
||||||
.date-item { margin: 10px 0; padding: 10px; background-color: white; border-radius: 4px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
<h1>Formation Manager Itinova</h1>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
${content}
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
<p>Cet email a été envoyé automatiquement par le système de gestion des formations Itinova.</p>
|
|
||||||
<p>Pour toute question, veuillez contacter le service RH.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`.trim();
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
/**
|
|
||||||
* Service d'export des données en Excel et PDF
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as XLSX from 'xlsx';
|
|
||||||
import { jsPDF } from 'jspdf';
|
|
||||||
import autoTable from 'jspdf-autotable';
|
|
||||||
|
|
||||||
interface InscriptionExport {
|
|
||||||
nom: string;
|
|
||||||
prenom: string;
|
|
||||||
email: string;
|
|
||||||
codeEtablissement: string;
|
|
||||||
fonction: string;
|
|
||||||
statut: string;
|
|
||||||
dateInscription: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SequenceInfo {
|
|
||||||
formationNom: string;
|
|
||||||
sequenceNom: string;
|
|
||||||
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
|
||||||
lieu: string;
|
|
||||||
publicCible: string;
|
|
||||||
inscriptions: InscriptionExport[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApprenantPresence {
|
|
||||||
nom: string;
|
|
||||||
prenom: string;
|
|
||||||
codeEtablissement: string;
|
|
||||||
fonction: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FeuillePresenceInfo {
|
|
||||||
formationNom: string;
|
|
||||||
sequenceNom: string;
|
|
||||||
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
|
||||||
lieu: string;
|
|
||||||
publicCible: string;
|
|
||||||
apprenants: ApprenantPresence[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère un fichier Excel avec la liste des inscrits
|
|
||||||
*/
|
|
||||||
export function generateExcelExport(sequenceInfo: SequenceInfo): Buffer {
|
|
||||||
// Préparer les données
|
|
||||||
const data = sequenceInfo.inscriptions.map(i => ({
|
|
||||||
'Nom': i.nom,
|
|
||||||
'Prénom': i.prenom,
|
|
||||||
'Email': i.email,
|
|
||||||
'Code établissement': i.codeEtablissement,
|
|
||||||
'Fonction': i.fonction === 'directeur' ? 'Directeur' : i.fonction === 'chef_service' ? 'Chef de service' : 'Autre',
|
|
||||||
'Statut': i.statut === 'confirmee' ? 'Confirmée' : i.statut === 'liste_attente' ? 'Liste d\'attente' : 'Annulée',
|
|
||||||
'Date d\'inscription': i.dateInscription.toLocaleDateString('fr-FR'),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Créer le workbook
|
|
||||||
const wb = XLSX.utils.book_new();
|
|
||||||
|
|
||||||
// Créer la feuille avec les informations de séquence
|
|
||||||
const infoData = [
|
|
||||||
['Formation', sequenceInfo.formationNom],
|
|
||||||
['Séquence', sequenceInfo.sequenceNom],
|
|
||||||
['Lieu', sequenceInfo.lieu],
|
|
||||||
['Public cible', sequenceInfo.publicCible === 'directeur' ? 'Directeurs' : sequenceInfo.publicCible === 'chef_service' ? 'Chefs de service' : 'Autre'],
|
|
||||||
['Nombre de dates', sequenceInfo.dates.length.toString()],
|
|
||||||
[],
|
|
||||||
['DATES DE FORMATION'],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Ajouter les dates
|
|
||||||
sequenceInfo.dates.forEach(date => {
|
|
||||||
infoData.push([
|
|
||||||
`Date ${date.ordre}`,
|
|
||||||
`Du ${date.dateDebut.toLocaleDateString('fr-FR', {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})} au ${date.dateFin.toLocaleDateString('fr-FR', {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}`
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
infoData.push([]);
|
|
||||||
infoData.push(['Nombre d\'inscrits', sequenceInfo.inscriptions.length.toString()]);
|
|
||||||
infoData.push([]);
|
|
||||||
|
|
||||||
// Créer la feuille principale
|
|
||||||
const ws = XLSX.utils.aoa_to_sheet(infoData);
|
|
||||||
|
|
||||||
// Ajouter les données des inscrits
|
|
||||||
XLSX.utils.sheet_add_json(ws, data, { origin: -1 });
|
|
||||||
|
|
||||||
// Ajouter la feuille au workbook
|
|
||||||
XLSX.utils.book_append_sheet(wb, ws, 'Inscrits');
|
|
||||||
|
|
||||||
// Générer le buffer
|
|
||||||
const excelBuffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' });
|
|
||||||
|
|
||||||
return excelBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère un fichier PDF avec la liste des inscrits
|
|
||||||
*/
|
|
||||||
export function generatePDFExport(sequenceInfo: SequenceInfo): Buffer {
|
|
||||||
const doc = new jsPDF();
|
|
||||||
|
|
||||||
// Titre
|
|
||||||
doc.setFontSize(18);
|
|
||||||
doc.text('Liste des inscrits', 14, 20);
|
|
||||||
|
|
||||||
// Informations de séquence
|
|
||||||
doc.setFontSize(10);
|
|
||||||
let yPos = 35;
|
|
||||||
doc.text(`Formation : ${sequenceInfo.formationNom}`, 14, yPos);
|
|
||||||
yPos += 6;
|
|
||||||
doc.text(`Séquence : ${sequenceInfo.sequenceNom}`, 14, yPos);
|
|
||||||
yPos += 6;
|
|
||||||
doc.text(`Lieu : ${sequenceInfo.lieu}`, 14, yPos);
|
|
||||||
yPos += 6;
|
|
||||||
doc.text(`Public cible : ${sequenceInfo.publicCible === 'directeur' ? 'Directeurs' : sequenceInfo.publicCible === 'chef_service' ? 'Chefs de service' : 'Autre'}`, 14, yPos);
|
|
||||||
yPos += 6;
|
|
||||||
doc.text(`Nombre de dates : ${sequenceInfo.dates.length}`, 14, yPos);
|
|
||||||
yPos += 8;
|
|
||||||
|
|
||||||
// Dates de formation
|
|
||||||
doc.setFontSize(9);
|
|
||||||
sequenceInfo.dates.forEach(date => {
|
|
||||||
doc.text(`Date ${date.ordre} : ${date.dateDebut.toLocaleDateString('fr-FR', {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}`, 14, yPos);
|
|
||||||
yPos += 5;
|
|
||||||
});
|
|
||||||
|
|
||||||
yPos += 3;
|
|
||||||
doc.setFontSize(10);
|
|
||||||
doc.text(`Nombre d'inscrits : ${sequenceInfo.inscriptions.length}`, 14, yPos);
|
|
||||||
|
|
||||||
// Tableau des inscrits
|
|
||||||
const tableData = sequenceInfo.inscriptions.map(i => [
|
|
||||||
i.nom,
|
|
||||||
i.prenom,
|
|
||||||
i.email,
|
|
||||||
i.codeEtablissement,
|
|
||||||
i.fonction === 'directeur' ? 'Directeur' : i.fonction === 'chef_service' ? 'Chef de service' : 'Autre',
|
|
||||||
i.statut === 'confirmee' ? 'Confirmée' : i.statut === 'liste_attente' ? 'Liste d\'attente' : 'Annulée',
|
|
||||||
]);
|
|
||||||
|
|
||||||
autoTable(doc, {
|
|
||||||
startY: yPos + 10,
|
|
||||||
head: [['Nom', 'Prénom', 'Email', 'Code étab.', 'Fonction', 'Statut']],
|
|
||||||
body: tableData,
|
|
||||||
styles: { fontSize: 9 },
|
|
||||||
headStyles: { fillColor: [37, 99, 235] },
|
|
||||||
});
|
|
||||||
|
|
||||||
return Buffer.from(doc.output('arraybuffer'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère une feuille de présence PDF
|
|
||||||
*/
|
|
||||||
export function generateFeuillePresence(feuilleInfo: FeuillePresenceInfo): Buffer {
|
|
||||||
const doc = new jsPDF();
|
|
||||||
|
|
||||||
// Titre
|
|
||||||
doc.setFontSize(18);
|
|
||||||
doc.text('Feuille de présence', 14, 20);
|
|
||||||
|
|
||||||
// Informations de séquence
|
|
||||||
doc.setFontSize(10);
|
|
||||||
let yPos = 35;
|
|
||||||
doc.text(`Formation : ${feuilleInfo.formationNom}`, 14, yPos);
|
|
||||||
yPos += 6;
|
|
||||||
doc.text(`Séquence : ${feuilleInfo.sequenceNom}`, 14, yPos);
|
|
||||||
yPos += 6;
|
|
||||||
doc.text(`Lieu : ${feuilleInfo.lieu}`, 14, yPos);
|
|
||||||
yPos += 6;
|
|
||||||
doc.text(`Public cible : ${feuilleInfo.publicCible === 'directeur' ? 'Directeurs' : feuilleInfo.publicCible === 'chef_service' ? 'Chefs de service' : 'Autre'}`, 14, yPos);
|
|
||||||
yPos += 8;
|
|
||||||
|
|
||||||
// Dates de formation
|
|
||||||
doc.setFontSize(9);
|
|
||||||
feuilleInfo.dates.forEach(date => {
|
|
||||||
doc.text(`Date ${date.ordre} : ${date.dateDebut.toLocaleDateString('fr-FR', {
|
|
||||||
weekday: 'long',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})}`, 14, yPos);
|
|
||||||
yPos += 5;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tableau de présence
|
|
||||||
const tableData = feuilleInfo.apprenants.map(i => [
|
|
||||||
i.nom,
|
|
||||||
i.prenom,
|
|
||||||
i.codeEtablissement,
|
|
||||||
i.fonction === 'directeur' ? 'Directeur' : i.fonction === 'chef_service' ? 'Chef de service' : 'Autre',
|
|
||||||
'', // Colonne signature
|
|
||||||
]);
|
|
||||||
|
|
||||||
autoTable(doc, {
|
|
||||||
startY: yPos + 10,
|
|
||||||
head: [['Nom', 'Prénom', 'Code établissement', 'Fonction', 'Signature']],
|
|
||||||
body: tableData,
|
|
||||||
styles: { fontSize: 10, cellPadding: 5 },
|
|
||||||
headStyles: { fillColor: [37, 99, 235] },
|
|
||||||
columnStyles: {
|
|
||||||
4: { cellWidth: 40 }, // Largeur pour la colonne signature
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return Buffer.from(doc.output('arraybuffer'));
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
/**
|
|
||||||
* Module de génération de fichiers ICS (iCalendar) pour les invitations Outlook
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface ICSEvent {
|
|
||||||
summary: string;
|
|
||||||
description?: string;
|
|
||||||
location: string;
|
|
||||||
startDate: Date;
|
|
||||||
endDate: Date;
|
|
||||||
attendeeEmail: string;
|
|
||||||
attendeeName: string;
|
|
||||||
organizerEmail?: string;
|
|
||||||
organizerName?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formate une date au format iCalendar (YYYYMMDDTHHMMSSZ)
|
|
||||||
*/
|
|
||||||
function formatICSDate(date: Date): string {
|
|
||||||
const year = date.getUTCFullYear();
|
|
||||||
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(date.getUTCDate()).padStart(2, '0');
|
|
||||||
const hours = String(date.getUTCHours()).padStart(2, '0');
|
|
||||||
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
|
|
||||||
const seconds = String(date.getUTCSeconds()).padStart(2, '0');
|
|
||||||
|
|
||||||
return `${year}${month}${day}T${hours}${minutes}${seconds}Z`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère un UID unique pour l'événement
|
|
||||||
*/
|
|
||||||
function generateUID(): string {
|
|
||||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}@itinova.com`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Échappe les caractères spéciaux pour le format ICS
|
|
||||||
*/
|
|
||||||
function escapeICS(text: string): string {
|
|
||||||
return text
|
|
||||||
.replace(/\\/g, '\\\\')
|
|
||||||
.replace(/;/g, '\\;')
|
|
||||||
.replace(/,/g, '\\,')
|
|
||||||
.replace(/\n/g, '\\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère un fichier ICS pour une invitation Outlook
|
|
||||||
*/
|
|
||||||
export function generateICS(event: ICSEvent): string {
|
|
||||||
const now = new Date();
|
|
||||||
const uid = generateUID();
|
|
||||||
|
|
||||||
const icsContent = [
|
|
||||||
'BEGIN:VCALENDAR',
|
|
||||||
'VERSION:2.0',
|
|
||||||
'PRODID:-//Itinova//Formation Manager//FR',
|
|
||||||
'CALSCALE:GREGORIAN',
|
|
||||||
'METHOD:REQUEST',
|
|
||||||
'BEGIN:VEVENT',
|
|
||||||
`UID:${uid}`,
|
|
||||||
`DTSTAMP:${formatICSDate(now)}`,
|
|
||||||
`DTSTART:${formatICSDate(event.startDate)}`,
|
|
||||||
`DTEND:${formatICSDate(event.endDate)}`,
|
|
||||||
`SUMMARY:${escapeICS(event.summary)}`,
|
|
||||||
event.description ? `DESCRIPTION:${escapeICS(event.description)}` : '',
|
|
||||||
`LOCATION:${escapeICS(event.location)}`,
|
|
||||||
'STATUS:CONFIRMED',
|
|
||||||
'TRANSP:OPAQUE', // Marque comme "occupé" dans le calendrier
|
|
||||||
'SEQUENCE:0',
|
|
||||||
`ORGANIZER;CN=${escapeICS(event.organizerName || 'Formation Itinova')}:mailto:${event.organizerEmail || 'formation@itinova.com'}`,
|
|
||||||
`ATTENDEE;CN=${escapeICS(event.attendeeName)};RSVP=TRUE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT:mailto:${event.attendeeEmail}`,
|
|
||||||
'BEGIN:VALARM',
|
|
||||||
'TRIGGER:-P1D', // Rappel 1 jour avant
|
|
||||||
'ACTION:DISPLAY',
|
|
||||||
`DESCRIPTION:Rappel: ${escapeICS(event.summary)}`,
|
|
||||||
'END:VALARM',
|
|
||||||
'END:VEVENT',
|
|
||||||
'END:VCALENDAR',
|
|
||||||
].filter(line => line !== '').join('\r\n');
|
|
||||||
|
|
||||||
return icsContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère un fichier ICS pour une session de formation
|
|
||||||
*/
|
|
||||||
export function generateFormationICS(params: {
|
|
||||||
formationNom: string;
|
|
||||||
sessionNom: string;
|
|
||||||
dateDebut: Date;
|
|
||||||
dateFin: Date;
|
|
||||||
lieu: string;
|
|
||||||
apprenantNom: string;
|
|
||||||
apprenantPrenom: string;
|
|
||||||
apprenantEmail: string;
|
|
||||||
}): string {
|
|
||||||
return generateICS({
|
|
||||||
summary: `Formation: ${params.formationNom} - ${params.sessionNom}`,
|
|
||||||
description: `Vous êtes inscrit à la formation "${params.formationNom}".\n\nSession: ${params.sessionNom}\n\nMerci de vous présenter à l'heure indiquée.`,
|
|
||||||
location: params.lieu,
|
|
||||||
startDate: params.dateDebut,
|
|
||||||
endDate: params.dateFin,
|
|
||||||
attendeeEmail: params.apprenantEmail,
|
|
||||||
attendeeName: `${params.apprenantPrenom} ${params.apprenantNom}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,109 +1,13 @@
|
|||||||
import { COOKIE_NAME } from "@shared/const";
|
import { COOKIE_NAME } from "@shared/const";
|
||||||
import { getSessionCookieOptions } from "./_core/cookies";
|
import { getSessionCookieOptions } from "./_core/cookies";
|
||||||
import { systemRouter } from "./_core/systemRouter";
|
import { systemRouter } from "./_core/systemRouter";
|
||||||
import { publicProcedure, protectedProcedure, router } from "./_core/trpc";
|
import { publicProcedure, router } from "./_core/trpc";
|
||||||
import { parseLocalDateTime } from "./dateUtils";
|
|
||||||
import { z } from "zod";
|
|
||||||
import * as db from "./db";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
|
||||||
import { sendInscriptionConfirmation, sendGroupEmail } from "./emailService";
|
|
||||||
import { generateExcelExport, generatePDFExport, generateFeuillePresence } from "./exportService";
|
|
||||||
|
|
||||||
// Procédure admin uniquement
|
|
||||||
const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
|
|
||||||
if (ctx.user.role !== 'admin') {
|
|
||||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès réservé aux administrateurs' });
|
|
||||||
}
|
|
||||||
return next({ ctx });
|
|
||||||
});
|
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
|
// if you need to use socket.io, read and register route in server/_core/index.ts, all api should start with '/api/' so that the gateway can route correctly
|
||||||
system: systemRouter,
|
system: systemRouter,
|
||||||
auth: router({
|
auth: router({
|
||||||
me: publicProcedure.query(opts => opts.ctx.user),
|
me: publicProcedure.query(opts => opts.ctx.user),
|
||||||
|
|
||||||
register: publicProcedure
|
|
||||||
.input(z.object({
|
|
||||||
email: z.string().email(),
|
|
||||||
password: z.string().min(6),
|
|
||||||
name: z.string().optional(),
|
|
||||||
}))
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
const { hashPassword, generateToken } = await import("./_core/auth");
|
|
||||||
|
|
||||||
// Vérifier si l'utilisateur existe déjà
|
|
||||||
const existingUser = await db.getUserByEmail(input.email);
|
|
||||||
if (existingUser) {
|
|
||||||
throw new TRPCError({ code: 'CONFLICT', message: 'Un compte existe déjà avec cet email' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hasher le mot de passe
|
|
||||||
const hashedPassword = await hashPassword(input.password);
|
|
||||||
|
|
||||||
// Créer l'utilisateur
|
|
||||||
const userId = await db.createUser({
|
|
||||||
email: input.email,
|
|
||||||
password: hashedPassword,
|
|
||||||
name: input.name,
|
|
||||||
emailVerified: false,
|
|
||||||
role: 'user',
|
|
||||||
isActive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Générer un token JWT
|
|
||||||
const token = generateToken(userId);
|
|
||||||
|
|
||||||
// Définir le cookie de session
|
|
||||||
const cookieOptions = getSessionCookieOptions(ctx.req);
|
|
||||||
ctx.res.cookie(COOKIE_NAME, token, { ...cookieOptions, maxAge: 30 * 24 * 60 * 60 * 1000 }); // 30 jours
|
|
||||||
|
|
||||||
return { success: true, userId, token };
|
|
||||||
}),
|
|
||||||
|
|
||||||
login: publicProcedure
|
|
||||||
.input(z.object({
|
|
||||||
email: z.string().email(),
|
|
||||||
password: z.string(),
|
|
||||||
}))
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
const { verifyPassword, generateToken } = await import("./_core/auth");
|
|
||||||
|
|
||||||
// Récupérer l'utilisateur
|
|
||||||
const user = await db.getUserByEmail(input.email);
|
|
||||||
if (!user) {
|
|
||||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Email ou mot de passe incorrect' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier le compte actif
|
|
||||||
if (!user.isActive) {
|
|
||||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Votre compte a été désactivé' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier que le mot de passe existe
|
|
||||||
if (!user.password) {
|
|
||||||
console.error('[LOGIN] User has no password:', user.email);
|
|
||||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Email ou mot de passe incorrect' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier le mot de passe
|
|
||||||
const isValid = await verifyPassword(input.password, user.password);
|
|
||||||
if (!isValid) {
|
|
||||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Email ou mot de passe incorrect' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour le dernier login
|
|
||||||
await db.updateUserLastSignedIn(user.id);
|
|
||||||
|
|
||||||
// Générer un token JWT
|
|
||||||
const token = generateToken(user.id);
|
|
||||||
|
|
||||||
// Définir le cookie de session
|
|
||||||
const cookieOptions = getSessionCookieOptions(ctx.req);
|
|
||||||
ctx.res.cookie(COOKIE_NAME, token, { ...cookieOptions, maxAge: 30 * 24 * 60 * 60 * 1000 }); // 30 jours
|
|
||||||
|
|
||||||
return { success: true, user: { id: user.id, email: user.email, name: user.name, role: user.role }, token };
|
|
||||||
}),
|
|
||||||
|
|
||||||
logout: publicProcedure.mutation(({ ctx }) => {
|
logout: publicProcedure.mutation(({ ctx }) => {
|
||||||
const cookieOptions = getSessionCookieOptions(ctx.req);
|
const cookieOptions = getSessionCookieOptions(ctx.req);
|
||||||
ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 });
|
ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 });
|
||||||
@@ -113,706 +17,12 @@ export const appRouter = router({
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// ===== FORMATIONS =====
|
// TODO: add feature routers here, e.g.
|
||||||
formations: router({
|
// todo: router({
|
||||||
list: adminProcedure.query(async () => {
|
// list: protectedProcedure.query(({ ctx }) =>
|
||||||
return db.getFormations();
|
// db.getUserTodos(ctx.user.id)
|
||||||
}),
|
// ),
|
||||||
|
// }),
|
||||||
getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
|
||||||
return db.getFormationById(input.id);
|
|
||||||
}),
|
|
||||||
|
|
||||||
getByLien: publicProcedure.input(z.object({ lien: z.string() })).query(async ({ input }) => {
|
|
||||||
return db.getFormationByLien(input.lien);
|
|
||||||
}),
|
|
||||||
|
|
||||||
create: adminProcedure.input(z.object({
|
|
||||||
nom: z.string().min(1),
|
|
||||||
description: z.string().optional(),
|
|
||||||
lienUnique: z.string().min(1),
|
|
||||||
actif: z.boolean().optional(),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
await db.createFormation(input);
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
|
|
||||||
update: adminProcedure.input(z.object({
|
|
||||||
id: z.number(),
|
|
||||||
nom: z.string().min(1).optional(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
lienUnique: z.string().min(1).optional(),
|
|
||||||
actif: z.boolean().optional(),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
const { id, ...data } = input;
|
|
||||||
await db.updateFormation(id, data);
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
|
|
||||||
delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
|
|
||||||
await db.deleteFormation(input.id);
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
// ===== APPRENANTS =====
|
|
||||||
apprenants: router({
|
|
||||||
list: adminProcedure.query(async () => {
|
|
||||||
return db.getApprenants();
|
|
||||||
}),
|
|
||||||
|
|
||||||
getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
|
||||||
return db.getApprenantById(input.id);
|
|
||||||
}),
|
|
||||||
|
|
||||||
getByEmail: publicProcedure.input(z.object({ email: z.string() })).query(async ({ input }) => {
|
|
||||||
return db.getApprenantByEmail(input.email);
|
|
||||||
}),
|
|
||||||
|
|
||||||
create: adminProcedure.input(z.object({
|
|
||||||
nom: z.string().min(1),
|
|
||||||
prenom: z.string().min(1),
|
|
||||||
email: z.string().email(),
|
|
||||||
codeEtablissement: z.string().min(1),
|
|
||||||
fonction: z.enum(["directeur", "chef_service", "autre"]),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
await db.createApprenant(input);
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
|
|
||||||
update: adminProcedure.input(z.object({
|
|
||||||
id: z.number(),
|
|
||||||
nom: z.string().min(1).optional(),
|
|
||||||
prenom: z.string().min(1).optional(),
|
|
||||||
email: z.string().email().optional(),
|
|
||||||
codeEtablissement: z.string().min(1).optional(),
|
|
||||||
fonction: z.enum(["directeur", "chef_service", "autre"]).optional(),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
const { id, ...data } = input;
|
|
||||||
await db.updateApprenant(id, data);
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
|
|
||||||
delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
|
|
||||||
await db.deleteApprenant(input.id);
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
// ===== SÉQUENCES =====
|
|
||||||
sequences: router({
|
|
||||||
list: adminProcedure.query(async () => {
|
|
||||||
const seqs = await db.getSequences();
|
|
||||||
// Récupérer les dates pour chaque séquence
|
|
||||||
const sequencesAvecDates = await Promise.all(
|
|
||||||
seqs.map(async (seq) => {
|
|
||||||
const dates = await db.getDatesBySequence(seq.id);
|
|
||||||
return { ...seq, dates };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return sequencesAvecDates;
|
|
||||||
}),
|
|
||||||
|
|
||||||
listByFormation: publicProcedure.input(z.object({ formationId: z.number() })).query(async ({ input }) => {
|
|
||||||
const seqs = await db.getSequencesByFormation(input.formationId);
|
|
||||||
// Récupérer les dates et le nombre d'inscrits pour chaque séquence
|
|
||||||
const sequencesAvecDates = await Promise.all(
|
|
||||||
seqs.map(async (seq) => {
|
|
||||||
const dates = await db.getDatesBySequence(seq.id);
|
|
||||||
const nbInscrits = await db.countInscriptionsBySequence(seq.id, 'confirmee');
|
|
||||||
return { ...seq, dates, nbInscrits };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return sequencesAvecDates;
|
|
||||||
}),
|
|
||||||
|
|
||||||
getById: publicProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
|
||||||
const seq = await db.getSequenceById(input.id);
|
|
||||||
if (!seq) return null;
|
|
||||||
const dates = await db.getDatesBySequence(input.id);
|
|
||||||
return { ...seq, dates };
|
|
||||||
}),
|
|
||||||
|
|
||||||
create: adminProcedure.input(z.object({
|
|
||||||
formationId: z.number(),
|
|
||||||
nom: z.string().min(1),
|
|
||||||
lieu: z.string().min(1),
|
|
||||||
publicCible: z.enum(["directeur", "chef_service", "autre"]),
|
|
||||||
capaciteMax: z.number().default(12),
|
|
||||||
dateBlocage: z.string(),
|
|
||||||
statut: z.enum(["ouverte", "bloquee", "terminee"]).optional(),
|
|
||||||
dates: z.array(z.object({
|
|
||||||
dateDebut: z.string(),
|
|
||||||
dateFin: z.string(),
|
|
||||||
ordre: z.number(),
|
|
||||||
})).min(1).max(4),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
const { dates, ...sequenceData } = input;
|
|
||||||
|
|
||||||
// Créer la séquence
|
|
||||||
const result = await db.createSequence({
|
|
||||||
...sequenceData,
|
|
||||||
dateBlocage: parseLocalDateTime(sequenceData.dateBlocage),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Récupérer l'ID de la séquence créée
|
|
||||||
const sequenceId = Number(result[0].insertId);
|
|
||||||
|
|
||||||
// Créer les dates de formation
|
|
||||||
for (const date of dates) {
|
|
||||||
await db.createDateFormation({
|
|
||||||
sequenceId,
|
|
||||||
dateDebut: parseLocalDateTime(date.dateDebut),
|
|
||||||
dateFin: parseLocalDateTime(date.dateFin),
|
|
||||||
ordre: date.ordre,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, sequenceId };
|
|
||||||
}),
|
|
||||||
|
|
||||||
update: adminProcedure.input(z.object({
|
|
||||||
id: z.number(),
|
|
||||||
formationId: z.number(),
|
|
||||||
nom: z.string().min(1),
|
|
||||||
lieu: z.string().min(1),
|
|
||||||
publicCible: z.enum(["directeur", "chef_service", "autre"]),
|
|
||||||
capaciteMax: z.number(),
|
|
||||||
dateBlocage: z.string(),
|
|
||||||
statut: z.enum(["ouverte", "bloquee", "terminee"]),
|
|
||||||
dates: z.array(z.object({
|
|
||||||
dateDebut: z.string(),
|
|
||||||
dateFin: z.string(),
|
|
||||||
ordre: z.number(),
|
|
||||||
})).min(1).max(4),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
const { id, dates, ...sequenceData } = input;
|
|
||||||
|
|
||||||
console.log('[UPDATE SEQUENCE] Input reçu:', JSON.stringify(input, null, 2));
|
|
||||||
|
|
||||||
// Mettre à jour la séquence
|
|
||||||
const updateData: any = {
|
|
||||||
...sequenceData,
|
|
||||||
dateBlocage: parseLocalDateTime(sequenceData.dateBlocage),
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('[UPDATE SEQUENCE] UpdateData:', JSON.stringify(updateData, null, 2));
|
|
||||||
await db.updateSequence(id, updateData);
|
|
||||||
console.log('[UPDATE SEQUENCE] Séquence mise à jour avec succès');
|
|
||||||
|
|
||||||
// Supprimer les anciennes dates
|
|
||||||
await db.deleteDatesBySequence(id);
|
|
||||||
console.log('[UPDATE SEQUENCE] Anciennes dates supprimées');
|
|
||||||
|
|
||||||
// Créer les nouvelles dates
|
|
||||||
for (const date of dates) {
|
|
||||||
console.log('[UPDATE SEQUENCE] Création date:', date);
|
|
||||||
await db.createDateFormation({
|
|
||||||
sequenceId: id,
|
|
||||||
dateDebut: parseLocalDateTime(date.dateDebut),
|
|
||||||
dateFin: parseLocalDateTime(date.dateFin),
|
|
||||||
ordre: date.ordre,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log('[UPDATE SEQUENCE] Toutes les dates ont été créées');
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
|
|
||||||
delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
|
|
||||||
// Supprimer d'abord les dates associées
|
|
||||||
await db.deleteDatesBySequence(input.id);
|
|
||||||
// Puis supprimer la séquence
|
|
||||||
await db.deleteSequence(input.id);
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
// ===== INSCRIPTIONS =====
|
|
||||||
inscriptions: router({
|
|
||||||
listAll: adminProcedure.query(async () => {
|
|
||||||
return db.getAllInscriptions();
|
|
||||||
}),
|
|
||||||
|
|
||||||
listBySequence: adminProcedure.input(z.object({ sequenceId: z.number() })).query(async ({ input }) => {
|
|
||||||
return db.getInscriptionsBySequence(input.sequenceId);
|
|
||||||
}),
|
|
||||||
|
|
||||||
listByApprenant: publicProcedure.input(z.object({ apprenantId: z.number() })).query(async ({ input }) => {
|
|
||||||
return db.getInscriptionsByApprenant(input.apprenantId);
|
|
||||||
}),
|
|
||||||
|
|
||||||
checkExisting: publicProcedure.input(z.object({
|
|
||||||
apprenantId: z.number(),
|
|
||||||
sequenceId: z.number(),
|
|
||||||
})).query(async ({ input }) => {
|
|
||||||
return db.checkExistingInscription(input.apprenantId, input.sequenceId);
|
|
||||||
}),
|
|
||||||
|
|
||||||
inscrire: publicProcedure.input(z.object({
|
|
||||||
apprenantId: z.number(),
|
|
||||||
sequenceId: z.number(),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
// Vérifier si la séquence existe
|
|
||||||
const sequence = await db.getSequenceById(input.sequenceId);
|
|
||||||
if (!sequence) {
|
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si la séquence est bloquée
|
|
||||||
const now = new Date();
|
|
||||||
if (sequence.statut === 'bloquee' || now >= new Date(sequence.dateBlocage)) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'FORBIDDEN',
|
|
||||||
message: 'Les inscriptions sont fermées pour cette séquence (J-15 dépassé)'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si l'apprenant a déjà une inscription
|
|
||||||
const existing = await db.checkExistingInscription(input.apprenantId, input.sequenceId);
|
|
||||||
if (existing) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'CONFLICT',
|
|
||||||
message: 'Vous êtes déjà inscrit à cette séquence'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier la capacité
|
|
||||||
const nbInscrits = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee');
|
|
||||||
const statut = nbInscrits >= sequence.capaciteMax ? 'liste_attente' : 'confirmee';
|
|
||||||
|
|
||||||
await db.createInscription({
|
|
||||||
apprenantId: input.apprenantId,
|
|
||||||
sequenceId: input.sequenceId,
|
|
||||||
statut,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Envoyer l'email de confirmation avec invitations Outlook pour toutes les dates
|
|
||||||
const inscriptionSequence = await db.getSequenceById(input.sequenceId);
|
|
||||||
const inscriptionApprenant = await db.getApprenantById(input.apprenantId);
|
|
||||||
const inscriptionFormation = inscriptionSequence ? await db.getFormationById(inscriptionSequence.formationId) : null;
|
|
||||||
const dates = inscriptionSequence ? await db.getDatesBySequence(inscriptionSequence.id) : [];
|
|
||||||
|
|
||||||
if (inscriptionSequence && inscriptionApprenant && inscriptionFormation && dates.length > 0) {
|
|
||||||
// Utiliser la première date pour l'email principal
|
|
||||||
const premiereDate = dates[0];
|
|
||||||
|
|
||||||
await sendInscriptionConfirmation({
|
|
||||||
apprenantEmail: inscriptionApprenant.email,
|
|
||||||
apprenantNom: inscriptionApprenant.nom,
|
|
||||||
apprenantPrenom: inscriptionApprenant.prenom,
|
|
||||||
apprenantFonction: inscriptionApprenant.fonction,
|
|
||||||
formationNom: inscriptionFormation.nom,
|
|
||||||
sequenceNom: inscriptionSequence.nom,
|
|
||||||
dates: dates.map(d => ({
|
|
||||||
dateDebut: new Date(d.dateDebut),
|
|
||||||
dateFin: new Date(d.dateFin),
|
|
||||||
ordre: d.ordre,
|
|
||||||
})),
|
|
||||||
lieu: inscriptionSequence.lieu,
|
|
||||||
statut,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, statut };
|
|
||||||
}),
|
|
||||||
|
|
||||||
desinscrire: publicProcedure.input(z.object({
|
|
||||||
apprenantId: z.number(),
|
|
||||||
sequenceId: z.number(),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
// Vérifier si la séquence existe
|
|
||||||
const sequence = await db.getSequenceById(input.sequenceId);
|
|
||||||
if (!sequence) {
|
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si la séquence est bloquée
|
|
||||||
const now = new Date();
|
|
||||||
if (sequence.statut === 'bloquee' || now >= new Date(sequence.dateBlocage)) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: 'FORBIDDEN',
|
|
||||||
message: 'Les désinscriptions sont fermées pour cette séquence (J-15 dépassé)'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trouver l'inscription
|
|
||||||
const inscription = await db.checkExistingInscription(input.apprenantId, input.sequenceId);
|
|
||||||
if (!inscription) {
|
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Inscription introuvable' });
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.updateInscription(inscription.id, { statut: 'annulee' });
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
|
|
||||||
updateStatut: adminProcedure.input(z.object({
|
|
||||||
id: z.number(),
|
|
||||||
statut: z.enum(['confirmee', 'liste_attente', 'annulee']),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
await db.updateInscription(input.id, { statut: input.statut });
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
|
|
||||||
sendGroupEmail: adminProcedure.input(z.object({
|
|
||||||
sequenceId: z.number(),
|
|
||||||
type: z.enum(['teaser', 'rappel']),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
const sequence = await db.getSequenceById(input.sequenceId);
|
|
||||||
if (!sequence) {
|
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const formation = await db.getFormationById(sequence.formationId);
|
|
||||||
if (!formation) {
|
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const dates = await db.getDatesBySequence(sequence.id);
|
|
||||||
if (dates.length === 0) {
|
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Aucune date trouvée pour cette séquence' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const inscriptions = await db.getInscriptionsBySequence(input.sequenceId);
|
|
||||||
const recipients = inscriptions
|
|
||||||
.filter(i => i.inscription.statut === 'confirmee' && i.apprenant)
|
|
||||||
.map(i => ({
|
|
||||||
email: i.apprenant!.email,
|
|
||||||
prenom: i.apprenant!.prenom,
|
|
||||||
nom: i.apprenant!.nom,
|
|
||||||
fonction: i.apprenant!.fonction,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const result = await sendGroupEmail({
|
|
||||||
recipients,
|
|
||||||
formationNom: formation.nom,
|
|
||||||
sequenceNom: sequence.nom,
|
|
||||||
type: input.type,
|
|
||||||
dates: dates.map(d => ({
|
|
||||||
dateDebut: new Date(d.dateDebut),
|
|
||||||
dateFin: new Date(d.dateFin),
|
|
||||||
ordre: d.ordre,
|
|
||||||
})),
|
|
||||||
lieu: sequence.lieu,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}),
|
|
||||||
|
|
||||||
exportExcel: adminProcedure.input(z.object({ sequenceId: z.number() })).mutation(async ({ input }) => {
|
|
||||||
const sequence = await db.getSequenceById(input.sequenceId);
|
|
||||||
if (!sequence) throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
|
||||||
|
|
||||||
const formation = await db.getFormationById(sequence.formationId);
|
|
||||||
if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' });
|
|
||||||
|
|
||||||
const inscriptions = await db.getInscriptionsBySequence(input.sequenceId);
|
|
||||||
const dates = await db.getDatesBySequence(input.sequenceId);
|
|
||||||
|
|
||||||
const data = inscriptions
|
|
||||||
.filter(i => i.apprenant)
|
|
||||||
.map(i => ({
|
|
||||||
nom: i.apprenant!.nom,
|
|
||||||
prenom: i.apprenant!.prenom,
|
|
||||||
email: i.apprenant!.email,
|
|
||||||
codeEtablissement: i.apprenant!.codeEtablissement,
|
|
||||||
fonction: i.apprenant!.fonction,
|
|
||||||
statut: i.inscription.statut,
|
|
||||||
dateInscription: i.inscription.dateInscription,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const buffer = await generateExcelExport({
|
|
||||||
formationNom: formation.nom,
|
|
||||||
sequenceNom: sequence.nom,
|
|
||||||
dates: dates.map(d => ({
|
|
||||||
dateDebut: new Date(d.dateDebut),
|
|
||||||
dateFin: new Date(d.dateFin),
|
|
||||||
ordre: d.ordre,
|
|
||||||
})),
|
|
||||||
lieu: sequence.lieu,
|
|
||||||
publicCible: sequence.publicCible,
|
|
||||||
inscriptions: data,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
buffer: buffer.toString('base64'),
|
|
||||||
filename: `inscriptions_${sequence.nom.replace(/\s+/g, '_')}.xlsx`,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
exportPDF: adminProcedure.input(z.object({ sequenceId: z.number() })).mutation(async ({ input }) => {
|
|
||||||
const sequence = await db.getSequenceById(input.sequenceId);
|
|
||||||
if (!sequence) throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
|
||||||
|
|
||||||
const formation = await db.getFormationById(sequence.formationId);
|
|
||||||
if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' });
|
|
||||||
|
|
||||||
const inscriptions = await db.getInscriptionsBySequence(input.sequenceId);
|
|
||||||
const dates = await db.getDatesBySequence(input.sequenceId);
|
|
||||||
|
|
||||||
const data = inscriptions
|
|
||||||
.filter(i => i.apprenant)
|
|
||||||
.map(i => ({
|
|
||||||
nom: i.apprenant!.nom,
|
|
||||||
prenom: i.apprenant!.prenom,
|
|
||||||
email: i.apprenant!.email,
|
|
||||||
codeEtablissement: i.apprenant!.codeEtablissement,
|
|
||||||
fonction: i.apprenant!.fonction,
|
|
||||||
statut: i.inscription.statut,
|
|
||||||
dateInscription: i.inscription.dateInscription,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const buffer = await generatePDFExport({
|
|
||||||
formationNom: formation.nom,
|
|
||||||
sequenceNom: sequence.nom,
|
|
||||||
dates: dates.map(d => ({
|
|
||||||
dateDebut: new Date(d.dateDebut),
|
|
||||||
dateFin: new Date(d.dateFin),
|
|
||||||
ordre: d.ordre,
|
|
||||||
})),
|
|
||||||
lieu: sequence.lieu,
|
|
||||||
publicCible: sequence.publicCible,
|
|
||||||
inscriptions: data,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
buffer: buffer.toString('base64'),
|
|
||||||
filename: `inscriptions_${sequence.nom.replace(/\s+/g, '_')}.pdf`,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
exportFeuillePresence: adminProcedure.input(z.object({ sequenceId: z.number() })).mutation(async ({ input }) => {
|
|
||||||
const sequence = await db.getSequenceById(input.sequenceId);
|
|
||||||
if (!sequence) throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
|
||||||
|
|
||||||
const formation = await db.getFormationById(sequence.formationId);
|
|
||||||
if (!formation) throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' });
|
|
||||||
|
|
||||||
const inscriptions = await db.getInscriptionsBySequence(input.sequenceId);
|
|
||||||
const dates = await db.getDatesBySequence(input.sequenceId);
|
|
||||||
|
|
||||||
const data = inscriptions
|
|
||||||
.filter(i => i.inscription.statut === 'confirmee' && i.apprenant)
|
|
||||||
.map(i => ({
|
|
||||||
nom: i.apprenant!.nom,
|
|
||||||
prenom: i.apprenant!.prenom,
|
|
||||||
codeEtablissement: i.apprenant!.codeEtablissement,
|
|
||||||
fonction: i.apprenant!.fonction,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const buffer = await generateFeuillePresence({
|
|
||||||
formationNom: formation.nom,
|
|
||||||
sequenceNom: sequence.nom,
|
|
||||||
dates: dates.map(d => ({
|
|
||||||
dateDebut: new Date(d.dateDebut),
|
|
||||||
dateFin: new Date(d.dateFin),
|
|
||||||
ordre: d.ordre,
|
|
||||||
})),
|
|
||||||
lieu: sequence.lieu,
|
|
||||||
publicCible: sequence.publicCible,
|
|
||||||
apprenants: data,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
buffer: buffer.toString('base64'),
|
|
||||||
filename: `feuille_presence_${sequence.nom.replace(/\s+/g, '_')}.pdf`,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
// ===== GESTION DES UTILISATEURS =====
|
|
||||||
users: router({
|
|
||||||
list: adminProcedure.query(async () => {
|
|
||||||
return db.getAllUsers();
|
|
||||||
}),
|
|
||||||
|
|
||||||
getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
|
|
||||||
return db.getUserById(input.id);
|
|
||||||
}),
|
|
||||||
|
|
||||||
create: adminProcedure.input(z.object({
|
|
||||||
email: z.string().email(),
|
|
||||||
password: z.string().min(6),
|
|
||||||
name: z.string().optional(),
|
|
||||||
role: z.enum(["user", "admin"]).default("user"),
|
|
||||||
isActive: z.boolean().default(true),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
const { hashPassword } = await import("./_core/auth");
|
|
||||||
|
|
||||||
// Vérifier si l'utilisateur existe déjà
|
|
||||||
const existingUser = await db.getUserByEmail(input.email);
|
|
||||||
if (existingUser) {
|
|
||||||
throw new TRPCError({ code: 'CONFLICT', message: 'Un compte existe déjà avec cet email' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hasher le mot de passe
|
|
||||||
const hashedPassword = await hashPassword(input.password);
|
|
||||||
|
|
||||||
// Créer l'utilisateur
|
|
||||||
await db.createUser({
|
|
||||||
email: input.email,
|
|
||||||
password: hashedPassword,
|
|
||||||
name: input.name,
|
|
||||||
emailVerified: false,
|
|
||||||
role: input.role,
|
|
||||||
isActive: input.isActive,
|
|
||||||
});
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
|
|
||||||
update: adminProcedure.input(z.object({
|
|
||||||
id: z.number(),
|
|
||||||
name: z.string().optional(),
|
|
||||||
email: z.string().email().optional(),
|
|
||||||
role: z.enum(["user", "admin"]).optional(),
|
|
||||||
isActive: z.boolean().optional(),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
const { id, ...data } = input;
|
|
||||||
await db.updateUser(id, data);
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
|
|
||||||
toggleStatus: adminProcedure.input(z.object({
|
|
||||||
id: z.number(),
|
|
||||||
isActive: z.boolean(),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
await db.toggleUserStatus(input.id, input.isActive);
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
|
|
||||||
delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
|
|
||||||
await db.deleteUser(input.id);
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
|
|
||||||
requestPasswordReset: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
|
|
||||||
const user = await db.getUserById(input.id);
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("Utilisateur introuvable");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.email) {
|
|
||||||
throw new Error("Cet utilisateur n'a pas d'adresse email");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Générer un token unique
|
|
||||||
const crypto = await import('crypto');
|
|
||||||
const token = crypto.randomBytes(32).toString('hex');
|
|
||||||
|
|
||||||
// Définir l'expiration à 24h
|
|
||||||
const expiresAt = new Date();
|
|
||||||
expiresAt.setHours(expiresAt.getHours() + 24);
|
|
||||||
|
|
||||||
// Sauvegarder le token en base
|
|
||||||
await db.createPasswordResetToken(user.id, token, expiresAt);
|
|
||||||
|
|
||||||
// Générer le lien de réinitialisation
|
|
||||||
// TODO: Remplacer par l'URL réelle de votre application en production
|
|
||||||
const resetLink = `https://votre-domaine.com/reset-password?token=${token}`;
|
|
||||||
|
|
||||||
// Envoyer l'email
|
|
||||||
const emailService = await import('./emailService');
|
|
||||||
const emailSent = await emailService.sendPasswordResetEmail({
|
|
||||||
apprenantEmail: user.email,
|
|
||||||
apprenantNom: user.name || 'Utilisateur',
|
|
||||||
apprenantPrenom: '',
|
|
||||||
resetLink,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!emailSent) {
|
|
||||||
throw new Error("Échec de l'envoi de l'email");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, message: "Email de réinitialisation envoyé" };
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
// ===== GESTION DES TEMPLATES D'EMAILS =====
|
|
||||||
emailTemplates: router({
|
|
||||||
list: adminProcedure.query(async () => {
|
|
||||||
return db.getAllEmailTemplates();
|
|
||||||
}),
|
|
||||||
|
|
||||||
getByType: adminProcedure
|
|
||||||
.input(z.object({ type: z.string() }))
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
return db.getEmailTemplateByType(input.type);
|
|
||||||
}),
|
|
||||||
|
|
||||||
upsert: adminProcedure
|
|
||||||
.input(z.object({
|
|
||||||
type: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
logoUrl: z.string().nullable(),
|
|
||||||
primaryColor: z.string(),
|
|
||||||
headerBgColor: z.string(),
|
|
||||||
headerTextColor: z.string(),
|
|
||||||
headerTitle: z.string(),
|
|
||||||
footerText: z.string().nullable(),
|
|
||||||
active: z.boolean(),
|
|
||||||
}))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return db.upsertEmailTemplate(input);
|
|
||||||
}),
|
|
||||||
|
|
||||||
delete: adminProcedure
|
|
||||||
.input(z.object({ type: z.string() }))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
await db.deleteEmailTemplate(input.type);
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
|
|
||||||
initializeDefaults: adminProcedure.mutation(async () => {
|
|
||||||
await db.initializeDefaultEmailTemplates();
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
emailConfig: router({
|
|
||||||
get: adminProcedure.query(async () => {
|
|
||||||
return db.getActiveEmailConfig();
|
|
||||||
}),
|
|
||||||
|
|
||||||
upsert: adminProcedure
|
|
||||||
.input(z.object({
|
|
||||||
provider: z.string(),
|
|
||||||
apiKey: z.string().nullable(),
|
|
||||||
fromEmail: z.string().email(),
|
|
||||||
fromName: z.string(),
|
|
||||||
mode: z.enum(["simulation", "production"]),
|
|
||||||
domainVerified: z.boolean(),
|
|
||||||
}))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
await db.upsertEmailConfig(input);
|
|
||||||
return { success: true };
|
|
||||||
}),
|
|
||||||
|
|
||||||
testEmail: adminProcedure
|
|
||||||
.input(z.object({
|
|
||||||
toEmail: z.string().email(),
|
|
||||||
}))
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
// Importer le service d'envoi
|
|
||||||
const { sendEmail } = await import("./_core/emailSender");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const success = await sendEmail({
|
|
||||||
to: input.toEmail,
|
|
||||||
subject: "Test d'envoi d'email - Formation Manager Itinova",
|
|
||||||
html: `
|
|
||||||
<h1>Test réussi !</h1>
|
|
||||||
<p>Cet email de test a été envoyé avec succès depuis votre configuration SMTP.</p>
|
|
||||||
<p>Votre configuration d'envoi d'emails fonctionne correctement.</p>
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success, message: success ? "Email envoyé avec succès" : "Échec de l'envoi" };
|
|
||||||
} catch (error: any) {
|
|
||||||
return { success: false, message: error.message };
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import bcrypt from 'bcrypt';
|
|
||||||
|
|
||||||
const password = 'admin123';
|
|
||||||
const hash = '$2b$10$N9qo8uLOickgx2ZMRZoMye7FRNv8va91kpMQH6.OfzrcBizxDbvSK';
|
|
||||||
|
|
||||||
console.log('Testing bcrypt...');
|
|
||||||
console.log('Password:', password);
|
|
||||||
console.log('Hash:', hash);
|
|
||||||
|
|
||||||
bcrypt.compare(password, hash).then(result => {
|
|
||||||
console.log('Result:', result);
|
|
||||||
if (result) {
|
|
||||||
console.log('✅ Password is correct!');
|
|
||||||
} else {
|
|
||||||
console.log('❌ Password is incorrect!');
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('Error:', err);
|
|
||||||
});
|
|
||||||
292
todo.md
292
todo.md
@@ -1,292 +0,0 @@
|
|||||||
# Gestion des Formations Manager Itinova - TODO
|
|
||||||
|
|
||||||
## Phase 1 : Planification et initialisation
|
|
||||||
- [x] Initialiser le projet avec architecture full-stack
|
|
||||||
- [x] Créer le plan de développement
|
|
||||||
- [x] Créer le fichier todo.md
|
|
||||||
|
|
||||||
## Phase 2 : Modèle de données
|
|
||||||
- [x] Créer la table des formations (nom, description, lien unique)
|
|
||||||
- [x] Créer la table des sessions (dates, lieu, capacité max 12, statut blocage J-15)
|
|
||||||
- [x] Créer la table des inscriptions (apprenant, session, statut)
|
|
||||||
- [x] Créer la table des apprenants (nom, prénom, email, code établissement)
|
|
||||||
- [x] Implémenter les relations entre tables
|
|
||||||
- [x] Ajouter les contraintes d'unicité pour éviter les doubles inscriptions
|
|
||||||
|
|
||||||
## Phase 3 : Interface d'administration
|
|
||||||
- [x] Créer le dashboard administrateur avec navigation
|
|
||||||
- [x] Créer l'interface de gestion des formations (CRUD)
|
|
||||||
- [x] Créer l'interface de gestion des sessions (CRUD)
|
|
||||||
- [x] Créer l'interface de gestion des apprenants (CRUD)
|
|
||||||
- [x] Afficher les listes d'inscrits par session
|
|
||||||
- [x] Implémenter la gestion des rôles (admin DRH, responsable, stagiaire)
|
|
||||||
|
|
||||||
## Phase 4 : Système d'inscription
|
|
||||||
- [x] Créer la page d'inscription publique avec lien unique par formation
|
|
||||||
- [x] Implémenter la validation de capacité maximale (12 participants)
|
|
||||||
- [x] Implémenter la validation anti-double inscription
|
|
||||||
- [x] Implémenter le blocage des inscriptions/désinscriptions à J-15
|
|
||||||
- [x] Créer le système de liste d'attente si session complète
|
|
||||||
- [x] Afficher les sessions disponibles avec places restantes
|
|
||||||
|
|
||||||
## Phase 5 : Invitations et emails automatiques
|
|
||||||
- [x] Implémenter la génération d'invitations Outlook (.ics) avec statut " occupé "
|
|
||||||
- [x] Inclure le lieu de formation dans les invitations
|
|
||||||
- [x] Créer le système d'envoi d'emails automatiques
|
|
||||||
- [x] Créer le template email de confirmation d'inscription
|
|
||||||
- [x] Créer le template email teaser pour chaque groupe
|
|
||||||
- [x] Créer le template email de rappel à J-7
|
|
||||||
- [x] Implémenter l'envoi automatique des emails selon les événements
|
|
||||||
|
|
||||||
## Phase 6 : Exports
|
|
||||||
- [x] Implémenter l'export Excel des listes d'inscrits par session
|
|
||||||
- [x] Implémenter l'export PDF des listes d'inscrits par session
|
|
||||||
- [x] Créer la génération de feuille de présence PDF (nom, prénom, code établissement)
|
|
||||||
- [x] Ajouter les boutons d'export dans l'interface admin
|
|
||||||
|
|
||||||
## Phase 7 : Tests et documentation
|
|
||||||
- [x] Tester le flux complet d'inscription
|
|
||||||
- [x] Tester les validations et contraintes
|
|
||||||
- [x] Tester la génération des invitations et emails
|
|
||||||
- [x] Tester les exports PDF/Excel
|
|
||||||
- [x] Créer la documentation utilisateur
|
|
||||||
- [x] Créer la documentation d'administration
|
|
||||||
|
|
||||||
## P## Phase 8 : Livraison
|
|
||||||
- [x] Vérifier toutes les fonctionnalités
|
|
||||||
- [x] Créer un checkpoint final
|
|
||||||
- [x] Présenter l'application à l'utilisateur
|
|
||||||
|
|
||||||
## Corrections à apporter
|
|
||||||
|
|
||||||
- [x] Corriger la mise à jour de la liste des apprenants après inscription
|
|
||||||
- [x] Implémenter l'envoi effectif des emails de confirmation
|
|
||||||
|
|
||||||
## Nouvelles fonctionnalités
|
|
||||||
|
|
||||||
- [x] Ajouter des filtres de tri sur la page des sessions (date, lieu, places restantes)
|
|
||||||
- [x] Ajouter des filtres de recherche sur la page des formations
|
|
||||||
- [x] Ajouter des filtres de recherche sur la page des apprenants
|
|
||||||
|
|
||||||
- [x] Ajouter des filtres complets sur la page de gestion des inscrits (recherche, statut, établissement, tri par date)
|
|
||||||
|
|
||||||
- [x] Ajouter le champ "Fonction" (Directeur, Chef de service, Autre) dans la fiche apprenant
|
|
||||||
- [x] Mettre à jour le schéma de base de données
|
|
||||||
- [x] Mettre à jour les formulaires d'inscription et de gestion
|
|
||||||
- [x] Ajouter la fonction dans les exports PDF/Excel
|
|
||||||
|
|
||||||
## Améliorations supplémentaires
|
|
||||||
|
|
||||||
- [x] Ajouter un filtre par fonction sur la page des apprenants
|
|
||||||
- [x] Ajouter un filtre par fonction sur la page des inscrits
|
|
||||||
- [x] Créer des statistiques de répartition par fonction sur le tableau de bord
|
|
||||||
- [x] Ajouter des graphiques de visualisation des données
|
|
||||||
- [x] Personnaliser les templates d'emails avec la fonction de l'apprenant
|
|
||||||
|
|
||||||
## Refonte : Sessions → Séquences avec dates multiples
|
|
||||||
|
|
||||||
- [x] Renommer "Session" en "Séquence" dans le schéma de base de données
|
|
||||||
- [x] Créer une table pour gérer jusqu'à 4 dates par séquence
|
|
||||||
- [x] Mettre à jour toutes les procédures tRPC pour utiliser "séquence"
|
|
||||||
- [x] Renommer toutes les interfaces et pages (AdminSessions → AdminSequences)
|
|
||||||
- [x] Adapter le formulaire de création/édition pour gérer 4 dates
|
|
||||||
- [x] Mettre à jour la page d'inscription pour afficher toutes les dates d'une séquence
|
|
||||||
- [x] Modifier la logique d'inscription pour inscrire à toutes les dates
|
|
||||||
- [x] Mettre à jour les exports PDF/Excel avec toutes les dates
|
|
||||||
- [x] Adapter les emails pour inclure toutes les dates de la séquence
|
|
||||||
- [x] Mettre à jour les invitations Outlook pour créer 4 événements si nécessaire
|
|
||||||
- [x] Renommer partout dans l'interface utilisateur (labels, titres, descriptions)
|
|
||||||
|
|
||||||
## Corrections urgentes
|
|
||||||
|
|
||||||
- [x] Corriger l'erreur React "Cannot update a component while rendering" dans Home.tsx
|
|
||||||
|
|
||||||
- [x] Corriger le bug de modification des horaires des séquences
|
|
||||||
|
|
||||||
- [x] Corriger le décalage d'une heure lors de l'enregistrement des dates (15h00 saisi devient 16h00 en base)
|
|
||||||
- [x] Corriger la mise à jour du nombre d'inscrits sur la page d'inscription publique
|
|
||||||
|
|
||||||
- [x] Corriger le décalage d'une heure entre la saisie et l'affichage des dates de séquences
|
|
||||||
|
|
||||||
- [x] Corriger le décalage d'une heure à l'affichage des dates (problème de formatage lors de la récupération)
|
|
||||||
|
|
||||||
- [x] Corriger les fonctions formatDate pour gérer les objets Date JavaScript en plus des chaînes MySQL
|
|
||||||
|
|
||||||
- [x] Forcer le fuseau horaire Europe/Paris dans la connexion MySQL pour éliminer le décalage d'une heure
|
|
||||||
|
|
||||||
- [x] Corriger le décalage horaire persistant dans parseLocalDateTime (09:48 envoyé devient 10:48 en base)
|
|
||||||
|
|
||||||
- [x] Corriger l'erreur "toISOString is not a function" lors de la modification de séquences
|
|
||||||
|
|
||||||
- [x] Ajouter le champ "Public cible" dans la fiche formation avec liste déroulante (Directeur, Chef de service, Autre)
|
|
||||||
- [x] Mettre à jour le schéma de base de données pour les formations
|
|
||||||
- [x] Mettre à jour les formulaires de gestion des formations
|
|
||||||
- [x] Afficher le public cible dans les listes et exports
|
|
||||||
|
|
||||||
- [x] Déplacer le champ "Public cible" des formations vers les séquences
|
|
||||||
- [x] Retirer publicCible du schéma formations
|
|
||||||
- [x] Ajouter publicCible au schéma sequences
|
|
||||||
- [x] Mettre à jour les procédures tRPC pour sequences
|
|
||||||
- [x] Mettre à jour le formulaire de création/édition de séquences
|
|
||||||
- [x] Afficher le public cible dans la liste des séquences
|
|
||||||
- [x] Afficher le public cible par séquence sur la page d'inscription publique
|
|
||||||
|
|
||||||
- [x] Corriger le formulaire de modification des séquences pour afficher le champ Public cible
|
|
||||||
|
|
||||||
## Bugs critiques à corriger
|
|
||||||
|
|
||||||
- [x] La validation d'une inscription ne fonctionne pas (déjà corrigé dans la version restaurée)
|
|
||||||
- [x] Lorsqu'un apprenant s'inscrit, il n'apparaît pas dans la liste des apprenants (déjà corrigé dans la version restaurée)
|
|
||||||
|
|
||||||
## Nouvelle fonctionnalité - Alerte public cible
|
|
||||||
|
|
||||||
- [x] Analyser le code d'inscription pour identifier où ajouter l'alerte
|
|
||||||
- [x] Implémenter la logique de vérification du public cible dans le formulaire d'inscription
|
|
||||||
- [x] Afficher une alerte d'information (non bloquante) si le public cible ne correspond pas
|
|
||||||
- [x] Tester l'alerte avec différents scénarios (correspondance, non-correspondance)
|
|
||||||
|
|
||||||
## Inclusion du public cible dans les exports
|
|
||||||
|
|
||||||
- [x] Analyser le code des exports Excel et PDF
|
|
||||||
- [x] Ajouter le public cible dans l'export Excel des listes d'inscrits
|
|
||||||
- [x] Ajouter le public cible dans l'export PDF des listes d'inscrits
|
|
||||||
- [x] Ajouter le public cible dans l'export PDF des feuilles de présence
|
|
||||||
- [x] Tester tous les exports pour vérifier l'affichage correct
|
|
||||||
|
|
||||||
## Ajout du filtre par public cible
|
|
||||||
|
|
||||||
- [x] Analyser la page AdminSequences pour comprendre le système de filtres existant
|
|
||||||
- [x] Ajouter un état pour le filtre publicCible
|
|
||||||
- [x] Ajouter le composant Select pour le filtre publicCible dans l'interface
|
|
||||||
- [x] Implémenter la logique de filtrage par public cible
|
|
||||||
- [x] Tester le filtre avec différentes valeurs
|
|
||||||
|
|
||||||
## Badges de couleur pour le public cible
|
|
||||||
|
|
||||||
- [x] Créer un composant Badge pour afficher le public cible avec des couleurs distinctes
|
|
||||||
- [x] Intégrer le Badge dans le tableau des séquences (AdminSequences)
|
|
||||||
- [x] Intégrer le Badge dans la page des inscrits (AdminSequenceInscrits)
|
|
||||||
- [x] Tester l'affichage des badges avec différents publics cibles
|
|
||||||
|
|
||||||
## Rapport d'analyse public cible vs fonction appre- [x] Créer une nouvelle page AdminRapportPublicCible
|
|
||||||
- [x] Ajouter la route dans App.tsx
|
|
||||||
- [x] Ajouter un lien dans le menu de navigation (DashboardLayout- [x] Créer une procédure tRPC pour récupérer les données d'analyse
|
|
||||||
- [x] Implémenter la logique de comparaison public cible vs fonction apprenants
|
|
||||||
- [x] Afficher les statistiques et écarts dans l'interface
|
|
||||||
- [x] Tester le rapport avec différentes données
|
|
||||||
|
|
||||||
## Gestion des utilisateurs de l'application
|
|
||||||
|
|
||||||
- [x] Analyser le schéma actuel de la table users
|
|
||||||
- [x] Ajouter les champs nécessaires (statut actif/inactif, date de création compte)
|
|
||||||
- [x] Créer les procédures tRPC pour la gestion des utilisateurs (liste, création, modification, désactivation)
|
|
||||||
- [x] Créer la page AdminUsers avec tableau des utilisateurs
|
|
||||||
- [x] Implémenter le formulaire de création d'utilisateur
|
|
||||||
- [x] Implémenter le formulaire de modification d'utilisateur
|
|
||||||
- [x] Ajouter la fonctionnalité d'activation/désactivation des comptes
|
|
||||||
- [x] Ajouter des filtres de recherche (nom, email, rôle, statut)
|
|
||||||
- [x] Ajouter la gestion des rôles (admin, user)
|
|
||||||
- [x] Intégrer la page dans le menu de navigation
|
|
||||||
- [x] Tester toutes les fonctionnalités de gestion des utilisateurs
|
|
||||||
|
|
||||||
## Ajout de la fonctionnalité de création d'utilisateurs
|
|
||||||
|
|
||||||
- [x] Ajouter une procédure tRPC pour créer un nouvel utilisateur
|
|
||||||
- [x] Ajouter un bouton "Ajouter un utilisateur" dans la page AdminUsers
|
|
||||||
- [x] Créer un dialogue de création avec formulaire (nom, email, rôle)
|
|
||||||
- [x] Tester la création d'un nouvel utilisateur
|
|
||||||
|
|
||||||
## Réinitialisation de mot de passe
|
|
||||||
|
|
||||||
- [x] Créer une table password_reset_tokens dans le schéma
|
|
||||||
- [x] Ajouter les fonctions de gestion des tokens dans db.ts
|
|
||||||
- [x] Créer une procédure tRPC pour générer un token et envoyer l'email
|
|
||||||
- [x] Ajouter un bouton "Réinitialiser le mot de passe" dans AdminUsers
|
|
||||||
- [x] Créer un dialogue de confirmation pour la réinitialisation
|
|
||||||
- [x] Tester la génération de token et l'envoi d'email
|
|
||||||
|
|
||||||
## Correction affichage menu latéral
|
|
||||||
|
|
||||||
- [x] Intégrer la page AdminUsers dans le DashboardLayout
|
|
||||||
- [x] Vérifier l'affichage du menu latéral sur la page utilisateurs
|
|
||||||
|
|
||||||
## Correction de l'envoi d'emails pour réinitialisation de mot de passe
|
|
||||||
|
|
||||||
- [x] Analyser le service d'envoi d'emails actuel
|
|
||||||
- [x] Rechercher l'API d'envoi d'emails disponible dans Manus
|
|
||||||
- [x] Intégrer un vrai service d'envoi d'emails (Resend)
|
|
||||||
- [x] Créer la documentation de configuration EMAIL_SETUP.md
|
|
||||||
|
|
||||||
## Interface de personnalisation des templates d'emails
|
|
||||||
|
|
||||||
- [x] Créer la table email_templates dans le schéma
|
|
||||||
- [x] Créer les procédures backend pour CRUD des templates
|
|
||||||
- [x] Créer la page AdminEmailTemplates
|
|
||||||
- [x] Implémenter l'éditeur de template (logo, couleurs, textes)
|
|
||||||
- [x] Ajouter la prévisualisation en temps réel
|
|
||||||
- [x] Modifier le service d'envoi pour utiliser les templates personnalisés
|
|
||||||
- [x] Ajouter le lien dans le menu de navigation
|
|
||||||
- [x] Tester la personnalisation et l'envoi avec templates personnalisés
|
|
||||||
|
|
||||||
## Configuration SMTP via interface
|
|
||||||
|
|
||||||
- [x] Créer la table email_config dans le schéma
|
|
||||||
- [x] Créer les procédures backend pour CRUD de la configuration
|
|
||||||
- [x] Ajouter une procédure de test d'envoi d'email
|
|
||||||
- [x] Modifier le service d'envoi pour utiliser la config en base
|
|
||||||
- [x] Créer la page AdminEmailConfig
|
|
||||||
- [x] Implémenter le formulaire de configuration (API key, email expéditeur, mode)
|
|
||||||
- [x] Ajouter le test d'envoi immédiat avec feedback
|
|
||||||
- [x] Ajouter la validation de domaine
|
|
||||||
- [x] Ajouter le basculement simulation/production
|
|
||||||
- [x] Ajouter le lien dans le menu de navigation
|
|
||||||
- [x] Tester la configuration et l'envoi d'emails
|
|
||||||
|
|
||||||
## Remplacement OAuth Manus par authentification locale
|
|
||||||
|
|
||||||
- [x] Modifier le schéma de la table users (ajouter password, emailVerified, supprimer openId)
|
|
||||||
- [x] Installer bcrypt pour le hashage des mots de passe
|
|
||||||
- [x] Créer les helpers d'authentification (hashPassword, verifyPassword)
|
|
||||||
- [x] Modifier les procédures auth (login, register, logout)
|
|
||||||
- [x] Modifier le système de context pour utiliser JWT
|
|
||||||
- [x] Créer la page de connexion
|
|
||||||
- [x] Créer la page d'inscription
|
|
||||||
- [x] Modifier la page de gestion des utilisateurs (ajouter mot de passe)
|
|
||||||
- [x] Mettre à jour useAuth pour utiliser la nouvelle authentification
|
|
||||||
- [x] Tester le flux de déconnexion et reconnexion
|
|
||||||
- [x] Mettre à jour AdminUsers pour permettre la création d'utilisateurs avec mot de passe
|
|
||||||
- [x] Tester l'inscription de nouveaux utilisateurs
|
|
||||||
- [x] Vérifier que toutes les routes protégées nécessitent une authentification
|
|
||||||
- [x] Nettoyer le code (supprimer les références à openId si nécessaire)
|
|
||||||
- [ ] Créer un guide de déploiement sans OAuth
|
|
||||||
|
|
||||||
## Correction des erreurs de compilation
|
|
||||||
|
|
||||||
- [x] Corriger les doublons de fonctions dans server/db.ts
|
|
||||||
- [x] Installer jsonwebtoken et corriger les imports
|
|
||||||
- [x] Vérifier que le serveur démarre sans erreur
|
|
||||||
|
|
||||||
## Correction boucle de connexion
|
|
||||||
|
|
||||||
- [x] Diagnostiquer pourquoi l'utilisateur est redirigé vers la page de connexion en boucle
|
|
||||||
- [x] Vérifier que le cookie de session est correctement défini après connexion
|
|
||||||
- [x] Vérifier que le contexte tRPC récupère correctement l'utilisateur depuis le cookie
|
|
||||||
- [x] Corriger le problème de redirection (redirection directe vers /admin au lieu de /)
|
|
||||||
- [x] Tester la connexion complète
|
|
||||||
|
|
||||||
## Correction vérification de session après connexion
|
|
||||||
|
|
||||||
- [ ] Diagnostiquer pourquoi la session n'est pas reconnue après connexion
|
|
||||||
- [ ] Vérifier que le cookie est correctement défini et lu
|
|
||||||
- [ ] Vérifier la logique de vérification JWT dans le contexte tRPC
|
|
||||||
- [ ] Corriger la vérification de session
|
|
||||||
- [ ] Tester la connexion complète avec vérification de session
|
|
||||||
|
|
||||||
## Implémentation authentification hybride (cookies + Authorization header)
|
|
||||||
|
|
||||||
- [x] Modifier le contexte tRPC pour lire le token depuis le header Authorization en plus des cookies
|
|
||||||
- [x] Modifier les procédures login/register pour retourner le token au client
|
|
||||||
- [x] Stocker le token dans localStorage côté client
|
|
||||||
- [x] Modifier le client tRPC pour envoyer le token dans le header Authorization
|
|
||||||
- [x] Tester la connexion dans l'environnement de preview Manus
|
|
||||||
- [x] Vérifier que la solution fonctionne aussi en production
|
|
||||||
Reference in New Issue
Block a user