Checkpoint: Ajout de la fonctionnalité d'import Excel pour créer des formations et séquences en masse. Inclut le template Excel, la logique backend de parsing et validation, l'interface frontend avec prévisualisation, et la documentation utilisateur complète.
This commit is contained in:
320
GUIDE_IMPORT_EXCEL.md
Normal file
320
GUIDE_IMPORT_EXCEL.md
Normal file
@@ -0,0 +1,320 @@
|
||||
# Guide d'Import Excel - Formations et Séquences
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
La fonctionnalité d'import Excel permet de créer plusieurs formations et séquences en une seule opération à partir d'un fichier Excel. Cela facilite grandement la saisie initiale des données ou les mises à jour massives.
|
||||
|
||||
---
|
||||
|
||||
## Accès à la Fonctionnalité
|
||||
|
||||
Dans le menu de navigation, cliquez sur **Gestion** > **Import Excel**.
|
||||
|
||||
---
|
||||
|
||||
## Étapes d'Import
|
||||
|
||||
### 1. Télécharger le Template Excel
|
||||
|
||||
Cliquez sur le bouton **"Télécharger le template"** pour obtenir le fichier Excel pré-formaté.
|
||||
|
||||
Le template contient deux feuilles :
|
||||
- **Formations** : Pour créer les formations
|
||||
- **Séquences** : Pour créer les séquences liées aux formations
|
||||
|
||||
Chaque feuille contient :
|
||||
- Des en-têtes de colonnes
|
||||
- Des exemples de données
|
||||
- Des instructions détaillées en bas de feuille
|
||||
|
||||
### 2. Remplir le Fichier Excel
|
||||
|
||||
#### Feuille "Formations"
|
||||
|
||||
| Colonne | Description | Obligatoire | Valeurs possibles |
|
||||
|---------|-------------|-------------|-------------------|
|
||||
| Nom de la formation | Nom unique de la formation | ✅ Oui | Texte libre |
|
||||
| Description | Description détaillée | ✅ Oui | Texte libre |
|
||||
| Public cible | Public visé par la formation | ✅ Oui | `directeur`, `chef_service`, `tous`, `autre` |
|
||||
|
||||
**Exemple** :
|
||||
```
|
||||
Nom de la formation | Description | Public cible
|
||||
Formation manager Itinova | Formation complète pour les managers | directeur
|
||||
Formation Leadership | Développer ses compétences en leadership | chef_service
|
||||
Formation Gestion du temps | Optimiser son organisation quotidienne | tous
|
||||
```
|
||||
|
||||
#### Feuille "Séquences"
|
||||
|
||||
| Colonne | Description | Obligatoire | Format |
|
||||
|---------|-------------|-------------|--------|
|
||||
| Formation (nom exact) | Nom de la formation (doit exister dans la feuille Formations) | ✅ Oui | Texte exact |
|
||||
| Nom de la séquence | Nom unique de la séquence | ✅ Oui | Texte libre |
|
||||
| Lieu | Lieu de la formation | ✅ Oui | Texte libre |
|
||||
| Public cible | Public visé | ✅ Oui | `directeur`, `chef_service`, `tous`, `autre` |
|
||||
| Capacité max | Nombre maximum de participants | ✅ Oui | Nombre (12 par défaut) |
|
||||
| Date blocage (J-15) | Date de blocage des inscriptions | ✅ Oui | `YYYY-MM-DD HH:MM` |
|
||||
| Statut | Statut de la séquence | ✅ Oui | `ouverte`, `bloquee`, `terminee` |
|
||||
| Date 1 - Début | Première date de début | ✅ Oui | `YYYY-MM-DD HH:MM` |
|
||||
| Date 1 - Fin | Première date de fin | ✅ Oui | `YYYY-MM-DD HH:MM` |
|
||||
| Date 2 - Début | Deuxième date de début (optionnel) | ❌ Non | `YYYY-MM-DD HH:MM` |
|
||||
| Date 2 - Fin | Deuxième date de fin (optionnel) | ❌ Non | `YYYY-MM-DD HH:MM` |
|
||||
| Date 3 - Début | Troisième date de début (optionnel) | ❌ Non | `YYYY-MM-DD HH:MM` |
|
||||
| Date 3 - Fin | Troisième date de fin (optionnel) | ❌ Non | `YYYY-MM-DD HH:MM` |
|
||||
| Date 4 - Début | Quatrième date de début (optionnel) | ❌ Non | `YYYY-MM-DD HH:MM` |
|
||||
| Date 4 - Fin | Quatrième date de fin (optionnel) | ❌ Non | `YYYY-MM-DD HH:MM` |
|
||||
|
||||
**Exemple** :
|
||||
```
|
||||
Formation | Nom séquence | Lieu | Public cible | Capacité | Date blocage | Statut | Date 1 Début | Date 1 Fin
|
||||
Formation manager... | Groupe A - Janvier | Siège - grande salle| directeur | 12 | 2026-01-05 10:00 | ouverte | 2026-01-20 09:00| 2026-01-20 17:00
|
||||
```
|
||||
|
||||
### 3. Importer le Fichier
|
||||
|
||||
1. Cliquez sur **"Choisir un fichier"**
|
||||
2. Sélectionnez votre fichier Excel rempli
|
||||
3. Une **prévisualisation** s'affiche automatiquement
|
||||
|
||||
La prévisualisation montre :
|
||||
- ✅ Les formations qui seront créées
|
||||
- ✅ Les séquences qui seront créées
|
||||
- ⚠️ Les avertissements (si présents)
|
||||
- ❌ Les erreurs (si présentes)
|
||||
|
||||
4. Si tout est correct, cliquez sur **"Importer les données"**
|
||||
5. Un message de confirmation affiche le nombre de formations et séquences créées
|
||||
|
||||
---
|
||||
|
||||
## Validation des Données
|
||||
|
||||
Le système effectue plusieurs validations avant l'import :
|
||||
|
||||
### Validations des Formations
|
||||
- ✅ Le nom est obligatoire et non vide
|
||||
- ✅ La description est obligatoire et non vide
|
||||
- ✅ Le public cible doit être : `directeur`, `chef_service`, `tous` ou `autre`
|
||||
|
||||
### Validations des Séquences
|
||||
- ✅ Le nom de la formation doit correspondre EXACTEMENT à une formation de la feuille "Formations"
|
||||
- ✅ Le nom de la séquence est obligatoire
|
||||
- ✅ Le lieu est obligatoire
|
||||
- ✅ Le public cible doit être valide
|
||||
- ✅ La date de blocage est obligatoire et au bon format
|
||||
- ✅ Le statut doit être : `ouverte`, `bloquee` ou `terminee`
|
||||
- ✅ Au moins une date (Date 1) est obligatoire
|
||||
- ⚠️ Maximum 4 dates autorisées (les dates supplémentaires sont ignorées)
|
||||
|
||||
### Messages d'Erreur
|
||||
|
||||
Si des erreurs sont détectées, elles s'affichent avec le numéro de ligne concerné :
|
||||
|
||||
**Exemples** :
|
||||
- `Formation ligne 3: Le nom est obligatoire`
|
||||
- `Séquence ligne 5: Formation "Formation XYZ" non trouvée dans la feuille Formations`
|
||||
- `Séquence ligne 7: Public cible invalide (directeure)`
|
||||
|
||||
**Important** : L'import est bloqué tant qu'il reste des erreurs. Corrigez-les dans le fichier Excel et réimportez.
|
||||
|
||||
### Messages d'Avertissement
|
||||
|
||||
Les avertissements n'empêchent pas l'import mais signalent des points d'attention :
|
||||
|
||||
**Exemples** :
|
||||
- `Séquence ligne 8: Maximum 4 dates autorisées, les dates supplémentaires seront ignorées`
|
||||
|
||||
---
|
||||
|
||||
## Conseils et Bonnes Pratiques
|
||||
|
||||
### Format des Dates
|
||||
|
||||
✅ **Format correct** : `2026-01-20 09:00`
|
||||
- Année sur 4 chiffres
|
||||
- Mois sur 2 chiffres (01-12)
|
||||
- Jour sur 2 chiffres (01-31)
|
||||
- Heure sur 2 chiffres (00-23)
|
||||
- Minutes sur 2 chiffres (00-59)
|
||||
|
||||
❌ **Formats incorrects** :
|
||||
- `20/01/2026 09:00` (format français)
|
||||
- `01-20-2026 9:00` (format américain)
|
||||
- `2026-1-20 9:0` (chiffres manquants)
|
||||
|
||||
### Correspondance Formations-Séquences
|
||||
|
||||
Le nom de la formation dans la feuille "Séquences" doit correspondre **EXACTEMENT** au nom dans la feuille "Formations" :
|
||||
|
||||
✅ **Correct** :
|
||||
- Formations : `Formation manager Itinova`
|
||||
- Séquences : `Formation manager Itinova`
|
||||
|
||||
❌ **Incorrect** :
|
||||
- Formations : `Formation manager Itinova`
|
||||
- Séquences : `Formation Manager Itinova` (majuscule différente)
|
||||
- Séquences : `Formation manager` (nom tronqué)
|
||||
|
||||
**Astuce** : Utilisez le copier-coller pour éviter les erreurs de frappe.
|
||||
|
||||
### Public Cible
|
||||
|
||||
Les valeurs acceptées (insensibles à la casse) :
|
||||
- `directeur`
|
||||
- `chef_service` ou `chef de service`
|
||||
- `tous`
|
||||
- `autre`
|
||||
|
||||
### Capacité Maximale
|
||||
|
||||
- Valeur par défaut : **12 participants**
|
||||
- Peut être modifiée selon vos besoins
|
||||
- Doit être un nombre entier positif
|
||||
|
||||
### Dates Multiples
|
||||
|
||||
- Minimum : **1 date** (Date 1 obligatoire)
|
||||
- Maximum : **4 dates**
|
||||
- Laissez les colonnes vides pour les dates non utilisées
|
||||
- Les dates doivent être dans l'ordre chronologique (recommandé mais non obligatoire)
|
||||
|
||||
---
|
||||
|
||||
## Exemples Complets
|
||||
|
||||
### Exemple 1 : Formation Simple avec 1 Date
|
||||
|
||||
**Feuille Formations** :
|
||||
```
|
||||
Nom de la formation | Description | Public cible
|
||||
Initiation Excel | Formation de base sur Excel | tous
|
||||
```
|
||||
|
||||
**Feuille Séquences** :
|
||||
```
|
||||
Formation | Nom séquence | Lieu | Public | Capacité | Date blocage | Statut | Date 1 Début | Date 1 Fin
|
||||
Initiation Excel | Session Mars | Salle A | tous | 15 | 2026-03-01 10:00 | ouverte | 2026-03-16 09:00| 2026-03-16 17:00
|
||||
```
|
||||
|
||||
### Exemple 2 : Formation avec 4 Dates
|
||||
|
||||
**Feuille Formations** :
|
||||
```
|
||||
Nom de la formation | Description | Public cible
|
||||
Formation manager Itinova | Formation complète pour les managers | directeur
|
||||
```
|
||||
|
||||
**Feuille Séquences** :
|
||||
```
|
||||
Formation | Nom séquence | Lieu | Public | Capacité | Date blocage | Statut | Date 1 Début | Date 1 Fin | Date 2 Début | Date 2 Fin | Date 3 Début | Date 3 Fin | Date 4 Début | Date 4 Fin
|
||||
Formation manager Itinova | Groupe A | Siège - grande salle| directeur | 12 | 2026-01-05 10:00 | ouverte | 2026-01-20 09:00| 2026-01-20 17:00| 2026-01-27 09:00| 2026-01-27 17:00| 2026-02-03 09:00| 2026-02-03 17:00| 2026-02-10 09:00| 2026-02-10 17:00
|
||||
```
|
||||
|
||||
### Exemple 3 : Import Multiple
|
||||
|
||||
**Feuille Formations** :
|
||||
```
|
||||
Nom de la formation | Description | Public cible
|
||||
Formation manager Itinova | Formation complète pour les managers | directeur
|
||||
Formation Leadership | Développer ses compétences en leadership | chef_service
|
||||
Formation Gestion du temps | Optimiser son organisation quotidienne | tous
|
||||
```
|
||||
|
||||
**Feuille Séquences** :
|
||||
```
|
||||
Formation | Nom séquence | Lieu | Public | Capacité | Date blocage | Statut | Date 1 Début | Date 1 Fin
|
||||
Formation manager Itinova | Groupe A - Janvier | Salle A | directeur | 12 | 2026-01-05 10:00 | ouverte | 2026-01-20 09:00| 2026-01-20 17:00
|
||||
Formation manager Itinova | Groupe B - Février | Salle A | directeur | 12 | 2026-02-01 10:00 | ouverte | 2026-02-15 09:00| 2026-02-15 17:00
|
||||
Formation Leadership | Session Printemps | Salle B | chef_service | 15 | 2026-03-01 10:00 | ouverte | 2026-03-16 09:00| 2026-03-16 12:00
|
||||
Formation Gestion du temps| Session Été | En ligne | tous | 20 | 2026-06-01 10:00 | ouverte | 2026-06-15 14:00| 2026-06-15 17:00
|
||||
```
|
||||
|
||||
Ce fichier créera :
|
||||
- ✅ 3 formations
|
||||
- ✅ 4 séquences
|
||||
|
||||
---
|
||||
|
||||
## Dépannage
|
||||
|
||||
### Problème : "Erreur lors de la lecture du fichier"
|
||||
|
||||
**Causes possibles** :
|
||||
- Le fichier n'est pas au format Excel (.xlsx ou .xls)
|
||||
- Le fichier est corrompu
|
||||
- Les feuilles "Formations" ou "Séquences" sont manquantes ou renommées
|
||||
|
||||
**Solution** :
|
||||
- Utilisez le template fourni
|
||||
- Vérifiez que les noms des feuilles sont exactement "Formations" et "Séquences"
|
||||
- Réenregistrez le fichier au format .xlsx
|
||||
|
||||
### Problème : "Formation non trouvée"
|
||||
|
||||
**Cause** :
|
||||
- Le nom de la formation dans la feuille "Séquences" ne correspond pas exactement à celui de la feuille "Formations"
|
||||
|
||||
**Solution** :
|
||||
- Vérifiez l'orthographe exacte
|
||||
- Vérifiez les espaces en début/fin
|
||||
- Utilisez le copier-coller pour éviter les erreurs
|
||||
|
||||
### Problème : "Public cible invalide"
|
||||
|
||||
**Cause** :
|
||||
- La valeur saisie ne fait pas partie des valeurs autorisées
|
||||
|
||||
**Solution** :
|
||||
- Utilisez uniquement : `directeur`, `chef_service`, `tous` ou `autre`
|
||||
- Vérifiez l'orthographe (pas d'accent, pas de majuscule)
|
||||
|
||||
### Problème : "Date de blocage obligatoire"
|
||||
|
||||
**Cause** :
|
||||
- La colonne "Date blocage" est vide
|
||||
|
||||
**Solution** :
|
||||
- Remplissez la date au format `YYYY-MM-DD HH:MM`
|
||||
- Exemple : `2026-01-05 10:00`
|
||||
|
||||
---
|
||||
|
||||
## Limites et Restrictions
|
||||
|
||||
- **Format de fichier** : Uniquement .xlsx et .xls
|
||||
- **Taille maximale** : Pas de limite technique, mais privilégiez des imports de moins de 1000 lignes pour des performances optimales
|
||||
- **Dates par séquence** : Maximum 4 dates
|
||||
- **Encodage** : UTF-8 recommandé pour les caractères accentués
|
||||
- **Doublons** : L'import ne vérifie pas les doublons avec les données existantes. Vérifiez manuellement avant l'import.
|
||||
|
||||
---
|
||||
|
||||
## Après l'Import
|
||||
|
||||
Une fois l'import réussi :
|
||||
|
||||
1. **Vérifiez les données** :
|
||||
- Allez dans **Gestion** > **Formations** pour voir les formations créées
|
||||
- Allez dans **Gestion** > **Séquences** pour voir les séquences créées
|
||||
|
||||
2. **Complétez les informations manquantes** :
|
||||
- Formateurs (à assigner manuellement)
|
||||
- Informations complémentaires
|
||||
|
||||
3. **Activez les inscriptions** :
|
||||
- Les liens d'inscription sont générés automatiquement
|
||||
- Partagez-les avec les apprenants
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
En cas de problème :
|
||||
- Vérifiez que vous utilisez le template fourni
|
||||
- Consultez les messages d'erreur détaillés
|
||||
- Contactez le support : o.pareige@itinova.org
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour** : 25 novembre 2025
|
||||
264
NOTES_VERSION.md
Normal file
264
NOTES_VERSION.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Formation Manager Itinova - Notes de Version
|
||||
|
||||
**Version actuelle** : 9fffa8c7
|
||||
**Date** : 25 novembre 2025
|
||||
**Statut** : Production Ready
|
||||
|
||||
---
|
||||
|
||||
## 📋 Vue d'ensemble
|
||||
|
||||
Application complète de gestion des formations Manager Itinova avec système d'inscription en ligne, gestion administrative avancée, et automatisation des communications.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Fonctionnalités Principales
|
||||
|
||||
### 🎓 Gestion des Formations
|
||||
- Création et gestion des formations avec liens d'inscription uniques
|
||||
- Configuration du public cible (Directeur, Chef de service, Tous, Autre)
|
||||
- Suivi du statut et des descriptions détaillées
|
||||
- Interface CRUD complète avec filtres et recherche
|
||||
|
||||
### 📅 Gestion des Séquences
|
||||
- Support de jusqu'à 4 dates par séquence de formation
|
||||
- Gestion des lieux, formateurs et capacité maximale (12 participants)
|
||||
- Statuts : Ouverte, Bloquée, Terminée
|
||||
- Blocage automatique des inscriptions à J-15
|
||||
- Barre de progression colorée du taux de remplissage
|
||||
- Filtres avancés (formation, lieu, statut, public cible)
|
||||
- **Badge coloré** pour le public cible
|
||||
|
||||
### 📆 Vue Calendrier
|
||||
- **Vue mensuelle** avec affichage des séquences par jour
|
||||
- **Vue hebdomadaire** avec détails des séquences
|
||||
- **Vue journalière** pour une vision détaillée
|
||||
- Filtres par formation, lieu et public cible
|
||||
- **Export PDF** du calendrier mensuel
|
||||
- **Export iCal** pour partage avec formateurs (compatible Outlook/Google Calendar)
|
||||
- Code couleur par public cible
|
||||
- Navigation intuitive entre les périodes
|
||||
|
||||
### 👥 Gestion des Apprenants
|
||||
- Fiche complète (nom, prénom, email, établissement, fonction)
|
||||
- Fonction : Directeur, Chef de service, Autre
|
||||
- Historique des inscriptions
|
||||
- Filtres par établissement et fonction
|
||||
- Import/export des données
|
||||
|
||||
### 📝 Système d'Inscription Publique
|
||||
- Page d'inscription accessible via lien unique par formation
|
||||
- Affichage des séquences disponibles avec places restantes
|
||||
- Validation anti-double inscription
|
||||
- Blocage automatique à J-15
|
||||
- Liste d'attente si capacité maximale atteinte
|
||||
- **Alerte d'information** si le public cible ne correspond pas à la fonction de l'apprenant
|
||||
|
||||
### 📧 Templates d'Emails Personnalisables
|
||||
- **Éditeur WYSIWYG** pour personnaliser les templates
|
||||
- Configuration du logo, couleurs primaire/secondaire
|
||||
- Templates disponibles :
|
||||
- Confirmation d'inscription
|
||||
- Rappel à J-7
|
||||
- Teaser pour chaque groupe
|
||||
- Réinitialisation de mot de passe
|
||||
- Prévisualisation en temps réel
|
||||
- Variables dynamiques (nom, dates, lieu, etc.)
|
||||
|
||||
### ⚙️ Configuration SMTP
|
||||
- Interface de configuration complète
|
||||
- Support Resend API
|
||||
- Mode simulation/production
|
||||
- Test d'envoi immédiat avec feedback
|
||||
- Validation de domaine
|
||||
- Gestion sécurisée des clés API
|
||||
|
||||
### 📊 Exports et Rapports
|
||||
- **Export Excel** des listes d'inscrits
|
||||
- **Export PDF** des listes d'inscrits
|
||||
- **Feuilles de présence PDF** (nom, prénom, établissement, fonction)
|
||||
- **Invitations Outlook (.ics)** avec statut "occupé" et lieu
|
||||
- **Rapport d'analyse** : Public cible vs Fonction des apprenants
|
||||
- **Tableau de bord analytique** avec graphiques interactifs
|
||||
|
||||
### 👤 Gestion des Utilisateurs
|
||||
- Rôles : Admin, Utilisateur
|
||||
- Activation/désactivation des comptes
|
||||
- Réinitialisation de mot de passe par email
|
||||
- Filtres de recherche (nom, email, rôle, statut)
|
||||
- Authentification locale (sans OAuth Manus)
|
||||
|
||||
### 🔔 Système de Rappels
|
||||
- Configuration des rappels automatiques
|
||||
- Déclenchement à J-7, J-1 ou dates personnalisées
|
||||
- Envoi par email aux apprenants inscrits
|
||||
- Gestion des templates de rappel
|
||||
|
||||
### 🏢 Analyses et Statistiques
|
||||
- **Tableau de bord** avec indicateurs clés
|
||||
- **Répartition par fonction** (graphique circulaire)
|
||||
- **Répartition par établissement** (top 10)
|
||||
- **Rapport Public Cible** : analyse des écarts entre public cible et fonction réelle
|
||||
- Statistiques de remplissage des séquences
|
||||
- Suivi des inscriptions en temps réel
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Technologies Utilisées
|
||||
|
||||
### Frontend
|
||||
- **React 19** avec TypeScript
|
||||
- **Tailwind CSS 4** pour le styling
|
||||
- **shadcn/ui** pour les composants
|
||||
- **Wouter** pour le routing
|
||||
- **TanStack Query** pour la gestion d'état
|
||||
- **Recharts** pour les graphiques
|
||||
- **date-fns** pour la gestion des dates
|
||||
|
||||
### Backend
|
||||
- **Express 4** avec TypeScript
|
||||
- **tRPC 11** pour l'API type-safe
|
||||
- **Drizzle ORM** pour la base de données
|
||||
- **MySQL/TiDB** comme base de données
|
||||
- **Zod** pour la validation
|
||||
- **Superjson** pour la sérialisation
|
||||
|
||||
### Services
|
||||
- **Resend** pour l'envoi d'emails
|
||||
- **JWT** pour l'authentification
|
||||
- **bcrypt** pour le hachage des mots de passe
|
||||
|
||||
---
|
||||
|
||||
## 📦 Structure du Projet
|
||||
|
||||
```
|
||||
formation-manager-itinova/
|
||||
├── client/ # Application React frontend
|
||||
│ ├── src/
|
||||
│ │ ├── pages/ # Pages de l'application
|
||||
│ │ ├── components/ # Composants réutilisables
|
||||
│ │ ├── contexts/ # Contextes React
|
||||
│ │ └── lib/ # Utilitaires et configuration
|
||||
│ └── public/ # Assets statiques
|
||||
├── server/ # Application Express backend
|
||||
│ ├── routers.ts # Routes tRPC
|
||||
│ ├── db.ts # Fonctions base de données
|
||||
│ └── _core/ # Infrastructure
|
||||
├── drizzle/ # Schéma et migrations DB
|
||||
├── shared/ # Types et constantes partagés
|
||||
└── deployment-package/ # Scripts de déploiement
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Configuration Requise
|
||||
|
||||
### Variables d'Environnement Système
|
||||
- `DATABASE_URL` : Connexion MySQL/TiDB
|
||||
- `JWT_SECRET` : Secret pour les tokens JWT
|
||||
- `VITE_APP_TITLE` : Titre de l'application
|
||||
- `VITE_APP_LOGO` : URL du logo
|
||||
|
||||
### Configuration SMTP (via interface)
|
||||
- API Key Resend
|
||||
- Email expéditeur vérifié
|
||||
- Mode simulation/production
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Déploiement
|
||||
|
||||
### Prérequis
|
||||
- Node.js 22.x
|
||||
- MySQL 8.x ou TiDB
|
||||
- Serveur avec Nginx (recommandé)
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
# 1. Extraire l'archive
|
||||
tar -xzf formation-manager-backup-YYYYMMDD-HHMMSS.tar.gz
|
||||
cd formation-manager-itinova
|
||||
|
||||
# 2. Installer les dépendances
|
||||
pnpm install
|
||||
|
||||
# 3. Configurer les variables d'environnement
|
||||
cp .env.example .env
|
||||
# Éditer .env avec vos valeurs
|
||||
|
||||
# 4. Initialiser la base de données
|
||||
pnpm db:push
|
||||
|
||||
# 5. Démarrer l'application
|
||||
pnpm dev # Développement
|
||||
pnpm build # Production
|
||||
pnpm start # Serveur production
|
||||
```
|
||||
|
||||
### Scripts de Déploiement
|
||||
- `deploy.sh` : Installation complète automatisée
|
||||
- `update.sh` : Mise à jour rapide
|
||||
|
||||
---
|
||||
|
||||
## 📝 Dernières Corrections (Version 9fffa8c7)
|
||||
|
||||
### Bug Corrigé : Public Cible "Tous"
|
||||
- ✅ Ajout de "tous" dans l'enum publicCible du schéma
|
||||
- ✅ Mise à jour des validations Zod
|
||||
- ✅ Modification de la colonne en base de données
|
||||
- ✅ Test réussi de création/modification avec "Tous"
|
||||
|
||||
### Améliorations Calendrier (Version 625cbedf)
|
||||
- ✅ Filtres dynamiques (formation, lieu, public cible)
|
||||
- ✅ Vues multiples (mois, semaine, jour)
|
||||
- ✅ Export PDF et iCal
|
||||
- ✅ Navigation adaptative selon la vue
|
||||
|
||||
### Correction Affichage (Version 14aa8c5f)
|
||||
- ✅ Résolution de l'erreur React #31 au survol des vignettes
|
||||
- ✅ Gestion robuste des types Date/string
|
||||
- ✅ Clés React stables pour éviter les re-renders
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Sécurité
|
||||
|
||||
- Authentification JWT avec tokens sécurisés
|
||||
- Mots de passe hachés avec bcrypt
|
||||
- Protection CSRF sur toutes les routes
|
||||
- Validation des entrées avec Zod
|
||||
- Gestion des rôles (admin/utilisateur)
|
||||
- Tokens de réinitialisation avec expiration
|
||||
|
||||
---
|
||||
|
||||
## 📖 Documentation Complète
|
||||
|
||||
- `GUIDE_DEPLOIEMENT.md` : Guide d'installation pas à pas
|
||||
- `CONFIGURATION.md` : Documentation des variables d'environnement
|
||||
- `RELEASE_NOTES.md` : Notes de version détaillées
|
||||
- `README.md` : Vue d'ensemble du projet
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Prochaines Évolutions Suggérées
|
||||
|
||||
1. **Page Établissements** : Interface de gestion avec statistiques
|
||||
2. **Tableau de bord formateurs** : Vue dédiée aux formateurs
|
||||
3. **Notifications automatiques** : Rappels à J-7 et J-1
|
||||
4. **Drag & drop calendrier** : Déplacer les séquences visuellement
|
||||
5. **Vue imprimable optimisée** : PDF personnalisé avec logo et filtres
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- **Email** : o.pareige@itinova.org
|
||||
- **Documentation** : Consultez les fichiers PDF inclus
|
||||
- **Support Manus** : https://help.manus.im
|
||||
|
||||
---
|
||||
|
||||
**Développé avec ❤️ pour Itinova**
|
||||
204
README_SAUVEGARDE.md
Normal file
204
README_SAUVEGARDE.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# Sauvegarde Complète - Formation Manager Itinova
|
||||
|
||||
**Date de sauvegarde** : 25 novembre 2025
|
||||
**Version** : 9fffa8c7
|
||||
**Taille de l'archive** : 2.6 MB
|
||||
|
||||
---
|
||||
|
||||
## 📦 Contenu de la Sauvegarde
|
||||
|
||||
Cette archive contient l'intégralité du code source de l'application Formation Manager Itinova, incluant :
|
||||
|
||||
### Code Source
|
||||
- **Frontend React** (client/) - Interface utilisateur complète
|
||||
- **Backend Express** (server/) - API tRPC et logique métier
|
||||
- **Schéma de base de données** (drizzle/) - Définitions et migrations
|
||||
- **Types partagés** (shared/) - Interfaces TypeScript communes
|
||||
|
||||
### Documentation
|
||||
- **NOTES_VERSION.md** - Liste complète des fonctionnalités et historique
|
||||
- **GUIDE_DEPLOIEMENT.md** - Instructions d'installation détaillées
|
||||
- **CONFIGURATION.md** - Documentation des variables d'environnement
|
||||
- **RELEASE_NOTES.md** - Notes de version et changelog
|
||||
|
||||
### Scripts de Déploiement
|
||||
- **deploy.sh** - Script d'installation automatisée
|
||||
- **update.sh** - Script de mise à jour rapide
|
||||
- **package.json** - Dépendances et scripts npm
|
||||
|
||||
### Fichiers de Configuration
|
||||
- **drizzle.config.ts** - Configuration ORM
|
||||
- **tsconfig.json** - Configuration TypeScript
|
||||
- **vite.config.ts** - Configuration Vite
|
||||
- **.env.example** - Template des variables d'environnement
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Restauration Rapide
|
||||
|
||||
### 1. Extraire l'Archive
|
||||
```bash
|
||||
tar -xzf formation-manager-backup-20251125-030004.tar.gz
|
||||
cd formation-manager-itinova
|
||||
```
|
||||
|
||||
### 2. Installer les Dépendances
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 3. Configurer l'Environnement
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Éditer .env avec vos valeurs (DATABASE_URL, JWT_SECRET, etc.)
|
||||
```
|
||||
|
||||
### 4. Initialiser la Base de Données
|
||||
```bash
|
||||
pnpm db:push
|
||||
```
|
||||
|
||||
### 5. Démarrer l'Application
|
||||
```bash
|
||||
# Mode développement
|
||||
pnpm dev
|
||||
|
||||
# Mode production
|
||||
pnpm build
|
||||
pnpm start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Variables d'Environnement Essentielles
|
||||
|
||||
### Base de Données
|
||||
```env
|
||||
DATABASE_URL=mysql://user:password@host:port/database?ssl={"rejectUnauthorized":true}
|
||||
```
|
||||
|
||||
### Authentification
|
||||
```env
|
||||
JWT_SECRET=votre_secret_jwt_aleatoire_securise
|
||||
```
|
||||
|
||||
### Application
|
||||
```env
|
||||
VITE_APP_TITLE=Formation Manager Itinova
|
||||
VITE_APP_LOGO=/logo.svg
|
||||
```
|
||||
|
||||
### Email (Configuration via interface admin)
|
||||
- API Key Resend
|
||||
- Email expéditeur vérifié
|
||||
- Mode simulation/production
|
||||
|
||||
---
|
||||
|
||||
## 📋 Fonctionnalités Incluses
|
||||
|
||||
### ✅ Gestion Complète
|
||||
- Formations avec liens d'inscription uniques
|
||||
- Séquences avec jusqu'à 4 dates
|
||||
- Apprenants avec fonction et établissement
|
||||
- Utilisateurs avec rôles (admin/user)
|
||||
|
||||
### ✅ Calendrier Avancé
|
||||
- Vues mois/semaine/jour
|
||||
- Filtres dynamiques
|
||||
- Export PDF et iCal
|
||||
|
||||
### ✅ Communication
|
||||
- Templates d'emails personnalisables
|
||||
- Configuration SMTP via interface
|
||||
- Invitations Outlook automatiques
|
||||
- Rappels programmables
|
||||
|
||||
### ✅ Analyses
|
||||
- Tableau de bord avec graphiques
|
||||
- Rapport public cible vs fonction
|
||||
- Statistiques par établissement
|
||||
- Exports Excel/PDF
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Technologies
|
||||
|
||||
- **Frontend** : React 19, TypeScript, Tailwind CSS 4, shadcn/ui
|
||||
- **Backend** : Express 4, tRPC 11, Drizzle ORM
|
||||
- **Base de données** : MySQL 8.x / TiDB
|
||||
- **Email** : Resend API
|
||||
- **Authentification** : JWT + bcrypt
|
||||
|
||||
---
|
||||
|
||||
## 📊 Structure de la Base de Données
|
||||
|
||||
### Tables Principales
|
||||
- `users` - Utilisateurs de l'application
|
||||
- `formations` - Formations disponibles
|
||||
- `sequences` - Séquences de formation avec dates
|
||||
- `dates_formation` - Dates multiples par séquence
|
||||
- `apprenants` - Participants aux formations
|
||||
- `inscriptions` - Inscriptions des apprenants
|
||||
- `formateurs` - Formateurs assignés
|
||||
- `email_templates` - Templates d'emails personnalisables
|
||||
- `email_config` - Configuration SMTP
|
||||
- `password_reset_tokens` - Tokens de réinitialisation
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Compte Administrateur par Défaut
|
||||
|
||||
**Email** : admin@formation.local
|
||||
**Mot de passe** : Admin123!
|
||||
|
||||
⚠️ **Important** : Changez ce mot de passe immédiatement après la première connexion via le menu Gestion > Utilisateurs.
|
||||
|
||||
---
|
||||
|
||||
## 📞 Support et Contact
|
||||
|
||||
- **Email** : o.pareige@itinova.org
|
||||
- **Documentation** : Consultez les fichiers .md inclus
|
||||
- **Support Manus** : https://help.manus.im
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Mise à Jour
|
||||
|
||||
Pour mettre à jour l'application vers une nouvelle version :
|
||||
|
||||
```bash
|
||||
# 1. Sauvegarder la base de données
|
||||
mysqldump -u user -p database > backup.sql
|
||||
|
||||
# 2. Extraire la nouvelle version
|
||||
tar -xzf formation-manager-backup-NOUVELLE_VERSION.tar.gz
|
||||
|
||||
# 3. Installer les nouvelles dépendances
|
||||
pnpm install
|
||||
|
||||
# 4. Appliquer les migrations
|
||||
pnpm db:push
|
||||
|
||||
# 5. Redémarrer l'application
|
||||
pnpm build
|
||||
pnpm start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✨ Dernières Corrections
|
||||
|
||||
**Version 9fffa8c7** (25 novembre 2025)
|
||||
- ✅ Correction du bug public cible "Tous"
|
||||
- ✅ Ajout des vues calendrier (mois/semaine/jour)
|
||||
- ✅ Export PDF et iCal du calendrier
|
||||
- ✅ Filtres dynamiques du calendrier
|
||||
|
||||
---
|
||||
|
||||
**Sauvegarde créée automatiquement par Manus AI**
|
||||
**Prête pour déploiement en production** 🚀
|
||||
BIN
client/public/template_import_formations.xlsx
Normal file
BIN
client/public/template_import_formations.xlsx
Normal file
Binary file not shown.
@@ -20,6 +20,7 @@ import AdminCalendrier from "./pages/AdminCalendrier";
|
||||
import AdminRappels from "./pages/AdminRappels";
|
||||
import AdminAnalytics from "./pages/AdminAnalytics";
|
||||
import AdminEtablissements from "./pages/AdminEtablissements";
|
||||
import AdminImportExcel from "./pages/AdminImportExcel";
|
||||
import Inscription from "./pages/Inscription";
|
||||
import Login from "./pages/Login";
|
||||
|
||||
@@ -44,6 +45,7 @@ function Router() {
|
||||
<Route path={"/admin/rappels"} component={AdminRappels} />
|
||||
<Route path={"/admin/analytics"} component={AdminAnalytics} />
|
||||
<Route path={"/admin/etablissements"} component={AdminEtablissements} />
|
||||
<Route path={"/admin/import-excel"} component={AdminImportExcel} />
|
||||
<Route path={"/404"} component={NotFound} />
|
||||
{/* Final fallback route */}
|
||||
<Route component={NotFound} />
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
} from "@/components/ui/sidebar";
|
||||
import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const";
|
||||
import { useIsMobile } from "@/hooks/useMobile";
|
||||
import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, CalendarDays, Bell, BarChart3, UserCog, Mail, Settings, TrendingUp, Building2 } from "lucide-react";
|
||||
import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, CalendarDays, Bell, BarChart3, UserCog, Mail, Settings, TrendingUp, Building2, FileSpreadsheet } from "lucide-react";
|
||||
import { CSSProperties, useEffect, useRef, useState } from "react";
|
||||
import { useLocation } from "wouter";
|
||||
import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
|
||||
@@ -41,6 +41,7 @@ const menuSections = [
|
||||
{ icon: Calendar, label: "Séquences", path: "/admin/sequences" },
|
||||
{ icon: CalendarDays, label: "Calendrier", path: "/admin/calendrier" },
|
||||
{ icon: Users, label: "Apprenants", path: "/admin/apprenants" },
|
||||
{ icon: FileSpreadsheet, label: "Import Excel", path: "/admin/import-excel" },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
330
client/src/pages/AdminImportExcel.tsx
Normal file
330
client/src/pages/AdminImportExcel.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
import { useState } from "react";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Upload,
|
||||
Download,
|
||||
FileSpreadsheet,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
AlertTriangle,
|
||||
Loader2
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function AdminImportExcel() {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [preview, setPreview] = useState<any>(null);
|
||||
const [importing, setImporting] = useState(false);
|
||||
|
||||
const previewMutation = trpc.formations.previewExcel.useMutation();
|
||||
const importMutation = trpc.formations.importExcel.useMutation();
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFile = e.target.files?.[0];
|
||||
if (!selectedFile) return;
|
||||
|
||||
if (!selectedFile.name.endsWith('.xlsx') && !selectedFile.name.endsWith('.xls')) {
|
||||
toast.error("Format de fichier invalide. Veuillez sélectionner un fichier Excel (.xlsx ou .xls)");
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(selectedFile);
|
||||
setPreview(null);
|
||||
|
||||
// Lire le fichier et générer la prévisualisation
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (event) => {
|
||||
try {
|
||||
const base64 = event.target?.result as string;
|
||||
const fileBase64 = base64.split(',')[1]; // Enlever le préfixe data:...;base64,
|
||||
|
||||
const result = await previewMutation.mutateAsync({ fileBase64 });
|
||||
setPreview(result);
|
||||
|
||||
if (result.validation.errors.length > 0) {
|
||||
toast.error(`${result.validation.errors.length} erreur(s) détectée(s) dans le fichier`);
|
||||
} else {
|
||||
toast.success("Fichier valide ! Vous pouvez procéder à l'import.");
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(`Erreur lors de la lecture du fichier: ${error.message}`);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(selectedFile);
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!file) return;
|
||||
|
||||
setImporting(true);
|
||||
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (event) => {
|
||||
try {
|
||||
const base64 = event.target?.result as string;
|
||||
const fileBase64 = base64.split(',')[1];
|
||||
|
||||
const result = await importMutation.mutateAsync({ fileBase64 });
|
||||
|
||||
if (result.success) {
|
||||
toast.success(
|
||||
`Import réussi ! ${result.formationsCreated} formation(s) et ${result.sequencesCreated} séquence(s) créée(s).`
|
||||
);
|
||||
setFile(null);
|
||||
setPreview(null);
|
||||
// Réinitialiser l'input file
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
} else {
|
||||
toast.error(`Import échoué: ${result.errors.join(', ')}`);
|
||||
}
|
||||
|
||||
setImporting(false);
|
||||
} catch (error: any) {
|
||||
toast.error(`Erreur lors de l'import: ${error.message}`);
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} catch (error: any) {
|
||||
toast.error(`Erreur: ${error.message}`);
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadTemplate = () => {
|
||||
// Télécharger le template depuis /public
|
||||
const link = document.createElement('a');
|
||||
link.href = '/template_import_formations.xlsx';
|
||||
link.download = 'template_import_formations.xlsx';
|
||||
link.click();
|
||||
toast.success("Template téléchargé !");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">Import Excel - Formations et Séquences</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Importez plusieurs formations et séquences en une seule fois à partir d'un fichier Excel
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileSpreadsheet className="h-5 w-5" />
|
||||
Instructions
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Suivez ces étapes pour importer vos données
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-primary-foreground text-sm font-bold">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Téléchargez le template Excel</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Le template contient deux feuilles : "Formations" et "Séquences" avec des exemples
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-2"
|
||||
onClick={handleDownloadTemplate}
|
||||
>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Télécharger le template
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-primary-foreground text-sm font-bold">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Remplissez le fichier Excel</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Suivez les instructions dans le fichier. Les colonnes obligatoires sont marquées.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-primary text-primary-foreground text-sm font-bold">
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Importez le fichier</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Sélectionnez votre fichier ci-dessous. Une prévisualisation s'affichera avant l'import.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Upload */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Sélectionner un fichier Excel</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-4">
|
||||
<input
|
||||
id="file-input"
|
||||
type="file"
|
||||
accept=".xlsx,.xls"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
<label htmlFor="file-input">
|
||||
<Button variant="outline" asChild>
|
||||
<span>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
Choisir un fichier
|
||||
</span>
|
||||
</Button>
|
||||
</label>
|
||||
{file && (
|
||||
<div className="flex items-center gap-2">
|
||||
<FileSpreadsheet className="h-5 w-5 text-green-600" />
|
||||
<span className="text-sm font-medium">{file.name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Prévisualisation */}
|
||||
{preview && (
|
||||
<div className="space-y-6">
|
||||
{/* Erreurs et Avertissements */}
|
||||
{preview.validation.errors.length > 0 && (
|
||||
<Alert variant="destructive">
|
||||
<XCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<p className="font-semibold mb-2">Erreurs détectées :</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
{preview.validation.errors.map((error: string, index: number) => (
|
||||
<li key={index} className="text-sm">{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{preview.validation.warnings.length > 0 && (
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<p className="font-semibold mb-2">Avertissements :</p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
{preview.validation.warnings.map((warning: string, index: number) => (
|
||||
<li key={index} className="text-sm">{warning}</li>
|
||||
))}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Formations */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
||||
Formations ({preview.formations.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{preview.formations.map((formation: any, index: number) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold">{formation.nom}</h4>
|
||||
<p className="text-sm text-muted-foreground mt-1">{formation.description}</p>
|
||||
</div>
|
||||
<Badge variant="outline">{formation.publicCible}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Séquences */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-5 w-5 text-green-600" />
|
||||
Séquences ({preview.sequences.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{preview.sequences.map((sequence: any, index: number) => (
|
||||
<div key={index} className="border rounded-lg p-4">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold">{sequence.nom}</h4>
|
||||
<p className="text-sm text-muted-foreground">Formation: {sequence.formationNom}</p>
|
||||
</div>
|
||||
<Badge variant="outline">{sequence.statut}</Badge>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Lieu:</span> {sequence.lieu}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Capacité:</span> {sequence.capaciteMax}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Public:</span> {sequence.publicCible}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Dates:</span> {sequence.dates.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Bouton d'import */}
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setFile(null);
|
||||
setPreview(null);
|
||||
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
}}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
disabled={preview.validation.errors.length > 0 || importing}
|
||||
>
|
||||
{importing && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
|
||||
{importing ? "Import en cours..." : "Importer les données"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
309
server/importExcel.ts
Normal file
309
server/importExcel.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import * as XLSX from 'xlsx';
|
||||
import { getDb } from './db';
|
||||
import { formations, sequences, datesFormation } from '../drizzle/schema';
|
||||
|
||||
export interface FormationImport {
|
||||
nom: string;
|
||||
description: string;
|
||||
publicCible: 'directeur' | 'chef_service' | 'tous' | 'autre';
|
||||
}
|
||||
|
||||
export interface SequenceImport {
|
||||
formationNom: string;
|
||||
nom: string;
|
||||
lieu: string;
|
||||
publicCible: 'directeur' | 'chef_service' | 'tous' | 'autre';
|
||||
capaciteMax: number;
|
||||
dateBlocage: string;
|
||||
statut: 'ouverte' | 'bloquee' | 'terminee';
|
||||
dates: Array<{ debut: string; fin: string }>;
|
||||
}
|
||||
|
||||
export interface ImportResult {
|
||||
success: boolean;
|
||||
formationsCreated: number;
|
||||
sequencesCreated: number;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse un fichier Excel et retourne les données structurées
|
||||
*/
|
||||
export function parseExcelFile(buffer: Buffer): { formations: FormationImport[]; sequences: SequenceImport[] } {
|
||||
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
||||
|
||||
const formations: FormationImport[] = [];
|
||||
const sequences: SequenceImport[] = [];
|
||||
|
||||
// Parser la feuille "Formations"
|
||||
if (workbook.SheetNames.includes('Formations')) {
|
||||
const sheet = workbook.Sheets['Formations'];
|
||||
const data = XLSX.utils.sheet_to_json<any>(sheet, { header: 1 });
|
||||
|
||||
// Ignorer la première ligne (en-têtes) et les lignes vides
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const row = data[i];
|
||||
if (!row || row.length === 0 || !row[0]) continue;
|
||||
|
||||
// Arrêter si on atteint les instructions
|
||||
if (String(row[0]).toUpperCase().includes('INSTRUCTIONS')) break;
|
||||
|
||||
formations.push({
|
||||
nom: String(row[0] || '').trim(),
|
||||
description: String(row[1] || '').trim(),
|
||||
publicCible: normalizePublicCible(String(row[2] || '').trim()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Parser la feuille "Séquences"
|
||||
if (workbook.SheetNames.includes('Séquences')) {
|
||||
const sheet = workbook.Sheets['Séquences'];
|
||||
const data = XLSX.utils.sheet_to_json<any>(sheet, { header: 1 });
|
||||
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const row = data[i];
|
||||
if (!row || row.length === 0 || !row[0]) continue;
|
||||
|
||||
// Arrêter si on atteint les instructions
|
||||
if (String(row[0]).toUpperCase().includes('INSTRUCTIONS')) break;
|
||||
|
||||
const dates: Array<{ debut: string; fin: string }> = [];
|
||||
|
||||
// Parser les 4 dates possibles (colonnes 7-14)
|
||||
for (let d = 0; d < 4; d++) {
|
||||
const debutIdx = 7 + (d * 2);
|
||||
const finIdx = 8 + (d * 2);
|
||||
|
||||
if (row[debutIdx] && row[finIdx]) {
|
||||
dates.push({
|
||||
debut: parseExcelDate(row[debutIdx]),
|
||||
fin: parseExcelDate(row[finIdx]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sequences.push({
|
||||
formationNom: String(row[0] || '').trim(),
|
||||
nom: String(row[1] || '').trim(),
|
||||
lieu: String(row[2] || '').trim(),
|
||||
publicCible: normalizePublicCible(String(row[3] || '').trim()),
|
||||
capaciteMax: parseInt(String(row[4] || '12')),
|
||||
dateBlocage: parseExcelDate(row[5]),
|
||||
statut: normalizeStatut(String(row[6] || '').trim()),
|
||||
dates,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { formations, sequences };
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide les données importées
|
||||
*/
|
||||
export function validateImportData(
|
||||
formations: FormationImport[],
|
||||
sequences: SequenceImport[]
|
||||
): { valid: boolean; errors: string[]; warnings: string[] } {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Valider les formations
|
||||
formations.forEach((formation, index) => {
|
||||
if (!formation.nom) {
|
||||
errors.push(`Formation ligne ${index + 2}: Le nom est obligatoire`);
|
||||
}
|
||||
if (!formation.description) {
|
||||
errors.push(`Formation ligne ${index + 2}: La description est obligatoire`);
|
||||
}
|
||||
if (!['directeur', 'chef_service', 'tous', 'autre'].includes(formation.publicCible)) {
|
||||
errors.push(`Formation ligne ${index + 2}: Public cible invalide (${formation.publicCible})`);
|
||||
}
|
||||
});
|
||||
|
||||
// Créer un set des noms de formations pour validation des séquences
|
||||
const formationNames = new Set(formations.map(f => f.nom.toLowerCase()));
|
||||
|
||||
// Valider les séquences
|
||||
sequences.forEach((sequence, index) => {
|
||||
if (!sequence.formationNom) {
|
||||
errors.push(`Séquence ligne ${index + 2}: Le nom de la formation est obligatoire`);
|
||||
} else if (!formationNames.has(sequence.formationNom.toLowerCase())) {
|
||||
errors.push(`Séquence ligne ${index + 2}: Formation "${sequence.formationNom}" non trouvée dans la feuille Formations`);
|
||||
}
|
||||
|
||||
if (!sequence.nom) {
|
||||
errors.push(`Séquence ligne ${index + 2}: Le nom est obligatoire`);
|
||||
}
|
||||
if (!sequence.lieu) {
|
||||
errors.push(`Séquence ligne ${index + 2}: Le lieu est obligatoire`);
|
||||
}
|
||||
if (!['directeur', 'chef_service', 'tous', 'autre'].includes(sequence.publicCible)) {
|
||||
errors.push(`Séquence ligne ${index + 2}: Public cible invalide`);
|
||||
}
|
||||
if (!sequence.dateBlocage) {
|
||||
errors.push(`Séquence ligne ${index + 2}: La date de blocage est obligatoire`);
|
||||
}
|
||||
if (!['ouverte', 'bloquee', 'terminee'].includes(sequence.statut)) {
|
||||
errors.push(`Séquence ligne ${index + 2}: Statut invalide`);
|
||||
}
|
||||
if (sequence.dates.length === 0) {
|
||||
errors.push(`Séquence ligne ${index + 2}: Au moins une date est obligatoire`);
|
||||
}
|
||||
if (sequence.dates.length > 4) {
|
||||
warnings.push(`Séquence ligne ${index + 2}: Maximum 4 dates autorisées, les dates supplémentaires seront ignorées`);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Importe les données dans la base de données
|
||||
*/
|
||||
export async function importToDatabase(
|
||||
formations: FormationImport[],
|
||||
sequences: SequenceImport[]
|
||||
): Promise<ImportResult> {
|
||||
const db = await getDb();
|
||||
if (!db) {
|
||||
return {
|
||||
success: false,
|
||||
formationsCreated: 0,
|
||||
sequencesCreated: 0,
|
||||
errors: ['Connexion à la base de données impossible'],
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
let formationsCreated = 0;
|
||||
let sequencesCreated = 0;
|
||||
|
||||
try {
|
||||
// Map pour stocker les IDs des formations créées
|
||||
const formationIdMap = new Map<string, number>();
|
||||
|
||||
// Importer les formations
|
||||
for (const formation of formations) {
|
||||
try {
|
||||
const result = await db.insert(formations).values({
|
||||
nom: formation.nom,
|
||||
description: formation.description,
|
||||
publicCible: formation.publicCible,
|
||||
lienInscription: generateUniqueLien(),
|
||||
});
|
||||
|
||||
const formationId = Number(result.insertId);
|
||||
formationIdMap.set(formation.nom.toLowerCase(), formationId);
|
||||
formationsCreated++;
|
||||
} catch (error: any) {
|
||||
errors.push(`Erreur lors de la création de la formation "${formation.nom}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Importer les séquences
|
||||
for (const sequence of sequences) {
|
||||
try {
|
||||
const formationId = formationIdMap.get(sequence.formationNom.toLowerCase());
|
||||
if (!formationId) {
|
||||
errors.push(`Formation "${sequence.formationNom}" non trouvée pour la séquence "${sequence.nom}"`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = await db.insert(sequences).values({
|
||||
formationId,
|
||||
nom: sequence.nom,
|
||||
lieu: sequence.lieu,
|
||||
publicCible: sequence.publicCible,
|
||||
capaciteMax: sequence.capaciteMax,
|
||||
dateBlocage: new Date(sequence.dateBlocage),
|
||||
statut: sequence.statut,
|
||||
});
|
||||
|
||||
const sequenceId = Number(result.insertId);
|
||||
|
||||
// Insérer les dates de formation
|
||||
for (const date of sequence.dates.slice(0, 4)) {
|
||||
await db.insert(datesFormation).values({
|
||||
sequenceId,
|
||||
dateDebut: new Date(date.debut),
|
||||
dateFin: new Date(date.fin),
|
||||
});
|
||||
}
|
||||
|
||||
sequencesCreated++;
|
||||
} catch (error: any) {
|
||||
errors.push(`Erreur lors de la création de la séquence "${sequence.nom}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: errors.length === 0,
|
||||
formationsCreated,
|
||||
sequencesCreated,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
formationsCreated,
|
||||
sequencesCreated,
|
||||
errors: [`Erreur générale: ${error.message}`],
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise le public cible
|
||||
*/
|
||||
function normalizePublicCible(value: string): 'directeur' | 'chef_service' | 'tous' | 'autre' {
|
||||
const normalized = value.toLowerCase().trim();
|
||||
if (normalized === 'directeur') return 'directeur';
|
||||
if (normalized === 'chef_service' || normalized === 'chef de service') return 'chef_service';
|
||||
if (normalized === 'tous') return 'tous';
|
||||
return 'autre';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalise le statut
|
||||
*/
|
||||
function normalizeStatut(value: string): 'ouverte' | 'bloquee' | 'terminee' {
|
||||
const normalized = value.toLowerCase().trim();
|
||||
if (normalized === 'ouverte') return 'ouverte';
|
||||
if (normalized === 'bloquee' || normalized === 'bloquée') return 'bloquee';
|
||||
if (normalized === 'terminee' || normalized === 'terminée') return 'terminee';
|
||||
return 'ouverte';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse une date Excel (peut être un nombre de série Excel ou une chaîne)
|
||||
*/
|
||||
function parseExcelDate(value: any): string {
|
||||
if (!value) return '';
|
||||
|
||||
// Si c'est un nombre (date Excel)
|
||||
if (typeof value === 'number') {
|
||||
const date = XLSX.SSF.parse_date_code(value);
|
||||
return `${date.y}-${String(date.m).padStart(2, '0')}-${String(date.d).padStart(2, '0')} ${String(date.H || 0).padStart(2, '0')}:${String(date.M || 0).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// Si c'est déjà une chaîne
|
||||
return String(value).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un lien unique pour une formation
|
||||
*/
|
||||
function generateUniqueLien(): string {
|
||||
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { generateAnalyticsExcel, generateAnalyticsPDF } from "./analyticsExportS
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { sendInscriptionConfirmation, sendGroupEmail } from "./emailService";
|
||||
import { generateExcelExport, generatePDFExport, generateFeuillePresence } from "./exportService";
|
||||
import { parseExcelFile, validateImportData, importToDatabase } from "./importExcel";
|
||||
|
||||
// Procédure admin uniquement
|
||||
const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
|
||||
@@ -72,6 +73,68 @@ export const appRouter = router({
|
||||
await db.deleteFormation(input.id);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
importExcel: adminProcedure.input(z.object({
|
||||
fileBase64: z.string(),
|
||||
})).mutation(async ({ input }) => {
|
||||
try {
|
||||
// Décoder le fichier base64
|
||||
const buffer = Buffer.from(input.fileBase64, 'base64');
|
||||
|
||||
// Parser le fichier Excel
|
||||
const { formations, sequences } = parseExcelFile(buffer);
|
||||
|
||||
// Valider les données
|
||||
const validation = validateImportData(formations, sequences);
|
||||
|
||||
if (!validation.valid) {
|
||||
return {
|
||||
success: false,
|
||||
formationsCreated: 0,
|
||||
sequencesCreated: 0,
|
||||
errors: validation.errors,
|
||||
warnings: validation.warnings,
|
||||
};
|
||||
}
|
||||
|
||||
// Importer dans la base de données
|
||||
const result = await importToDatabase(formations, sequences);
|
||||
|
||||
return {
|
||||
...result,
|
||||
warnings: [...validation.warnings, ...result.warnings],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
formationsCreated: 0,
|
||||
sequencesCreated: 0,
|
||||
errors: [`Erreur lors du traitement du fichier: ${error.message}`],
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
previewExcel: adminProcedure.input(z.object({
|
||||
fileBase64: z.string(),
|
||||
})).mutation(async ({ input }) => {
|
||||
try {
|
||||
const buffer = Buffer.from(input.fileBase64, 'base64');
|
||||
const { formations, sequences } = parseExcelFile(buffer);
|
||||
const validation = validateImportData(formations, sequences);
|
||||
|
||||
return {
|
||||
formations,
|
||||
sequences,
|
||||
validation,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: `Erreur lors de la lecture du fichier: ${error.message}`,
|
||||
});
|
||||
}
|
||||
}),
|
||||
}),
|
||||
|
||||
// ===== APPRENANTS =====
|
||||
|
||||
20
todo.md
20
todo.md
@@ -631,3 +631,23 @@
|
||||
- [x] Identifier la cause du problème dans le code
|
||||
- [x] Corriger le bug
|
||||
- [x] Tester la création/modification avec public cible "Tous"
|
||||
|
||||
## Problème de démarrage du service sur VPS
|
||||
|
||||
- [x] Diagnostiquer la cause de l'erreur (code exit 1)
|
||||
- [x] Vérifier les logs du service
|
||||
- [x] Identifier les problèmes de configuration (absence de build)
|
||||
- [x] Fournir les solutions de correction
|
||||
- [x] Créer une documentation de dépannage
|
||||
|
||||
## Import Excel - Formations et Séquences
|
||||
|
||||
- [x] Créer le template Excel avec colonnes définies
|
||||
- [x] Développer la procédure backend pour parser le fichier Excel
|
||||
- [x] Implémenter la validation des données importées
|
||||
- [x] Créer la logique d'insertion en base de données
|
||||
- [x] Développer l'interface frontend d'upload
|
||||
- [x] Ajouter la prévisualisation des données avant import
|
||||
- [x] Gérer les erreurs et retours utilisateur
|
||||
- [x] Créer la documentation utilisateur avec exemple
|
||||
- [ ] Tester l'import avec différents fichiers Excel
|
||||
|
||||
Reference in New Issue
Block a user