diff --git a/BACKUP.md b/BACKUP.md index 267acb8..15ed485 100644 --- a/BACKUP.md +++ b/BACKUP.md @@ -70,12 +70,14 @@ BACKUP_STORAGE_PATH="/var/backups/towercontrol" npm run backup:create ### Interface graphique #### Paramètres Avancés + - **Visualisation** du statut en temps réel - **Création manuelle** de sauvegardes - **Vérification** de l'intégrité - **Lien** vers la gestion complète #### Page de gestion complète + - **Configuration** détaillée du système - **Liste** de toutes les sauvegardes - **Actions** (supprimer, restaurer) @@ -153,6 +155,7 @@ Par défaut : `./backups/` (relatif au dossier du projet) ### Métadonnées Chaque sauvegarde contient : + - **Horodatage** précis de création - **Taille** du fichier - **Type** (manuelle ou automatique) @@ -172,11 +175,13 @@ Chaque sauvegarde contient : ### Procédure #### Via interface (développement uniquement) + 1. Aller dans la gestion des sauvegardes 2. Cliquer sur **"Restaurer"** à côté du fichier souhaité 3. Confirmer l'action #### Via CLI + ```bash # Restaurer avec confirmation tsx scripts/backup-manager.ts restore towercontrol_2025-01-15T10-30-00-000Z.db.gz @@ -236,6 +241,7 @@ Les opérations de sauvegarde sont loggées dans la console de l'application. ### Problèmes courants #### Erreur "sqlite3 command not found" + ```bash # Sur macOS brew install sqlite @@ -245,6 +251,7 @@ sudo apt-get install sqlite3 ``` #### Permissions insuffisantes + ```bash # Vérifier les permissions du dossier de sauvegarde ls -la backups/ @@ -254,6 +261,7 @@ chmod 755 backups/ ``` #### Espace disque insuffisant + ```bash # Vérifier l'espace disponible df -h @@ -268,9 +276,11 @@ tsx scripts/backup-manager.ts delete Pour activer le debug détaillé, modifier `services/database.ts` : ```typescript -export const prisma = globalThis.__prisma || new PrismaClient({ - log: ['query', 'info', 'warn', 'error'], // Debug activé -}); +export const prisma = + globalThis.__prisma || + new PrismaClient({ + log: ['query', 'info', 'warn', 'error'], // Debug activé + }); ``` ## Sécurité @@ -298,18 +308,19 @@ En environnement Docker, tout est centralisé dans le dossier `data/` : ```yaml # docker-compose.yml environment: - DATABASE_URL: "file:./data/prod.db" # Base de données Prisma - BACKUP_DATABASE_PATH: "./data/prod.db" # Base à sauvegarder - BACKUP_STORAGE_PATH: "./data/backups" # Dossier des sauvegardes + DATABASE_URL: 'file:./data/prod.db' # Base de données Prisma + BACKUP_DATABASE_PATH: './data/prod.db' # Base à sauvegarder + BACKUP_STORAGE_PATH: './data/backups' # Dossier des sauvegardes volumes: - - ./data:/app/data # Bind mount vers dossier local + - ./data:/app/data # Bind mount vers dossier local ``` **Structure des dossiers :** + ``` ./data/ # Dossier local mappé ├── prod.db # Base de données production -├── dev.db # Base de données développement +├── dev.db # Base de données développement └── backups/ # Sauvegardes (créé automatiquement) ├── towercontrol_*.db.gz └── ... @@ -333,7 +344,7 @@ POST /api/backups/[filename] # Restaurer (dev seulement) const response = await fetch('/api/backups', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action: 'create' }) + body: JSON.stringify({ action: 'create' }), }); // Lister les sauvegardes @@ -366,15 +377,16 @@ scripts/ ## Roadmap ### Version actuelle ✅ + - Sauvegardes automatiques et manuelles - Interface graphique complète - CLI d'administration - Compression et rétention ### Améliorations futures 🚧 + - Sauvegarde vers cloud (S3, Google Drive) - Chiffrement des sauvegardes - Notifications par email - Métriques de performance - Sauvegarde incrémentale - diff --git a/DOCKER.md b/DOCKER.md index 168a951..037e229 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -5,6 +5,7 @@ Guide d'utilisation de TowerControl avec Docker. ## 🚀 Démarrage rapide ### Production + ```bash # Démarrer le service de production docker-compose up -d towercontrol @@ -14,6 +15,7 @@ open http://localhost:3006 ``` ### Développement + ```bash # Démarrer le service de développement avec live reload docker-compose --profile dev up towercontrol-dev @@ -25,6 +27,7 @@ open http://localhost:3005 ## 📋 Services disponibles ### 🚀 `towercontrol` (Production) + - **Port** : 3006 - **Base de données** : `./data/prod.db` - **Sauvegardes** : `./data/backups/` @@ -32,6 +35,7 @@ open http://localhost:3005 - **Restart** : Automatique ### 🛠️ `towercontrol-dev` (Développement) + - **Port** : 3005 - **Base de données** : `./data/dev.db` - **Sauvegardes** : `./data/backups/` (partagées) @@ -54,13 +58,13 @@ open http://localhost:3005 ### Variables d'environnement -| Variable | Production | Développement | Description | -|----------|------------|---------------|-------------| -| `NODE_ENV` | `production` | `development` | Mode d'exécution | -| `DATABASE_URL` | `file:./data/prod.db` | `file:./data/dev.db` | Base Prisma | -| `BACKUP_DATABASE_PATH` | `./data/prod.db` | `./data/dev.db` | Source backup | -| `BACKUP_STORAGE_PATH` | `./data/backups` | `./data/backups` | Dossier backup | -| `TZ` | `Europe/Paris` | `Europe/Paris` | Fuseau horaire | +| Variable | Production | Développement | Description | +| ---------------------- | --------------------- | -------------------- | ---------------- | +| `NODE_ENV` | `production` | `development` | Mode d'exécution | +| `DATABASE_URL` | `file:./data/prod.db` | `file:./data/dev.db` | Base Prisma | +| `BACKUP_DATABASE_PATH` | `./data/prod.db` | `./data/dev.db` | Source backup | +| `BACKUP_STORAGE_PATH` | `./data/backups` | `./data/backups` | Dossier backup | +| `TZ` | `Europe/Paris` | `Europe/Paris` | Fuseau horaire | ### Ports @@ -70,6 +74,7 @@ open http://localhost:3005 ## 📚 Commandes utiles ### Gestion des conteneurs + ```bash # Voir les logs docker-compose logs -f towercontrol @@ -86,6 +91,7 @@ docker-compose down -v --rmi all ``` ### Gestion des données + ```bash # Sauvegarder les données docker-compose exec towercontrol npm run backup:create @@ -98,6 +104,7 @@ docker-compose exec towercontrol sh ``` ### Base de données + ```bash # Migrations Prisma docker-compose exec towercontrol npx prisma migrate deploy @@ -112,6 +119,7 @@ docker-compose exec towercontrol-dev npx prisma studio ## 🔍 Debugging ### Vérifier la santé + ```bash # Health check curl http://localhost:3006/api/health @@ -122,6 +130,7 @@ docker-compose exec towercontrol env | grep -E "(DATABASE|BACKUP|NODE_ENV)" ``` ### Logs détaillés + ```bash # Logs avec timestamps docker-compose logs -f -t towercontrol @@ -135,6 +144,7 @@ docker-compose logs --tail=100 towercontrol ### Problèmes courants **Port déjà utilisé** + ```bash # Trouver le processus qui utilise le port lsof -i :3006 @@ -142,12 +152,14 @@ kill -9 ``` **Base de données corrompue** + ```bash # Restaurer depuis une sauvegarde docker-compose exec towercontrol npm run backup:restore filename.db.gz ``` **Permissions** + ```bash # Corriger les permissions du dossier data sudo chown -R $USER:$USER ./data @@ -156,6 +168,7 @@ sudo chown -R $USER:$USER ./data ## 📊 Monitoring ### Espace disque + ```bash # Taille du dossier data du -sh ./data @@ -165,6 +178,7 @@ df -h . ``` ### Performance + ```bash # Stats des conteneurs docker stats @@ -176,6 +190,7 @@ docker-compose exec towercontrol free -h ## 🔒 Production ### Recommandations + - Utiliser un reverse proxy (nginx, traefik) - Configurer HTTPS - Sauvegarder régulièrement `./data/` @@ -183,11 +198,12 @@ docker-compose exec towercontrol free -h - Logs centralisés ### Exemple nginx + ```nginx server { listen 80; server_name towercontrol.example.com; - + location / { proxy_pass http://localhost:3006; proxy_set_header Host $host; diff --git a/README.md b/README.md index 9af14b4..a17663b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ TowerControl est un gestionnaire de tâches **standalone** conçu pour les déve - **Local first** : Base SQLite, pas de cloud requis - **Architecture moderne** : Next.js 15 + React 19 + TypeScript + Prisma -- **Design minimaliste** : Interface dark/light avec focus sur la productivité +- **Design minimaliste** : Interface dark/light avec focus sur la productivité - **Intégrations intelligentes** : Sync unidirectionnelle Jira sans pollution --- @@ -20,6 +20,7 @@ TowerControl est un gestionnaire de tâches **standalone** conçu pour les déve ## ✨ Fonctionnalités principales ### 🏗️ Kanban moderne + - **Drag & drop fluide** avec @dnd-kit (optimistic updates) - **Colonnes configurables** : backlog, todo, in_progress, done, cancelled, freeze, archived - **Vues multiples** : Kanban classique + swimlanes par priorité @@ -27,18 +28,21 @@ TowerControl est un gestionnaire de tâches **standalone** conçu pour les déve - **Création rapide** : Ajout inline dans chaque colonne ### 🏷️ Système de tags avancé + - **Tags colorés** avec sélecteur de couleur - **Autocomplete intelligent** lors de la saisie - **Filtrage en temps réel** par tags - **Gestion complète** avec page dédiée `/tags` ### 📊 Filtrage et recherche + - **Recherche temps réel** dans les titres et descriptions - **Filtres combinables** : statut, priorité, tags, source - **Tri flexible** : date, priorité, alphabétique - **Interface intuitive** avec dropdowns et toggles ### 📝 Daily Notes + - **Checkboxes quotidiennes** avec sections "Hier" / "Aujourd'hui" - **Navigation par date** (précédent/suivant) - **Liaison optionnelle** avec les tâches existantes @@ -46,6 +50,7 @@ TowerControl est un gestionnaire de tâches **standalone** conçu pour les déve - **Historique calendaire** des dailies ### 🔗 Intégration Jira Cloud + - **Synchronisation unidirectionnelle** (Jira → local) - **Authentification sécurisée** (email + API token) - **Mapping intelligent** des statuts Jira @@ -54,6 +59,7 @@ TowerControl est un gestionnaire de tâches **standalone** conçu pour les déve - **Interface de configuration** complète ### 🎨 Interface & UX + - **Thème adaptatif** : dark/light + détection système - **Design cohérent** : palette cyberpunk/tech avec Tailwind CSS - **Composants modulaires** : Button, Input, Card, Modal, Badge @@ -61,6 +67,7 @@ TowerControl est un gestionnaire de tâches **standalone** conçu pour les déve - **Responsive design** pour tous les écrans ### ⚡ Performance & Architecture + - **Server Actions** pour les mutations rapides (vs API routes) - **Architecture SSR** avec hydratation optimisée - **Base de données SQLite** ultra-rapide @@ -72,7 +79,8 @@ TowerControl est un gestionnaire de tâches **standalone** conçu pour les déve ## 🛠️ Installation ### Prérequis -- **Node.js** 18+ + +- **Node.js** 18+ - **npm** ou **yarn** ### Installation locale @@ -115,10 +123,12 @@ docker compose --profile dev up -d ``` **Accès :** + - **Production** : http://localhost:3006 - **Développement** : http://localhost:3005 **Gestion des données :** + ```bash # Utiliser votre base locale existante (décommentez dans docker-compose.yml) # - ./prisma/dev.db:/app/data/prod.db @@ -134,9 +144,10 @@ docker compose down -v ``` **Avantages Docker :** + - ✅ **Isolation complète** - Pas de pollution de l'environnement local - ✅ **Base persistante** - Volumes Docker pour SQLite -- ✅ **Prêt pour prod** - Configuration optimisée +- ✅ **Prêt pour prod** - Configuration optimisée - ✅ **Healthcheck intégré** - Monitoring automatique - ✅ **Hot-reload** - Mode dev avec synchronisation du code @@ -234,7 +245,7 @@ towercontrol/ ### Démarrage rapide -1. **Créer une tâche** : +1. **Créer une tâche** : - Clic sur `+ Ajouter` dans une colonne - Ou bouton `+ Nouvelle tâche` global @@ -289,10 +300,10 @@ npm run seed # Ajouter des données de test ```typescript // lib/config.ts export const UI_CONFIG = { - theme: 'system', // 'light' | 'dark' | 'system' - itemsPerPage: 50, // Pagination - enableDragAndDrop: true, // Drag & drop - autoSave: true // Sauvegarde auto + theme: 'system', // 'light' | 'dark' | 'system' + itemsPerPage: 50, // Pagination + enableDragAndDrop: true, // Drag & drop + autoSave: true, // Sauvegarde auto }; ``` @@ -322,6 +333,7 @@ DATABASE_URL="postgresql://user:pass@localhost:5432/towercontrol" ## 🚧 Roadmap ### ✅ Version 2.0 (Actuelle) + - Interface Kanban moderne avec drag & drop - Système de tags avancé - Daily notes avec navigation @@ -330,12 +342,14 @@ DATABASE_URL="postgresql://user:pass@localhost:5432/towercontrol" - Server Actions pour les performances ### 🔄 Version 2.1 (En cours) + - [ ] Page dashboard avec analytics - [ ] Système de sauvegarde automatique (configurable) - [ ] Métriques de productivité et graphiques - [ ] Actions en lot (sélection multiple) ### 🎯 Version 2.2 (Futur) + - [ ] Sous-tâches et hiérarchie - [ ] Dates d'échéance et rappels - [ ] Collaboration et assignation @@ -343,6 +357,7 @@ DATABASE_URL="postgresql://user:pass@localhost:5432/towercontrol" - [ ] Mode PWA et offline ### 🚀 Version 3.0 (Vision) + - [ ] Analytics d'équipe avancées - [ ] Intégrations multiples (GitHub, Linear, etc.) - [ ] API publique et webhooks @@ -379,11 +394,11 @@ MIT License - Voir le fichier [LICENSE](LICENSE) pour plus de détails. ## 🙏 Remerciements - **Next.js** pour le framework moderne -- **Prisma** pour l'ORM élégant +- **Prisma** pour l'ORM élégant - **@dnd-kit** pour le drag & drop fluide - **Tailwind CSS** pour le styling rapide - **Jira API** pour l'intégration robuste --- -**Développé avec ❤️ pour optimiser la productivité des équipes tech** \ No newline at end of file +**Développé avec ❤️ pour optimiser la productivité des équipes tech** diff --git a/TFS_UPGRADE_SUMMARY.md b/TFS_UPGRADE_SUMMARY.md index 4f0b229..6fe266a 100644 --- a/TFS_UPGRADE_SUMMARY.md +++ b/TFS_UPGRADE_SUMMARY.md @@ -1,6 +1,7 @@ # Mise à niveau TFS : Récupération des PRs assignées à l'utilisateur ## 🎯 Objectif + Permettre au service TFS de récupérer **toutes** les Pull Requests assignées à l'utilisateur sur l'ensemble de son organisation Azure DevOps, plutôt que de se limiter à un projet spécifique. ## ⚡ Changements apportés @@ -8,17 +9,20 @@ Permettre au service TFS de récupérer **toutes** les Pull Requests assignées ### 1. Service TFS (`src/services/tfs.ts`) #### Nouvelles méthodes ajoutées : + - **`getMyPullRequests()`** : Récupère toutes les PRs concernant l'utilisateur - **`getPullRequestsByCreator()`** : PRs créées par l'utilisateur - **`getPullRequestsByReviewer()`** : PRs où l'utilisateur est reviewer - **`filterPullRequests()`** : Applique les filtres de configuration #### Méthode syncTasks refactorisée : + - Utilise maintenant `getMyPullRequests()` au lieu de parcourir tous les repositories - Plus efficace et centrée sur l'utilisateur - Récupération directe via l'API Azure DevOps avec critères `@me` #### Configuration mise à jour : + - **`projectName`** devient **optionnel** - Validation assouplie dans les factories - Comportement adaptatif : projet spécifique OU toute l'organisation @@ -26,12 +30,14 @@ Permettre au service TFS de récupérer **toutes** les Pull Requests assignées ### 2. Interface utilisateur (`src/components/settings/TfsConfigForm.tsx`) #### Modifications du formulaire : + - Champ "Nom du projet" marqué comme **optionnel** - Validation `required` supprimée -- Placeholder mis à jour : *"laisser vide pour toute l'organisation"* -- Affichage du statut : *"Toute l'organisation"* si pas de projet +- Placeholder mis à jour : _"laisser vide pour toute l'organisation"_ +- Affichage du statut : _"Toute l'organisation"_ si pas de projet #### Instructions mises à jour : + - Explique le nouveau comportement **synchronisation intelligente** - Précise que les PRs sont récupérées automatiquement selon l'assignation - Note sur la portée projet vs organisation @@ -39,17 +45,20 @@ Permettre au service TFS de récupérer **toutes** les Pull Requests assignées ### 3. Endpoints API #### `/api/tfs/test/route.ts` + - Validation mise à jour (projectName optionnel) - Message de réponse enrichi avec portée (projet/organisation) - Retour détaillé du scope de synchronisation #### `/api/tfs/sync/route.ts` + - Validation assouplie pour les deux méthodes GET/POST - Configuration adaptative selon la présence du projectName ## 🔧 API Azure DevOps utilisées ### Nouvelles requêtes : + ```typescript // PRs créées par l'utilisateur /_apis/git/pullrequests?searchCriteria.creatorId=@me&searchCriteria.status=active @@ -59,6 +68,7 @@ Permettre au service TFS de récupérer **toutes** les Pull Requests assignées ``` ### Comportement intelligent : + - **Fusion automatique** des deux types de PRs - **Déduplication** basée sur `pullRequestId` - **Filtrage** selon la configuration (repositories, branches, projet) @@ -74,11 +84,13 @@ Permettre au service TFS de récupérer **toutes** les Pull Requests assignées ## 🎨 Interface utilisateur ### Avant : + - Champ projet **obligatoire** - Synchronisation limitée à UN projet - Configuration rigide ### Après : + - Champ projet **optionnel** - Synchronisation intelligente de TOUTES les PRs assignées - Configuration flexible et adaptative @@ -94,10 +106,11 @@ Permettre au service TFS de récupérer **toutes** les Pull Requests assignées ## 🚀 Déploiement La migration est **transparente** : + - Les configurations existantes continuent à fonctionner - Possibilité de supprimer le `projectName` pour étendre la portée - Pas de rupture de compatibilité --- -*Cette mise à niveau transforme le service TFS d'un outil de surveillance de projet en un assistant personnel intelligent pour Azure DevOps.* 🎯 \ No newline at end of file +_Cette mise à niveau transforme le service TFS d'un outil de surveillance de projet en un assistant personnel intelligent pour Azure DevOps._ 🎯 diff --git a/TODO.md b/TODO.md index 46a1e39..1e1a66d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,11 +1,13 @@ # TowerControl v2.0 - Gestionnaire de tâches moderne ## Fix + - [ ] Calendrier n'a plus le bouton calendrier d'ouverture du calendrier visuel dans les inputs datetime - [ ] Un raccourci pour chercher dans la page de Kanban - [ ] Bouton cloner une tache dans la modale d'edition ## Idées à developper + - [ ] Optimisations Perf : requetes DB - [ ] PWA et mode offline @@ -14,8 +16,9 @@ ## 🐛 Problèmes relevés en réunion - Corrections UI/UX ### 🎨 Design et Interface -- [X] **Homepage cards** : toute en variant glass -- [X] **Icône Kanban homepage** - Changer icône sur la page d'accueil, pas lisible (utiliser une lib) + +- [x] **Homepage cards** : toute en variant glass +- [x] **Icône Kanban homepage** - Changer icône sur la page d'accueil, pas lisible (utiliser une lib) - [x] **Lisibilité label graph par tag** - Améliorer la lisibilité des labels dans les graphiques par tag - [x] **Tag homepage** - Problème d'affichage des graphs de tags sur la homepage côté lisibilité, certaines icones ne sont pas entièrement visible, et la légende est trop proche du graphe. - [x] **Tâches récentes** - Revoir l'affichage et la logique des tâches récentes @@ -33,18 +36,19 @@ - [ ] **Deux modales** - Problème de duplication de modales - [ ] **Control panel et select** - Problème avec les contrôles et sélecteurs - [ ] **TaskCard et Kanban transparence** - Appliquer la transparence sur le background et non sur la card -- [X] **Recherche Kanban desktop controls** - Ajouter icône et label : "rechercher" pour rapetir +- [x] **Recherche Kanban desktop controls** - Ajouter icône et label : "rechercher" pour rapetir - [ ] **Largeur page Kanban** - Réduire légèrement la largeur et revoir toutes les autres pages - [x] **Icône thème à gauche du profil** - Repositionner l'icône de thème dans le header - [ ] **Déconnexion trop petit et couleur** - Améliorer le bouton de déconnexion - [ ] **Fond modal trop opaque** - Réduire l'opacité du fond des modales - [ ] **Couleurs thème clair et TFS Jira Kanban** - Harmoniser les couleurs du thème clair -- [X] **États sélectionnés desktop control** - Revoir les couleurs des états sélectionnés pour avoir le joli bleu du dropdown partout +- [x] **États sélectionnés desktop control** - Revoir les couleurs des états sélectionnés pour avoir le joli bleu du dropdown partout - [ ] **Dépasse 1000 caractères en edit modal task** - Corriger la limite (pas de limite) et revoir la quickcard description - [ ] **UI si échéance et trop de labels dans le footer de card** - Améliorer l'affichage en mode détaillé TaskCard; certains boutons sont sur deux lignes ce qui casse l'affichage - [ ] **Gravatar** - Implémenter l'affichage des avatars Gravatar ### 🔧 Fonctionnalités et Intégrations + - [ ] **Synchro Jira et TFS shortcuts** - Ajouter des raccourcis et bouton dans Kanban - [x] **Intégration suppressions Jira/TFS** - Aligner la gestion des suppressions sur TFS, je veux que ce qu'on a récupéré dans la synchro, quand ca devient terminé dans Jira ou TFS, soit marqué comme terminé dans le Kanban et non supprimé du kanban. - [ ] **Log d'activité** - Implémenter un système de log d'activité (feature potentielle) @@ -54,6 +58,7 @@ ## 🚀 Nouvelles idées & fonctionnalités futures ### 🎯 Jira - Suivi des demandes en attente + - [ ] **Page "Jiras en attente"** - [ ] Liste des Jiras créés par moi mais non assignés à mon équipe - [ ] Suivi des demandes formulées à d'autres équipes @@ -66,10 +71,12 @@ ### 👥 Gestion multi-utilisateurs (PROJET MAJEUR) #### **Architecture actuelle → Multi-tenant** + - **Problème** : App mono-utilisateur avec données globales - **Solution** : Transformation en app multi-utilisateurs avec isolation des données + système de rôles #### **Plan de migration** + - [ ] **Phase 1: Authentification** - [ ] Système de login/mot de passe (NextAuth.js) - [ ] Gestion des sessions sécurisées @@ -152,6 +159,7 @@ - [ ] Historique des modifications par utilisateur #### **Considérations techniques** + - **Base de données** : Ajouter `userId` partout + contraintes - **Sécurité** : Validation côté serveur de l'isolation des données - **Performance** : Index sur `userId`, pagination pour gros volumes @@ -210,6 +218,7 @@ ### **Fonctionnalités IA concrètes** #### 🎯 **Smart Task Creation** + - [ ] **Bouton "Créer avec IA" dans le Kanban** - [ ] Input libre : "Préparer présentation client pour vendredi" - [ ] IA génère : titre, description, estimation durée, sous-tâches @@ -217,6 +226,7 @@ - [ ] Validation/modification avant création #### 🧠 **Daily Assistant** + - [ ] **Bouton "Smart Daily" dans la page Daily** - [ ] Input libre : "Réunion client 14h, finir le rapport, appeler le fournisseur" - [ ] IA génère une liste de checkboxes structurées @@ -226,6 +236,7 @@ - [ ] Pendant la saisie, IA propose des checkboxes similaires #### 🎨 **Smart Tagging** + - [ ] **Auto-tagging des nouvelles tâches** - [ ] IA analyse le titre/description - [ ] Propose automatiquement 2-3 tags **existants** pertinents @@ -235,6 +246,7 @@ - [ ] Tri par fréquence d'usage et pertinence #### 💬 **Chat Assistant** + - [ ] **Widget chat en bas à droite** - [ ] "Quelles sont mes tâches urgentes cette semaine ?" - [ ] "Comment optimiser mon planning demain ?" @@ -245,6 +257,7 @@ - [ ] Recherche par contexte, pas juste mots-clés #### 📈 **Smart Reports** + - [ ] **Génération automatique de rapports** - [ ] Bouton "Générer rapport IA" dans analytics - [ ] IA analyse les données et génère un résumé textuel @@ -255,6 +268,7 @@ - [ ] Notifications contextuelles et actionables #### ⚡ **Quick Actions** + - [ ] **Bouton "Optimiser" sur une tâche** - [ ] IA suggère des améliorations (titre, description) - [ ] Propose des **tags existants** pertinents @@ -268,4 +282,4 @@ --- -*Focus sur l'expérience utilisateur et le design moderne. App standalone prête pour évoluer vers une plateforme d'intégration complète.* +_Focus sur l'expérience utilisateur et le design moderne. App standalone prête pour évoluer vers une plateforme d'intégration complète._ diff --git a/TODO_ARCHIVE.md b/TODO_ARCHIVE.md index 14c7913..3befee7 100644 --- a/TODO_ARCHIVE.md +++ b/TODO_ARCHIVE.md @@ -3,19 +3,22 @@ ## ✅ Phase 1: Nettoyage et architecture (TERMINÉ) ### 1.1 Configuration projet Next.js + - [x] Initialiser Next.js avec TypeScript -- [x] Configurer ESLint, Prettier +- [x] Configurer ESLint, Prettier - [x] Setup structure de dossiers selon les règles du workspace - [x] Configurer base de données (SQLite local) - [x] Setup Prisma ORM ### 1.2 Architecture backend standalone + - [x] Créer `services/database.ts` - Pool de connexion DB - [x] Créer `services/tasks.ts` - Service CRUD pour les tâches - [x] Créer `lib/types.ts` - Types partagés (Task, Tag, etc.) - [x] Nettoyer l'ancien code de synchronisation ### 1.3 API moderne et propre + - [x] `app/api/tasks/route.ts` - API CRUD complète (GET, POST, PATCH, DELETE) - [x] Supprimer les routes de synchronisation obsolètes - [x] Configuration moderne dans `lib/config.ts` @@ -25,19 +28,22 @@ ## 🎯 Phase 2: Interface utilisateur moderne (EN COURS) ### 2.1 Système de design et composants UI + - [x] Créer les composants UI de base (Button, Input, Card, Modal, Badge) - [x] Implémenter le système de design tech dark (couleurs, typographie, spacing) - [x] Setup Tailwind CSS avec classes utilitaires personnalisées - [x] Créer une palette de couleurs tech/cyberpunk ### 2.2 Composants Kanban existants (à améliorer) + - [x] `components/kanban/Board.tsx` - Tableau Kanban principal -- [x] `components/kanban/Column.tsx` - Colonnes du Kanban +- [x] `components/kanban/Column.tsx` - Colonnes du Kanban - [x] `components/kanban/TaskCard.tsx` - Cartes de tâches - [x] `components/ui/Header.tsx` - Header avec statistiques - [x] Refactoriser les composants pour utiliser le nouveau système UI ### 2.3 Gestion des tâches (CRUD) + - [x] Formulaire de création de tâche (Modal + Form) - [x] Création rapide inline dans les colonnes (QuickAddTask) - [x] Formulaire d'édition de tâche (Modal + Form avec pré-remplissage) @@ -47,6 +53,7 @@ - [x] Validation des formulaires et gestion d'erreurs ### 2.4 Gestion des tags + - [x] Créer/éditer des tags avec sélecteur de couleur - [x] Autocomplete pour les tags existants - [x] Suppression de tags (avec vérification des dépendances) @@ -66,6 +73,7 @@ - [x] Intégration des filtres dans KanbanBoard ### 2.5 Clients HTTP et hooks + - [x] `clients/tasks-client.ts` - Client pour les tâches (CRUD complet) - [x] `clients/tags-client.ts` - Client pour les tags - [x] `clients/base/http-client.ts` - Client HTTP de base @@ -76,6 +84,7 @@ - [x] Architecture SSR + hydratation client optimisée ### 2.6 Fonctionnalités Kanban avancées + - [x] Drag & drop entre colonnes (@dnd-kit avec React 19) - [x] Drag & drop optimiste (mise à jour immédiate + rollback si erreur) - [x] Filtrage par statut/priorité/assigné @@ -85,6 +94,7 @@ - [x] Tri des tâches (date, priorité, alphabétique) ### 2.7 Système de thèmes (clair/sombre) + - [x] Créer le contexte de thème (ThemeContext + ThemeProvider) - [x] Ajouter toggle de thème dans le Header (bouton avec icône soleil/lune) - [x] Définir les variables CSS pour le thème clair @@ -99,6 +109,7 @@ ## 📊 Phase 3: Intégrations et analytics (Priorité 3) ### 3.1 Gestion du Daily + - [x] Créer `services/daily.ts` - Service de gestion des daily notes - [x] Modèle de données Daily (date, checkboxes hier/aujourd'hui) - [x] Interface Daily avec sections "Hier" et "Aujourd'hui" @@ -111,6 +122,7 @@ - [x] Vue calendar/historique des dailies ### 3.2 Intégration Jira Cloud + - [x] Créer `services/jira.ts` - Service de connexion à l'API Jira Cloud - [x] Configuration Jira (URL, email, API token) dans `lib/config.ts` - [x] Authentification Basic Auth (email + API token) @@ -127,6 +139,7 @@ - [x] Gestion des erreurs et timeouts API ### 3.3 Page d'accueil/dashboard + - [x] Créer une page d'accueil moderne avec vue d'ensemble - [x] Widgets de statistiques (tâches par statut, priorité, etc.) - [x] Déplacer kanban vers /kanban et créer nouveau dashboard à la racine @@ -137,6 +150,7 @@ - [x] Intégration des analytics dans le dashboard ### 3.4 Analytics et métriques + - [x] `services/analytics.ts` - Calculs statistiques - [x] Métriques de productivité (vélocité, temps moyen, etc.) - [x] Graphiques avec Recharts (tendances, vélocité, distribution) @@ -144,6 +158,7 @@ - [x] Insights automatiques et métriques visuelles ## Autre Todo + - [x] Avoir un bouton pour réduire/agrandir la font des taches dans les kanban (swimlane et classique) - [x] Refactorer les couleurs des priorités dans un seul endroit - [x] Settings synchro Jira : ajouter une liste de projet à ignorer, doit etre pris en compte par le service bien sur @@ -161,16 +176,17 @@ - [x] Vérification d'intégrité et restauration sécurisée - [x] Option de restauration depuis une sauvegarde sélectionnée - ## 🔧 Phase 4: Server Actions - Migration API Routes (Nouveau) ### 4.1 Migration vers Server Actions - Actions rapides + **Objectif** : Remplacer les API routes par des server actions pour les actions simples et fréquentes #### Actions TaskCard (Priorité 1) + - [x] Créer `actions/tasks.ts` avec server actions de base - [x] `updateTaskStatus(taskId, status)` - Changement de statut -- [x] `updateTaskTitle(taskId, title)` - Édition inline du titre +- [x] `updateTaskTitle(taskId, title)` - Édition inline du titre - [x] `deleteTask(taskId)` - Suppression de tâche - [x] Modifier `TaskCard.tsx` pour utiliser server actions directement - [x] Remplacer les props callbacks par calls directs aux actions @@ -180,7 +196,8 @@ - [x] **Nettoyage** : Simplifier `tasks-client.ts` (garder GET et POST uniquement) - [x] **Nettoyage** : Modifier `useTasks.ts` pour remplacer mutations par server actions -#### Actions Daily (Priorité 2) +#### Actions Daily (Priorité 2) + - [x] Créer `actions/daily.ts` pour les checkboxes - [x] `toggleCheckbox(checkboxId)` - Toggle état checkbox - [x] `addCheckboxToDaily(dailyId, content)` - Ajouter checkbox @@ -193,9 +210,10 @@ - [x] **Nettoyage** : Modifier hook `useDaily.ts` pour `useTransition` #### Actions User Preferences (Priorité 3) + - [x] Créer `actions/preferences.ts` pour les toggles - [x] `updateViewPreferences(preferences)` - Préférences d'affichage -- [x] `updateKanbanFilters(filters)` - Filtres Kanban +- [x] `updateKanbanFilters(filters)` - Filtres Kanban - [x] `updateColumnVisibility(columns)` - Visibilité colonnes - [x] `updateTheme(theme)` - Changement de thème - [x] Remplacer les hooks par server actions directes @@ -204,6 +222,7 @@ - [x] **Nettoyage** : Modifier `UserPreferencesContext.tsx` pour server actions #### Actions Tags (Priorité 4) + - [x] Créer `actions/tags.ts` pour la gestion tags - [x] `createTag(name, color)` - Création tag - [x] `updateTag(tagId, data)` - Modification tag @@ -214,37 +233,43 @@ - [x] **Nettoyage** : Modifier `useTags.ts` pour server actions directes #### Migration progressive avec nettoyage immédiat + **Principe** : Pour chaque action migrée → nettoyage immédiat des routes et code obsolètes ### 4.2 Conservation API Routes - Endpoints complexes + **À GARDER en API routes** (pas de migration) #### Endpoints de fetching initial + - ✅ `GET /api/tasks` - Récupération avec filtres complexes - ✅ `GET /api/daily` - Vue daily avec logique métier - ✅ `GET /api/tags` - Liste tags avec recherche - ✅ `GET /api/user-preferences` - Préférences initiales -#### Endpoints d'intégration externe +#### Endpoints d'intégration externe + - ✅ `POST /api/jira/sync` - Synchronisation Jira complexe - ✅ `GET /api/jira/logs` - Logs de synchronisation - ✅ Configuration Jira (formulaires complexes) #### Raisons de conservation + - **API publique** : Réutilisable depuis mobile/externe - **Logique complexe** : Synchronisation, analytics, rapports - **Monitoring** : Besoin de logs HTTP séparés - **Real-time futur** : WebSockets/SSE non compatibles server actions ### 4.3 Architecture hybride cible + ``` Actions rapides → Server Actions directes ├── TaskCard actions (status, title, delete) -├── Daily checkboxes (toggle, add, edit) +├── Daily checkboxes (toggle, add, edit) ├── Preferences toggles (theme, filters) └── Tags CRUD (create, update, delete) -Endpoints complexes → API Routes conservées +Endpoints complexes → API Routes conservées ├── Fetching initial avec filtres ├── Intégrations externes (Jira, webhooks) ├── Analytics et rapports @@ -252,6 +277,7 @@ Endpoints complexes → API Routes conservées ``` ### 4.4 Avantages attendus + - **🚀 Performance** : Pas de sérialisation HTTP pour actions rapides - **🔄 Cache intelligent** : `revalidatePath()` automatique - **📦 Bundle reduction** : Moins de code client HTTP @@ -261,6 +287,7 @@ Endpoints complexes → API Routes conservées ## 📊 Phase 5: Surveillance Jira - Analytics d'équipe (Priorité 5) ### 5.1 Configuration projet Jira + - [x] Ajouter champ `projectKey` dans la config Jira (settings) - [x] Interface pour sélectionner le projet à surveiller - [x] Validation de l'existence du projet via API Jira @@ -268,6 +295,7 @@ Endpoints complexes → API Routes conservées - [x] Test de connexion spécifique au projet configuré ### 5.2 Service d'analytics Jira + - [x] Créer `services/jira-analytics.ts` - Métriques avancées - [x] Récupération des tickets du projet (toute l'équipe, pas seulement assignés) - [x] Calculs de vélocité d'équipe (story points par sprint) @@ -278,6 +306,7 @@ Endpoints complexes → API Routes conservées - [x] Cache intelligent des métriques (éviter API rate limits) ### 5.3 Page de surveillance `/jira-dashboard` + - [x] Créer page dédiée avec navigation depuis settings Jira - [x] Vue d'ensemble du projet (nom, lead, statut global) - [x] Sélecteur de période (7j, 30j, 3 mois, sprint actuel) @@ -287,6 +316,7 @@ Endpoints complexes → API Routes conservées - [x] Alertes visuelles (tickets en retard, sprints déviants) ### 5.4 Métriques et graphiques avancés + - [x] **Vélocité** : Story points complétés par sprint - [x] **Burndown chart** : Progression vs planifié - [x] **Cycle time** : Temps moyen par type de ticket @@ -297,6 +327,7 @@ Endpoints complexes → API Routes conservées - [x] **Collaboration** : Matrice d'interactions entre assignees ### 5.5 Fonctionnalités de surveillance + - [x] **Cache serveur intelligent** : Cache en mémoire avec invalidation manuelle - [x] **Export des métriques** : Export CSV/JSON avec téléchargement automatique - [x] **Comparaison inter-sprints** : Tendances, prédictions et recommandations @@ -308,11 +339,13 @@ Endpoints complexes → API Routes conservées ### 📁 Refactoring structure des dossiers (PRIORITÉ HAUTE) #### **Problème actuel** + - Structure mixte : `src/app/`, `src/actions/`, `src/contexts/` mais `components/`, `lib/`, `services/`, etc. à la racine - Alias TypeScript incohérents dans `tsconfig.json` - Non-conformité avec les bonnes pratiques Next.js 13+ App Router #### **Plan de migration** + - [x] **Phase 1: Migration des dossiers** - [x] `mv components/ src/components/` - [x] `mv lib/ src/lib/` @@ -321,6 +354,7 @@ Endpoints complexes → API Routes conservées - [x] `mv services/ src/services/` - [x] **Phase 2: Mise à jour tsconfig.json** + ```json "paths": { "@/*": ["./src/*"] @@ -350,6 +384,7 @@ Endpoints complexes → API Routes conservées - [x] Tester les fonctionnalités principales #### **Structure finale attendue** + ``` src/ ├── app/ # Pages Next.js (déjà OK) @@ -378,12 +413,14 @@ src/ ### Organisation cible des services: ``` + src/services/ -├── core/ # Services fondamentaux -├── analytics/ # Analytics et métriques +├── core/ # Services fondamentaux +├── analytics/ # Analytics et métriques ├── data-management/# Backup, système, base -├── integrations/ # Services externes +├── integrations/ # Services externes ├── task-management/# Gestion des tâches + ``` ### Phase 1: Services Core (infrastructure) ✅ @@ -455,8 +492,8 @@ src/services/ ``` - ### 🔄 Intégration TFS/Azure DevOps + - [x] **Lecture des Pull Requests TFS** : Synchronisation des PR comme tâches - [x] PR arrivent en backlog avec filtrage par team project - [x] Synchronisation aussi riche que Jira (statuts, assignés, commentaires) @@ -468,6 +505,7 @@ src/services/ - [x] Système de plugins pour ajouter facilement de nouveaux services ### 📋 Daily - Gestion des tâches non cochées + - [x] **Section des tâches en attente** - [x] Liste de toutes les todos non cochées (historique complet) - [x] Filtrage par date (7/14/30 jours), catégorie (tâches/réunions), ancienneté @@ -482,12 +520,12 @@ src/services/ - [ ] Possibilité de désarchiver une tâche - [ ] Champ dédié en base de données (actuellement via texte) - --- ## 🖼️ **IMAGE DE FOND PERSONNALISÉE** ✅ TERMINÉ ### **Fonctionnalités implémentées :** + - [x] **Sélecteur d'images de fond** dans les paramètres généraux - [x] **Images prédéfinies** : dégradés bleu, violet, coucher de soleil, océan, forêt - [x] **URL personnalisée** : possibilité d'ajouter une image via URL @@ -498,6 +536,7 @@ src/services/ - [x] **Interface intuitive** : sélection facile avec aperçus visuels ### **Architecture technique :** + - **Types** : `backgroundImage` ajouté à `ViewPreferences` - **Service** : `userPreferencesService` mis à jour - **Actions** : `setBackgroundImage` server action créée @@ -508,6 +547,7 @@ src/services/ ## 🔄 **SCHEDULER TFS** ✅ TERMINÉ ### **Fonctionnalités implémentées :** + - [x] **Scheduler TFS automatique** basé sur le modèle Jira - [x] **Configuration dans UserPreferences** : `tfsAutoSync` et `tfsSyncInterval` - [x] **Intervalles configurables** : hourly, daily, weekly @@ -517,6 +557,7 @@ src/services/ - [x] **Status et monitoring** du scheduler ### **Architecture technique :** + - **Service** : `TfsScheduler` dans `src/services/integrations/tfs/scheduler.ts` - **Configuration** : Champs `tfsAutoSync` et `tfsSyncInterval` dans `UserPreferences` - **Migration** : Méthode `ensureTfsSchedulerFields()` pour compatibilité @@ -525,11 +566,13 @@ src/services/ - **Logs** : Console logs détaillés pour monitoring ### **Différences avec Jira :** + - **Pas de board d'équipe** : TFS se concentre sur les Pull Requests individuelles - **Configuration simplifiée** : Pas de `ignoredProjects`, mais `ignoredRepositories` - **Focus utilisateur** : Synchronisation basée sur les PRs assignées à l'utilisateur ### **Interface utilisateur :** + - **TfsSchedulerConfig** : Configuration du scheduler automatique avec statut et contrôles - **TfsSync** : Interface de synchronisation manuelle avec détails et statistiques - **API Routes** : `/api/tfs/scheduler-config` et `/api/tfs/scheduler-status` pour la gestion @@ -540,6 +583,7 @@ src/services/ ## 🎨 **REFACTORING THÈME & PERSONNALISATION COULEURS** ### **Phase 1: Nettoyage Architecture Thème** + - [x] **Décider de la stratégie** : CSS Variables vs Tailwind Dark Mode vs Hybride - [x] **Configurer tailwind.config.js** avec `darkMode: 'class'` si nécessaire - [x] **Supprimer la double application** du thème (layout.tsx + ThemeContext + UserPreferencesContext) @@ -548,4 +592,4 @@ src/services/ - [ ] **Corriger les problèmes d'hydration** mismatch et flashs de thème - [ ] **Créer un système de design cohérent** avec tokens de couleur ---- \ No newline at end of file +--- diff --git a/UI_COMPONENTS_GUIDE.md b/UI_COMPONENTS_GUIDE.md index 32ec17e..5751bdf 100644 --- a/UI_COMPONENTS_GUIDE.md +++ b/UI_COMPONENTS_GUIDE.md @@ -29,9 +29,7 @@ function TaskCard({ task }) { return ( - + ); @@ -41,6 +39,7 @@ function TaskCard({ task }) { ## 📦 Composants UI Disponibles ### Button + ```tsx @@ -49,6 +48,7 @@ function TaskCard({ task }) { ``` ### Badge + ```tsx Tag Succès @@ -56,6 +56,7 @@ function TaskCard({ task }) { ``` ### Alert + ```tsx Succès @@ -64,12 +65,14 @@ function TaskCard({ task }) { ``` ### Input + ```tsx ``` ### StyledCard + ```tsx Contenu avec style coloré @@ -77,6 +80,7 @@ function TaskCard({ task }) { ``` ### Avatar + ```tsx // Avatar avec URL personnalisée @@ -91,14 +95,17 @@ function TaskCard({ task }) { ## 🔄 Migration ### Étape 1: Identifier les patterns + - Rechercher `var(--` dans les composants métier - Identifier les patterns répétés (boutons, cartes, badges) ### Étape 2: Créer des composants UI + - Encapsuler les styles dans des composants UI - Utiliser des variants pour les variations ### Étape 3: Remplacer dans les composants métier + - Importer les composants UI - Remplacer les éléments HTML par les composants UI diff --git a/data/README.md b/data/README.md index d11b338..547c491 100644 --- a/data/README.md +++ b/data/README.md @@ -18,11 +18,13 @@ data/ ## 🎯 Utilisation ### En développement local + - La base de données principale est dans `prisma/dev.db` - Ce dossier `data/` est utilisé uniquement par Docker - Les sauvegardes locales sont dans `backups/` (racine du projet) ### En production Docker + - Base de données : `data/prod.db` ou `data/dev.db` - Sauvegardes : `data/backups/` - Tout ce dossier est mappé vers `/app/data` dans le conteneur @@ -45,12 +47,14 @@ BACKUP_STORAGE_PATH="./data/backups" ## 🗂️ Fichiers ### Bases de données SQLite + - **prod.db** : Base de données de production - **dev.db** : Base de données de développement Docker - Format : SQLite 3 - Contient : Tasks, Tags, User Preferences, Sync Logs, etc. ### Sauvegardes + - **Format** : `towercontrol_YYYY-MM-DDTHH-mm-ss-sssZ.db.gz` - **Compression** : gzip - **Rétention** : Configurable (défaut: 5 sauvegardes) diff --git a/docker-compose.yml b/docker-compose.yml index 1e62fa8..b2f0706 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,21 +5,21 @@ services: dockerfile: Dockerfile target: runner ports: - - "3006:3000" + - '3006:3000' environment: NODE_ENV: production - DATABASE_URL: "file:../data/dev.db" # Prisma - BACKUP_DATABASE_PATH: "./data/dev.db" # Base de données à sauvegarder - BACKUP_STORAGE_PATH: "./data/backups" # Dossier des sauvegardes + DATABASE_URL: 'file:../data/dev.db' # Prisma + BACKUP_DATABASE_PATH: './data/dev.db' # Base de données à sauvegarder + BACKUP_STORAGE_PATH: './data/backups' # Dossier des sauvegardes TZ: Europe/Paris # NextAuth.js - NEXTAUTH_SECRET: "TbwIWAmQgBcOlg7jRZrhkeEUDTpSr8Cj/Cc7W58fAyw=" - NEXTAUTH_URL: "http://localhost:3006" + NEXTAUTH_SECRET: 'TbwIWAmQgBcOlg7jRZrhkeEUDTpSr8Cj/Cc7W58fAyw=' + NEXTAUTH_URL: 'http://localhost:3006' volumes: - - ./data:/app/data # Dossier local data/ vers /app/data + - ./data:/app/data # Dossier local data/ vers /app/data restart: unless-stopped healthcheck: - test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"] + test: ['CMD', 'wget', '-qO-', 'http://localhost:3000/api/health'] interval: 30s timeout: 10s retries: 3 @@ -31,21 +31,21 @@ services: dockerfile: Dockerfile target: base ports: - - "3005:3000" + - '3005:3000' environment: NODE_ENV: development - DATABASE_URL: "file:../data/dev.db" # Prisma - BACKUP_DATABASE_PATH: "./data/dev.db" # Base de données à sauvegarder - BACKUP_STORAGE_PATH: "./data/backups" # Dossier des sauvegardes + DATABASE_URL: 'file:../data/dev.db' # Prisma + BACKUP_DATABASE_PATH: './data/dev.db' # Base de données à sauvegarder + BACKUP_STORAGE_PATH: './data/backups' # Dossier des sauvegardes TZ: Europe/Paris # NextAuth.js - NEXTAUTH_SECRET: "TbwIWAmQgBcOlg7jRZrhkeEUDTpSr8Cj/Cc7W58fAyw=" - NEXTAUTH_URL: "http://localhost:3005" + NEXTAUTH_SECRET: 'TbwIWAmQgBcOlg7jRZrhkeEUDTpSr8Cj/Cc7W58fAyw=' + NEXTAUTH_URL: 'http://localhost:3005' volumes: - - .:/app # code en live - - /app/node_modules # vol anonyme pour ne pas écraser ceux du conteneur + - .:/app # code en live + - /app/node_modules # vol anonyme pour ne pas écraser ceux du conteneur - /app/.next - - ./data:/app/data # Dossier local data/ vers /app/data + - ./data:/app/data # Dossier local data/ vers /app/data command: > sh -c "npm install && npx prisma generate && @@ -53,7 +53,6 @@ services: npm run dev" profiles: - dev - # 📁 Structure des données : # ./data/ -> /app/data (bind mount) # ├── prod.db -> Base de données production @@ -61,4 +60,4 @@ services: # └── backups/ -> Sauvegardes automatiques # # 🔧 Configuration via .env.docker -# 📚 Documentation : ./data/README.md \ No newline at end of file +# 📚 Documentation : ./data/README.md diff --git a/eslint.config.mjs b/eslint.config.mjs index 719cea2..fa167c8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,6 @@ -import { dirname } from "path"; -import { fileURLToPath } from "url"; -import { FlatCompat } from "@eslint/eslintrc"; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { FlatCompat } from '@eslint/eslintrc'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -10,14 +10,14 @@ const compat = new FlatCompat({ }); const eslintConfig = [ - ...compat.extends("next/core-web-vitals", "next/typescript"), + ...compat.extends('next/core-web-vitals', 'next/typescript'), { ignores: [ - "node_modules/**", - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", + 'node_modules/**', + '.next/**', + 'out/**', + 'build/**', + 'next-env.d.ts', ], }, ]; diff --git a/next.config.ts b/next.config.ts index fbe3e9b..20ed16e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,4 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; const nextConfig: NextConfig = { output: 'standalone', @@ -45,7 +45,7 @@ const nextConfig: NextConfig = { turbopack: { rules: { '*.sql': ['raw'], - } + }, }, }; diff --git a/postcss.config.mjs b/postcss.config.mjs index c7bcb4b..ba720fe 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,5 +1,5 @@ const config = { - plugins: ["@tailwindcss/postcss"], + plugins: ['@tailwindcss/postcss'], }; export default config; diff --git a/scripts/backup-manager.ts b/scripts/backup-manager.ts index bb4b032..ca7d1e9 100644 --- a/scripts/backup-manager.ts +++ b/scripts/backup-manager.ts @@ -4,7 +4,10 @@ * Usage: tsx scripts/backup-manager.ts [command] [options] */ -import { backupService, BackupConfig } from '../src/services/data-management/backup'; +import { + backupService, + BackupConfig, +} from '../src/services/data-management/backup'; import { backupScheduler } from '../src/services/data-management/backup-scheduler'; import { formatDateForDisplay } from '../src/lib/date-utils'; @@ -57,7 +60,7 @@ OPTIONS: for (let i = 1; i < args.length; i++) { const arg = args[i]; - + if (arg === '--force') { options.force = true; } else if (arg === '--help') { @@ -70,9 +73,12 @@ OPTIONS: return options; } - private async confirmAction(message: string, force?: boolean): Promise { + private async confirmAction( + message: string, + force?: boolean + ): Promise { if (force) return true; - + // Simulation d'une confirmation (en CLI réel, utiliser readline) console.log(`⚠️ ${message}`); console.log('✅ Action confirmée (--force activé ou mode auto)'); @@ -83,12 +89,12 @@ OPTIONS: const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; - + while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } - + return `${size.toFixed(1)} ${units[unitIndex]}`; } @@ -170,15 +176,19 @@ OPTIONS: } private async createBackup(force: boolean = false): Promise { - console.log('🔄 Création d\'une sauvegarde...'); + console.log("🔄 Création d'une sauvegarde..."); const result = await backupService.createBackup('manual', force); - + if (result === null) { - console.log('⏭️ Sauvegarde sautée: Aucun changement détecté depuis la dernière sauvegarde'); - console.log(' 💡 Utilisez --force pour créer une sauvegarde malgré tout'); + console.log( + '⏭️ Sauvegarde sautée: Aucun changement détecté depuis la dernière sauvegarde' + ); + console.log( + ' 💡 Utilisez --force pour créer une sauvegarde malgré tout' + ); return; } - + if (result.status === 'success') { console.log(`✅ Sauvegarde créée: ${result.filename}`); console.log(` Taille: ${this.formatFileSize(result.size)}`); @@ -194,24 +204,28 @@ OPTIONS: private async listBackups(): Promise { console.log('📋 Liste des sauvegardes:\n'); const backups = await backupService.listBackups(); - + if (backups.length === 0) { console.log(' Aucune sauvegarde disponible'); return; } - console.log(`${'Nom'.padEnd(40)} ${'Taille'.padEnd(10)} ${'Type'.padEnd(12)} ${'Date'}`); + console.log( + `${'Nom'.padEnd(40)} ${'Taille'.padEnd(10)} ${'Type'.padEnd(12)} ${'Date'}` + ); console.log('─'.repeat(80)); - + for (const backup of backups) { const name = backup.filename.padEnd(40); const size = this.formatFileSize(backup.size).padEnd(10); - const type = (backup.type === 'manual' ? 'Manuelle' : 'Automatique').padEnd(12); + const type = ( + backup.type === 'manual' ? 'Manuelle' : 'Automatique' + ).padEnd(12); const date = this.formatDate(backup.createdAt); - + console.log(`${name} ${size} ${type} ${date}`); } - + console.log(`\n📊 Total: ${backups.length} sauvegarde(s)`); } @@ -220,7 +234,7 @@ OPTIONS: `Supprimer la sauvegarde "${filename}" ?`, force ); - + if (!confirmed) { console.log('❌ Suppression annulée'); return; @@ -230,12 +244,15 @@ OPTIONS: console.log(`✅ Sauvegarde supprimée: ${filename}`); } - private async restoreBackup(filename: string, force?: boolean): Promise { + private async restoreBackup( + filename: string, + force?: boolean + ): Promise { const confirmed = await this.confirmAction( `Restaurer la base de données depuis "${filename}" ? ATTENTION: Cela remplacera toutes les données actuelles !`, force ); - + if (!confirmed) { console.log('❌ Restauration annulée'); return; @@ -247,7 +264,7 @@ OPTIONS: } private async verifyDatabase(): Promise { - console.log('🔍 Vérification de l\'intégrité de la base...'); + console.log("🔍 Vérification de l'intégrité de la base..."); await backupService.verifyDatabaseHealth(); console.log('✅ Base de données vérifiée avec succès'); } @@ -255,21 +272,29 @@ OPTIONS: private async showConfig(): Promise { const config = backupService.getConfig(); const status = backupScheduler.getStatus(); - + console.log('⚙️ Configuration des sauvegardes:\n'); - console.log(` Activé: ${config.enabled ? '✅ Oui' : '❌ Non'}`); + console.log( + ` Activé: ${config.enabled ? '✅ Oui' : '❌ Non'}` + ); console.log(` Fréquence: ${config.interval}`); console.log(` Max sauvegardes: ${config.maxBackups}`); - console.log(` Compression: ${config.compression ? '✅ Oui' : '❌ Non'}`); + console.log( + ` Compression: ${config.compression ? '✅ Oui' : '❌ Non'}` + ); console.log(` Chemin: ${config.backupPath}`); console.log(`\n📊 Statut du planificateur:`); - console.log(` En cours: ${status.isRunning ? '✅ Oui' : '❌ Non'}`); - console.log(` Prochaine: ${status.nextBackup ? this.formatDate(status.nextBackup) : 'Non planifiée'}`); + console.log( + ` En cours: ${status.isRunning ? '✅ Oui' : '❌ Non'}` + ); + console.log( + ` Prochaine: ${status.nextBackup ? this.formatDate(status.nextBackup) : 'Non planifiée'}` + ); } private async setConfig(configString: string): Promise { const [key, value] = configString.split('='); - + if (!key || !value) { console.error('❌ Format invalide. Utilisez: key=value'); process.exit(1); @@ -283,7 +308,9 @@ OPTIONS: break; case 'interval': if (!['hourly', 'daily', 'weekly'].includes(value)) { - console.error('❌ Interval invalide. Utilisez: hourly, daily, ou weekly'); + console.error( + '❌ Interval invalide. Utilisez: hourly, daily, ou weekly' + ); process.exit(1); } newConfig.interval = value as BackupConfig['interval']; @@ -306,7 +333,7 @@ OPTIONS: backupService.updateConfig(newConfig); console.log(`✅ Configuration mise à jour: ${key} = ${value}`); - + // Redémarrer le scheduler si nécessaire if (key === 'enabled' || key === 'interval') { backupScheduler.restart(); @@ -326,12 +353,18 @@ OPTIONS: private async schedulerStatus(): Promise { const status = backupScheduler.getStatus(); - + console.log('📊 Statut du planificateur:\n'); - console.log(` État: ${status.isRunning ? '✅ Actif' : '❌ Arrêté'}`); - console.log(` Activé: ${status.isEnabled ? '✅ Oui' : '❌ Non'}`); + console.log( + ` État: ${status.isRunning ? '✅ Actif' : '❌ Arrêté'}` + ); + console.log( + ` Activé: ${status.isEnabled ? '✅ Oui' : '❌ Non'}` + ); console.log(` Fréquence: ${status.interval}`); - console.log(` Prochaine: ${status.nextBackup ? this.formatDate(status.nextBackup) : 'Non planifiée'}`); + console.log( + ` Prochaine: ${status.nextBackup ? this.formatDate(status.nextBackup) : 'Non planifiée'}` + ); console.log(` Max sauvegardes: ${status.maxBackups}`); } } @@ -340,7 +373,7 @@ OPTIONS: if (require.main === module) { const cli = new BackupManagerCLI(); const args = process.argv.slice(2); - + cli.run(args).catch((error) => { console.error('❌ Erreur fatale:', error); process.exit(1); diff --git a/scripts/cache-monitor.ts b/scripts/cache-monitor.ts index c6115fd..3eaf0d5 100644 --- a/scripts/cache-monitor.ts +++ b/scripts/cache-monitor.ts @@ -10,18 +10,18 @@ import * as readline from 'readline'; function displayCacheStats() { console.log('\n📊 === STATISTIQUES DU CACHE JIRA ANALYTICS ==='); - + const stats = jiraAnalyticsCache.getStats(); - + console.log(`\n📈 Total des entrées: ${stats.totalEntries}`); - + if (stats.projects.length === 0) { console.log('📭 Aucune donnée en cache'); return; } - + console.log('\n📋 Projets en cache:'); - stats.projects.forEach(project => { + stats.projects.forEach((project) => { const status = project.isExpired ? '❌ EXPIRÉ' : '✅ VALIDE'; console.log(` • ${project.projectKey}:`); console.log(` - Âge: ${project.age}`); @@ -44,13 +44,13 @@ function displayCacheActions() { async function monitorRealtime() { console.log('\n👀 Surveillance en temps réel (Ctrl+C pour arrêter)...'); - + const interval = setInterval(() => { console.clear(); displayCacheStats(); console.log('\n⏰ Mise à jour toutes les 5 secondes...'); }, 5000); - + // Gérer l'arrêt propre process.on('SIGINT', () => { clearInterval(interval); @@ -61,74 +61,77 @@ async function monitorRealtime() { async function main() { console.log('🚀 Cache Monitor Jira Analytics'); - + const args = process.argv.slice(2); const command = args[0]; - + switch (command) { case 'stats': displayCacheStats(); break; - + case 'cleanup': console.log('\n🧹 Nettoyage forcé du cache...'); const cleaned = jiraAnalyticsCache.forceCleanup(); console.log(`✅ ${cleaned} entrées supprimées`); break; - + case 'clear': console.log('\n🗑️ Invalidation de tout le cache...'); jiraAnalyticsCache.invalidateAll(); console.log('✅ Cache vidé'); break; - + case 'monitor': await monitorRealtime(); break; - + default: displayCacheStats(); displayCacheActions(); - + // Interface interactive simple const rl = readline.createInterface({ input: process.stdin, - output: process.stdout + output: process.stdout, }); - + const askAction = () => { - rl.question('\nChoisissez une action (1-5): ', async (answer: string) => { - switch (answer.trim()) { - case '1': - displayCacheStats(); - askAction(); - break; - case '2': - const cleaned = jiraAnalyticsCache.forceCleanup(); - console.log(`✅ ${cleaned} entrées supprimées`); - askAction(); - break; - case '3': - jiraAnalyticsCache.invalidateAll(); - console.log('✅ Cache vidé'); - askAction(); - break; - case '4': - rl.close(); - await monitorRealtime(); - break; - case '5': - console.log('👋 Au revoir !'); - rl.close(); - process.exit(0); - break; - default: - console.log('❌ Action invalide'); - askAction(); + rl.question( + '\nChoisissez une action (1-5): ', + async (answer: string) => { + switch (answer.trim()) { + case '1': + displayCacheStats(); + askAction(); + break; + case '2': + const cleaned = jiraAnalyticsCache.forceCleanup(); + console.log(`✅ ${cleaned} entrées supprimées`); + askAction(); + break; + case '3': + jiraAnalyticsCache.invalidateAll(); + console.log('✅ Cache vidé'); + askAction(); + break; + case '4': + rl.close(); + await monitorRealtime(); + break; + case '5': + console.log('👋 Au revoir !'); + rl.close(); + process.exit(0); + break; + default: + console.log('❌ Action invalide'); + askAction(); + } } - }); + ); }; - + askAction(); } } diff --git a/scripts/reset-database.ts b/scripts/reset-database.ts index 7a4499c..dc76909 100644 --- a/scripts/reset-database.ts +++ b/scripts/reset-database.ts @@ -10,9 +10,13 @@ async function resetDatabase() { try { // Compter les tâches avant suppression const beforeCount = await prisma.task.count(); - const manualCount = await prisma.task.count({ where: { source: 'manual' } }); - const remindersCount = await prisma.task.count({ where: { source: 'reminders' } }); - + const manualCount = await prisma.task.count({ + where: { source: 'manual' }, + }); + const remindersCount = await prisma.task.count({ + where: { source: 'reminders' }, + }); + console.log(`📊 État actuel:`); console.log(` Total: ${beforeCount} tâches`); console.log(` Manuelles: ${manualCount} tâches`); @@ -22,8 +26,8 @@ async function resetDatabase() { // Supprimer toutes les tâches de synchronisation const deletedTasks = await prisma.task.deleteMany({ where: { - source: 'reminders' - } + source: 'reminders', + }, }); console.log(`✅ Supprimé ${deletedTasks.count} tâches de synchronisation`); @@ -38,11 +42,11 @@ async function resetDatabase() { // Compter après nettoyage const afterCount = await prisma.task.count(); - + console.log(''); console.log('🎉 Base de données nettoyée !'); console.log(`📊 Résultat: ${afterCount} tâches restantes`); - + // Afficher les tâches restantes if (afterCount > 0) { console.log(''); @@ -51,30 +55,32 @@ async function resetDatabase() { include: { taskTags: { include: { - tag: true - } - } + tag: true, + }, + }, }, - orderBy: { createdAt: 'desc' } + orderBy: { createdAt: 'desc' }, }); - + remainingTasks.forEach((task, index) => { - const statusEmoji = { - 'todo': '⏳', - 'in_progress': '🔄', - 'done': '✅', - 'cancelled': '❌' - }[task.status] || '❓'; - + const statusEmoji = + { + todo: '⏳', + in_progress: '🔄', + done: '✅', + cancelled: '❌', + }[task.status] || '❓'; + // Utiliser les relations TaskTag - const tags = task.taskTags ? task.taskTags.map(tt => tt.tag.name) : []; - + const tags = task.taskTags + ? task.taskTags.map((tt) => tt.tag.name) + : []; + const tagsStr = tags.length > 0 ? ` [${tags.join(', ')}]` : ''; - + console.log(` ${index + 1}. ${statusEmoji} ${task.title}${tagsStr}`); }); } - } catch (error) { console.error('❌ Erreur lors du reset:', error); throw error; @@ -83,14 +89,16 @@ async function resetDatabase() { // Exécuter le script if (require.main === module) { - resetDatabase().then(() => { - console.log(''); - console.log('✨ Reset terminé avec succès !'); - process.exit(0); - }).catch((error) => { - console.error('💥 Erreur fatale:', error); - process.exit(1); - }); + resetDatabase() + .then(() => { + console.log(''); + console.log('✨ Reset terminé avec succès !'); + process.exit(0); + }) + .catch((error) => { + console.error('💥 Erreur fatale:', error); + process.exit(1); + }); } export { resetDatabase }; diff --git a/scripts/seed-data.ts b/scripts/seed-data.ts index 98f31b9..64683a5 100644 --- a/scripts/seed-data.ts +++ b/scripts/seed-data.ts @@ -11,19 +11,21 @@ async function seedTestData() { const testTasks = [ { title: '🎨 Design System Implementation', - description: 'Create and implement a comprehensive design system with reusable components', + description: + 'Create and implement a comprehensive design system with reusable components', status: 'in_progress' as TaskStatus, priority: 'high' as TaskPriority, tags: ['design', 'ui', 'frontend'], - dueDate: new Date('2025-12-31') + dueDate: new Date('2025-12-31'), }, { title: '🔧 API Performance Optimization', - description: 'Optimize API endpoints response time and implement pagination', + description: + 'Optimize API endpoints response time and implement pagination', status: 'todo' as TaskStatus, priority: 'medium' as TaskPriority, tags: ['backend', 'performance', 'api'], - dueDate: new Date('2025-12-15') + dueDate: new Date('2025-12-15'), }, { title: '✅ Test Coverage Improvement', @@ -31,7 +33,7 @@ async function seedTestData() { status: 'todo' as TaskStatus, priority: 'medium' as TaskPriority, tags: ['testing', 'quality'], - dueDate: new Date('2025-12-20') + dueDate: new Date('2025-12-20'), }, { title: '📱 Mobile Responsive Design', @@ -39,7 +41,7 @@ async function seedTestData() { status: 'todo' as TaskStatus, priority: 'high' as TaskPriority, tags: ['frontend', 'mobile', 'ui'], - dueDate: new Date('2025-12-10') + dueDate: new Date('2025-12-10'), }, { title: '🔒 Security Audit', @@ -47,8 +49,8 @@ async function seedTestData() { status: 'backlog' as TaskStatus, priority: 'urgent' as TaskPriority, tags: ['security', 'audit'], - dueDate: new Date('2026-01-15') - } + dueDate: new Date('2026-01-15'), + }, ]; let createdCount = 0; @@ -57,34 +59,39 @@ async function seedTestData() { for (const taskData of testTasks) { try { const task = await tasksService.createTask(taskData); - + const statusEmoji = { - 'backlog': '📋', - 'todo': '⏳', - 'in_progress': '🔄', - 'freeze': '🧊', - 'done': '✅', - 'cancelled': '❌', - 'archived': '📦' + backlog: '📋', + todo: '⏳', + in_progress: '🔄', + freeze: '🧊', + done: '✅', + cancelled: '❌', + archived: '📦', }[task.status]; - + const priorityEmoji = { - 'low': '🔵', - 'medium': '🟡', - 'high': '🔴', - 'urgent': '🚨' + low: '🔵', + medium: '🟡', + high: '🔴', + urgent: '🚨', }[task.priority]; - + console.log(` ${statusEmoji} ${priorityEmoji} ${task.title}`); console.log(` Tags: ${task.tags?.join(', ') || 'aucun'}`); if (task.dueDate) { - console.log(` Échéance: ${task.dueDate.toLocaleDateString('fr-FR')}`); + console.log( + ` Échéance: ${task.dueDate.toLocaleDateString('fr-FR')}` + ); } console.log(''); - + createdCount++; } catch (error) { - console.error(` ❌ Erreur pour "${taskData.title}":`, error instanceof Error ? error.message : error); + console.error( + ` ❌ Erreur pour "${taskData.title}":`, + error instanceof Error ? error.message : error + ); errorCount++; } } @@ -92,7 +99,7 @@ async function seedTestData() { console.log('📊 Résumé:'); console.log(` ✅ Tâches créées: ${createdCount}`); console.log(` ❌ Erreurs: ${errorCount}`); - + // Afficher les stats finales const stats = await tasksService.getTaskStats(); console.log(''); @@ -107,14 +114,16 @@ async function seedTestData() { // Exécuter le script if (require.main === module) { - seedTestData().then(() => { - console.log(''); - console.log('✨ Données de test ajoutées avec succès !'); - process.exit(0); - }).catch((error) => { - console.error('💥 Erreur fatale:', error); - process.exit(1); - }); + seedTestData() + .then(() => { + console.log(''); + console.log('✨ Données de test ajoutées avec succès !'); + process.exit(0); + }) + .catch((error) => { + console.error('💥 Erreur fatale:', error); + process.exit(1); + }); } export { seedTestData }; diff --git a/scripts/test-jira-fields.ts b/scripts/test-jira-fields.ts index b2be8a7..2d971a0 100644 --- a/scripts/test-jira-fields.ts +++ b/scripts/test-jira-fields.ts @@ -10,13 +10,18 @@ import { userPreferencesService } from '../src/services/core/user-preferences'; async function testJiraFields() { console.log('🔍 Identification des champs personnalisés Jira\n'); - + try { // Récupérer la config Jira pour l'utilisateur spécifié ou 'default' const userId = process.argv[2] || 'default'; const jiraConfig = await userPreferencesService.getJiraConfig(userId); - - if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) { + + if ( + !jiraConfig.enabled || + !jiraConfig.baseUrl || + !jiraConfig.email || + !jiraConfig.apiToken + ) { console.log('❌ Configuration Jira manquante'); return; } @@ -27,14 +32,14 @@ async function testJiraFields() { } console.log(`📋 Analyse du projet: ${jiraConfig.projectKey}`); - + // Créer le service Jira const jiraService = new JiraService(jiraConfig); - + // Récupérer un seul ticket pour analyser tous ses champs const jql = `project = "${jiraConfig.projectKey}" ORDER BY updated DESC`; const issues = await jiraService.searchIssues(jql); - + if (issues.length === 0) { console.log('❌ Aucun ticket trouvé'); return; @@ -44,17 +49,23 @@ async function testJiraFields() { console.log(`\n📄 Analyse du ticket: ${firstIssue.key}`); console.log(`Titre: ${firstIssue.summary}`); console.log(`Type: ${firstIssue.issuetype.name}`); - + // Afficher les story points actuels - console.log(`\n🎯 Story Points actuels: ${firstIssue.storyPoints || 'Non défini'}`); - + console.log( + `\n🎯 Story Points actuels: ${firstIssue.storyPoints || 'Non défini'}` + ); + console.log('\n💡 Pour identifier le bon champ story points:'); console.log('1. Connectez-vous à votre instance Jira'); console.log('2. Allez dans Administration > Projets > [Votre projet]'); console.log('3. Regardez dans "Champs" ou "Story Points"'); - console.log('4. Notez le nom du champ personnalisé (ex: customfield_10003)'); - console.log('5. Modifiez le code dans src/services/integrations/jira/jira.ts ligne 167'); - + console.log( + '4. Notez le nom du champ personnalisé (ex: customfield_10003)' + ); + console.log( + '5. Modifiez le code dans src/services/integrations/jira/jira.ts ligne 167' + ); + console.log('\n🔧 Champs couramment utilisés pour les story points:'); console.log('• customfield_10002 (par défaut)'); console.log('• customfield_10003'); @@ -65,7 +76,7 @@ async function testJiraFields() { console.log('• customfield_10008'); console.log('• customfield_10009'); console.log('• customfield_10010'); - + console.log('\n📝 Alternative: Utiliser les estimations par type'); console.log('Le système utilise déjà des estimations intelligentes:'); console.log('• Epic: 13 points'); @@ -73,7 +84,6 @@ async function testJiraFields() { console.log('• Task: 3 points'); console.log('• Bug: 2 points'); console.log('• Subtask: 1 point'); - } catch (error) { console.error('❌ Erreur lors du test:', error); } @@ -81,4 +91,3 @@ async function testJiraFields() { // Exécution du script testJiraFields().catch(console.error); - diff --git a/scripts/test-story-points.ts b/scripts/test-story-points.ts index 7a33cca..2c7d747 100644 --- a/scripts/test-story-points.ts +++ b/scripts/test-story-points.ts @@ -10,13 +10,18 @@ import { userPreferencesService } from '../src/services/core/user-preferences'; async function testStoryPoints() { console.log('🧪 Test de récupération des story points Jira\n'); - + try { // Récupérer la config Jira pour l'utilisateur spécifié ou 'default' const userId = process.argv[2] || 'default'; const jiraConfig = await userPreferencesService.getJiraConfig(userId); - - if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) { + + if ( + !jiraConfig.enabled || + !jiraConfig.baseUrl || + !jiraConfig.email || + !jiraConfig.apiToken + ) { console.log('❌ Configuration Jira manquante'); return; } @@ -27,41 +32,47 @@ async function testStoryPoints() { } console.log(`📋 Test sur le projet: ${jiraConfig.projectKey}`); - + // Créer le service Jira const jiraService = new JiraService(jiraConfig); - + // Récupérer quelques tickets pour tester const jql = `project = "${jiraConfig.projectKey}" ORDER BY updated DESC`; const issues = await jiraService.searchIssues(jql); - + console.log(`\n📊 Analyse de ${issues.length} tickets:\n`); - + let totalStoryPoints = 0; let ticketsWithStoryPoints = 0; let ticketsWithoutStoryPoints = 0; - + const storyPointsDistribution: Record = {}; - const typeDistribution: Record = {}; - + const typeDistribution: Record< + string, + { count: number; totalPoints: number } + > = {}; + issues.slice(0, 20).forEach((issue, index) => { const storyPoints = issue.storyPoints || 0; const issueType = issue.issuetype.name; - + console.log(`${index + 1}. ${issue.key} (${issueType})`); console.log(` Titre: ${issue.summary.substring(0, 50)}...`); - console.log(` Story Points: ${storyPoints > 0 ? storyPoints : 'Non défini'}`); + console.log( + ` Story Points: ${storyPoints > 0 ? storyPoints : 'Non défini'}` + ); console.log(` Statut: ${issue.status.name}`); console.log(''); - + if (storyPoints > 0) { ticketsWithStoryPoints++; totalStoryPoints += storyPoints; - storyPointsDistribution[storyPoints] = (storyPointsDistribution[storyPoints] || 0) + 1; + storyPointsDistribution[storyPoints] = + (storyPointsDistribution[storyPoints] || 0) + 1; } else { ticketsWithoutStoryPoints++; } - + // Distribution par type if (!typeDistribution[issueType]) { typeDistribution[issueType] = { count: 0, totalPoints: 0 }; @@ -69,37 +80,47 @@ async function testStoryPoints() { typeDistribution[issueType].count++; typeDistribution[issueType].totalPoints += storyPoints; }); - + console.log('📈 === RÉSUMÉ ===\n'); console.log(`Total tickets analysés: ${issues.length}`); console.log(`Tickets avec story points: ${ticketsWithStoryPoints}`); console.log(`Tickets sans story points: ${ticketsWithoutStoryPoints}`); console.log(`Total story points: ${totalStoryPoints}`); - console.log(`Moyenne par ticket: ${issues.length > 0 ? (totalStoryPoints / issues.length).toFixed(2) : 0}`); - + console.log( + `Moyenne par ticket: ${issues.length > 0 ? (totalStoryPoints / issues.length).toFixed(2) : 0}` + ); + console.log('\n📊 Distribution des story points:'); Object.entries(storyPointsDistribution) .sort(([a], [b]) => parseInt(a) - parseInt(b)) .forEach(([points, count]) => { console.log(` ${points} points: ${count} tickets`); }); - + console.log('\n🏷️ Distribution par type:'); Object.entries(typeDistribution) - .sort(([,a], [,b]) => b.count - a.count) + .sort(([, a], [, b]) => b.count - a.count) .forEach(([type, stats]) => { - const avgPoints = stats.count > 0 ? (stats.totalPoints / stats.count).toFixed(2) : '0'; - console.log(` ${type}: ${stats.count} tickets, ${stats.totalPoints} points total, ${avgPoints} points moyen`); + const avgPoints = + stats.count > 0 ? (stats.totalPoints / stats.count).toFixed(2) : '0'; + console.log( + ` ${type}: ${stats.count} tickets, ${stats.totalPoints} points total, ${avgPoints} points moyen` + ); }); - + if (ticketsWithoutStoryPoints > 0) { console.log('\n⚠️ Recommandations:'); - console.log('• Vérifiez que le champ "Story Points" est configuré dans votre projet Jira'); + console.log( + '• Vérifiez que le champ "Story Points" est configuré dans votre projet Jira' + ); console.log('• Le champ par défaut est "customfield_10002"'); - console.log('• Si votre projet utilise un autre champ, modifiez le code dans jira.ts'); - console.log('• En attendant, le système utilise des estimations basées sur le type de ticket'); + console.log( + '• Si votre projet utilise un autre champ, modifiez le code dans jira.ts' + ); + console.log( + '• En attendant, le système utilise des estimations basées sur le type de ticket' + ); } - } catch (error) { console.error('❌ Erreur lors du test:', error); } @@ -107,4 +128,3 @@ async function testStoryPoints() { // Exécution du script testStoryPoints().catch(console.error); - diff --git a/src/actions/backup.ts b/src/actions/backup.ts index 415505d..ea7c5da 100644 --- a/src/actions/backup.ts +++ b/src/actions/backup.ts @@ -6,28 +6,32 @@ import { revalidatePath } from 'next/cache'; export async function createBackupAction(force: boolean = false) { try { const result = await backupService.createBackup('manual', force); - + // Invalider le cache de la page pour forcer le rechargement des données SSR revalidatePath('/settings/backup'); - + if (result === null) { return { success: true, skipped: true, - message: 'Sauvegarde sautée : aucun changement détecté. Utilisez "Forcer" pour créer malgré tout.' + message: + 'Sauvegarde sautée : aucun changement détecté. Utilisez "Forcer" pour créer malgré tout.', }; } - + return { success: true, data: result, - message: `Sauvegarde créée : ${result.filename}` + message: `Sauvegarde créée : ${result.filename}`, }; } catch (error) { console.error('Failed to create backup:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur lors de la création de la sauvegarde' + error: + error instanceof Error + ? error.message + : 'Erreur lors de la création de la sauvegarde', }; } } @@ -37,13 +41,13 @@ export async function verifyDatabaseAction() { await backupService.verifyDatabaseHealth(); return { success: true, - message: 'Intégrité vérifiée' + message: 'Intégrité vérifiée', }; } catch (error) { console.error('Database verification failed:', error); return { success: false, - error: error instanceof Error ? error.message : 'Vérification échouée' + error: error instanceof Error ? error.message : 'Vérification échouée', }; } } diff --git a/src/actions/daily.ts b/src/actions/daily.ts index f5e0801..c06b0f3 100644 --- a/src/actions/daily.ts +++ b/src/actions/daily.ts @@ -1,9 +1,18 @@ 'use server'; import { dailyService } from '@/services/task-management/daily'; -import { UpdateDailyCheckboxData, DailyCheckbox, CreateDailyCheckboxData } from '@/lib/types'; +import { + UpdateDailyCheckboxData, + DailyCheckbox, + CreateDailyCheckboxData, +} from '@/lib/types'; import { revalidatePath } from 'next/cache'; -import { getToday, getPreviousWorkday, parseDate, normalizeDate } from '@/lib/date-utils'; +import { + getToday, + getPreviousWorkday, + parseDate, + normalizeDate, +} from '@/lib/date-utils'; /** * Toggle l'état d'une checkbox @@ -18,41 +27,44 @@ export async function toggleCheckbox(checkboxId: string): Promise<{ // En absence de getCheckboxById, nous allons essayer de la trouver via une vue daily // Pour l'instant, nous allons simplement toggle via updateCheckbox // (le front-end gère déjà l'état optimiste) - + // Récupérer toutes les checkboxes d'aujourd'hui et hier pour trouver celle à toggle const today = getToday(); const dailyView = await dailyService.getDailyView(today); - - let checkbox = dailyView.today.find(cb => cb.id === checkboxId); + + let checkbox = dailyView.today.find((cb) => cb.id === checkboxId); if (!checkbox) { - checkbox = dailyView.yesterday.find(cb => cb.id === checkboxId); + checkbox = dailyView.yesterday.find((cb) => cb.id === checkboxId); } - + if (!checkbox) { return { success: false, error: 'Checkbox non trouvée' }; } - + // Toggle l'état const updatedCheckbox = await dailyService.updateCheckbox(checkboxId, { - isChecked: !checkbox.isChecked + isChecked: !checkbox.isChecked, }); - + revalidatePath('/daily'); return { success: true, data: updatedCheckbox }; } catch (error) { console.error('Erreur toggleCheckbox:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } - /** * Ajoute une checkbox pour aujourd'hui */ -export async function addTodayCheckbox(content: string, type?: 'task' | 'meeting', taskId?: string): Promise<{ +export async function addTodayCheckbox( + content: string, + type?: 'task' | 'meeting', + taskId?: string +): Promise<{ success: boolean; data?: DailyCheckbox; error?: string; @@ -62,16 +74,16 @@ export async function addTodayCheckbox(content: string, type?: 'task' | 'meeting date: getToday(), text: content, type: type || 'task', - taskId + taskId, }); - + revalidatePath('/daily'); return { success: true, data: newCheckbox }; } catch (error) { console.error('Erreur addTodayCheckbox:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -79,51 +91,57 @@ export async function addTodayCheckbox(content: string, type?: 'task' | 'meeting /** * Ajoute une checkbox pour hier */ -export async function addYesterdayCheckbox(content: string, type?: 'task' | 'meeting', taskId?: string): Promise<{ +export async function addYesterdayCheckbox( + content: string, + type?: 'task' | 'meeting', + taskId?: string +): Promise<{ success: boolean; data?: DailyCheckbox; error?: string; }> { try { const yesterday = getPreviousWorkday(getToday()); - + const newCheckbox = await dailyService.addCheckbox({ date: yesterday, text: content, type: type || 'task', - taskId + taskId, }); - + revalidatePath('/daily'); return { success: true, data: newCheckbox }; } catch (error) { console.error('Erreur addYesterdayCheckbox:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } - /** * Met à jour une checkbox complète */ -export async function updateCheckbox(checkboxId: string, data: UpdateDailyCheckboxData): Promise<{ +export async function updateCheckbox( + checkboxId: string, + data: UpdateDailyCheckboxData +): Promise<{ success: boolean; data?: DailyCheckbox; error?: string; }> { try { const updatedCheckbox = await dailyService.updateCheckbox(checkboxId, data); - + revalidatePath('/daily'); return { success: true, data: updatedCheckbox }; } catch (error) { console.error('Erreur updateCheckbox:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -137,14 +155,14 @@ export async function deleteCheckbox(checkboxId: string): Promise<{ }> { try { await dailyService.deleteCheckbox(checkboxId); - + revalidatePath('/daily'); return { success: true }; } catch (error) { console.error('Erreur deleteCheckbox:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -152,7 +170,11 @@ export async function deleteCheckbox(checkboxId: string): Promise<{ /** * Ajoute un todo lié à une tâche */ -export async function addTodoToTask(taskId: string, text: string, date?: Date): Promise<{ +export async function addTodoToTask( + taskId: string, + text: string, + date?: Date +): Promise<{ success: boolean; data?: DailyCheckbox; error?: string; @@ -165,11 +187,11 @@ export async function addTodoToTask(taskId: string, text: string, date?: Date): text: text.trim(), type: 'task', taskId: taskId, - isChecked: false + isChecked: false, }; const checkbox = await dailyService.addCheckbox(checkboxData); - + revalidatePath('/daily'); revalidatePath('/kanban'); return { success: true, data: checkbox }; @@ -177,7 +199,7 @@ export async function addTodoToTask(taskId: string, text: string, date?: Date): console.error('Erreur addTodoToTask:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -185,23 +207,26 @@ export async function addTodoToTask(taskId: string, text: string, date?: Date): /** * Réorganise les checkboxes d'une date */ -export async function reorderCheckboxes(dailyId: string, checkboxIds: string[]): Promise<{ +export async function reorderCheckboxes( + dailyId: string, + checkboxIds: string[] +): Promise<{ success: boolean; error?: string; }> { try { // Le dailyId correspond à la date au format YYYY-MM-DD const date = parseDate(dailyId); - + await dailyService.reorderCheckboxes(date, checkboxIds); - + revalidatePath('/daily'); return { success: true }; } catch (error) { console.error('Erreur reorderCheckboxes:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -216,14 +241,14 @@ export async function moveCheckboxToToday(checkboxId: string): Promise<{ }> { try { const updatedCheckbox = await dailyService.moveCheckboxToToday(checkboxId); - + revalidatePath('/daily'); return { success: true, data: updatedCheckbox }; } catch (error) { console.error('Erreur moveCheckboxToToday:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } diff --git a/src/actions/jira-analytics.ts b/src/actions/jira-analytics.ts index 821b5e2..5fea0ad 100644 --- a/src/actions/jira-analytics.ts +++ b/src/actions/jira-analytics.ts @@ -15,7 +15,9 @@ export type JiraAnalyticsResult = { /** * Server Action pour récupérer les analytics Jira du projet configuré */ -export async function getJiraAnalytics(forceRefresh = false): Promise { +export async function getJiraAnalytics( + forceRefresh = false +): Promise { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { @@ -23,45 +25,56 @@ export async function getJiraAnalytics(forceRefresh = false): Promise { +export async function detectJiraAnomalies( + forceRefresh = false +): Promise { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { @@ -23,12 +32,19 @@ export async function detectJiraAnomalies(forceRefresh = false): Promise) { +export async function updateAnomalyDetectionConfig( + config: Partial +) { try { jiraAnomalyDetection.updateConfig(config); - + return { success: true, - data: jiraAnomalyDetection.getConfig() + data: jiraAnomalyDetection.getConfig(), }; } catch (error) { console.error('❌ Erreur lors de la mise à jour de la config:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -83,13 +103,13 @@ export async function getAnomalyDetectionConfig() { try { return { success: true, - data: jiraAnomalyDetection.getConfig() + data: jiraAnomalyDetection.getConfig(), }; } catch (error) { console.error('❌ Erreur lors de la récupération de la config:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } diff --git a/src/actions/jira-export.ts b/src/actions/jira-export.ts index be1fd71..68925b8 100644 --- a/src/actions/jira-export.ts +++ b/src/actions/jira-export.ts @@ -91,15 +91,17 @@ export interface JiraAnalytics { /** * Server Action pour exporter les analytics Jira au format CSV ou JSON */ -export async function exportJiraAnalytics(format: ExportFormat = 'csv'): Promise { +export async function exportJiraAnalytics( + format: ExportFormat = 'csv' +): Promise { try { // Récupérer les analytics (force refresh pour avoir les données les plus récentes) const analyticsResult = await getJiraAnalytics(true); - + if (!analyticsResult.success || !analyticsResult.data) { return { success: false, - error: analyticsResult.error || 'Impossible de récupérer les analytics' + error: analyticsResult.error || 'Impossible de récupérer les analytics', }; } @@ -111,25 +113,24 @@ export async function exportJiraAnalytics(format: ExportFormat = 'csv'): Promise return { success: true, data: JSON.stringify(analytics, null, 2), - filename: `jira-analytics-${projectKey}-${timestamp}.json` + filename: `jira-analytics-${projectKey}-${timestamp}.json`, }; } // Format CSV const csvData = generateCSV(analytics); - + return { success: true, data: csvData, - filename: `jira-analytics-${projectKey}-${timestamp}.csv` + filename: `jira-analytics-${projectKey}-${timestamp}.csv`, }; - } catch (error) { - console.error('❌ Erreur lors de l\'export des analytics:', error); - + console.error("❌ Erreur lors de l'export des analytics:", error); + return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -143,103 +144,126 @@ function generateCSV(analytics: JiraAnalytics): string { // Header du rapport lines.push('# Rapport Analytics Jira'); lines.push(`# Projet: ${analytics.project.name} (${analytics.project.key})`); - lines.push(`# Généré le: ${formatDateForDisplay(getToday(), 'DISPLAY_LONG')}`); + lines.push( + `# Généré le: ${formatDateForDisplay(getToday(), 'DISPLAY_LONG')}` + ); lines.push(`# Total tickets: ${analytics.project.totalIssues}`); lines.push(''); // Section 1: Métriques d'équipe - lines.push('## Répartition de l\'équipe'); - lines.push('Assignee,Nom,Total Tickets,Tickets Complétés,Tickets En Cours,Pourcentage'); - analytics.teamMetrics.issuesDistribution.forEach((assignee: AssigneeMetrics) => { - lines.push([ - escapeCsv(assignee.assignee), - escapeCsv(assignee.displayName), - assignee.totalIssues, - assignee.completedIssues, - assignee.inProgressIssues, - assignee.percentage.toFixed(1) + '%' - ].join(',')); - }); + lines.push("## Répartition de l'équipe"); + lines.push( + 'Assignee,Nom,Total Tickets,Tickets Complétés,Tickets En Cours,Pourcentage' + ); + analytics.teamMetrics.issuesDistribution.forEach( + (assignee: AssigneeMetrics) => { + lines.push( + [ + escapeCsv(assignee.assignee), + escapeCsv(assignee.displayName), + assignee.totalIssues, + assignee.completedIssues, + assignee.inProgressIssues, + assignee.percentage.toFixed(1) + '%', + ].join(',') + ); + } + ); lines.push(''); // Section 2: Historique des sprints lines.push('## Historique des sprints'); - lines.push('Sprint,Date Début,Date Fin,Points Planifiés,Points Complétés,Taux de Complétion'); + lines.push( + 'Sprint,Date Début,Date Fin,Points Planifiés,Points Complétés,Taux de Complétion' + ); analytics.velocityMetrics.sprintHistory.forEach((sprint: SprintHistory) => { - lines.push([ - escapeCsv(sprint.sprintName), - sprint.startDate.slice(0, 10), - sprint.endDate.slice(0, 10), - sprint.plannedPoints, - sprint.completedPoints, - sprint.completionRate + '%' - ].join(',')); + lines.push( + [ + escapeCsv(sprint.sprintName), + sprint.startDate.slice(0, 10), + sprint.endDate.slice(0, 10), + sprint.plannedPoints, + sprint.completedPoints, + sprint.completionRate + '%', + ].join(',') + ); }); lines.push(''); // Section 3: Cycle time par type lines.push('## Cycle Time par type de ticket'); - lines.push('Type de Ticket,Temps Moyen (jours),Temps Médian (jours),Échantillons'); - analytics.cycleTimeMetrics.cycleTimeByType.forEach((type: CycleTimeByType) => { - lines.push([ - escapeCsv(type.issueType), - type.averageDays, - type.medianDays, - type.samples - ].join(',')); - }); + lines.push( + 'Type de Ticket,Temps Moyen (jours),Temps Médian (jours),Échantillons' + ); + analytics.cycleTimeMetrics.cycleTimeByType.forEach( + (type: CycleTimeByType) => { + lines.push( + [ + escapeCsv(type.issueType), + type.averageDays, + type.medianDays, + type.samples, + ].join(',') + ); + } + ); lines.push(''); // Section 4: Work in Progress lines.push('## Work in Progress par statut'); lines.push('Statut,Nombre,Pourcentage'); analytics.workInProgress.byStatus.forEach((status: WorkInProgressStatus) => { - lines.push([ - escapeCsv(status.status), - status.count, - status.percentage + '%' - ].join(',')); + lines.push( + [escapeCsv(status.status), status.count, status.percentage + '%'].join( + ',' + ) + ); }); lines.push(''); // Section 5: Charge de travail par assignee lines.push('## Charge de travail par assignee'); lines.push('Assignee,Nom,À Faire,En Cours,En Revue,Total Actif'); - analytics.workInProgress.byAssignee.forEach((assignee: WorkInProgressAssignee) => { - lines.push([ - escapeCsv(assignee.assignee), - escapeCsv(assignee.displayName), - assignee.todoCount, - assignee.inProgressCount, - assignee.reviewCount, - assignee.totalActive - ].join(',')); - }); + analytics.workInProgress.byAssignee.forEach( + (assignee: WorkInProgressAssignee) => { + lines.push( + [ + escapeCsv(assignee.assignee), + escapeCsv(assignee.displayName), + assignee.todoCount, + assignee.inProgressCount, + assignee.reviewCount, + assignee.totalActive, + ].join(',') + ); + } + ); lines.push(''); // Section 6: Métriques résumé lines.push('## Métriques de résumé'); lines.push('Métrique,Valeur'); - lines.push([ - 'Total membres équipe', - analytics.teamMetrics.totalAssignees - ].join(',')); - lines.push([ - 'Membres actifs', - analytics.teamMetrics.activeAssignees - ].join(',')); - lines.push([ - 'Points complétés sprint actuel', - analytics.velocityMetrics.currentSprintPoints - ].join(',')); - lines.push([ - 'Vélocité moyenne', - analytics.velocityMetrics.averageVelocity - ].join(',')); - lines.push([ - 'Cycle time moyen (jours)', - analytics.cycleTimeMetrics.averageCycleTime - ].join(',')); + lines.push( + ['Total membres équipe', analytics.teamMetrics.totalAssignees].join(',') + ); + lines.push( + ['Membres actifs', analytics.teamMetrics.activeAssignees].join(',') + ); + lines.push( + [ + 'Points complétés sprint actuel', + analytics.velocityMetrics.currentSprintPoints, + ].join(',') + ); + lines.push( + ['Vélocité moyenne', analytics.velocityMetrics.averageVelocity].join(',') + ); + lines.push( + [ + 'Cycle time moyen (jours)', + analytics.cycleTimeMetrics.averageCycleTime, + ].join(',') + ); return lines.join('\n'); } @@ -249,12 +273,12 @@ function generateCSV(analytics: JiraAnalytics): string { */ function escapeCsv(value: string): string { if (typeof value !== 'string') return String(value); - + // Si la valeur contient des guillemets, virgules ou retours à la ligne if (value.includes('"') || value.includes(',') || value.includes('\n')) { // Doubler les guillemets et entourer de guillemets return '"' + value.replace(/"/g, '""') + '"'; } - + return value; } diff --git a/src/actions/jira-filters.ts b/src/actions/jira-filters.ts index 5102b4f..aa0ae83 100644 --- a/src/actions/jira-filters.ts +++ b/src/actions/jira-filters.ts @@ -1,9 +1,16 @@ 'use server'; -import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/integrations/jira/analytics'; +import { + JiraAnalyticsService, + JiraAnalyticsConfig, +} from '@/services/integrations/jira/analytics'; import { JiraAdvancedFiltersService } from '@/services/integrations/jira/advanced-filters'; import { userPreferencesService } from '@/services/core/user-preferences'; -import { AvailableFilters, JiraAnalyticsFilters, JiraAnalytics } from '@/lib/types'; +import { + AvailableFilters, + JiraAnalyticsFilters, + JiraAnalytics, +} from '@/lib/types'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; @@ -30,12 +37,19 @@ export async function getAvailableJiraFilters(): Promise { } // Récupérer la config Jira - const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); - - if (!jiraConfig?.baseUrl || !jiraConfig?.email || !jiraConfig?.apiToken || !jiraConfig?.projectKey) { + const jiraConfig = await userPreferencesService.getJiraConfig( + session.user.id + ); + + if ( + !jiraConfig?.baseUrl || + !jiraConfig?.email || + !jiraConfig?.apiToken || + !jiraConfig?.projectKey + ) { return { success: false, - error: 'Configuration Jira incomplète' + error: 'Configuration Jira incomplète', }; } @@ -43,24 +57,27 @@ export async function getAvailableJiraFilters(): Promise { if (!jiraConfig.baseUrl || !jiraConfig.projectKey) { return { success: false, error: 'Configuration Jira incomplète' }; } - - const analyticsService = new JiraAnalyticsService(jiraConfig as JiraAnalyticsConfig); - + + const analyticsService = new JiraAnalyticsService( + jiraConfig as JiraAnalyticsConfig + ); + // Récupérer la liste des issues pour extraire les filtres const allIssues = await analyticsService.getAllProjectIssues(); - + // Extraire les filtres disponibles - const availableFilters = JiraAdvancedFiltersService.extractAvailableFilters(allIssues); + const availableFilters = + JiraAdvancedFiltersService.extractAvailableFilters(allIssues); return { success: true, - data: availableFilters + data: availableFilters, }; } catch (error) { console.error('❌ Erreur lors de la récupération des filtres:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -68,7 +85,9 @@ export async function getAvailableJiraFilters(): Promise { /** * Applique des filtres aux analytics et retourne les données filtrées */ -export async function getFilteredJiraAnalytics(filters: Partial): Promise { +export async function getFilteredJiraAnalytics( + filters: Partial +): Promise { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { @@ -76,12 +95,19 @@ export async function getFilteredJiraAnalytics(filters: Partial { +export async function getSprintDetails( + sprintName: string +): Promise { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { @@ -25,12 +35,19 @@ export async function getSprintDetails(sprintName: string): Promise s.sprintName === sprintName); + + const sprint = analytics.velocityMetrics.sprintHistory.find( + (s) => s.sprintName === sprintName + ); if (!sprint) { return { success: false, - error: `Sprint "${sprintName}" introuvable` + error: `Sprint "${sprintName}" introuvable`, }; } // Récupérer toutes les issues du projet pour filtrer par sprint const allIssues = await analyticsService.getAllProjectIssues(); - + // Filtrer les issues pour ce sprint spécifique // Note: En réalité, il faudrait une requête JQL plus précise pour récupérer les issues d'un sprint // Pour simplifier, on prend les issues dans la période du sprint const sprintStart = parseDate(sprint.startDate); const sprintEnd = parseDate(sprint.endDate); - - const sprintIssues = allIssues.filter(issue => { + + const sprintIssues = allIssues.filter((issue) => { const issueDate = parseDate(issue.created); return issueDate >= sprintStart && issueDate <= sprintEnd; }); // Calculer les métriques du sprint const sprintMetrics = calculateSprintMetrics(sprintIssues, sprint); - + // Calculer la distribution par assigné pour ce sprint const assigneeDistribution = calculateAssigneeDistribution(sprintIssues); - + // Calculer la distribution par statut pour ce sprint const statusDistribution = calculateStatusDistribution(sprintIssues); @@ -78,18 +99,21 @@ export async function getSprintDetails(sprintName: string): Promise - issue.status.category === 'Done' || - issue.status.name.toLowerCase().includes('done') || - issue.status.name.toLowerCase().includes('closed') + const completedIssues = issues.filter( + (issue) => + issue.status.category === 'Done' || + issue.status.name.toLowerCase().includes('done') || + issue.status.name.toLowerCase().includes('closed') ).length; - - const inProgressIssues = issues.filter(issue => - issue.status.category === 'In Progress' || - issue.status.name.toLowerCase().includes('progress') || - issue.status.name.toLowerCase().includes('review') + + const inProgressIssues = issues.filter( + (issue) => + issue.status.category === 'In Progress' || + issue.status.name.toLowerCase().includes('progress') || + issue.status.name.toLowerCase().includes('review') ).length; - - const blockedIssues = issues.filter(issue => - issue.status.name.toLowerCase().includes('blocked') || - issue.status.name.toLowerCase().includes('waiting') + + const blockedIssues = issues.filter( + (issue) => + issue.status.name.toLowerCase().includes('blocked') || + issue.status.name.toLowerCase().includes('waiting') ).length; // Calcul du cycle time moyen pour ce sprint - const completedIssuesWithDates = issues.filter(issue => - issue.status.category === 'Done' && issue.created && issue.updated + const completedIssuesWithDates = issues.filter( + (issue) => + issue.status.category === 'Done' && issue.created && issue.updated ); - + let averageCycleTime = 0; if (completedIssuesWithDates.length > 0) { const totalCycleTime = completedIssuesWithDates.reduce((total, issue) => { const created = parseDate(issue.created); const updated = parseDate(issue.updated); - const cycleTime = (updated.getTime() - created.getTime()) / (1000 * 60 * 60 * 24); // en jours + const cycleTime = + (updated.getTime() - created.getTime()) / (1000 * 60 * 60 * 24); // en jours return total + cycleTime; }, 0); averageCycleTime = totalCycleTime / completedIssuesWithDates.length; @@ -146,40 +175,51 @@ function calculateSprintMetrics(issues: JiraTask[], sprint: SprintVelocity) { inProgressIssues, blockedIssues, averageCycleTime, - velocityTrend + velocityTrend, }; } /** * Calcule la distribution par assigné pour le sprint */ -function calculateAssigneeDistribution(issues: JiraTask[]): AssigneeDistribution[] { - const assigneeMap = new Map(); - - issues.forEach(issue => { +function calculateAssigneeDistribution( + issues: JiraTask[] +): AssigneeDistribution[] { + const assigneeMap = new Map< + string, + { total: number; completed: number; inProgress: number } + >(); + + issues.forEach((issue) => { const assigneeName = issue.assignee?.displayName || 'Non assigné'; - const current = assigneeMap.get(assigneeName) || { total: 0, completed: 0, inProgress: 0 }; - + const current = assigneeMap.get(assigneeName) || { + total: 0, + completed: 0, + inProgress: 0, + }; + current.total++; - + if (issue.status.category === 'Done') { current.completed++; } else if (issue.status.category === 'In Progress') { current.inProgress++; } - + assigneeMap.set(assigneeName, current); }); - return Array.from(assigneeMap.entries()).map(([displayName, stats]) => ({ - assignee: displayName === 'Non assigné' ? '' : displayName, - displayName, - totalIssues: stats.total, - completedIssues: stats.completed, - inProgressIssues: stats.inProgress, - percentage: issues.length > 0 ? (stats.total / issues.length) * 100 : 0, - count: stats.total // Ajout pour compatibilité - })).sort((a, b) => b.totalIssues - a.totalIssues); + return Array.from(assigneeMap.entries()) + .map(([displayName, stats]) => ({ + assignee: displayName === 'Non assigné' ? '' : displayName, + displayName, + totalIssues: stats.total, + completedIssues: stats.completed, + inProgressIssues: stats.inProgress, + percentage: issues.length > 0 ? (stats.total / issues.length) * 100 : 0, + count: stats.total, // Ajout pour compatibilité + })) + .sort((a, b) => b.totalIssues - a.totalIssues); } /** @@ -187,14 +227,19 @@ function calculateAssigneeDistribution(issues: JiraTask[]): AssigneeDistribution */ function calculateStatusDistribution(issues: JiraTask[]): StatusDistribution[] { const statusMap = new Map(); - - issues.forEach(issue => { - statusMap.set(issue.status.name, (statusMap.get(issue.status.name) || 0) + 1); + + issues.forEach((issue) => { + statusMap.set( + issue.status.name, + (statusMap.get(issue.status.name) || 0) + 1 + ); }); - return Array.from(statusMap.entries()).map(([status, count]) => ({ - status, - count, - percentage: issues.length > 0 ? (count / issues.length) * 100 : 0 - })).sort((a, b) => b.count - a.count); + return Array.from(statusMap.entries()) + .map(([status, count]) => ({ + status, + count, + percentage: issues.length > 0 ? (count / issues.length) * 100 : 0, + })) + .sort((a, b) => b.count - a.count); } diff --git a/src/actions/metrics.ts b/src/actions/metrics.ts index c3dc186..9b6894f 100644 --- a/src/actions/metrics.ts +++ b/src/actions/metrics.ts @@ -1,6 +1,10 @@ 'use server'; -import { MetricsService, WeeklyMetricsOverview, VelocityTrend } from '@/services/analytics/metrics'; +import { + MetricsService, + WeeklyMetricsOverview, + VelocityTrend, +} from '@/services/analytics/metrics'; import { getToday } from '@/lib/date-utils'; /** @@ -14,16 +18,19 @@ export async function getWeeklyMetrics(date?: Date): Promise<{ try { const targetDate = date || getToday(); const metrics = await MetricsService.getWeeklyMetrics(targetDate); - + return { success: true, - data: metrics + data: metrics, }; } catch (error) { console.error('Error fetching weekly metrics:', error); return { success: false, - error: error instanceof Error ? error.message : 'Failed to fetch weekly metrics' + error: + error instanceof Error + ? error.message + : 'Failed to fetch weekly metrics', }; } } @@ -40,22 +47,24 @@ export async function getVelocityTrends(weeksBack: number = 4): Promise<{ if (weeksBack < 1 || weeksBack > 12) { return { success: false, - error: 'Invalid weeksBack parameter (must be 1-12)' + error: 'Invalid weeksBack parameter (must be 1-12)', }; } - + const trends = await MetricsService.getVelocityTrends(weeksBack); - + return { success: true, - data: trends + data: trends, }; } catch (error) { console.error('Error fetching velocity trends:', error); return { success: false, - error: error instanceof Error ? error.message : 'Failed to fetch velocity trends' + error: + error instanceof Error + ? error.message + : 'Failed to fetch velocity trends', }; } } - diff --git a/src/actions/preferences.ts b/src/actions/preferences.ts index 16fb43f..e833242 100644 --- a/src/actions/preferences.ts +++ b/src/actions/preferences.ts @@ -1,7 +1,12 @@ 'use server'; import { userPreferencesService } from '@/services/core/user-preferences'; -import { KanbanFilters, ViewPreferences, ColumnVisibility, TaskStatus } from '@/lib/types'; +import { + KanbanFilters, + ViewPreferences, + ColumnVisibility, + TaskStatus, +} from '@/lib/types'; import { Theme } from '@/lib/ui-config'; import { revalidatePath } from 'next/cache'; import { getServerSession } from 'next-auth'; @@ -10,7 +15,9 @@ import { authOptions } from '@/lib/auth'; /** * Met à jour les préférences de vue */ -export async function updateViewPreferences(updates: Partial): Promise<{ +export async function updateViewPreferences( + updates: Partial +): Promise<{ success: boolean; error?: string; }> { @@ -19,15 +26,18 @@ export async function updateViewPreferences(updates: Partial): if (!session?.user?.id) { return { success: false, error: 'Non authentifié' }; } - - await userPreferencesService.updateViewPreferences(session.user.id, updates); + + await userPreferencesService.updateViewPreferences( + session.user.id, + updates + ); revalidatePath('/'); return { success: true }; } catch (error) { console.error('Erreur updateViewPreferences:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -35,7 +45,9 @@ export async function updateViewPreferences(updates: Partial): /** * Met à jour l'image de fond */ -export async function setBackgroundImage(backgroundImage: string | undefined): Promise<{ +export async function setBackgroundImage( + backgroundImage: string | undefined +): Promise<{ success: boolean; error?: string; }> { @@ -44,15 +56,17 @@ export async function setBackgroundImage(backgroundImage: string | undefined): P if (!session?.user?.id) { return { success: false, error: 'Non authentifié' }; } - - await userPreferencesService.updateViewPreferences(session.user.id, { backgroundImage }); + + await userPreferencesService.updateViewPreferences(session.user.id, { + backgroundImage, + }); revalidatePath('/'); return { success: true }; } catch (error) { console.error('Erreur setBackgroundImage:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -60,7 +74,9 @@ export async function setBackgroundImage(backgroundImage: string | undefined): P /** * Met à jour les filtres Kanban */ -export async function updateKanbanFilters(updates: Partial): Promise<{ +export async function updateKanbanFilters( + updates: Partial +): Promise<{ success: boolean; error?: string; }> { @@ -69,7 +85,7 @@ export async function updateKanbanFilters(updates: Partial): Prom if (!session?.user?.id) { return { success: false, error: 'Non authentifié' }; } - + await userPreferencesService.updateKanbanFilters(session.user.id, updates); revalidatePath('/kanban'); return { success: true }; @@ -77,7 +93,7 @@ export async function updateKanbanFilters(updates: Partial): Prom console.error('Erreur updateKanbanFilters:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -85,7 +101,9 @@ export async function updateKanbanFilters(updates: Partial): Prom /** * Met à jour la visibilité des colonnes */ -export async function updateColumnVisibility(updates: Partial): Promise<{ +export async function updateColumnVisibility( + updates: Partial +): Promise<{ success: boolean; error?: string; }> { @@ -94,21 +112,26 @@ export async function updateColumnVisibility(updates: Partial) if (!session?.user?.id) { return { success: false, error: 'Non authentifié' }; } - - const preferences = await userPreferencesService.getAllPreferences(session.user.id); + + const preferences = await userPreferencesService.getAllPreferences( + session.user.id + ); const newColumnVisibility: ColumnVisibility = { ...preferences.columnVisibility, - ...updates + ...updates, }; - - await userPreferencesService.saveColumnVisibility(session.user.id, newColumnVisibility); + + await userPreferencesService.saveColumnVisibility( + session.user.id, + newColumnVisibility + ); revalidatePath('/kanban'); return { success: true }; } catch (error) { console.error('Erreur updateColumnVisibility:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -125,18 +148,22 @@ export async function toggleObjectivesVisibility(): Promise<{ if (!session?.user?.id) { return { success: false, error: 'Non authentifié' }; } - - const preferences = await userPreferencesService.getAllPreferences(session.user.id); + + const preferences = await userPreferencesService.getAllPreferences( + session.user.id + ); const showObjectives = !preferences.viewPreferences.showObjectives; - - await userPreferencesService.updateViewPreferences(session.user.id, { showObjectives }); + + await userPreferencesService.updateViewPreferences(session.user.id, { + showObjectives, + }); revalidatePath('/'); return { success: true }; } catch (error) { console.error('Erreur toggleObjectivesVisibility:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -153,18 +180,22 @@ export async function toggleObjectivesCollapse(): Promise<{ if (!session?.user?.id) { return { success: false, error: 'Non authentifié' }; } - - const preferences = await userPreferencesService.getAllPreferences(session.user.id); + + const preferences = await userPreferencesService.getAllPreferences( + session.user.id + ); const collapseObjectives = !preferences.viewPreferences.collapseObjectives; - - await userPreferencesService.updateViewPreferences(session.user.id, { collapseObjectives }); + + await userPreferencesService.updateViewPreferences(session.user.id, { + collapseObjectives, + }); revalidatePath('/'); return { success: true }; } catch (error) { console.error('Erreur toggleObjectivesCollapse:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -181,15 +212,17 @@ export async function setTheme(theme: Theme): Promise<{ if (!session?.user?.id) { return { success: false, error: 'Non authentifié' }; } - - await userPreferencesService.updateViewPreferences(session.user.id, { theme }); + + await userPreferencesService.updateViewPreferences(session.user.id, { + theme, + }); revalidatePath('/'); return { success: true }; } catch (error) { console.error('Erreur setTheme:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -206,18 +239,23 @@ export async function toggleTheme(): Promise<{ if (!session?.user?.id) { return { success: false, error: 'Non authentifié' }; } - - const preferences = await userPreferencesService.getAllPreferences(session.user.id); - const newTheme = preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark'; - - await userPreferencesService.updateViewPreferences(session.user.id, { theme: newTheme }); + + const preferences = await userPreferencesService.getAllPreferences( + session.user.id + ); + const newTheme = + preferences.viewPreferences.theme === 'dark' ? 'light' : 'dark'; + + await userPreferencesService.updateViewPreferences(session.user.id, { + theme: newTheme, + }); revalidatePath('/'); return { success: true }; } catch (error) { console.error('Erreur toggleTheme:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -234,21 +272,31 @@ export async function toggleFontSize(): Promise<{ if (!session?.user?.id) { return { success: false, error: 'Non authentifié' }; } - - const preferences = await userPreferencesService.getAllPreferences(session.user.id); - const fontSizes: ('small' | 'medium' | 'large')[] = ['small', 'medium', 'large']; - const currentIndex = fontSizes.indexOf(preferences.viewPreferences.fontSize); + + const preferences = await userPreferencesService.getAllPreferences( + session.user.id + ); + const fontSizes: ('small' | 'medium' | 'large')[] = [ + 'small', + 'medium', + 'large', + ]; + const currentIndex = fontSizes.indexOf( + preferences.viewPreferences.fontSize + ); const nextIndex = (currentIndex + 1) % fontSizes.length; const newFontSize = fontSizes[nextIndex]; - - await userPreferencesService.updateViewPreferences(session.user.id, { fontSize: newFontSize }); + + await userPreferencesService.updateViewPreferences(session.user.id, { + fontSize: newFontSize, + }); revalidatePath('/'); return { success: true }; } catch (error) { console.error('Erreur toggleFontSize:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } @@ -266,26 +314,28 @@ export async function toggleColumnVisibility(status: TaskStatus): Promise<{ return { success: false, error: 'Non authentifié' }; } - const preferences = await userPreferencesService.getAllPreferences(session.user.id); + const preferences = await userPreferencesService.getAllPreferences( + session.user.id + ); const hiddenStatuses = new Set(preferences.columnVisibility.hiddenStatuses); - + if (hiddenStatuses.has(status)) { hiddenStatuses.delete(status); } else { hiddenStatuses.add(status); } - + await userPreferencesService.saveColumnVisibility(session.user.id, { - hiddenStatuses: Array.from(hiddenStatuses) + hiddenStatuses: Array.from(hiddenStatuses), }); - + revalidatePath('/kanban'); return { success: true }; } catch (error) { console.error('Erreur toggleColumnVisibility:', error); return { success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' + error: error instanceof Error ? error.message : 'Erreur inconnue', }; } } diff --git a/src/actions/profile.ts b/src/actions/profile.ts index 30b4b78..a052d19 100644 --- a/src/actions/profile.ts +++ b/src/actions/profile.ts @@ -1,55 +1,67 @@ -'use server' +'use server'; -import { getServerSession } from 'next-auth/next' -import { authOptions } from '@/lib/auth' -import { usersService } from '@/services/users' -import { revalidatePath } from 'next/cache' -import { getGravatarUrl } from '@/lib/gravatar' +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; +import { usersService } from '@/services/users'; +import { revalidatePath } from 'next/cache'; +import { getGravatarUrl } from '@/lib/gravatar'; export async function updateProfile(formData: { - name?: string - firstName?: string - lastName?: string - avatar?: string - useGravatar?: boolean + name?: string; + firstName?: string; + lastName?: string; + avatar?: string; + useGravatar?: boolean; }) { try { - const session = await getServerSession(authOptions) - + const session = await getServerSession(authOptions); + if (!session?.user?.id) { - return { success: false, error: 'Non authentifié' } + return { success: false, error: 'Non authentifié' }; } // Validation if (formData.firstName && formData.firstName.length > 50) { - return { success: false, error: 'Le prénom ne peut pas dépasser 50 caractères' } + return { + success: false, + error: 'Le prénom ne peut pas dépasser 50 caractères', + }; } if (formData.lastName && formData.lastName.length > 50) { - return { success: false, error: 'Le nom ne peut pas dépasser 50 caractères' } + return { + success: false, + error: 'Le nom ne peut pas dépasser 50 caractères', + }; } if (formData.name && formData.name.length > 100) { - return { success: false, error: 'Le nom d\'affichage ne peut pas dépasser 100 caractères' } + return { + success: false, + error: "Le nom d'affichage ne peut pas dépasser 100 caractères", + }; } if (formData.avatar && formData.avatar.length > 500) { - return { success: false, error: 'L\'URL de l\'avatar ne peut pas dépasser 500 caractères' } + return { + success: false, + error: "L'URL de l'avatar ne peut pas dépasser 500 caractères", + }; } // Déterminer l'URL de l'avatar - let finalAvatarUrl: string | null = null - + let finalAvatarUrl: string | null = null; + if (formData.useGravatar) { // Utiliser Gravatar si demandé - finalAvatarUrl = getGravatarUrl(session.user.email || '', { size: 200 }) + finalAvatarUrl = getGravatarUrl(session.user.email || '', { size: 200 }); } else if (formData.avatar) { // Utiliser l'URL custom si fournie - finalAvatarUrl = formData.avatar + finalAvatarUrl = formData.avatar; } else { // Garder l'avatar actuel ou null - const currentUser = await usersService.getUserById(session.user.id) - finalAvatarUrl = currentUser?.avatar || null + const currentUser = await usersService.getUserById(session.user.id); + finalAvatarUrl = currentUser?.avatar || null; } // Mettre à jour l'utilisateur @@ -58,10 +70,10 @@ export async function updateProfile(formData: { firstName: formData.firstName || null, lastName: formData.lastName || null, avatar: finalAvatarUrl, - }) + }); // Revalider la page de profil - revalidatePath('/profile') + revalidatePath('/profile'); return { success: true, @@ -75,27 +87,26 @@ export async function updateProfile(formData: { role: updatedUser.role, createdAt: updatedUser.createdAt.toISOString(), lastLoginAt: updatedUser.lastLoginAt?.toISOString() || null, - } - } - + }, + }; } catch (error) { - console.error('Profile update error:', error) - return { success: false, error: 'Erreur lors de la mise à jour du profil' } + console.error('Profile update error:', error); + return { success: false, error: 'Erreur lors de la mise à jour du profil' }; } } export async function getProfile() { try { - const session = await getServerSession(authOptions) - + const session = await getServerSession(authOptions); + if (!session?.user?.id) { - return { success: false, error: 'Non authentifié' } + return { success: false, error: 'Non authentifié' }; } - const user = await usersService.getUserById(session.user.id) - + const user = await usersService.getUserById(session.user.id); + if (!user) { - return { success: false, error: 'Utilisateur non trouvé' } + return { success: false, error: 'Utilisateur non trouvé' }; } return { @@ -110,37 +121,39 @@ export async function getProfile() { role: user.role, createdAt: user.createdAt.toISOString(), lastLoginAt: user.lastLoginAt?.toISOString() || null, - } - } - + }, + }; } catch (error) { - console.error('Profile get error:', error) - return { success: false, error: 'Erreur lors de la récupération du profil' } + console.error('Profile get error:', error); + return { + success: false, + error: 'Erreur lors de la récupération du profil', + }; } } export async function applyGravatar() { try { - const session = await getServerSession(authOptions) - + const session = await getServerSession(authOptions); + if (!session?.user?.id) { - return { success: false, error: 'Non authentifié' } + return { success: false, error: 'Non authentifié' }; } if (!session.user?.email) { - return { success: false, error: 'Email requis pour Gravatar' } + return { success: false, error: 'Email requis pour Gravatar' }; } // Générer l'URL Gravatar - const gravatarUrl = getGravatarUrl(session.user.email, { size: 200 }) + const gravatarUrl = getGravatarUrl(session.user.email, { size: 200 }); // Mettre à jour l'utilisateur const updatedUser = await usersService.updateUser(session.user.id, { avatar: gravatarUrl, - }) + }); // Revalider la page de profil - revalidatePath('/profile') + revalidatePath('/profile'); return { success: true, @@ -154,11 +167,10 @@ export async function applyGravatar() { role: updatedUser.role, createdAt: updatedUser.createdAt.toISOString(), lastLoginAt: updatedUser.lastLoginAt?.toISOString() || null, - } - } - + }, + }; } catch (error) { - console.error('Gravatar update error:', error) - return { success: false, error: 'Erreur lors de la mise à jour Gravatar' } + console.error('Gravatar update error:', error); + return { success: false, error: 'Erreur lors de la mise à jour Gravatar' }; } } diff --git a/src/actions/system-info.ts b/src/actions/system-info.ts index 88006da..da5d9a0 100644 --- a/src/actions/system-info.ts +++ b/src/actions/system-info.ts @@ -8,9 +8,10 @@ export async function getSystemInfo() { return { success: true, data: systemInfo }; } catch (error) { console.error('Error getting system info:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to get system info' + return { + success: false, + error: + error instanceof Error ? error.message : 'Failed to get system info', }; } } diff --git a/src/actions/tags.ts b/src/actions/tags.ts index 6882117..3cf2c92 100644 --- a/src/actions/tags.ts +++ b/src/actions/tags.ts @@ -19,18 +19,18 @@ export async function createTag( ): Promise> { try { const tag = await tagsService.createTag({ name, color }); - + // Revalider les pages qui utilisent les tags revalidatePath('/'); revalidatePath('/kanban'); revalidatePath('/tags'); - + return { success: true, data: tag }; } catch (error) { console.error('Error creating tag:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to create tag' + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create tag', }; } } @@ -44,22 +44,22 @@ export async function updateTag( ): Promise> { try { const tag = await tagsService.updateTag(tagId, data); - + if (!tag) { return { success: false, error: 'Tag non trouvé' }; } - + // Revalider les pages qui utilisent les tags revalidatePath('/'); revalidatePath('/kanban'); revalidatePath('/tags'); - + return { success: true, data: tag }; } catch (error) { console.error('Error updating tag:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to update tag' + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to update tag', }; } } @@ -70,19 +70,18 @@ export async function updateTag( export async function deleteTag(tagId: string): Promise { try { await tagsService.deleteTag(tagId); - + // Revalider les pages qui utilisent les tags revalidatePath('/'); revalidatePath('/kanban'); revalidatePath('/tags'); - + return { success: true }; } catch (error) { console.error('Error deleting tag:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to delete tag' + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to delete tag', }; } } - diff --git a/src/actions/tasks.ts b/src/actions/tasks.ts index b4a9048..f5456b9 100644 --- a/src/actions/tasks.ts +++ b/src/actions/tasks.ts @@ -1,4 +1,4 @@ -'use server' +'use server'; import { tasksService } from '@/services/task-management/tasks'; import { revalidatePath } from 'next/cache'; @@ -14,22 +14,23 @@ export type ActionResult = { * Server Action pour mettre à jour le statut d'une tâche */ export async function updateTaskStatus( - taskId: string, + taskId: string, status: TaskStatus ): Promise { try { const task = await tasksService.updateTask(taskId, { status }); - + // Revalidation automatique du cache revalidatePath('/'); revalidatePath('/tasks'); - + return { success: true, data: task }; } catch (error) { console.error('Error updating task status:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to update task status' + return { + success: false, + error: + error instanceof Error ? error.message : 'Failed to update task status', }; } } @@ -38,7 +39,7 @@ export async function updateTaskStatus( * Server Action pour mettre à jour le titre d'une tâche */ export async function updateTaskTitle( - taskId: string, + taskId: string, title: string ): Promise { try { @@ -47,17 +48,18 @@ export async function updateTaskTitle( } const task = await tasksService.updateTask(taskId, { title: title.trim() }); - + // Revalidation automatique du cache revalidatePath('/'); revalidatePath('/tasks'); - + return { success: true, data: task }; } catch (error) { console.error('Error updating task title:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to update task title' + return { + success: false, + error: + error instanceof Error ? error.message : 'Failed to update task title', }; } } @@ -68,17 +70,17 @@ export async function updateTaskTitle( export async function deleteTask(taskId: string): Promise { try { await tasksService.deleteTask(taskId); - + // Revalidation automatique du cache revalidatePath('/'); revalidatePath('/tasks'); - + return { success: true }; } catch (error) { console.error('Error deleting task:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to delete task' + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to delete task', }; } } @@ -98,33 +100,35 @@ export async function updateTask(data: { }): Promise { try { const updateData: Record = {}; - + if (data.title !== undefined) { if (!data.title.trim()) { return { success: false, error: 'Title cannot be empty' }; } updateData.title = data.title.trim(); } - - if (data.description !== undefined) updateData.description = data.description.trim(); + + if (data.description !== undefined) + updateData.description = data.description.trim(); if (data.status !== undefined) updateData.status = data.status; if (data.priority !== undefined) updateData.priority = data.priority; if (data.tags !== undefined) updateData.tags = data.tags; - if (data.primaryTagId !== undefined) updateData.primaryTagId = data.primaryTagId; + if (data.primaryTagId !== undefined) + updateData.primaryTagId = data.primaryTagId; if (data.dueDate !== undefined) updateData.dueDate = data.dueDate; const task = await tasksService.updateTask(data.taskId, updateData); - + // Revalidation automatique du cache revalidatePath('/'); revalidatePath('/tasks'); - + return { success: true, data: task }; } catch (error) { console.error('Error updating task:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to update task' + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to update task', }; } } @@ -151,19 +155,19 @@ export async function createTask(data: { status: data.status || 'todo', priority: data.priority || 'medium', tags: data.tags || [], - primaryTagId: data.primaryTagId + primaryTagId: data.primaryTagId, }); - + // Revalidation automatique du cache revalidatePath('/'); revalidatePath('/tasks'); - + return { success: true, data: task }; } catch (error) { console.error('Error creating task:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to create task' + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to create task', }; } } diff --git a/src/actions/tfs.ts b/src/actions/tfs.ts index 7d2e428..0309711 100644 --- a/src/actions/tfs.ts +++ b/src/actions/tfs.ts @@ -17,10 +17,10 @@ export async function saveTfsConfig(config: TfsConfig) { } await userPreferencesService.saveTfsConfig(session.user.id, config); - + // Réinitialiser le service pour prendre en compte la nouvelle config tfsService.reset(); - + revalidatePath('/settings/integrations'); return { success: true, diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index b6149fb..0a4c217 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,6 +1,6 @@ -import NextAuth from "next-auth" -import { authOptions } from "@/lib/auth" +import NextAuth from 'next-auth'; +import { authOptions } from '@/lib/auth'; -const handler = NextAuth(authOptions) +const handler = NextAuth(authOptions); -export { handler as GET, handler as POST } +export { handler as GET, handler as POST }; diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index 06cf94e..5741674 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -1,32 +1,32 @@ -import { NextRequest, NextResponse } from 'next/server' -import { usersService } from '@/services/users' +import { NextRequest, NextResponse } from 'next/server'; +import { usersService } from '@/services/users'; export async function POST(request: NextRequest) { try { - const { email, name, firstName, lastName, password } = await request.json() + const { email, name, firstName, lastName, password } = await request.json(); // Validation if (!email || !password) { return NextResponse.json( { error: 'Email et mot de passe requis' }, { status: 400 } - ) + ); } if (password.length < 6) { return NextResponse.json( { error: 'Le mot de passe doit contenir au moins 6 caractères' }, { status: 400 } - ) + ); } // Vérifier si l'email existe déjà - const emailExists = await usersService.emailExists(email) + const emailExists = await usersService.emailExists(email); if (emailExists) { return NextResponse.json( { error: 'Un compte avec cet email existe déjà' }, { status: 400 } - ) + ); } // Créer l'utilisateur @@ -36,7 +36,7 @@ export async function POST(request: NextRequest) { firstName, lastName, password, - }) + }); return NextResponse.json({ message: 'Compte créé avec succès', @@ -46,14 +46,13 @@ export async function POST(request: NextRequest) { name: user.name, firstName: user.firstName, lastName: user.lastName, - } - }) - + }, + }); } catch (error) { - console.error('Registration error:', error) + console.error('Registration error:', error); return NextResponse.json( { error: 'Erreur lors de la création du compte' }, { status: 500 } - ) + ); } } diff --git a/src/app/api/backups/[filename]/route.ts b/src/app/api/backups/[filename]/route.ts index e93d6d7..feed90a 100644 --- a/src/app/api/backups/[filename]/route.ts +++ b/src/app/api/backups/[filename]/route.ts @@ -7,16 +7,15 @@ interface RouteParams { }>; } -export async function DELETE( - request: NextRequest, - { params }: RouteParams -) { +export async function DELETE(request: NextRequest, { params }: RouteParams) { try { const { filename } = await params; - + // Vérification de sécurité - s'assurer que c'est bien un fichier de backup - if (!filename.startsWith('towercontrol_') || - (!filename.endsWith('.db') && !filename.endsWith('.db.gz'))) { + if ( + !filename.startsWith('towercontrol_') || + (!filename.endsWith('.db') && !filename.endsWith('.db.gz')) + ) { return NextResponse.json( { success: false, error: 'Invalid backup filename' }, { status: 400 } @@ -24,27 +23,25 @@ export async function DELETE( } await backupService.deleteBackup(filename); - + return NextResponse.json({ success: true, - message: `Backup ${filename} deleted successfully` + message: `Backup ${filename} deleted successfully`, }); } catch (error) { console.error('Error deleting backup:', error); return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Failed to delete backup' + { + success: false, + error: + error instanceof Error ? error.message : 'Failed to delete backup', }, { status: 500 } ); } } -export async function POST( - request: NextRequest, - { params }: RouteParams -) { +export async function POST(request: NextRequest, { params }: RouteParams) { try { const { filename } = await params; const body = await request.json(); @@ -52,8 +49,10 @@ export async function POST( if (action === 'restore') { // Vérification de sécurité - if (!filename.startsWith('towercontrol_') || - (!filename.endsWith('.db') && !filename.endsWith('.db.gz'))) { + if ( + !filename.startsWith('towercontrol_') || + (!filename.endsWith('.db') && !filename.endsWith('.db.gz')) + ) { return NextResponse.json( { success: false, error: 'Invalid backup filename' }, { status: 400 } @@ -63,16 +62,19 @@ export async function POST( // Protection environnement de production if (process.env.NODE_ENV === 'production') { return NextResponse.json( - { success: false, error: 'Restore not allowed in production via API' }, + { + success: false, + error: 'Restore not allowed in production via API', + }, { status: 403 } ); } await backupService.restoreBackup(filename); - + return NextResponse.json({ success: true, - message: `Database restored from ${filename}` + message: `Database restored from ${filename}`, }); } @@ -83,12 +85,11 @@ export async function POST( } catch (error) { console.error('Error in backup operation:', error); return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Operation failed' + { + success: false, + error: error instanceof Error ? error.message : 'Operation failed', }, { status: 500 } ); } } - diff --git a/src/app/api/backups/route.ts b/src/app/api/backups/route.ts index 55b86d6..a1107ea 100644 --- a/src/app/api/backups/route.ts +++ b/src/app/api/backups/route.ts @@ -10,57 +10,61 @@ export async function GET(request: NextRequest) { if (action === 'logs') { const maxLines = parseInt(searchParams.get('maxLines') || '100'); const logs = await backupService.getBackupLogs(maxLines); - + return NextResponse.json({ success: true, - data: { logs } + data: { logs }, }); } if (action === 'stats') { const days = parseInt(searchParams.get('days') || '30'); const stats = await backupService.getBackupStats(days); - + return NextResponse.json({ success: true, - data: stats + data: stats, }); } console.log('🔄 API GET /api/backups called'); - + // Test de la configuration d'abord const config = backupService.getConfig(); console.log('✅ Config loaded:', config); - + // Test du scheduler const schedulerStatus = backupScheduler.getStatus(); console.log('✅ Scheduler status:', schedulerStatus); - + // Test de la liste des backups const backups = await backupService.listBackups(); console.log('✅ Backups loaded:', backups.length); - + const response = { success: true, data: { backups, scheduler: schedulerStatus, config, - } + }, }; - + console.log('✅ API response ready'); return NextResponse.json(response); } catch (error) { console.error('❌ Error fetching backups:', error); - console.error('Error stack:', error instanceof Error ? error.stack : 'Unknown'); - + console.error( + 'Error stack:', + error instanceof Error ? error.stack : 'Unknown' + ); + return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Failed to fetch backups', - details: error instanceof Error ? error.stack : undefined + { + success: false, + error: + error instanceof Error ? error.message : 'Failed to fetch backups', + details: error instanceof Error ? error.stack : undefined, }, { status: 500 } ); @@ -76,34 +80,38 @@ export async function POST(request: NextRequest) { case 'create': const forceCreate = params.force === true; const backup = await backupService.createBackup('manual', forceCreate); - + if (backup === null) { - return NextResponse.json({ - success: true, - skipped: true, - message: 'No changes detected since last backup. Use force=true to create anyway.' + return NextResponse.json({ + success: true, + skipped: true, + message: + 'No changes detected since last backup. Use force=true to create anyway.', }); } - + return NextResponse.json({ success: true, data: backup }); case 'verify': await backupService.verifyDatabaseHealth(); - return NextResponse.json({ - success: true, - message: 'Database health check passed' + return NextResponse.json({ + success: true, + message: 'Database health check passed', }); case 'config': await backupService.updateConfig(params.config); // Redémarrer le scheduler si la config a changé - if (params.config.enabled !== undefined || params.config.interval !== undefined) { + if ( + params.config.enabled !== undefined || + params.config.interval !== undefined + ) { backupScheduler.restart(); } - return NextResponse.json({ - success: true, + return NextResponse.json({ + success: true, message: 'Configuration updated', - data: backupService.getConfig() + data: backupService.getConfig(), }); case 'scheduler': @@ -112,9 +120,9 @@ export async function POST(request: NextRequest) { } else { backupScheduler.stop(); } - return NextResponse.json({ - success: true, - data: backupScheduler.getStatus() + return NextResponse.json({ + success: true, + data: backupScheduler.getStatus(), }); default: @@ -126,9 +134,9 @@ export async function POST(request: NextRequest) { } catch (error) { console.error('Error in backup operation:', error); return NextResponse.json( - { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' + { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', }, { status: 500 } ); diff --git a/src/app/api/daily/checkboxes/[id]/archive/route.ts b/src/app/api/daily/checkboxes/[id]/archive/route.ts index c25b98e..7b473bf 100644 --- a/src/app/api/daily/checkboxes/[id]/archive/route.ts +++ b/src/app/api/daily/checkboxes/[id]/archive/route.ts @@ -7,7 +7,7 @@ export async function PATCH( ) { try { const { id: checkboxId } = await params; - + if (!checkboxId) { return NextResponse.json( { error: 'Checkbox ID is required' }, diff --git a/src/app/api/daily/dates/route.ts b/src/app/api/daily/dates/route.ts index 36c426a..33a5f19 100644 --- a/src/app/api/daily/dates/route.ts +++ b/src/app/api/daily/dates/route.ts @@ -9,7 +9,6 @@ export async function GET() { try { const dates = await dailyService.getDailyDates(); return NextResponse.json({ dates }); - } catch (error) { console.error('Erreur lors de la récupération des dates:', error); return NextResponse.json( diff --git a/src/app/api/daily/pending/route.ts b/src/app/api/daily/pending/route.ts index c0b8383..a32bb1e 100644 --- a/src/app/api/daily/pending/route.ts +++ b/src/app/api/daily/pending/route.ts @@ -5,17 +5,21 @@ import { DailyCheckboxType } from '@/lib/types'; export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); - - const maxDays = searchParams.get('maxDays') ? parseInt(searchParams.get('maxDays')!) : undefined; + + const maxDays = searchParams.get('maxDays') + ? parseInt(searchParams.get('maxDays')!) + : undefined; const excludeToday = searchParams.get('excludeToday') === 'true'; const type = searchParams.get('type') as DailyCheckboxType | undefined; - const limit = searchParams.get('limit') ? parseInt(searchParams.get('limit')!) : undefined; + const limit = searchParams.get('limit') + ? parseInt(searchParams.get('limit')!) + : undefined; const pendingCheckboxes = await dailyService.getPendingCheckboxes({ maxDays, excludeToday, type, - limit + limit, }); return NextResponse.json(pendingCheckboxes); diff --git a/src/app/api/daily/route.ts b/src/app/api/daily/route.ts index 221ac80..057cf2e 100644 --- a/src/app/api/daily/route.ts +++ b/src/app/api/daily/route.ts @@ -1,6 +1,11 @@ import { NextResponse } from 'next/server'; import { dailyService } from '@/services/task-management/daily'; -import { getToday, parseDate, isValidAPIDate, createDateFromParts } from '@/lib/date-utils'; +import { + getToday, + parseDate, + isValidAPIDate, + createDateFromParts, +} from '@/lib/date-utils'; /** * API route pour récupérer la vue daily (hier + aujourd'hui) @@ -8,33 +13,36 @@ import { getToday, parseDate, isValidAPIDate, createDateFromParts } from '@/lib/ export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); - + const action = searchParams.get('action'); const date = searchParams.get('date'); - + if (action === 'history') { // Récupérer l'historique const limit = parseInt(searchParams.get('limit') || '30'); const history = await dailyService.getCheckboxHistory(limit); return NextResponse.json(history); } - + if (action === 'search') { // Recherche dans les checkboxes const query = searchParams.get('q') || ''; const limit = parseInt(searchParams.get('limit') || '20'); - + if (!query.trim()) { - return NextResponse.json({ error: 'Query parameter required' }, { status: 400 }); + return NextResponse.json( + { error: 'Query parameter required' }, + { status: 400 } + ); } - + const checkboxes = await dailyService.searchCheckboxes(query, limit); return NextResponse.json(checkboxes); } - + // Vue daily pour une date donnée (ou aujourd'hui par défaut) let targetDate: Date; - + if (date) { if (!isValidAPIDate(date)) { return NextResponse.json( @@ -46,10 +54,9 @@ export async function GET(request: Request) { } else { targetDate = getToday(); } - + const dailyView = await dailyService.getDailyView(targetDate); return NextResponse.json(dailyView); - } catch (error) { console.error('Erreur lors de la récupération du daily:', error); return NextResponse.json( @@ -65,7 +72,7 @@ export async function GET(request: Request) { export async function POST(request: Request) { try { const body = await request.json(); - + // Validation des données if (!body.date || !body.text) { return NextResponse.json( @@ -73,7 +80,7 @@ export async function POST(request: Request) { { status: 400 } ); } - + // Parser la date de façon plus robuste let date: Date; if (typeof body.date === 'string') { @@ -83,27 +90,26 @@ export async function POST(request: Request) { } else { date = parseDate(body.date); } - + if (isNaN(date.getTime())) { return NextResponse.json( { error: 'Format de date invalide. Utilisez YYYY-MM-DD' }, { status: 400 } ); } - + const checkbox = await dailyService.addCheckbox({ date, text: body.text, type: body.type, taskId: body.taskId, order: body.order, - isChecked: body.isChecked + isChecked: body.isChecked, }); - + return NextResponse.json(checkbox, { status: 201 }); - } catch (error) { - console.error('Erreur lors de l\'ajout de la checkbox:', error); + console.error("Erreur lors de l'ajout de la checkbox:", error); return NextResponse.json( { error: 'Erreur interne du serveur' }, { status: 500 } diff --git a/src/app/api/jira/logs/route.ts b/src/app/api/jira/logs/route.ts index b951c4f..53153e9 100644 --- a/src/app/api/jira/logs/route.ts +++ b/src/app/api/jira/logs/route.ts @@ -9,28 +9,27 @@ export async function GET(request: NextRequest) { try { const { searchParams } = new URL(request.url); const limit = parseInt(searchParams.get('limit') || '10'); - + const logs = await prisma.syncLog.findMany({ where: { - source: 'jira' + source: 'jira', }, orderBy: { - createdAt: 'desc' + createdAt: 'desc', }, - take: limit + take: limit, }); return NextResponse.json({ - data: logs + data: logs, }); - } catch (error) { console.error('❌ Erreur récupération logs Jira:', error); - + return NextResponse.json( - { + { error: 'Erreur lors de la récupération des logs', - details: error instanceof Error ? error.message : 'Erreur inconnue' + details: error instanceof Error ? error.message : 'Erreur inconnue', }, { status: 500 } ); diff --git a/src/app/api/jira/sync/route.ts b/src/app/api/jira/sync/route.ts index f6d2ee3..cf13fb7 100644 --- a/src/app/api/jira/sync/route.ts +++ b/src/app/api/jira/sync/route.ts @@ -1,5 +1,8 @@ import { NextResponse } from 'next/server'; -import { createJiraService, JiraService } from '@/services/integrations/jira/jira'; +import { + createJiraService, + JiraService, +} from '@/services/integrations/jira/jira'; import { userPreferencesService } from '@/services/core/user-preferences'; import { jiraScheduler } from '@/services/integrations/jira/scheduler'; import { getServerSession } from 'next-auth'; @@ -33,23 +36,23 @@ export async function POST(request: Request) { } else { jiraScheduler.stop(); } - return NextResponse.json({ - success: true, - data: await jiraScheduler.getStatus(session.user.id) + return NextResponse.json({ + success: true, + data: await jiraScheduler.getStatus(session.user.id), }); case 'config': await userPreferencesService.saveJiraSchedulerConfig( session.user.id, - params.jiraAutoSync, + params.jiraAutoSync, params.jiraSyncInterval ); // Redémarrer le scheduler si la config a changé await jiraScheduler.restart(session.user.id); - return NextResponse.json({ - success: true, + return NextResponse.json({ + success: true, message: 'Configuration scheduler mise à jour', - data: await jiraScheduler.getStatus(session.user.id) + data: await jiraScheduler.getStatus(session.user.id), }); default: @@ -61,11 +64,18 @@ export async function POST(request: Request) { } // Synchronisation normale (manuelle) - const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); - + const jiraConfig = await userPreferencesService.getJiraConfig( + session.user.id + ); + let jiraService: JiraService | null = null; - - if (jiraConfig.enabled && jiraConfig.baseUrl && jiraConfig.email && jiraConfig.apiToken) { + + if ( + jiraConfig.enabled && + jiraConfig.baseUrl && + jiraConfig.email && + jiraConfig.apiToken + ) { // Utiliser la config depuis la base de données jiraService = new JiraService({ enabled: jiraConfig.enabled, @@ -73,27 +83,33 @@ export async function POST(request: Request) { email: jiraConfig.email, apiToken: jiraConfig.apiToken, projectKey: jiraConfig.projectKey, - ignoredProjects: jiraConfig.ignoredProjects || [] + ignoredProjects: jiraConfig.ignoredProjects || [], }); } else { // Fallback sur les variables d'environnement jiraService = createJiraService(); } - + if (!jiraService) { return NextResponse.json( - { error: 'Configuration Jira manquante. Configurez Jira dans les paramètres ou vérifiez les variables d\'environnement.' }, + { + error: + "Configuration Jira manquante. Configurez Jira dans les paramètres ou vérifiez les variables d'environnement.", + }, { status: 400 } ); } console.log('🔄 Début de la synchronisation Jira manuelle...'); - + // Tester la connexion d'abord const connectionOk = await jiraService.testConnection(); if (!connectionOk) { return NextResponse.json( - { error: 'Impossible de se connecter à Jira. Vérifiez la configuration.' }, + { + error: + 'Impossible de se connecter à Jira. Vérifiez la configuration.', + }, { status: 401 } ); } @@ -111,37 +127,36 @@ export async function POST(request: Request) { tasksDeleted: syncResult.stats.deleted, errors: syncResult.errors, unknownStatuses: syncResult.unknownStatuses || [], // Nouveaux statuts inconnus - actions: syncResult.actions.map(action => ({ + actions: syncResult.actions.map((action) => ({ type: action.type as 'created' | 'updated' | 'skipped' | 'deleted', taskKey: action.itemId.toString(), taskTitle: action.title, reason: action.message, - changes: action.message ? [action.message] : undefined - })) + changes: action.message ? [action.message] : undefined, + })), }; if (syncResult.success) { return NextResponse.json({ message: 'Synchronisation Jira terminée avec succès', - data: jiraSyncResult + data: jiraSyncResult, }); } else { return NextResponse.json( - { + { error: 'Synchronisation Jira terminée avec des erreurs', - data: jiraSyncResult + data: jiraSyncResult, }, { status: 207 } // Multi-Status ); } - } catch (error) { console.error('❌ Erreur API sync Jira:', error); - + return NextResponse.json( - { + { error: 'Erreur interne lors de la synchronisation', - details: error instanceof Error ? error.message : 'Erreur inconnue' + details: error instanceof Error ? error.message : 'Erreur inconnue', }, { status: 500 } ); @@ -163,11 +178,18 @@ export async function GET() { } // Essayer d'abord la config depuis la base de données - const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); - + const jiraConfig = await userPreferencesService.getJiraConfig( + session.user.id + ); + let jiraService: JiraService | null = null; - - if (jiraConfig.enabled && jiraConfig.baseUrl && jiraConfig.email && jiraConfig.apiToken) { + + if ( + jiraConfig.enabled && + jiraConfig.baseUrl && + jiraConfig.email && + jiraConfig.apiToken + ) { // Utiliser la config depuis la base de données jiraService = new JiraService({ enabled: jiraConfig.enabled, @@ -175,54 +197,55 @@ export async function GET() { email: jiraConfig.email, apiToken: jiraConfig.apiToken, projectKey: jiraConfig.projectKey, - ignoredProjects: jiraConfig.ignoredProjects || [] + ignoredProjects: jiraConfig.ignoredProjects || [], }); } else { // Fallback sur les variables d'environnement jiraService = createJiraService(); } - + if (!jiraService) { - return NextResponse.json( - { - connected: false, - message: 'Configuration Jira manquante' - } - ); + return NextResponse.json({ + connected: false, + message: 'Configuration Jira manquante', + }); } const connected = await jiraService.testConnection(); - + // Si connexion OK et qu'un projet est configuré, tester aussi le projet let projectValidation = null; if (connected && jiraConfig.projectKey) { - projectValidation = await jiraService.validateProject(jiraConfig.projectKey); + projectValidation = await jiraService.validateProject( + jiraConfig.projectKey + ); } - + // Récupérer aussi le statut du scheduler avec l'utilisateur connecté const schedulerStatus = await jiraScheduler.getStatus(session.user.id); - + return NextResponse.json({ connected, - message: connected ? 'Connexion Jira OK' : 'Impossible de se connecter à Jira', - project: projectValidation ? { - key: jiraConfig.projectKey, - exists: projectValidation.exists, - name: projectValidation.name, - error: projectValidation.error - } : null, - scheduler: schedulerStatus + message: connected + ? 'Connexion Jira OK' + : 'Impossible de se connecter à Jira', + project: projectValidation + ? { + key: jiraConfig.projectKey, + exists: projectValidation.exists, + name: projectValidation.name, + error: projectValidation.error, + } + : null, + scheduler: schedulerStatus, }); - } catch (error) { console.error('❌ Erreur test connexion Jira:', error); - - return NextResponse.json( - { - connected: false, - message: 'Erreur lors du test de connexion', - details: error instanceof Error ? error.message : 'Erreur inconnue' - } - ); + + return NextResponse.json({ + connected: false, + message: 'Erreur lors du test de connexion', + details: error instanceof Error ? error.message : 'Erreur inconnue', + }); } } diff --git a/src/app/api/jira/validate-project/route.ts b/src/app/api/jira/validate-project/route.ts index a11146f..dafc53c 100644 --- a/src/app/api/jira/validate-project/route.ts +++ b/src/app/api/jira/validate-project/route.ts @@ -12,10 +12,7 @@ export async function POST(request: NextRequest) { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { - return NextResponse.json( - { error: 'Non authentifié' }, - { status: 401 } - ); + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); } const body = await request.json(); @@ -29,11 +26,21 @@ export async function POST(request: NextRequest) { } // Récupérer la config Jira depuis la base de données - const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); - - if (!jiraConfig.enabled || !jiraConfig.baseUrl || !jiraConfig.email || !jiraConfig.apiToken) { + const jiraConfig = await userPreferencesService.getJiraConfig( + session.user.id + ); + + if ( + !jiraConfig.enabled || + !jiraConfig.baseUrl || + !jiraConfig.email || + !jiraConfig.apiToken + ) { return NextResponse.json( - { error: 'Configuration Jira manquante. Configurez Jira dans les paramètres.' }, + { + error: + 'Configuration Jira manquante. Configurez Jira dans les paramètres.', + }, { status: 400 } ); } @@ -42,37 +49,44 @@ export async function POST(request: NextRequest) { const jiraService = createJiraService(); if (!jiraService) { return NextResponse.json( - { error: 'Impossible de créer le service Jira. Vérifiez la configuration.' }, + { + error: + 'Impossible de créer le service Jira. Vérifiez la configuration.', + }, { status: 500 } ); } // Valider le projet - const validation = await jiraService.validateProject(projectKey.trim().toUpperCase()); - + const validation = await jiraService.validateProject( + projectKey.trim().toUpperCase() + ); + if (validation.exists) { return NextResponse.json({ success: true, exists: true, projectName: validation.name, - message: `Projet "${projectKey}" trouvé : ${validation.name}` + message: `Projet "${projectKey}" trouvé : ${validation.name}`, }); } else { - return NextResponse.json({ - success: false, - exists: false, - error: validation.error, - message: validation.error || `Projet "${projectKey}" introuvable` - }, { status: 404 }); + return NextResponse.json( + { + success: false, + exists: false, + error: validation.error, + message: validation.error || `Projet "${projectKey}" introuvable`, + }, + { status: 404 } + ); } - } catch (error) { console.error('Erreur lors de la validation du projet Jira:', error); return NextResponse.json( - { + { success: false, error: 'Erreur lors de la validation du projet', - message: error instanceof Error ? error.message : 'Erreur inconnue' + message: error instanceof Error ? error.message : 'Erreur inconnue', }, { status: 500 } ); diff --git a/src/app/api/tags/[id]/route.ts b/src/app/api/tags/[id]/route.ts index 4e840ac..52a7c13 100644 --- a/src/app/api/tags/[id]/route.ts +++ b/src/app/api/tags/[id]/route.ts @@ -13,23 +13,19 @@ export async function GET( const tag = await tagsService.getTagById(id); if (!tag) { - return NextResponse.json( - { error: 'Tag non trouvé' }, - { status: 404 } - ); + return NextResponse.json({ error: 'Tag non trouvé' }, { status: 404 }); } return NextResponse.json({ data: tag, - message: 'Tag récupéré avec succès' + message: 'Tag récupéré avec succès', }); - } catch (error) { console.error('Erreur lors de la récupération du tag:', error); return NextResponse.json( - { + { error: 'Erreur lors de la récupération du tag', - message: error instanceof Error ? error.message : 'Erreur inconnue' + message: error instanceof Error ? error.message : 'Erreur inconnue', }, { status: 500 } ); diff --git a/src/app/api/tags/route.ts b/src/app/api/tags/route.ts index 5f9c751..23d2f77 100644 --- a/src/app/api/tags/route.ts +++ b/src/app/api/tags/route.ts @@ -26,15 +26,14 @@ export async function GET(request: NextRequest) { return NextResponse.json({ data: tags, - message: 'Tags récupérés avec succès' + message: 'Tags récupérés avec succès', }); - } catch (error) { console.error('Erreur lors de la récupération des tags:', error); return NextResponse.json( - { + { error: 'Erreur lors de la récupération des tags', - message: error instanceof Error ? error.message : 'Erreur inconnue' + message: error instanceof Error ? error.message : 'Erreur inconnue', }, { status: 500 } ); diff --git a/src/app/api/tasks/[id]/checkboxes/route.ts b/src/app/api/tasks/[id]/checkboxes/route.ts index 85c38b3..7f1916a 100644 --- a/src/app/api/tasks/[id]/checkboxes/route.ts +++ b/src/app/api/tasks/[id]/checkboxes/route.ts @@ -2,12 +2,12 @@ import { NextRequest, NextResponse } from 'next/server'; import { tasksService } from '@/services/task-management/tasks'; export async function GET( - request: NextRequest, - { params }: { params: Promise<{ id: string }> } + request: NextRequest, + { params }: { params: Promise<{ id: string }> } ) { try { const { id } = await params; - + if (!id) { return NextResponse.json( { error: 'ID de tâche requis' }, diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 2240b0b..4f7346b 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -8,7 +8,7 @@ import { TaskStatus } from '@/lib/types'; export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); - + // Extraire les paramètres de filtre const filters: { status?: TaskStatus[]; @@ -17,27 +17,27 @@ export async function GET(request: Request) { limit?: number; offset?: number; } = {}; - + const status = searchParams.get('status'); if (status) { filters.status = status.split(',') as TaskStatus[]; } - + const source = searchParams.get('source'); if (source) { filters.source = source.split(','); } - + const search = searchParams.get('search'); if (search) { filters.search = search; } - + const limit = searchParams.get('limit'); if (limit) { filters.limit = parseInt(limit); } - + const offset = searchParams.get('offset'); if (offset) { filters.offset = parseInt(offset); @@ -52,21 +52,23 @@ export async function GET(request: Request) { data: tasks, stats, filters: filters, - count: tasks.length + count: tasks.length, }); - } catch (error) { console.error('❌ Erreur lors de la récupération des tâches:', error); - - return NextResponse.json({ - success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' - }, { status: 500 }); + + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue', + }, + { status: 500 } + ); } } // POST, PATCH, DELETE methods have been migrated to Server Actions // See /src/actions/tasks.ts for: // - createTask (replaces POST) -// - updateTask, updateTaskStatus, updateTaskTitle (replaces PATCH) +// - updateTask, updateTaskStatus, updateTaskTitle (replaces PATCH) // - deleteTask (replaces DELETE) diff --git a/src/app/api/tfs/delete-all/route.ts b/src/app/api/tfs/delete-all/route.ts index 01aedb9..06f30c9 100644 --- a/src/app/api/tfs/delete-all/route.ts +++ b/src/app/api/tfs/delete-all/route.ts @@ -7,34 +7,40 @@ import { tfsService } from '@/services/integrations/tfs/tfs'; export async function DELETE() { try { console.log('🔄 Début de la suppression des tâches TFS...'); - + // Supprimer via le service singleton const result = await tfsService.deleteAllTasks(); - + if (result.success) { return NextResponse.json({ success: true, - message: result.deletedCount > 0 - ? `${result.deletedCount} tâche(s) TFS supprimée(s) avec succès` - : 'Aucune tâche TFS trouvée à supprimer', + message: + result.deletedCount > 0 + ? `${result.deletedCount} tâche(s) TFS supprimée(s) avec succès` + : 'Aucune tâche TFS trouvée à supprimer', data: { - deletedCount: result.deletedCount - } + deletedCount: result.deletedCount, + }, }); } else { - return NextResponse.json({ - success: false, - error: result.error || 'Erreur lors de la suppression', - }, { status: 500 }); + return NextResponse.json( + { + success: false, + error: result.error || 'Erreur lors de la suppression', + }, + { status: 500 } + ); } - } catch (error) { console.error('❌ Erreur lors de la suppression des tâches TFS:', error); - - return NextResponse.json({ - success: false, - error: 'Erreur lors de la suppression des tâches TFS', - details: error instanceof Error ? error.message : 'Erreur inconnue' - }, { status: 500 }); + + return NextResponse.json( + { + success: false, + error: 'Erreur lors de la suppression des tâches TFS', + details: error instanceof Error ? error.message : 'Erreur inconnue', + }, + { status: 500 } + ); } -} \ No newline at end of file +} diff --git a/src/app/api/tfs/scheduler-config/route.ts b/src/app/api/tfs/scheduler-config/route.ts index 5989ccb..dd930bc 100644 --- a/src/app/api/tfs/scheduler-config/route.ts +++ b/src/app/api/tfs/scheduler-config/route.ts @@ -12,21 +12,32 @@ export async function GET() { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { - return NextResponse.json({ success: false, error: 'Non authentifié' }, { status: 401 }); + return NextResponse.json( + { success: false, error: 'Non authentifié' }, + { status: 401 } + ); } - const schedulerConfig = await userPreferencesService.getTfsSchedulerConfig(session.user.id); - + const schedulerConfig = await userPreferencesService.getTfsSchedulerConfig( + session.user.id + ); + return NextResponse.json({ success: true, - data: schedulerConfig + data: schedulerConfig, }); } catch (error) { console.error('Erreur récupération config scheduler TFS:', error); - return NextResponse.json({ - success: false, - error: error instanceof Error ? error.message : 'Erreur lors de la récupération' - }, { status: 500 }); + return NextResponse.json( + { + success: false, + error: + error instanceof Error + ? error.message + : 'Erreur lors de la récupération', + }, + { status: 500 } + ); } } @@ -38,24 +49,33 @@ export async function POST(request: NextRequest) { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { - return NextResponse.json({ success: false, error: 'Non authentifié' }, { status: 401 }); + return NextResponse.json( + { success: false, error: 'Non authentifié' }, + { status: 401 } + ); } const body = await request.json(); const { tfsAutoSync, tfsSyncInterval } = body; if (typeof tfsAutoSync !== 'boolean') { - return NextResponse.json({ - success: false, - error: 'tfsAutoSync doit être un booléen' - }, { status: 400 }); + return NextResponse.json( + { + success: false, + error: 'tfsAutoSync doit être un booléen', + }, + { status: 400 } + ); } if (!['hourly', 'daily', 'weekly'].includes(tfsSyncInterval)) { - return NextResponse.json({ - success: false, - error: 'tfsSyncInterval doit être hourly, daily ou weekly' - }, { status: 400 }); + return NextResponse.json( + { + success: false, + error: 'tfsSyncInterval doit être hourly, daily ou weekly', + }, + { status: 400 } + ); } await userPreferencesService.saveTfsSchedulerConfig( @@ -73,13 +93,19 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true, message: 'Configuration scheduler TFS mise à jour', - data: status + data: status, }); } catch (error) { console.error('Erreur sauvegarde config scheduler TFS:', error); - return NextResponse.json({ - success: false, - error: error instanceof Error ? error.message : 'Erreur lors de la sauvegarde' - }, { status: 500 }); + return NextResponse.json( + { + success: false, + error: + error instanceof Error + ? error.message + : 'Erreur lors de la sauvegarde', + }, + { status: 500 } + ); } } diff --git a/src/app/api/tfs/scheduler-status/route.ts b/src/app/api/tfs/scheduler-status/route.ts index ee4fa82..1caebb0 100644 --- a/src/app/api/tfs/scheduler-status/route.ts +++ b/src/app/api/tfs/scheduler-status/route.ts @@ -11,20 +11,29 @@ export async function GET() { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { - return NextResponse.json({ success: false, error: 'Non authentifié' }, { status: 401 }); + return NextResponse.json( + { success: false, error: 'Non authentifié' }, + { status: 401 } + ); } const status = await tfsScheduler.getStatus(session.user.id); - + return NextResponse.json({ success: true, - data: status + data: status, }); } catch (error) { console.error('Erreur récupération statut scheduler TFS:', error); - return NextResponse.json({ - success: false, - error: error instanceof Error ? error.message : 'Erreur lors de la récupération' - }, { status: 500 }); + return NextResponse.json( + { + success: false, + error: + error instanceof Error + ? error.message + : 'Erreur lors de la récupération', + }, + { status: 500 } + ); } } diff --git a/src/app/api/tfs/sync/route.ts b/src/app/api/tfs/sync/route.ts index 05f5e2e..1d72b35 100644 --- a/src/app/api/tfs/sync/route.ts +++ b/src/app/api/tfs/sync/route.ts @@ -94,4 +94,3 @@ export async function GET() { ); } } - diff --git a/src/app/api/user-preferences/jira-config/route.ts b/src/app/api/user-preferences/jira-config/route.ts index 6a641a4..4038677 100644 --- a/src/app/api/user-preferences/jira-config/route.ts +++ b/src/app/api/user-preferences/jira-config/route.ts @@ -12,13 +12,12 @@ export async function GET() { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { - return NextResponse.json( - { error: 'Non authentifié' }, - { status: 401 } - ); + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); } - const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); + const jiraConfig = await userPreferencesService.getJiraConfig( + session.user.id + ); return NextResponse.json({ jiraConfig }); } catch (error) { console.error('Erreur lors de la récupération de la config Jira:', error); @@ -37,10 +36,7 @@ export async function PUT(request: NextRequest) { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { - return NextResponse.json( - { error: 'Non authentifié' }, - { status: 401 } - ); + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); } const body = await request.json(); @@ -79,20 +75,22 @@ export async function PUT(request: NextRequest) { apiToken: apiToken.trim(), enabled: true, projectKey: projectKey ? projectKey.trim().toUpperCase() : undefined, - ignoredProjects: Array.isArray(ignoredProjects) - ? ignoredProjects.map((p: string) => p.trim().toUpperCase()).filter((p: string) => p.length > 0) - : [] + ignoredProjects: Array.isArray(ignoredProjects) + ? ignoredProjects + .map((p: string) => p.trim().toUpperCase()) + .filter((p: string) => p.length > 0) + : [], }; await userPreferencesService.saveJiraConfig(session.user.id, jiraConfig); - return NextResponse.json({ - success: true, + return NextResponse.json({ + success: true, message: 'Configuration Jira sauvegardée avec succès', jiraConfig: { ...jiraConfig, - apiToken: '••••••••' // Masquer le token dans la réponse - } + apiToken: '••••••••', // Masquer le token dans la réponse + }, }); } catch (error) { console.error('Erreur lors de la sauvegarde de la config Jira:', error); @@ -111,10 +109,7 @@ export async function DELETE() { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { - return NextResponse.json( - { error: 'Non authentifié' }, - { status: 401 } - ); + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); } const defaultConfig: JiraConfig = { @@ -122,14 +117,14 @@ export async function DELETE() { email: '', apiToken: '', enabled: false, - ignoredProjects: [] + ignoredProjects: [], }; await userPreferencesService.saveJiraConfig(session.user.id, defaultConfig); - return NextResponse.json({ - success: true, - message: 'Configuration Jira réinitialisée avec succès' + return NextResponse.json({ + success: true, + message: 'Configuration Jira réinitialisée avec succès', }); } catch (error) { console.error('Erreur lors de la suppression de la config Jira:', error); diff --git a/src/app/api/user-preferences/route.ts b/src/app/api/user-preferences/route.ts index b6bed9a..c7f14e9 100644 --- a/src/app/api/user-preferences/route.ts +++ b/src/app/api/user-preferences/route.ts @@ -16,18 +16,20 @@ export async function GET() { ); } - const preferences = await userPreferencesService.getAllPreferences(session.user.id); - + const preferences = await userPreferencesService.getAllPreferences( + session.user.id + ); + return NextResponse.json({ success: true, - data: preferences + data: preferences, }); } catch (error) { console.error('Erreur lors de la récupération des préférences:', error); return NextResponse.json( - { - success: false, - error: 'Erreur lors de la récupération des préférences' + { + success: false, + error: 'Erreur lors de la récupération des préférences', }, { status: 500 } ); @@ -48,19 +50,22 @@ export async function PUT(request: NextRequest) { } const preferences = await request.json(); - - await userPreferencesService.saveAllPreferences(session.user.id, preferences); - + + await userPreferencesService.saveAllPreferences( + session.user.id, + preferences + ); + return NextResponse.json({ success: true, - message: 'Préférences sauvegardées avec succès' + message: 'Préférences sauvegardées avec succès', }); } catch (error) { console.error('Erreur lors de la sauvegarde des préférences:', error); return NextResponse.json( - { - success: false, - error: 'Erreur lors de la sauvegarde des préférences' + { + success: false, + error: 'Erreur lors de la sauvegarde des préférences', }, { status: 500 } ); diff --git a/src/app/daily/DailyPageClient.tsx b/src/app/daily/DailyPageClient.tsx index 919a33b..fe1b6dd 100644 --- a/src/app/daily/DailyPageClient.tsx +++ b/src/app/daily/DailyPageClient.tsx @@ -13,7 +13,12 @@ import { DailySection } from '@/components/daily/DailySection'; import { PendingTasksSection } from '@/components/daily/PendingTasksSection'; import { dailyClient } from '@/clients/daily-client'; import { Header } from '@/components/ui/Header'; -import { getPreviousWorkday, formatDateLong, isToday, generateDateTitle } from '@/lib/date-utils'; +import { + getPreviousWorkday, + formatDateLong, + isToday, + generateDateTitle, +} from '@/lib/date-utils'; import { useGlobalKeyboardShortcuts } from '@/hooks/useGlobalKeyboardShortcuts'; import { Emoji } from '@/components/ui/Emoji'; @@ -25,12 +30,12 @@ interface DailyPageClientProps { initialPendingTasks?: DailyCheckbox[]; } -export function DailyPageClient({ - initialDailyView, - initialDailyDates = [], +export function DailyPageClient({ + initialDailyView, + initialDailyDates = [], initialDate, initialDeadlineMetrics, - initialPendingTasks = [] + initialPendingTasks = [], }: DailyPageClientProps = {}) { const { dailyView, @@ -51,7 +56,7 @@ export function DailyPageClient({ goToNextDay, goToToday, setDate, - refreshDailySilent + refreshDailySilent, } = useDaily(initialDate, initialDailyView); const [dailyDates, setDailyDates] = useState(initialDailyDates); @@ -69,19 +74,28 @@ export function DailyPageClient({ // Charger les dates avec des dailies pour le calendrier (seulement si pas de données SSR) useEffect(() => { if (initialDailyDates.length === 0) { - import('@/clients/daily-client').then(({ dailyClient }) => { - return dailyClient.getDailyDates(); - }).then(setDailyDates).catch(console.error); + import('@/clients/daily-client') + .then(({ dailyClient }) => { + return dailyClient.getDailyDates(); + }) + .then(setDailyDates) + .catch(console.error); } }, [initialDailyDates.length]); - const handleAddTodayCheckbox = async (text: string, type: DailyCheckboxType) => { + const handleAddTodayCheckbox = async ( + text: string, + type: DailyCheckboxType + ) => { await addTodayCheckbox(text, type); // Recharger aussi les dates pour le calendrier await refreshDailyDates(); }; - const handleAddYesterdayCheckbox = async (text: string, type: DailyCheckboxType) => { + const handleAddYesterdayCheckbox = async ( + text: string, + type: DailyCheckboxType + ) => { await addYesterdayCheckbox(text, type); // Recharger aussi les dates pour le calendrier await refreshDailyDates(); @@ -91,7 +105,7 @@ export function DailyPageClient({ useGlobalKeyboardShortcuts({ onNavigatePrevious: goToPreviousDay, onNavigateNext: goToNextDay, - onGoToToday: goToToday + onGoToToday: goToToday, }); const handleToggleCheckbox = async (checkboxId: string) => { @@ -104,12 +118,18 @@ export function DailyPageClient({ await refreshDailyDates(); }; - const handleUpdateCheckbox = async (checkboxId: string, text: string, type: DailyCheckboxType, taskId?: string, date?: Date) => { - await updateCheckbox(checkboxId, { - text, - type, + const handleUpdateCheckbox = async ( + checkboxId: string, + text: string, + type: DailyCheckboxType, + taskId?: string, + date?: Date + ) => { + await updateCheckbox(checkboxId, { + text, + type, taskId, // Permet la liaison tâche pour tous les types - date // Permet la modification de la date/heure + date, // Permet la modification de la date/heure }); // Refresh dates après modification pour mettre à jour le calendrier si la date a changé if (date) { @@ -121,7 +141,6 @@ export function DailyPageClient({ await reorderCheckboxes({ date, checkboxIds }); }; - const getYesterdayDate = () => { return getPreviousWorkday(currentDate); }; @@ -144,49 +163,71 @@ export function DailyPageClient({ const getTodayTitle = () => { const { emoji, text } = generateDateTitle(currentDate, '🎯'); - return <> {text}; + return ( + <> + {text} + + ); }; const getYesterdayTitle = () => { const yesterdayDate = getYesterdayDate(); const { emoji, text } = generateDateTitle(yesterdayDate, '📋'); - return <> {text}; + return ( + <> + {text} + + ); }; // Convertir les métriques de deadline en AlertItem - const convertDeadlineMetricsToAlertItems = (metrics: DeadlineMetrics | null): AlertItem[] => { + const convertDeadlineMetricsToAlertItems = ( + metrics: DeadlineMetrics | null + ): AlertItem[] => { if (!metrics) return []; - + const urgentTasks = [ ...metrics.overdue, ...metrics.critical, - ...metrics.warning + ...metrics.warning, ].sort((a, b) => { - const urgencyOrder: Record = { 'overdue': 0, 'critical': 1, 'warning': 2 }; + const urgencyOrder: Record = { + overdue: 0, + critical: 1, + warning: 2, + }; if (urgencyOrder[a.urgencyLevel] !== urgencyOrder[b.urgencyLevel]) { return urgencyOrder[a.urgencyLevel] - urgencyOrder[b.urgencyLevel]; } return a.daysRemaining - b.daysRemaining; }); - return urgentTasks.map(task => ({ + return urgentTasks.map((task) => ({ id: task.id, title: task.title, - icon: task.urgencyLevel === 'overdue' ? '🔴' : - task.urgencyLevel === 'critical' ? '🟠' : '🟡', + icon: + task.urgencyLevel === 'overdue' + ? '🔴' + : task.urgencyLevel === 'critical' + ? '🟠' + : '🟡', urgency: task.urgencyLevel as 'low' | 'medium' | 'high' | 'critical', source: task.source, - metadata: task.urgencyLevel === 'overdue' ? - (task.daysRemaining === -1 ? 'En retard de 1 jour' : `En retard de ${Math.abs(task.daysRemaining)} jours`) : - task.urgencyLevel === 'critical' ? - (task.daysRemaining === 0 ? 'Échéance aujourd\'hui' : - task.daysRemaining === 1 ? 'Échéance demain' : - `Dans ${task.daysRemaining} jours`) : - `Dans ${task.daysRemaining} jours` + metadata: + task.urgencyLevel === 'overdue' + ? task.daysRemaining === -1 + ? 'En retard de 1 jour' + : `En retard de ${Math.abs(task.daysRemaining)} jours` + : task.urgencyLevel === 'critical' + ? task.daysRemaining === 0 + ? "Échéance aujourd'hui" + : task.daysRemaining === 1 + ? 'Échéance demain' + : `Dans ${task.daysRemaining} jours` + : `Dans ${task.daysRemaining} jours`, })); }; - if (loading) { return (
@@ -217,8 +258,8 @@ export function DailyPageClient({ return (
{/* Header uniforme */} -
@@ -235,7 +276,7 @@ export function DailyPageClient({ > ← - +
{formatCurrentDate()} @@ -266,7 +307,9 @@ export function DailyPageClient({
{ @@ -296,7 +339,7 @@ export function DailyPageClient({ saving={saving} refreshing={refreshing} /> - + {/* Calendrier en bas sur mobile */} Daily pour {formatCurrentDate()} {' • '} - {dailyView.yesterday.length + dailyView.today.length} tâche{dailyView.yesterday.length + dailyView.today.length > 1 ? 's' : ''} au total + {dailyView.yesterday.length + dailyView.today.length} tâche + {dailyView.yesterday.length + dailyView.today.length > 1 + ? 's' + : ''}{' '} + au total {' • '} - {dailyView.yesterday.filter(cb => cb.isChecked).length + dailyView.today.filter(cb => cb.isChecked).length} complétée{(dailyView.yesterday.filter(cb => cb.isChecked).length + dailyView.today.filter(cb => cb.isChecked).length) > 1 ? 's' : ''} + {dailyView.yesterday.filter((cb) => cb.isChecked).length + + dailyView.today.filter((cb) => cb.isChecked).length}{' '} + complétée + {dailyView.yesterday.filter((cb) => cb.isChecked).length + + dailyView.today.filter((cb) => cb.isChecked).length > + 1 + ? 's' + : ''}
)}
); -} \ No newline at end of file +} diff --git a/src/app/daily/page.tsx b/src/app/daily/page.tsx index d98d836..aa922a8 100644 --- a/src/app/daily/page.tsx +++ b/src/app/daily/page.tsx @@ -15,21 +15,24 @@ export const metadata: Metadata = { export default async function DailyPage() { // Récupérer les données côté serveur const today = getToday(); - + try { - const [dailyView, dailyDates, deadlineMetrics, pendingTasks] = await Promise.all([ - dailyService.getDailyView(today), - dailyService.getDailyDates(), - DeadlineAnalyticsService.getDeadlineMetrics().catch(() => null), // Graceful fallback - dailyService.getPendingCheckboxes({ - maxDays: 7, - excludeToday: true, - limit: 50 - }).catch(() => []) // Graceful fallback - ]); + const [dailyView, dailyDates, deadlineMetrics, pendingTasks] = + await Promise.all([ + dailyService.getDailyView(today), + dailyService.getDailyDates(), + DeadlineAnalyticsService.getDeadlineMetrics().catch(() => null), // Graceful fallback + dailyService + .getPendingCheckboxes({ + maxDays: 7, + excludeToday: true, + limit: 50, + }) + .catch(() => []), // Graceful fallback + ]); return ( - ('current'); - const [selectedSprint, setSelectedSprint] = useState(null); + const [selectedSprint, setSelectedSprint] = useState( + null + ); const [showSprintModal, setShowSprintModal] = useState(false); - const [activeTab, setActiveTab] = useState<'overview' | 'velocity' | 'analytics' | 'quality'>('overview'); + const [activeTab, setActiveTab] = useState< + 'overview' | 'velocity' | 'analytics' | 'quality' + >('overview'); // Filtrer les analytics selon la période sélectionnée et les filtres avancés const analytics = useMemo(() => { // Si on a des filtres actifs ET des analytics filtrées, utiliser celles-ci // Sinon utiliser les analytics brutes // Si on est en train de charger les filtres, garder les données originales - const baseAnalytics = hasActiveFilters && filteredAnalytics ? filteredAnalytics : rawAnalytics; + const baseAnalytics = + hasActiveFilters && filteredAnalytics ? filteredAnalytics : rawAnalytics; if (!baseAnalytics) return null; return filterAnalyticsByPeriod(baseAnalytics, selectedPeriod); }, [rawAnalytics, filteredAnalytics, selectedPeriod, hasActiveFilters]); @@ -66,10 +91,19 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: useEffect(() => { // Charger les analytics au montage seulement si Jira est configuré ET qu'on n'a pas déjà des données - if (initialJiraConfig.enabled && initialJiraConfig.projectKey && !initialAnalytics) { + if ( + initialJiraConfig.enabled && + initialJiraConfig.projectKey && + !initialAnalytics + ) { loadAnalytics(); } - }, [initialJiraConfig.enabled, initialJiraConfig.projectKey, loadAnalytics, initialAnalytics]); + }, [ + initialJiraConfig.enabled, + initialJiraConfig.projectKey, + loadAnalytics, + initialAnalytics, + ]); // Gestion du clic sur un sprint const handleSprintClick = (sprint: SprintVelocity) => { @@ -82,19 +116,24 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: setSelectedSprint(null); }; - const loadSprintDetails = async (sprintName: string): Promise => { + const loadSprintDetails = async ( + sprintName: string + ): Promise => { const result = await getSprintDetails(sprintName); if (result.success && result.data) { return result.data; } else { - throw new Error(result.error || 'Erreur lors du chargement des détails du sprint'); + throw new Error( + result.error || 'Erreur lors du chargement des détails du sprint' + ); } }; // Vérifier si Jira est configuré - const isJiraConfigured = initialJiraConfig.enabled && - initialJiraConfig.baseUrl && - initialJiraConfig.email && + const isJiraConfigured = + initialJiraConfig.enabled && + initialJiraConfig.baseUrl && + initialJiraConfig.email && initialJiraConfig.apiToken; const hasProjectConfigured = isJiraConfigured && initialJiraConfig.projectKey; @@ -102,25 +141,26 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: if (!isJiraConfigured) { return (
-
- +
-

Configuration requise

+

+ Configuration requise +

- Jira n'est pas configuré. Vous devez d'abord configurer votre connexion Jira - pour accéder aux analytics d'équipe. + Jira n'est pas configuré. Vous devez d'abord + configurer votre connexion Jira pour accéder aux analytics + d'équipe.

- +
@@ -132,25 +172,26 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: if (!hasProjectConfigured) { return (
-
- +
-

Projet requis

+

+ Projet requis +

- Aucun projet n'est configuré pour les analytics d'équipe. - Configurez un projet spécifique à surveiller dans les paramètres Jira. + Aucun projet n'est configuré pour les analytics + d'équipe. Configurez un projet spécifique à surveiller dans + les paramètres Jira.

- +
@@ -161,20 +202,26 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: return (
-
- +
{/* Breadcrumb */}
- + Paramètres / - + Intégrations / @@ -189,16 +236,19 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:

- Surveillance en temps réel du projet {initialJiraConfig.projectKey} + Surveillance en temps réel du projet{' '} + {initialJiraConfig.projectKey}

{periodInfo.icon} {periodInfo.label} - • {periodInfo.description} + + • {periodInfo.description} +

- +
{/* Sélecteur de période */} setSelectedPeriod(value as PeriodFilter)} + onValueChange={(value) => + setSelectedPeriod(value as PeriodFilter) + } /> - +
{analytics && ( <>
💾 Données en cache
- + {/* Boutons d'export */}
- {/* Contenu principal */} {error && ( )} - {isLoading && !analytics && ( - - )} + {isLoading && !analytics && } {analytics && (
@@ -300,23 +363,23 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: { title: 'Tickets', value: analytics.project.totalIssues, - color: 'primary' + color: 'primary', }, { title: 'Équipe', value: analytics.teamMetrics.totalAssignees, - color: 'default' + color: 'default', }, { title: 'Actifs', value: analytics.teamMetrics.activeAssignees, - color: 'success' + color: 'success', }, { title: 'Points', value: analytics.velocityMetrics.currentSprintPoints, - color: 'warning' - } + color: 'warning', + }, ]} />
@@ -337,13 +400,17 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: {/* Onglets de navigation */} setActiveTab(tabId as 'overview' | 'velocity' | 'analytics' | 'quality')} + onTabChange={(tabId) => + setActiveTab( + tabId as 'overview' | 'velocity' | 'analytics' | 'quality' + ) + } /> {/* Contenu des onglets */} @@ -351,200 +418,244 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
{/* Info discrète sur le calcul des points */}
- Points : Utilise les story points Jira si définis, sinon Epic(13), Story(5), Task(3), Bug(2), Subtask(1) + Points : Utilise les + story points Jira si définis, sinon Epic(13), Story(5), + Task(3), Bug(2), Subtask(1)
- {/* Graphiques principaux */} -
- - -

Répartition de l'équipe

-
- - - -
+ {/* Graphiques principaux */} +
+ + +

+ Répartition de l'équipe +

+
+ + + +
- - -

Vélocité des sprints

-
- - - -
-
+ + +

+ Vélocité des sprints +

+
+ + + +
+
- {/* Métriques de temps et cycle time */} -
- - -

Cycle Time par type

-
- - -
-
- {analytics.cycleTimeMetrics.averageCycleTime} -
-
- jours en moyenne globale -
-
-
-
- - - -

🚀 Vélocité

-
- -
-
- {analytics.velocityMetrics.averageVelocity} -
-
- points par sprint -
-
-
- {analytics.velocityMetrics.sprintHistory.map(sprint => ( -
-
- {sprint.sprintName} - - {sprint.completedPoints}/{sprint.plannedPoints} - + {/* Métriques de temps et cycle time */} +
+ + +

+ Cycle Time par type +

+
+ + +
+
+ {analytics.cycleTimeMetrics.averageCycleTime}
-
-
+
+ jours en moyenne globale
- ))} -
-
-
-
+ + - {/* Métriques avancées */} -
- - -

Burndown Chart

-
- -
- -
-
-
- - - -

Throughput

-
- -
- -
-
-
-
- - {/* Métriques de qualité */} - - -

Métriques de qualité

-
- -
- + + +

🚀 Vélocité

+
+ +
+
+ {analytics.velocityMetrics.averageVelocity} +
+
+ points par sprint +
+
+
+ {analytics.velocityMetrics.sprintHistory.map( + (sprint) => ( +
+
+ {sprint.sprintName} + + {sprint.completedPoints}/ + {sprint.plannedPoints} + +
+
+
+
+
+ ) + )} +
+
+
-
-
- {/* Métriques de predictabilité */} - - -

Predictabilité

-
- -
- -
-
-
+ {/* Métriques avancées */} +
+ + +

+ Burndown Chart +

+
+ +
+ +
+
+
- {/* Matrice de collaboration - ligne entière */} - - -

Matrice de collaboration

-
- -
- + + +

+ Throughput +

+
+ +
+ +
+
+
-
-
- {/* Comparaison inter-sprints */} - - -

Comparaison inter-sprints

-
- -
- -
-
-
+ {/* Métriques de qualité */} + + +

+ Métriques de qualité +

+
+ +
+ +
+
+
- {/* Heatmap d'activité de l'équipe */} - - -

Heatmap d'activité de l'équipe

-
- -
- -
-
-
+ {/* Métriques de predictabilité */} + + +

+ Predictabilité +

+
+ +
+ +
+
+
+ + {/* Matrice de collaboration - ligne entière */} + + +

+ Matrice de collaboration +

+
+ +
+ +
+
+
+ + {/* Comparaison inter-sprints */} + + +

+ Comparaison inter-sprints +

+
+ +
+ +
+
+
+ + {/* Heatmap d'activité de l'équipe */} + + +

+ Heatmap d'activité de + l'équipe +

+
+ +
+ +
+
+
)} @@ -553,12 +664,16 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: {/* Graphique de vélocité */} -

Vélocité des sprints

+

+ Vélocité des sprints +

- @@ -570,12 +685,16 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
-

Burndown Chart

+

+ Burndown Chart +

-
@@ -584,12 +703,16 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: -

Throughput

+

+ Throughput +

-
@@ -600,12 +723,16 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: {/* Comparaison des sprints */} -

Comparaison des sprints

+

+ Comparaison des sprints +

-
@@ -620,18 +747,24 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
-

Cycle Time par type

+

+ Cycle Time par type +

-
- {analytics.cycleTimeMetrics.averageCycleTime.toFixed(1)} + {analytics.cycleTimeMetrics.averageCycleTime.toFixed( + 1 + )}
Cycle time moyen (jours) @@ -642,13 +775,19 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: -

🔥 Heatmap d'activité

+

+ 🔥 Heatmap d'activité +

-
@@ -660,11 +799,13 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
-

Métriques de qualité

+

+ Métriques de qualité +

- @@ -674,12 +815,16 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: -

Predictabilité

+

+ Predictabilité +

-
@@ -695,12 +840,16 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }:
-

Répartition de l'équipe

+

+ Répartition de l'équipe +

-
@@ -709,11 +858,13 @@ export function JiraDashboardPageClient({ initialJiraConfig, initialAnalytics }: -

Matrice de collaboration

+

+ Matrice de collaboration +

- diff --git a/src/app/jira-dashboard/page.tsx b/src/app/jira-dashboard/page.tsx index 87d18a9..3fbf80c 100644 --- a/src/app/jira-dashboard/page.tsx +++ b/src/app/jira-dashboard/page.tsx @@ -14,8 +14,10 @@ export default async function JiraDashboardPage() { } // Récupérer la config Jira côté serveur - const jiraConfig = await userPreferencesService.getJiraConfig(session.user.id); - + const jiraConfig = await userPreferencesService.getJiraConfig( + session.user.id + ); + // Récupérer les analytics côté serveur (utilise le cache du service) let initialAnalytics = null; if (jiraConfig.enabled && jiraConfig.projectKey) { @@ -26,8 +28,8 @@ export default async function JiraDashboardPage() { } return ( - ); diff --git a/src/app/kanban/KanbanPageClient.tsx b/src/app/kanban/KanbanPageClient.tsx index 4e176b8..f0f7412 100644 --- a/src/app/kanban/KanbanPageClient.tsx +++ b/src/app/kanban/KanbanPageClient.tsx @@ -20,8 +20,15 @@ interface KanbanPageClientProps { } function KanbanPageContent() { - const { syncing, createTask, activeFiltersCount, kanbanFilters, setKanbanFilters } = useTasksContext(); - const { preferences, updateViewPreferences, toggleFontSize } = useUserPreferences(); + const { + syncing, + createTask, + activeFiltersCount, + kanbanFilters, + setKanbanFilters, + } = useTasksContext(); + const { preferences, updateViewPreferences, toggleFontSize } = + useUserPreferences(); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const isMobile = useIsMobile(768); // Tailwind md breakpoint const searchParams = useSearchParams(); @@ -51,9 +58,9 @@ function KanbanPageContent() { }; const handleToggleDueDateFilter = () => { - setKanbanFilters({ - ...kanbanFilters, - showWithDueDate: !kanbanFilters.showWithDueDate + setKanbanFilters({ + ...kanbanFilters, + showWithDueDate: !kanbanFilters.showWithDueDate, }); }; @@ -74,19 +81,21 @@ function KanbanPageContent() { onToggleFontSize: toggleFontSize, onOpenSearch: () => { // Focus sur le champ de recherche dans les contrôles desktop - const searchInput = document.querySelector('input[placeholder*="Rechercher"]') as HTMLInputElement; + const searchInput = document.querySelector( + 'input[placeholder*="Rechercher"]' + ) as HTMLInputElement; searchInput?.focus(); - } + }, }); return (
-
- + {/* Barre de contrôles responsive */} {isMobile ? ( setIsCreateModalOpen(true)} /> )} - +
- + ); diff --git a/src/app/kanban/page.tsx b/src/app/kanban/page.tsx index fbf21b4..d016468 100644 --- a/src/app/kanban/page.tsx +++ b/src/app/kanban/page.tsx @@ -9,13 +9,10 @@ export default async function KanbanPage() { // SSR - Récupération des données côté serveur const [initialTasks, initialTags] = await Promise.all([ tasksService.getTasks(), - tagsService.getTags() + tagsService.getTags(), ]); return ( - + ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8e931f2..2307d62 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,32 +1,32 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; -import { ThemeProvider } from "@/contexts/ThemeContext"; -import { BackgroundProvider } from "@/contexts/BackgroundContext"; -import { JiraConfigProvider } from "@/contexts/JiraConfigContext"; -import { UserPreferencesProvider } from "@/contexts/UserPreferencesContext"; -import { KeyboardShortcutsProvider } from "@/contexts/KeyboardShortcutsContext"; -import { userPreferencesService } from "@/services/core/user-preferences"; -import { KeyboardShortcuts } from "@/components/KeyboardShortcuts"; -import { GlobalKeyboardShortcuts } from "@/components/GlobalKeyboardShortcuts"; -import { ToastProvider } from "@/components/ui/Toast"; -import { AuthProvider } from "../components/AuthProvider"; +import type { Metadata } from 'next'; +import { Geist, Geist_Mono } from 'next/font/google'; +import './globals.css'; +import { ThemeProvider } from '@/contexts/ThemeContext'; +import { BackgroundProvider } from '@/contexts/BackgroundContext'; +import { JiraConfigProvider } from '@/contexts/JiraConfigContext'; +import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext'; +import { KeyboardShortcutsProvider } from '@/contexts/KeyboardShortcutsContext'; +import { userPreferencesService } from '@/services/core/user-preferences'; +import { KeyboardShortcuts } from '@/components/KeyboardShortcuts'; +import { GlobalKeyboardShortcuts } from '@/components/GlobalKeyboardShortcuts'; +import { ToastProvider } from '@/components/ui/Toast'; +import { AuthProvider } from '../components/AuthProvider'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], + variable: '--font-geist-sans', + subsets: ['latin'], }); const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], + variable: '--font-geist-mono', + subsets: ['latin'], }); export const metadata: Metadata = { - title: "Tower control", - description: "Tour de controle (Kanban, tache, daily, ...)", + title: 'Tower control', + description: 'Tour de controle (Kanban, tache, daily, ...)', }; export default async function RootLayout({ @@ -36,13 +36,13 @@ export default async function RootLayout({ }>) { // Récupérer la session côté serveur pour le SSR const session = await getServerSession(authOptions); - + // Charger les préférences seulement si l'utilisateur est connecté // Sinon, les préférences par défaut seront chargées côté client - const initialPreferences = session?.user?.id + const initialPreferences = session?.user?.id ? await userPreferencesService.getAllPreferences(session.user.id) : undefined; - + return ( - - - + + {children} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 6c74dbb..92c580b 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,16 +1,16 @@ -'use client' +'use client'; -import { useState, useEffect } from 'react' -import { signIn, getSession, useSession } from 'next-auth/react' -import { useRouter } from 'next/navigation' -import Link from 'next/link' -import { Button } from '@/components/ui/Button' -import { Input } from '@/components/ui/Input' -import { TowerLogo } from '@/components/TowerLogo' -import { TowerBackground } from '@/components/TowerBackground' -import { THEME_CONFIG } from '@/lib/ui-config' -import { useTheme } from '@/contexts/ThemeContext' -import { PRESET_BACKGROUNDS } from '@/lib/ui-config' +import { useState, useEffect } from 'react'; +import { signIn, getSession, useSession } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import { TowerLogo } from '@/components/TowerLogo'; +import { TowerBackground } from '@/components/TowerBackground'; +import { THEME_CONFIG } from '@/lib/ui-config'; +import { useTheme } from '@/contexts/ThemeContext'; +import { PRESET_BACKGROUNDS } from '@/lib/ui-config'; function RandomThemeApplier() { const { setTheme } = useTheme(); @@ -19,9 +19,12 @@ function RandomThemeApplier() { useEffect(() => { if (!applied) { // Sélectionner un thème aléatoire côté client seulement - const randomTheme = THEME_CONFIG.allThemes[Math.floor(Math.random() * THEME_CONFIG.allThemes.length)]; + const randomTheme = + THEME_CONFIG.allThemes[ + Math.floor(Math.random() * THEME_CONFIG.allThemes.length) + ]; console.log('Applying random theme:', randomTheme); - + // Utiliser setTheme du ThemeContext pour forcer le changement setTheme(randomTheme); setApplied(true); @@ -37,20 +40,25 @@ function RandomBackground() { useEffect(() => { // Sélectionner un background aléatoire parmi les presets (sauf 'none') - const availableBackgrounds = PRESET_BACKGROUNDS.filter(bg => bg.id !== 'none'); - const randomBackground = availableBackgrounds[Math.floor(Math.random() * availableBackgrounds.length)]; + const availableBackgrounds = PRESET_BACKGROUNDS.filter( + (bg) => bg.id !== 'none' + ); + const randomBackground = + availableBackgrounds[ + Math.floor(Math.random() * availableBackgrounds.length) + ]; setBackground(randomBackground.preview); setMounted(true); }, []); - + return ( -
{/* Effet de profondeur */}
- + {/* Effet de lumière */}
@@ -58,47 +66,47 @@ function RandomBackground() { } function LoginPageContent() { - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState('') - const [isLoading, setIsLoading] = useState(false) - const router = useRouter() - const { data: session, status } = useSession() + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + const { data: session, status } = useSession(); // Redirection si l'utilisateur est déjà connecté useEffect(() => { if (status === 'authenticated' && session) { - router.push('/') + router.push('/'); } - }, [status, session, router]) + }, [status, session, router]); const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setIsLoading(true) - setError('') + e.preventDefault(); + setIsLoading(true); + setError(''); try { const result = await signIn('credentials', { email, password, redirect: false, - }) + }); if (result?.error) { - setError('Email ou mot de passe incorrect') + setError('Email ou mot de passe incorrect'); } else { // Vérifier que la session est bien créée - const session = await getSession() + const session = await getSession(); if (session) { - router.push('/') + router.push('/'); } } } catch { - setError('Une erreur est survenue') + setError('Une erreur est survenue'); } finally { - setIsLoading(false) + setIsLoading(false); } - } + }; // Afficher un loader pendant la vérification de session if (status === 'loading') { @@ -109,12 +117,12 @@ function LoginPageContent() {
Chargement...
- ) + ); } // Ne pas afficher le formulaire si l'utilisateur est connecté if (status === 'authenticated') { - return null + return null; } return ( @@ -136,7 +144,10 @@ function LoginPageContent() {
-
- +
-
- ) + ); } export default function LoginPage() { - return + return ; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 36a73d2..1783121 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,17 +10,24 @@ export const dynamic = 'force-dynamic'; export default async function HomePage() { // SSR - Récupération des données côté serveur - const [initialTasks, initialTags, initialStats, productivityMetrics, deadlineMetrics, tagMetrics] = await Promise.all([ + const [ + initialTasks, + initialTags, + initialStats, + productivityMetrics, + deadlineMetrics, + tagMetrics, + ] = await Promise.all([ tasksService.getTasks(), tagsService.getTags(), tasksService.getTaskStats(), AnalyticsService.getProductivityMetrics(), DeadlineAnalyticsService.getDeadlineMetrics(), - TagAnalyticsService.getTagDistributionMetrics() + TagAnalyticsService.getTagDistributionMetrics(), ]); return ( - (null) - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState('') - const [success, setSuccess] = useState('') + const { data: session, update } = useSession(); + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + const [profile, setProfile] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); // Form data const [formData, setFormData] = useState({ @@ -38,110 +49,114 @@ export default function ProfilePage() { firstName: '', lastName: '', avatar: '', - }) + }); useEffect(() => { if (!session) { - router.push('/login') - return + router.push('/login'); + return; } - fetchProfile() - }, [session, router]) + fetchProfile(); + }, [session, router]); const fetchProfile = async () => { try { - const result = await getProfile() + const result = await getProfile(); if (!result.success || !result.user) { - throw new Error(result.error || 'Erreur lors du chargement du profil') + throw new Error(result.error || 'Erreur lors du chargement du profil'); } - setProfile(result.user) + setProfile(result.user); setFormData({ name: result.user.name || '', firstName: result.user.firstName || '', lastName: result.user.lastName || '', avatar: result.user.avatar || '', - }) + }); } catch (error) { - setError(error instanceof Error ? error.message : 'Erreur inconnue') + setError(error instanceof Error ? error.message : 'Erreur inconnue'); } finally { - setIsLoading(false) + setIsLoading(false); } - } + }; const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError('') - setSuccess('') + e.preventDefault(); + setError(''); + setSuccess(''); startTransition(async () => { try { - const result = await updateProfile(formData) - + const result = await updateProfile(formData); + if (!result.success || !result.user) { - setError(result.error || 'Erreur lors de la mise à jour') - return + setError(result.error || 'Erreur lors de la mise à jour'); + return; } - setProfile(result.user) - setSuccess('Profil mis à jour avec succès') - + setProfile(result.user); + setSuccess('Profil mis à jour avec succès'); + // Mettre à jour la session NextAuth await update({ ...session, user: { ...session?.user, - name: result.user.name || `${result.user.firstName || ''} ${result.user.lastName || ''}`.trim() || result.user.email, + name: + result.user.name || + `${result.user.firstName || ''} ${result.user.lastName || ''}`.trim() || + result.user.email, firstName: result.user.firstName, lastName: result.user.lastName, avatar: result.user.avatar, - } - }) - + }, + }); } catch (error) { - setError(error instanceof Error ? error.message : 'Erreur inconnue') + setError(error instanceof Error ? error.message : 'Erreur inconnue'); } - }) - } + }); + }; const handleUseGravatar = async () => { - setError('') - setSuccess('') + setError(''); + setSuccess(''); startTransition(async () => { try { - const result = await applyGravatar() - + const result = await applyGravatar(); + if (!result.success || !result.user) { - setError(result.error || 'Erreur lors de la mise à jour Gravatar') - return + setError(result.error || 'Erreur lors de la mise à jour Gravatar'); + return; } - setProfile(result.user) - setFormData(prev => ({ ...prev, avatar: result.user!.avatar || '' })) - setSuccess('Avatar Gravatar appliqué avec succès') - + setProfile(result.user); + setFormData((prev) => ({ ...prev, avatar: result.user!.avatar || '' })); + setSuccess('Avatar Gravatar appliqué avec succès'); + // Mettre à jour la session NextAuth await update({ ...session, user: { ...session?.user, - name: result.user.name || `${result.user.firstName || ''} ${result.user.lastName || ''}`.trim() || result.user.email, + name: + result.user.name || + `${result.user.firstName || ''} ${result.user.lastName || ''}`.trim() || + result.user.email, firstName: result.user.firstName, lastName: result.user.lastName, avatar: result.user.avatar, - } - }) - + }, + }); } catch (error) { - setError(error instanceof Error ? error.message : 'Erreur inconnue') + setError(error instanceof Error ? error.message : 'Erreur inconnue'); } - }) - } + }); + }; const handleChange = (field: string, value: string) => { - setFormData(prev => ({ ...prev, [field]: value })) - } + setFormData((prev) => ({ ...prev, [field]: value })); + }; if (isLoading) { return ( @@ -155,7 +170,7 @@ export default function ProfilePage() {
- ) + ); } if (!profile) { @@ -170,13 +185,13 @@ export default function ProfilePage() {
- ) + ); } return (
- +
{/* Header avec avatar et infos principales */} @@ -184,8 +199,8 @@ export default function ProfilePage() {
{/* Avatar section */}
- {isGravatarUrl(profile.avatar) ? ( - + ) : ( - + )}
)}
- + {/* User info */}

- {profile.name || `${profile.firstName || ''} ${profile.lastName || ''}`.trim() || profile.email} + {profile.name || + `${profile.firstName || ''} ${profile.lastName || ''}`.trim() || + profile.email}

-

{profile.email}

- +

+ {profile.email} +

+
- {profile.role} + + {profile.role} +
- Membre depuis {new Date(profile.createdAt).toLocaleDateString('fr-FR')} + Membre depuis{' '} + {new Date(profile.createdAt).toLocaleDateString('fr-FR')}
@@ -232,21 +260,29 @@ export default function ProfilePage() { Informations générales - +
-
{profile.email}
-
Email principal
+
+ {profile.email} +
+
+ Email principal +
-
{profile.role}
-
Rôle utilisateur
+
+ {profile.role} +
+
+ Rôle utilisateur +
@@ -257,7 +293,9 @@ export default function ProfilePage() {
{new Date(profile.lastLoginAt).toLocaleString('fr-FR')}
-
Dernière connexion
+
+ Dernière connexion +
)} @@ -274,19 +312,27 @@ export default function ProfilePage() {
-
-
- ) + ); } diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx index 46b78a4..deda422 100644 --- a/src/app/register/page.tsx +++ b/src/app/register/page.tsx @@ -1,49 +1,49 @@ -'use client' +'use client'; -import { useState, useEffect } from 'react' -import { useRouter } from 'next/navigation' -import { useSession } from 'next-auth/react' -import { Button } from '@/components/ui/Button' -import { Input } from '@/components/ui/Input' -import Link from 'next/link' -import { TowerLogo } from '@/components/TowerLogo' -import { TowerBackground } from '@/components/TowerBackground' +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useSession } from 'next-auth/react'; +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import Link from 'next/link'; +import { TowerLogo } from '@/components/TowerLogo'; +import { TowerBackground } from '@/components/TowerBackground'; export default function RegisterPage() { - const [email, setEmail] = useState('') - const [name, setName] = useState('') - const [firstName, setFirstName] = useState('') - const [lastName, setLastName] = useState('') - const [password, setPassword] = useState('') - const [confirmPassword, setConfirmPassword] = useState('') - const [error, setError] = useState('') - const [isLoading, setIsLoading] = useState(false) - const router = useRouter() - const { data: session, status } = useSession() + const [email, setEmail] = useState(''); + const [name, setName] = useState(''); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + const { data: session, status } = useSession(); // Redirection si l'utilisateur est déjà connecté useEffect(() => { if (status === 'authenticated' && session) { - router.push('/') + router.push('/'); } - }, [status, session, router]) + }, [status, session, router]); const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setIsLoading(true) - setError('') + e.preventDefault(); + setIsLoading(true); + setError(''); // Validation côté client if (password !== confirmPassword) { - setError('Les mots de passe ne correspondent pas') - setIsLoading(false) - return + setError('Les mots de passe ne correspondent pas'); + setIsLoading(false); + return; } if (password.length < 6) { - setError('Le mot de passe doit contenir au moins 6 caractères') - setIsLoading(false) - return + setError('Le mot de passe doit contenir au moins 6 caractères'); + setIsLoading(false); + return; } try { @@ -59,22 +59,24 @@ export default function RegisterPage() { lastName, password, }), - }) + }); - const data = await response.json() + const data = await response.json(); if (!response.ok) { - throw new Error(data.error || 'Une erreur est survenue') + throw new Error(data.error || 'Une erreur est survenue'); } // Rediriger vers la page de login avec un message de succès - router.push('/login?message=Compte créé avec succès') + router.push('/login?message=Compte créé avec succès'); } catch (error) { - setError(error instanceof Error ? error.message : 'Une erreur est survenue') + setError( + error instanceof Error ? error.message : 'Une erreur est survenue' + ); } finally { - setIsLoading(false) + setIsLoading(false); } - } + }; // Afficher un loader pendant la vérification de session if (status === 'loading') { @@ -85,12 +87,12 @@ export default function RegisterPage() {
Chargement...
- ) + ); } // Ne pas afficher le formulaire si l'utilisateur est connecté if (status === 'authenticated') { - return null + return null; } return ( @@ -111,7 +113,10 @@ export default function RegisterPage() {
-
- +
-