20 Commits

Author SHA1 Message Date
Julien Froidefond
4ba6ba2c0b refactor: unify date handling with utility functions
- Replaced direct date manipulations with utility functions like `getToday`, `parseDate`, and `createDateFromParts` across various components and services for consistency.
- Updated date initialization in `JiraAnalyticsService`, `BackupService`, and `DailyClient` to improve clarity and maintainability.
- Enhanced date parsing in forms and API routes to ensure proper handling of date strings.
2025-09-21 13:04:34 +02:00
Julien Froidefond
c3c1d24fa2 refactor: enhance date handling across components
- Replaced direct date manipulations with utility functions for consistency and readability.
- Updated date formatting in `DailyCalendar`, `RecentTasks`, `CompletionRateChart`, and other components to use `formatDateShort` and `formatDateForDisplay`.
- Improved date parsing in `JiraLogs`, `JiraSchedulerConfig`, and `BackupSettingsPageClient` to ensure proper handling of date strings.
- Streamlined date initialization in `useDaily` and `DailyService` to utilize `getToday` and `getYesterday` for better clarity.
2025-09-21 12:02:06 +02:00
Julien Froidefond
557cdebc13 refactor: date utils and all calls 2025-09-21 11:41:17 +02:00
Julien Froidefond
799a21df5c feat: implement Jira auto-sync scheduler and UI configuration
- Added `jiraAutoSync` and `jiraSyncInterval` fields to user preferences for scheduler configuration.
- Created `JiraScheduler` service to manage automatic synchronization with Jira based on user settings.
- Updated API route to handle scheduler actions and configuration updates.
- Introduced `JiraSchedulerConfig` component for user interface to control scheduler settings.
- Enhanced `TODO.md` to reflect completed tasks related to Jira synchronization features.
2025-09-21 11:30:41 +02:00
Julien Froidefond
a0e2a78372 feat: update Daily and Jira dashboard pages with dynamic titles and improved UI
- Implemented `getTodayTitle` and `getYesterdayTitle` functions in `DailyPageClient` to dynamically set section titles based on the current date.
- Updated `TODO.md` to mark completed tasks related to the Jira dashboard UI consistency.
- Enhanced card content in `JiraDashboardPageClient` to ensure charts are responsive and maintain consistent styling.
- Removed unused date formatting function in `DailySection` for cleaner code.
2025-09-21 10:49:39 +02:00
Julien Froidefond
4152b0bdfc chore: refactor project structure and clean up unused components
- Updated `TODO.md` to reflect new testing tasks and final structure expectations.
- Simplified TypeScript path mappings in `tsconfig.json` for better clarity.
- Revised business logic separation rules in `.cursor/rules` to align with new directory structure.
- Deleted unused client components and services to streamline the codebase.
- Adjusted import paths in scripts to match the new structure.
2025-09-21 10:26:35 +02:00
Julien Froidefond
9dc1fafa76 feat: expand TODO.md with multi-user and mobile interface plans
- Added detailed sections for transitioning to a multi-tenant architecture, including authentication, data model adjustments, and service modifications.
- Introduced a comprehensive migration plan for user data isolation and security considerations.
- Outlined phases for developing a dedicated mobile interface, addressing current usability issues and enhancing user experience on mobile devices.
- Included specific tasks for mobile components and UX optimizations.
2025-09-21 10:12:54 +02:00
Julien Froidefond
d7140507e5 chore: update TODO.md with new feature ideas and refactoring plans
- Added sections for future features including TFS/Azure DevOps integration, task management, and modular architecture.
- Detailed a migration plan for restructuring the project directory to align with Next.js 13+ best practices.
- Included specific tasks for improving integration interfaces and enhancing the user experience.
2025-09-21 09:14:52 +02:00
Julien Froidefond
43998425e6 feat: enhance backup functionality and logging
- Updated `createBackup` method to accept a `force` parameter, allowing backups to be created even if no changes are detected.
- Added user alerts in `AdvancedSettingsPageClient` and `BackupSettingsPageClient` for backup status feedback.
- Implemented `getBackupLogs` method in `BackupService` to retrieve backup logs, with a new API route for accessing logs.
- Enhanced UI in `BackupSettingsPageClient` to display backup logs and provide a refresh option.
- Updated `BackupManagerCLI` to support forced backups via command line.
2025-09-21 07:27:23 +02:00
Julien Froidefond
618e774a30 fix: update database path in README and remove backup link
- Changed `DATABASE_URL` in `data/README.md` to use a relative path for better compatibility.
- Removed the reference to `BACKUP.md` in `DOCKER.md` as it is no longer relevant.
2025-09-21 07:18:49 +02:00
Julien Froidefond
c5bfcc50f8 fix: refine loading states in MetricsTab
- Simplified loading logic by removing unnecessary trends loading check.
- Enhanced UI feedback by disabling the weeks selection during trends loading and added a loading state for the trends chart.
- Improved user experience by displaying a message when no velocity data is available.
2025-09-21 06:42:26 +02:00
Julien Froidefond
6e2b0abc8d chore: update TODO list with new tasks
- Added tasks for backup changes, date refactoring, and component splitting to the TODO.md file.
2025-09-21 06:42:18 +02:00
Julien Froidefond
9da824993d fix: update fs import in SystemInfoService for eslint compliance
- Changed the fs import in `system-info.ts` to comply with eslint rules by adding a comment to disable the specific linting error.
2025-09-21 06:36:13 +02:00
Julien Froidefond
e88b1aad32 chore: remove unused system info functionality
- Deleted `system-info.ts` as it is no longer needed in the codebase.
- No changes made to `workday-utils.ts`, just added a new line for consistency.
2025-09-21 06:34:43 +02:00
Julien Froidefond
3c20df95d9 feat: add system info and backup functionalities to settings page
- Integrated system info fetching in `SettingsPage` for improved user insights.
- Enhanced `SettingsIndexPageClient` with manual backup creation and Jira connection testing features.
- Added loading states and auto-dismiss messages for user feedback during actions.
- Updated UI to display system info and backup statistics dynamically.
2025-09-20 16:38:33 +02:00
Julien Froidefond
da0565472d feat: enhance settings and backup functionality
- Updated status descriptions in `SettingsIndexPageClient` to reflect current functionality.
- Added a new backup management section in settings for better user access.
- Modified `BackupService` to include backup type in filenames, improving clarity and organization.
- Enhanced backup file parsing to support both new and old filename formats, ensuring backward compatibility.
2025-09-20 16:21:50 +02:00
Julien Froidefond
9a33d1ee48 fix: improve date formatting and backup path handling
- Updated `formatTimeAgo` in `AdvancedSettingsPageClient` to use a fixed format for hydration consistency.
- Refined `formatDate` in `BackupSettingsPageClient` for consistent server/client formatting.
- Refactored `BackupService` to use `getCurrentBackupPath` for all backup path references, ensuring up-to-date paths and avoiding caching issues.
- Added `getCurrentBackupPath` method to dynamically retrieve the current backup path based on environment variables.
2025-09-20 16:12:01 +02:00
Julien Froidefond
ee442de773 chore: refine database and backup paths in configuration
- Updated `.gitignore` to only exclude `.db` files in the `/data/` directory and preserve backups.
- Modified `docker-compose.yml` to switch database and backup paths to `dev.db`, aligning with the current development setup.
2025-09-20 15:58:53 +02:00
Julien Froidefond
329018161c chore: update backup configurations and directory structure
- Modified `.gitignore` to exclude all files in the `/data/` directory.
- Enhanced `BACKUP.md` with customization options for backup storage paths and updated database path configurations.
- Updated `docker-compose.yml` to reflect new paths for database and backup storage.
- Adjusted `Dockerfile` to create a dedicated backups directory.
- Refactored `BackupService` to utilize environment variables for backup paths, improving flexibility and reliability.
- Deleted `dev.db` as it is no longer needed in the repository.
2025-09-20 15:45:56 +02:00
Julien Froidefond
dfa8d34855 feat: add workday utility functions
- Introduced utility functions for workday calculations in `workday-utils.ts`, including `getPreviousWorkday`, `getNextWorkday`, `isWorkday`, and `getDayName`.
- Updated `DailyService` and `DailyPageClient` to utilize `getPreviousWorkday` for accurate date handling instead of simple date subtraction.
2025-09-20 15:43:38 +02:00
158 changed files with 4024 additions and 1440 deletions

View File

@@ -9,7 +9,7 @@ description: Enforce business logic separation between frontend and backend
All business logic, data processing, and domain rules MUST be implemented in the backend services layer. The frontend is purely for presentation and user interaction. All business logic, data processing, and domain rules MUST be implemented in the backend services layer. The frontend is purely for presentation and user interaction.
## ✅ ALLOWED in Frontend ([components/](mdc:components/), [hooks/](mdc:hooks/), [clients/](mdc:clients/)) ## ✅ ALLOWED in Frontend ([src/components/](mdc:src/components/), [src/hooks/](mdc:src/hooks/), [src/clients/](mdc:src/clients/))
### Components ### Components
- UI rendering and presentation logic - UI rendering and presentation logic
@@ -73,7 +73,7 @@ const calculateTeamVelocity = (sprints) => {
// This belongs in services/team-analytics.ts // This belongs in services/team-analytics.ts
``` ```
## ✅ REQUIRED in Backend ([services/](mdc:services/), [app/api/](mdc:app/api/)) ## ✅ REQUIRED in Backend ([src/services/](mdc:src/services/), [src/app/api/](mdc:src/app/api/))
### Services Layer ### Services Layer
- All business rules and domain logic - All business rules and domain logic

View File

@@ -1,10 +1,10 @@
--- ---
globs: components/**/*.tsx globs: src/components/**/*.tsx
--- ---
# Components Rules # Components Rules
1. UI components MUST be in components/ui/ 1. UI components MUST be in src/components/ui/
2. Feature components MUST be in their feature folder 2. Feature components MUST be in their feature folder
3. Components MUST use clients for data fetching 3. Components MUST use clients for data fetching
4. Components MUST be properly typed 4. Components MUST be properly typed

View File

@@ -5,26 +5,26 @@ alwaysApply: true
# Project Structure Rules # Project Structure Rules
1. Backend: 1. Backend:
- [services/](mdc:services/) - ALL database access - [src/services/](mdc:src/services/) - ALL database access
- [app/api/](mdc:app/api/) - API routes using services - [src/app/api/](mdc:src/app/api/) - API routes using services
2. Frontend: 2. Frontend:
- [clients/](mdc:clients/) - HTTP clients - [src/clients/](mdc:src/clients/) - HTTP clients
- [components/](mdc:components/) - React components (organized by domain) - [src/components/](mdc:src/components/) - React components (organized by domain)
- [hooks/](mdc:hooks/) - React hooks - [src/hooks/](mdc:src/hooks/) - React hooks
3. Shared: 3. Shared:
- [lib/](mdc:lib/) - Types and utilities - [src/lib/](mdc:src/lib/) - Types and utilities
- [scripts/](mdc:scripts/) - Utility scripts - [scripts/](mdc:scripts/) - Utility scripts
Key Files: Key Files:
- [services/database.ts](mdc:services/database.ts) - Database pool - [src/services/database.ts](mdc:src/services/database.ts) - Database pool
- [clients/base/http-client.ts](mdc:clients/base/http-client.ts) - Base HTTP client - [src/clients/base/http-client.ts](mdc:src/clients/base/http-client.ts) - Base HTTP client
- [lib/types.ts](mdc:lib/types.ts) - Shared types - [src/lib/types.ts](mdc:src/lib/types.ts) - Shared types
❌ FORBIDDEN: ❌ FORBIDDEN:
- Database access outside services/ - Database access outside src/services/
- HTTP calls outside clients/ - HTTP calls outside src/clients/
- Business logic in components/ - Business logic in src/components/

View File

@@ -1,5 +1,5 @@
--- ---
globs: services/*.ts globs: src/services/*.ts
--- ---
# Services Rules # Services Rules
@@ -7,7 +7,7 @@ globs: services/*.ts
1. Services MUST contain ALL PostgreSQL queries 1. Services MUST contain ALL PostgreSQL queries
2. Services are the ONLY layer allowed to communicate with the database 2. Services are the ONLY layer allowed to communicate with the database
3. Each service MUST: 3. Each service MUST:
- Use the pool from [services/database.ts](mdc:services/database.ts) - Use the pool from [src/services/database.ts](mdc:src/services/database.ts)
- Implement proper transaction management - Implement proper transaction management
- Handle errors and logging - Handle errors and logging
- Validate data before insertion - Validate data before insertion
@@ -37,6 +37,6 @@ export class MyService {
❌ FORBIDDEN: ❌ FORBIDDEN:
- Direct database queries outside services - Direct database queries outside src/services
- Raw SQL in API routes - Raw SQL in API routes
- Database logic in components - Database logic in components

3
.gitignore vendored
View File

@@ -43,4 +43,5 @@ next-env.d.ts
/src/generated/prisma /src/generated/prisma
/prisma/dev.db /prisma/dev.db
backups/ /data/*.db
/data/backups/*

View File

@@ -52,6 +52,19 @@ tsx scripts/backup-manager.ts config-set maxBackups=10
tsx scripts/backup-manager.ts config-set compression=true tsx scripts/backup-manager.ts config-set compression=true
``` ```
### Personnalisation du dossier de sauvegarde
```bash
# Via variable d'environnement permanente (.env)
BACKUP_STORAGE_PATH="./custom-backups"
# Via variable temporaire (une seule fois)
BACKUP_STORAGE_PATH="./my-backups" npm run backup:create
# Exemple avec un chemin absolu
BACKUP_STORAGE_PATH="/var/backups/towercontrol" npm run backup:create
```
## Utilisation ## Utilisation
### Interface graphique ### Interface graphique
@@ -272,8 +285,34 @@ export const prisma = globalThis.__prisma || new PrismaClient({
### Variables d'environnement ### Variables d'environnement
```bash ```bash
# Optionnel : personnaliser le chemin de la base # Configuration des chemins de base de données
DATABASE_URL="file:./custom/path/dev.db" DATABASE_URL="file:./prisma/dev.db" # Pour Prisma
BACKUP_DATABASE_PATH="./prisma/dev.db" # Base à sauvegarder (optionnel)
BACKUP_STORAGE_PATH="./backups" # Dossier des sauvegardes (optionnel)
```
### Docker
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
volumes:
- ./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
└── backups/ # Sauvegardes (créé automatiquement)
├── towercontrol_*.db.gz
└── ...
``` ```
## API ## API

201
DOCKER.md Normal file
View File

@@ -0,0 +1,201 @@
# 🐳 Docker - TowerControl
Guide d'utilisation de TowerControl avec Docker.
## 🚀 Démarrage rapide
### Production
```bash
# Démarrer le service de production
docker-compose up -d towercontrol
# Accéder à l'application
open http://localhost:3006
```
### Développement
```bash
# Démarrer le service de développement avec live reload
docker-compose --profile dev up towercontrol-dev
# Accéder à l'application
open http://localhost:3005
```
## 📋 Services disponibles
### 🚀 `towercontrol` (Production)
- **Port** : 3006
- **Base de données** : `./data/prod.db`
- **Sauvegardes** : `./data/backups/`
- **Mode** : Optimisé, standalone
- **Restart** : Automatique
### 🛠️ `towercontrol-dev` (Développement)
- **Port** : 3005
- **Base de données** : `./data/dev.db`
- **Sauvegardes** : `./data/backups/` (partagées)
- **Mode** : Live reload, debug
- **Profile** : `dev`
## 📁 Structure des données
```
./data/ # Mappé vers /app/data dans les conteneurs
├── README.md # Documentation du dossier data
├── prod.db # Base SQLite production
├── dev.db # Base SQLite développement
└── backups/ # Sauvegardes automatiques
├── towercontrol_2025-01-15T10-30-00-000Z.db.gz
└── ...
```
## 🔧 Configuration
### 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 |
### Ports
- **Production** : `3006:3000`
- **Développement** : `3005:3000`
## 📚 Commandes utiles
### Gestion des conteneurs
```bash
# Voir les logs
docker-compose logs -f towercontrol
docker-compose logs -f towercontrol-dev
# Arrêter les services
docker-compose down
# Reconstruire les images
docker-compose build
# Nettoyer tout
docker-compose down -v --rmi all
```
### Gestion des données
```bash
# Sauvegarder les données
docker-compose exec towercontrol npm run backup:create
# Lister les sauvegardes
docker-compose exec towercontrol npm run backup:list
# Accéder au shell du conteneur
docker-compose exec towercontrol sh
```
### Base de données
```bash
# Migrations Prisma
docker-compose exec towercontrol npx prisma migrate deploy
# Reset de la base (dev uniquement)
docker-compose exec towercontrol-dev npx prisma migrate reset
# Studio Prisma (dev)
docker-compose exec towercontrol-dev npx prisma studio
```
## 🔍 Debugging
### Vérifier la santé
```bash
# Health check
curl http://localhost:3006/api/health
curl http://localhost:3005/api/health
# Vérifier les variables d'env
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
# Logs des 100 dernières lignes
docker-compose logs --tail=100 towercontrol
```
## 🚨 Dépannage
### Problèmes courants
**Port déjà utilisé**
```bash
# Trouver le processus qui utilise le port
lsof -i :3006
kill -9 <PID>
```
**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
```
## 📊 Monitoring
### Espace disque
```bash
# Taille du dossier data
du -sh ./data
# Espace libre
df -h .
```
### Performance
```bash
# Stats des conteneurs
docker stats
# Utilisation mémoire
docker-compose exec towercontrol free -h
```
## 🔒 Production
### Recommandations
- Utiliser un reverse proxy (nginx, traefik)
- Configurer HTTPS
- Sauvegarder régulièrement `./data/`
- Monitorer l'espace disque
- 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;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
---
📚 **Voir aussi** : [data/README.md](./data/README.md)

View File

@@ -35,8 +35,8 @@ RUN npm run build
# Production image, copy all the files and run next # Production image, copy all the files and run next
FROM base AS runner FROM base AS runner
# Set timezone to Europe/Paris # Set timezone to Europe/Paris and install sqlite3 for backups
RUN apk add --no-cache tzdata RUN apk add --no-cache tzdata sqlite
RUN ln -snf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone RUN ln -snf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
WORKDIR /app WORKDIR /app
@@ -64,8 +64,8 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
# Create data directory for SQLite # Create data directory for SQLite and backups
RUN mkdir -p /app/data && chown nextjs:nodejs /app/data RUN mkdir -p /app/data/backups && chown -R nextjs:nodejs /app/data
# Set all ENV vars before switching user # Set all ENV vars before switching user
ENV PORT=3000 ENV PORT=3000

541
TODO.md
View File

@@ -1,313 +1,13 @@
# TowerControl v2.0 - Gestionnaire de tâches moderne # TowerControl v2.0 - Gestionnaire de tâches moderne
## ✅ Phase 1: Nettoyage et architecture (TERMINÉ)
### 1.1 Configuration projet Next.js
- [x] Initialiser Next.js avec TypeScript
- [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`
**Architecture finale** : App standalone avec backend propre et API REST moderne
## 🎯 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/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)
- [x] Édition inline du titre des tâches (clic sur titre → input)
- [x] Suppression de tâche (icône discrète + API call)
- [x] Changement de statut par drag & drop (@dnd-kit)
- [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)
- [x] Affichage des tags avec couleurs personnalisées
- [x] Service tags avec CRUD complet (Prisma)
- [x] API routes /api/tags avec validation
- [x] Client HTTP et hook useTags
- [x] Composants UI (TagInput, TagDisplay, TagForm)
- [x] Intégration dans les formulaires (TagInput avec autocomplete)
- [x] Intégration dans les TaskCards (TagDisplay avec couleurs)
- [x] Contexte global pour partager les tags
- [x] Page de gestion des tags (/tags) avec interface complète
- [x] Navigation dans le Header (Kanban ↔ Tags)
- [x] Filtrage par tags (intégration dans Kanban)
- [x] Interface de filtrage complète (recherche, priorités, tags)
- [x] Logique de filtrage temps réel dans le contexte
- [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
- [x] `hooks/useTasks.ts` - Hook pour la gestion des tâches (CRUD complet)
- [x] `hooks/useTags.ts` - Hook pour la gestion des tags
- [x] Drag & drop avec @dnd-kit (intégré directement dans Board.tsx)
- [x] Gestion des erreurs et loading states
- [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é
- [x] Recherche en temps réel dans les tâches
- [x] Interface de filtrage complète (KanbanFilters.tsx)
- [x] Logique de filtrage dans TasksContext
- [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
- [x] Adapter tous les composants UI pour supporter les deux thèmes
- [x] Modifier la palette de couleurs pour le mode clair
- [x] Adapter les composants Kanban (Board, TaskCard, Column)
- [x] Adapter les formulaires et modales
- [x] Adapter la page de gestion des tags
- [x] Sauvegarder la préférence de thème (localStorage)
- [x] Configuration par défaut selon préférence système (prefers-color-scheme)
## 📊 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"
- [x] Checkboxes interactives avec état coché/non-coché
- [x] Liaison optionnelle checkbox ↔ tâche existante
- [x] Cocher une checkbox NE change PAS le statut de la tâche liée
- [x] Navigation par date (daily précédent/suivant)
- [x] Auto-création du daily du jour si inexistant
- [x] UX améliorée : édition au clic, focus persistant, input large
- [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)
- [x] Récupération des tickets assignés à l'utilisateur
- [x] Mapping des statuts Jira vers statuts internes (todo, in_progress, done, etc.)
- [x] Synchronisation unidirectionnelle (Jira → local uniquement)
- [x] Gestion des diffs - ne pas écraser les modifications locales
- [x] Style visuel distinct pour les tâches Jira (bordure spéciale)
- [x] Métadonnées Jira (projet, clé, assignee) dans la base
- [x] Possibilité d'affecter des tags locaux aux tâches Jira
- [x] Interface de configuration dans les paramètres
- [x] Synchronisation manuelle via bouton (pas d'auto-sync)
- [x] Logs de synchronisation pour debug
- [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
- [x] Actions rapides vers les différentes sections
- [x] Affichage des tâches récentes
- [x] Graphiques de productivité (tâches complétées par jour/semaine)
- [x] Indicateurs de performance personnels
- [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)
- [x] Composants de graphiques (CompletionTrend, Velocity, Priority, Weekly)
- [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
- [x] Faire des pages à part entière pour les sous-pages de la page config + SSR
- [x] Afficher dans l'édition de task les todo reliés. Pouvoir en ajouter directement avec une date ou sans.
- [x] Dans les titres de colonnes des swimlanes, je n'ai pas les couleurs des statuts
- [x] Système de sauvegarde automatique base de données
- [x] Sauvegarde automatique configurable (hourly/daily/weekly)
- [x] Configuration complète dans les paramètres avec interface dédiée
- [x] Rotation automatique des sauvegardes (configurable)
- [x] Format de sauvegarde avec timestamp + compression optionnelle
- [x] Interface complète pour visualiser et gérer les sauvegardes
- [x] CLI d'administration pour les opérations avancées
- [x] API REST complète pour la gestion programmatique
- [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] `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
- [x] Intégrer `useTransition` pour les loading states natifs
- [x] Tester la revalidation automatique du cache
- [x] **Nettoyage** : Supprimer props obsolètes dans tous les composants Kanban
- [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)
- [x] Créer `actions/daily.ts` pour les checkboxes
- [x] `toggleCheckbox(checkboxId)` - Toggle état checkbox
- [x] `addCheckboxToDaily(dailyId, content)` - Ajouter checkbox
- [x] `updateCheckboxContent(checkboxId, content)` - Éditer contenu
- [x] `deleteCheckbox(checkboxId)` - Supprimer checkbox
- [x] `reorderCheckboxes(dailyId, checkboxIds)` - Réorganiser
- [x] Modifier les composants Daily pour utiliser server actions
- [x] **Nettoyage** : Supprimer routes `/api/daily/checkboxes` (POST, PATCH, DELETE)
- [x] **Nettoyage** : Simplifier `daily-client.ts` (garder GET uniquement)
- [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] `updateColumnVisibility(columns)` - Visibilité colonnes
- [x] `updateTheme(theme)` - Changement de thème
- [x] Remplacer les hooks par server actions directes
- [x] **Nettoyage** : Supprimer routes `/api/user-preferences/*` (PUT/PATCH)
- [x] **Nettoyage** : Simplifier `user-preferences-client.ts` (GET uniquement)
- [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
- [x] `deleteTag(tagId)` - Suppression tag
- [x] Modifier les formulaires tags pour server actions
- [x] **Nettoyage** : Supprimer routes `/api/tags` (POST, PATCH, DELETE)
- [x] **Nettoyage** : Simplifier `tags-client.ts` (GET et search uniquement)
- [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
-`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)
├── Preferences toggles (theme, filters)
└── Tags CRUD (create, update, delete)
Endpoints complexes → API Routes conservées
├── Fetching initial avec filtres
├── Intégrations externes (Jira, webhooks)
├── Analytics et rapports
└── Future real-time features
```
### 4.4 Avantages attendus
- **🚀 Performance** : Pas de sérialisation HTTP pour actions rapides
- **🔄 Cache intelligent** : `revalidatePath()` automatique
- **📦 Bundle reduction** : Moins de code client HTTP
- **⚡ UX** : `useTransition` loading states natifs
- **🎯 Simplicité** : Moins de boilerplate pour actions simples
## 📊 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
- [x] Sauvegarde de la configuration projet dans les préférences
- [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)
- [x] Métriques de cycle time (temps entre statuts)
- [x] Analyse de la répartition des tâches par assignee
- [x] Détection des goulots d'étranglement (tickets bloqués)
- [x] Historique des sprints et burndown charts
- [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)
- [x] Graphiques de vélocité avec Recharts
- [x] Heatmap d'activité de l'équipe
- [x] Timeline des releases et milestones
- [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
- [x] **Throughput** : Nombre de tickets complétés par période
- [x] **Work in Progress** : Répartition par statut et assignee
- [x] **Quality metrics** : Ratio bugs/features, retours clients
- [x] **Predictability** : Variance entre estimé et réel
- [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
- [x] Détection automatique d'anomalies (alertes)
- [x] Filtrage par composant, version, type de ticket
- [x] Vue détaillée par sprint avec drill-down
- [x] ~~Intégration avec les daily notes (mentions des blockers)~~ (supprimé)
## Autre Todos #2 ## Autre Todos #2
- [ ] Synchro Jira auto en background timé comme pour la synchro de sauvegarde - [x] Synchro Jira auto en background timé comme pour la synchro de sauvegarde
- [ ] refacto des allpreferences : ca devrait eter un contexte dans le layout qui balance serverside dans le hook - [ ] refacto des getallpreferences en frontend : ca devrait eter un contexte dans le layout qui balance serverside dans le hook
- [x] backups : ne backuper que si il y a eu un changement entre le dernier backup et la base actuelle
- [x] refacto des dates avec le utils qui pour l'instant n'est pas utilisé
- [ ] split de certains gros composants.
- [x] Page jira-dashboard : onglets analytics avancés et Qualité et collaboration : les charts sortent des cards; il faut reprendre la UI pour que ce soit consistant.
- [x] Page Daily : les mots aujourd'hui et hier ne fonctionnent dans les titres que si c'est vraiment aujourd'hui :)
## 🔧 Phase 6: Fonctionnalités avancées (Priorité 6) ## 🔧 Phase 6: Fonctionnalités avancées (Priorité 6)
@@ -338,57 +38,202 @@ Endpoints complexes → API Routes conservées
- [ ] Cache côté client - [ ] Cache côté client
- [ ] PWA et mode offline - [ ] PWA et mode offline
## 🛠️ Configuration technique ---
### Stack moderne ## 🚀 Nouvelles idées & fonctionnalités futures
- **Frontend**: Next.js 14, React, TypeScript, Tailwind CSS
- **Backend**: Next.js API Routes, Prisma ORM
- **Database**: SQLite (local) → PostgreSQL (production future)
- **UI**: Composants custom + Shadcn/ui, React Beautiful DnD
- **Charts**: Recharts ou Chart.js pour les analytics
### Architecture respectée ### 🔄 Intégration TFS/Azure DevOps
``` - [ ] **Lecture des Pull Requests TFS** : Synchronisation des PR comme tâches
src/app/ - [ ] PR arrivent en backlog avec filtrage par team project
├── api/tasks/ # API CRUD complète - [ ] Synchronisation aussi riche que Jira (statuts, assignés, commentaires)
├── page.tsx # Page principale - [ ] Filtrage par team project, repository, auteur
└── layout.tsx - [ ] **Architecture plug-and-play pour intégrations**
- [ ] Refactoriser pour interfaces génériques d'intégration
- [ ] Interface `IntegrationService` commune (Jira, TFS, GitHub, etc.)
- [ ] UI générique de configuration des intégrations
- [ ] Système de plugins pour ajouter facilement de nouveaux services
services/ ### 📋 Daily - Gestion des tâches non cochées
├── database.ts # Pool Prisma - [ ] **Page des tâches en attente**
└── tasks.ts # Service tâches standalone - [ ] Liste de toutes les todos non cochées (historique complet)
- [ ] Filtrage par date, catégorie, ancienneté
- [ ] Action "Archiver" pour les tâches ni résolues ni à faire
- [ ] **Nouveau statut "Archivé"**
- [ ] État intermédiaire entre "à faire" et "terminé"
- [ ] Interface pour voir/gérer les tâches archivées
- [ ] Possibilité de désarchiver une tâche
components/ ### 🎯 Jira - Suivi des demandes en attente
├── kanban/ # Board Kanban - [ ] **Page "Jiras en attente"**
├── ui/ # Composants UI de base - [ ] Liste des Jiras créés par moi mais non assignés à mon équipe
└── dashboard/ # Widgets dashboard (futur) - [ ] Suivi des demandes formulées à d'autres équipes
- [ ] Filtrage par projet, équipe cible, ancienneté
- [ ] **Nouveau modèle de données**
- [ ] Table séparée pour les "demandes en attente" (différent des tâches Kanban)
- [ ] Champs spécifiques : demandeur, équipe cible, statut de traitement
- [ ] Notifications quand une demande change de statut
clients/ # Clients HTTP (à créer) ### 🏗️ Architecture & technique
hooks/ # Hooks React (à créer) - [ ] **Système d'intégrations modulaire**
lib/ - [ ] Interface `IntegrationProvider` standardisée
├── types.ts # Types TypeScript - [ ] Configuration dynamique des intégrations
└── config.ts # Config app moderne - [ ] Gestion des credentials par intégration
- [ ] **Modèles de données étendus**
- [ ] `PullRequest` pour TFS/GitHub
- [ ] `PendingRequest` pour les demandes Jira
- [ ] `ArchivedTask` pour les daily archivées
- [ ] **UI générique**
- [ ] Composants réutilisables pour toutes les intégrations
- [ ] Configuration unifiée des filtres et synchronisations
- [ ] Dashboard multi-intégrations
### 📁 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/`
- [x] `mv hooks/ src/hooks/`
- [x] `mv clients/ src/clients/`
- [x] `mv services/ src/services/`
- [x] **Phase 2: Mise à jour tsconfig.json**
```json
"paths": {
"@/*": ["./src/*"]
// Supprimer les alias spécifiques devenus inutiles
}
``` ```
## 🎯 Prochaines étapes immédiates - [x] **Phase 3: Correction des imports**
- [x] Tous les imports `@/services/*` → `@/services/*` (déjà OK)
- [x] Tous les imports `@/lib/*` → `@/lib/*` (déjà OK)
- [x] Tous les imports `@/components/*` → `@/components/*` (déjà OK)
- [x] Tous les imports `@/clients/*` → `@/clients/*` (déjà OK)
- [x] Tous les imports `@/hooks/*` → `@/hooks/*` (déjà OK)
- [x] Vérifier les imports relatifs dans les scripts/
1. **Drag & drop entre colonnes** - react-beautiful-dnd pour changer les statuts - [x] **Phase 4: Mise à jour des règles Cursor**
2. **Gestion avancée des tags** - Couleurs, autocomplete, filtrage - [x] Règle "services" : Mettre à jour les exemples avec `src/services/`
3. **Recherche et filtres** - Filtrage temps réel par titre, tags, statut - [x] Règle "components" : Mettre à jour avec `src/components/`
4. **Dashboard et analytics** - Graphiques de productivité - [x] Règle "clients" : Mettre à jour avec `src/clients/`
- [x] Vérifier tous les liens MDC dans les règles
## ✅ **Fonctionnalités terminées (Phase 2.1-2.3)** - [x] **Phase 5: Tests et validation**
- [x] `npm run build` - Vérifier que le build passe
- [x] `npm run dev` - Vérifier que le dev fonctionne
- [x] `npm run lint` - Vérifier ESLint
- [x] `npx tsc --noEmit` - Vérifier TypeScript
- [x] Tester les fonctionnalités principales
- ✅ Système de design tech dark complet #### **Structure finale attendue**
- ✅ Composants UI de base (Button, Input, Card, Modal, Badge) ```
- ✅ Architecture SSR + hydratation client src/
- ✅ CRUD tâches complet (création, édition, suppression) ├── app/ # Pages Next.js (déjà OK)
- ✅ Création rapide inline (QuickAddTask) ├── actions/ # Server Actions (déjà OK)
- ✅ Édition inline du titre (clic sur titre → input éditable) ├── contexts/ # React Contexts (déjà OK)
- ✅ Drag & drop entre colonnes (@dnd-kit) + optimiste ├── components/ # Composants React (à déplacer)
- ✅ Client HTTP et hooks React ├── lib/ # Utilitaires et types (à déplacer)
- Refactoring Kanban avec nouveaux composants ├── hooks/ # Hooks React (à déplacer)
├── clients/ # Clients HTTP (à déplacer)
└── services/ # Services backend (à déplacer)
```
### 👥 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
#### **Plan de migration**
- [ ] **Phase 1: Authentification**
- [ ] Système de login/mot de passe (NextAuth.js ou custom)
- [ ] Gestion des sessions sécurisées
- [ ] Pages de connexion/inscription/mot de passe oublié
- [ ] Middleware de protection des routes
- [ ] **Phase 2: Modèle de données multi-tenant**
- [ ] Ajouter `userId` à toutes les tables (tasks, daily, tags, preferences, etc.)
- [ ] Migration des données existantes vers un utilisateur par défaut
- [ ] Contraintes de base de données pour l'isolation
- [ ] Index sur `userId` pour les performances
- [ ] **Phase 3: Services et API**
- [ ] Modifier tous les services pour filtrer par `userId`
- [ ] Middleware d'injection automatique du `userId` dans les requêtes
- [ ] Validation que chaque utilisateur ne voit que ses données
- [ ] API d'administration (optionnel)
- [ ] **Phase 4: UI et UX**
- [ ] Header avec profil utilisateur et déconnexion
- [ ] Onboarding pour nouveaux utilisateurs
- [ ] Gestion du profil utilisateur
- [ ] Partage optionnel entre utilisateurs (équipes)
#### **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
- **Migration** : Script de migration des données existantes
### 📱 Interface mobile adaptée (PROJET MAJEUR)
#### **Problème actuel**
- Kanban non adapté aux écrans tactiles petits
- Drag & drop difficile sur mobile
- Interface desktop-first
#### **Solution : Interface mobile dédiée**
- [ ] **Phase 1: Détection et responsive**
- [ ] Détection mobile/desktop (useMediaQuery)
- [ ] Composant de switch automatique d'interface
- [ ] Breakpoints adaptés pour tablettes
- [ ] **Phase 2: Interface mobile pour les tâches**
- [ ] **Vue liste simple** : Remplacement du Kanban
- [ ] Liste verticale avec statuts en badges
- [ ] Actions par swipe (marquer terminé, changer statut)
- [ ] Filtres simplifiés (dropdown au lieu de sidebar)
- [ ] **Actions tactiles**
- [ ] Tap pour voir détails
- [ ] Long press pour menu contextuel
- [ ] Swipe left/right pour actions rapides
- [ ] **Navigation mobile**
- [ ] Bottom navigation bar
- [ ] Sections : Tâches, Daily, Jira, Profil
- [ ] **Phase 3: Daily mobile optimisé**
- [ ] Checkboxes plus grandes (touch-friendly)
- [ ] Ajout rapide par bouton flottant
- [ ] Calendrier mobile avec navigation par swipe
- [ ] **Phase 4: Jira mobile**
- [ ] Métriques simplifiées (cartes au lieu de graphiques complexes)
- [ ] Filtres en modal/drawer
- [ ] Synchronisation en background
#### **Composants mobiles spécifiques**
```typescript
// Exemples de composants à créer
- MobileTaskList.tsx // Remplace le Kanban
- MobileTaskCard.tsx // Version tactile des cartes
- MobileNavigation.tsx // Bottom nav
- SwipeActions.tsx // Actions par swipe
- MobileDailyView.tsx // Daily optimisé mobile
- MobileFilters.tsx // Filtres en modal
```
#### **Considérations UX mobile**
- **Simplicité** : Moins d'options visibles, plus de navigation
- **Tactile** : Boutons plus grands, zones de touch optimisées
- **Performance** : Lazy loading, virtualisation pour longues listes
- **Offline** : Cache local pour usage sans réseau (PWA)
--- ---
*Focus sur l'expérience utilisateur et le design moderne. App standalone prête pour évoluer.* *Focus sur l'expérience utilisateur et le design moderne. App standalone prête pour évoluer vers une plateforme d'intégration complète.*

306
TODO_ARCHIVE.md Normal file
View File

@@ -0,0 +1,306 @@
# TowerControl v2.0 - Gestionnaire de tâches moderne
## ✅ Phase 1: Nettoyage et architecture (TERMINÉ)
### 1.1 Configuration projet Next.js
- [x] Initialiser Next.js avec TypeScript
- [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`
**Architecture finale** : App standalone avec backend propre et API REST moderne
## 🎯 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/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)
- [x] Édition inline du titre des tâches (clic sur titre → input)
- [x] Suppression de tâche (icône discrète + API call)
- [x] Changement de statut par drag & drop (@dnd-kit)
- [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)
- [x] Affichage des tags avec couleurs personnalisées
- [x] Service tags avec CRUD complet (Prisma)
- [x] API routes /api/tags avec validation
- [x] Client HTTP et hook useTags
- [x] Composants UI (TagInput, TagDisplay, TagForm)
- [x] Intégration dans les formulaires (TagInput avec autocomplete)
- [x] Intégration dans les TaskCards (TagDisplay avec couleurs)
- [x] Contexte global pour partager les tags
- [x] Page de gestion des tags (/tags) avec interface complète
- [x] Navigation dans le Header (Kanban ↔ Tags)
- [x] Filtrage par tags (intégration dans Kanban)
- [x] Interface de filtrage complète (recherche, priorités, tags)
- [x] Logique de filtrage temps réel dans le contexte
- [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
- [x] `hooks/useTasks.ts` - Hook pour la gestion des tâches (CRUD complet)
- [x] `hooks/useTags.ts` - Hook pour la gestion des tags
- [x] Drag & drop avec @dnd-kit (intégré directement dans Board.tsx)
- [x] Gestion des erreurs et loading states
- [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é
- [x] Recherche en temps réel dans les tâches
- [x] Interface de filtrage complète (KanbanFilters.tsx)
- [x] Logique de filtrage dans TasksContext
- [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
- [x] Adapter tous les composants UI pour supporter les deux thèmes
- [x] Modifier la palette de couleurs pour le mode clair
- [x] Adapter les composants Kanban (Board, TaskCard, Column)
- [x] Adapter les formulaires et modales
- [x] Adapter la page de gestion des tags
- [x] Sauvegarder la préférence de thème (localStorage)
- [x] Configuration par défaut selon préférence système (prefers-color-scheme)
## 📊 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"
- [x] Checkboxes interactives avec état coché/non-coché
- [x] Liaison optionnelle checkbox ↔ tâche existante
- [x] Cocher une checkbox NE change PAS le statut de la tâche liée
- [x] Navigation par date (daily précédent/suivant)
- [x] Auto-création du daily du jour si inexistant
- [x] UX améliorée : édition au clic, focus persistant, input large
- [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)
- [x] Récupération des tickets assignés à l'utilisateur
- [x] Mapping des statuts Jira vers statuts internes (todo, in_progress, done, etc.)
- [x] Synchronisation unidirectionnelle (Jira → local uniquement)
- [x] Gestion des diffs - ne pas écraser les modifications locales
- [x] Style visuel distinct pour les tâches Jira (bordure spéciale)
- [x] Métadonnées Jira (projet, clé, assignee) dans la base
- [x] Possibilité d'affecter des tags locaux aux tâches Jira
- [x] Interface de configuration dans les paramètres
- [x] Synchronisation manuelle via bouton (pas d'auto-sync)
- [x] Logs de synchronisation pour debug
- [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
- [x] Actions rapides vers les différentes sections
- [x] Affichage des tâches récentes
- [x] Graphiques de productivité (tâches complétées par jour/semaine)
- [x] Indicateurs de performance personnels
- [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)
- [x] Composants de graphiques (CompletionTrend, Velocity, Priority, Weekly)
- [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
- [x] Faire des pages à part entière pour les sous-pages de la page config + SSR
- [x] Afficher dans l'édition de task les todo reliés. Pouvoir en ajouter directement avec une date ou sans.
- [x] Dans les titres de colonnes des swimlanes, je n'ai pas les couleurs des statuts
- [x] Système de sauvegarde automatique base de données
- [x] Sauvegarde automatique configurable (hourly/daily/weekly)
- [x] Configuration complète dans les paramètres avec interface dédiée
- [x] Rotation automatique des sauvegardes (configurable)
- [x] Format de sauvegarde avec timestamp + compression optionnelle
- [x] Interface complète pour visualiser et gérer les sauvegardes
- [x] CLI d'administration pour les opérations avancées
- [x] API REST complète pour la gestion programmatique
- [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] `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
- [x] Intégrer `useTransition` pour les loading states natifs
- [x] Tester la revalidation automatique du cache
- [x] **Nettoyage** : Supprimer props obsolètes dans tous les composants Kanban
- [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)
- [x] Créer `actions/daily.ts` pour les checkboxes
- [x] `toggleCheckbox(checkboxId)` - Toggle état checkbox
- [x] `addCheckboxToDaily(dailyId, content)` - Ajouter checkbox
- [x] `updateCheckboxContent(checkboxId, content)` - Éditer contenu
- [x] `deleteCheckbox(checkboxId)` - Supprimer checkbox
- [x] `reorderCheckboxes(dailyId, checkboxIds)` - Réorganiser
- [x] Modifier les composants Daily pour utiliser server actions
- [x] **Nettoyage** : Supprimer routes `/api/daily/checkboxes` (POST, PATCH, DELETE)
- [x] **Nettoyage** : Simplifier `daily-client.ts` (garder GET uniquement)
- [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] `updateColumnVisibility(columns)` - Visibilité colonnes
- [x] `updateTheme(theme)` - Changement de thème
- [x] Remplacer les hooks par server actions directes
- [x] **Nettoyage** : Supprimer routes `/api/user-preferences/*` (PUT/PATCH)
- [x] **Nettoyage** : Simplifier `user-preferences-client.ts` (GET uniquement)
- [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
- [x] `deleteTag(tagId)` - Suppression tag
- [x] Modifier les formulaires tags pour server actions
- [x] **Nettoyage** : Supprimer routes `/api/tags` (POST, PATCH, DELETE)
- [x] **Nettoyage** : Simplifier `tags-client.ts` (GET et search uniquement)
- [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
-`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)
├── Preferences toggles (theme, filters)
└── Tags CRUD (create, update, delete)
Endpoints complexes → API Routes conservées
├── Fetching initial avec filtres
├── Intégrations externes (Jira, webhooks)
├── Analytics et rapports
└── Future real-time features
```
### 4.4 Avantages attendus
- **🚀 Performance** : Pas de sérialisation HTTP pour actions rapides
- **🔄 Cache intelligent** : `revalidatePath()` automatique
- **📦 Bundle reduction** : Moins de code client HTTP
- **⚡ UX** : `useTransition` loading states natifs
- **🎯 Simplicité** : Moins de boilerplate pour actions simples
## 📊 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
- [x] Sauvegarde de la configuration projet dans les préférences
- [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)
- [x] Métriques de cycle time (temps entre statuts)
- [x] Analyse de la répartition des tâches par assignee
- [x] Détection des goulots d'étranglement (tickets bloqués)
- [x] Historique des sprints et burndown charts
- [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)
- [x] Graphiques de vélocité avec Recharts
- [x] Heatmap d'activité de l'équipe
- [x] Timeline des releases et milestones
- [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
- [x] **Throughput** : Nombre de tickets complétés par période
- [x] **Work in Progress** : Répartition par statut et assignee
- [x] **Quality metrics** : Ratio bugs/features, retours clients
- [x] **Predictability** : Variance entre estimé et réel
- [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
- [x] Détection automatique d'anomalies (alertes)
- [x] Filtrage par composant, version, type de ticket
- [x] Vue détaillée par sprint avec drill-down
- [x] ~~Intégration avec les daily notes (mentions des blockers)~~ (supprimé)

View File

@@ -1,36 +0,0 @@
/**
* Client pour l'API Jira
*/
import { HttpClient } from './base/http-client';
import { JiraSyncResult } from '@/services/jira';
export interface JiraConnectionStatus {
connected: boolean;
message: string;
details?: string;
}
export class JiraClient extends HttpClient {
constructor() {
super('/api/jira');
}
/**
* Teste la connexion à Jira
*/
async testConnection(): Promise<JiraConnectionStatus> {
return this.get<JiraConnectionStatus>('/sync');
}
/**
* Lance la synchronisation manuelle des tickets Jira
*/
async syncTasks(): Promise<JiraSyncResult> {
const response = await this.post<{ data: JiraSyncResult }>('/sync');
return response.data;
}
}
// Instance singleton
export const jiraClient = new JiraClient();

View File

@@ -1,220 +0,0 @@
'use client';
import { UserPreferences } from '@/lib/types';
import { Header } from '@/components/ui/Header';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext';
import Link from 'next/link';
interface SettingsIndexPageClientProps {
initialPreferences: UserPreferences;
}
export function SettingsIndexPageClient({ initialPreferences }: SettingsIndexPageClientProps) {
const settingsPages = [
{
href: '/settings/general',
icon: '⚙️',
title: 'Paramètres généraux',
description: 'Interface, thème, préférences d\'affichage',
status: 'En développement'
},
{
href: '/settings/integrations',
icon: '🔌',
title: 'Intégrations',
description: 'Jira, GitHub, Slack et autres services externes',
status: 'Fonctionnel'
},
{
href: '/settings/advanced',
icon: '🛠️',
title: 'Paramètres avancés',
description: 'Sauvegarde, logs, debug et maintenance',
status: 'Prochainement'
}
];
return (
<UserPreferencesProvider initialPreferences={initialPreferences}>
<div className="min-h-screen bg-[var(--background)]">
<Header
title="TowerControl"
subtitle="Configuration & Paramètres"
/>
<div className="container mx-auto px-4 py-4">
<div className="max-w-4xl mx-auto">
{/* Page Header */}
<div className="mb-8">
<h1 className="text-3xl font-mono font-bold text-[var(--foreground)] mb-3">
Paramètres
</h1>
<p className="text-[var(--muted-foreground)] text-lg">
Configuration de TowerControl et de ses intégrations
</p>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<span className="text-2xl">🎨</span>
<div>
<p className="text-sm text-[var(--muted-foreground)]">Thème actuel</p>
<p className="font-medium capitalize">{initialPreferences.viewPreferences.theme}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<span className="text-2xl">🔌</span>
<div>
<p className="text-sm text-[var(--muted-foreground)]">Jira</p>
<p className="font-medium">
{initialPreferences.jiraConfig.enabled ? 'Configuré' : 'Non configuré'}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<span className="text-2xl">📏</span>
<div>
<p className="text-sm text-[var(--muted-foreground)]">Taille police</p>
<p className="font-medium capitalize">{initialPreferences.viewPreferences.fontSize}</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Settings Sections */}
<div className="space-y-4">
<h2 className="text-xl font-semibold text-[var(--foreground)] mb-4">
Sections de configuration
</h2>
<div className="grid grid-cols-1 md:grid-cols-1 gap-4">
{settingsPages.map((page) => (
<Link key={page.href} href={page.href}>
<Card className="transition-all hover:shadow-md hover:border-[var(--primary)]/30 cursor-pointer">
<CardContent className="p-6">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
<span className="text-3xl">{page.icon}</span>
<div className="flex-1">
<h3 className="text-lg font-semibold text-[var(--foreground)] mb-1">
{page.title}
</h3>
<p className="text-[var(--muted-foreground)] mb-2">
{page.description}
</p>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${
page.status === 'Fonctionnel'
? 'bg-[var(--success)]/20 text-[var(--success)]'
: page.status === 'En développement'
? 'bg-[var(--warning)]/20 text-[var(--warning)]'
: 'bg-[var(--muted)]/20 text-[var(--muted-foreground)]'
}`}>
{page.status}
</span>
</div>
</div>
</div>
<svg
className="w-5 h-5 text-[var(--muted-foreground)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
{/* Quick Actions */}
<div className="mt-8">
<h2 className="text-xl font-semibold text-[var(--foreground)] mb-4">
Actions rapides
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium mb-1">Sauvegarde manuelle</h3>
<p className="text-sm text-[var(--muted-foreground)]">
Créer une sauvegarde des données
</p>
</div>
<button className="px-3 py-1.5 bg-[var(--primary)] text-[var(--primary-foreground)] rounded text-sm">
Sauvegarder
</button>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium mb-1">Test Jira</h3>
<p className="text-sm text-[var(--muted-foreground)]">
Tester la connexion Jira
</p>
</div>
<button
className="px-3 py-1.5 bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] rounded text-sm"
disabled={!initialPreferences.jiraConfig.enabled}
>
Tester
</button>
</div>
</CardContent>
</Card>
</div>
</div>
{/* System Info */}
<Card className="mt-8">
<CardHeader>
<h2 className="text-lg font-semibold"> Informations système</h2>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<p className="text-[var(--muted-foreground)]">Version</p>
<p className="font-medium">TowerControl v1.0.0</p>
</div>
<div>
<p className="text-[var(--muted-foreground)]">Dernière maj</p>
<p className="font-medium">Il y a 2 jours</p>
</div>
<div>
<p className="text-[var(--muted-foreground)]">Env</p>
<p className="font-medium">Development</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</UserPreferencesProvider>
);
}

102
data/README.md Normal file
View File

@@ -0,0 +1,102 @@
# 📁 Dossier Data - TowerControl
Ce dossier contient toutes les données persistantes de l'application TowerControl.
## 📋 Structure
```
data/
├── README.md # Ce fichier
├── prod.db # Base de données production (Docker)
├── dev.db # Base de données développement (Docker)
└── backups/ # Sauvegardes automatiques et manuelles
├── towercontrol_2025-01-15T10-30-00-000Z.db.gz
├── towercontrol_2025-01-15T11-30-00-000Z.db.gz
└── ...
```
## 🎯 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
## 🔧 Configuration
Les chemins sont configurés via les variables d'environnement :
```bash
# Base de données
DATABASE_URL="file:../data/prod.db"
# Chemin de la base pour les backups
BACKUP_DATABASE_PATH="./data/prod.db"
# Dossier de stockage des sauvegardes
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)
- **Fréquence** : Configurable (défaut: horaire)
## 🚀 Commandes utiles
```bash
# Créer une sauvegarde manuelle
npm run backup:create
# Lister les sauvegardes
npm run backup:list
# Voir la configuration
npm run backup:config
# Restaurer une sauvegarde (dev uniquement)
npm run backup:restore filename.db.gz
```
## ⚠️ Important
- **Ne pas modifier** les fichiers `.db` directement
- **Ne pas supprimer** ce dossier en production
- **Sauvegarder régulièrement** le contenu de ce dossier
- **Vérifier l'espace disque** disponible pour les sauvegardes
## 🔒 Sécurité
- Ce dossier est ignoré par Git (`.gitignore`)
- Contient des données sensibles en production
- Accès restreint recommandé sur le serveur
- Chiffrement recommandé pour les sauvegardes externes
## 📊 Monitoring
Pour surveiller l'espace disque :
```bash
# Taille du dossier data
du -sh data/
# Taille des sauvegardes
du -sh data/backups/
# Nombre de sauvegardes
ls -1 data/backups/ | wc -l
```

0
dev.db
View File

View File

@@ -1,31 +1,27 @@
version: '3.8'
services: services:
towercontrol: towercontrol:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
target: runner
ports: ports:
- "3006:3000" - "3006:3000"
environment: environment:
- NODE_ENV=production NODE_ENV: production
- DATABASE_URL=file:/app/data/prod.db DATABASE_URL: "file:../data/dev.db" # Prisma
- TZ=Europe/Paris BACKUP_DATABASE_PATH: "./data/dev.db" # Base de données à sauvegarder
BACKUP_STORAGE_PATH: "./data/backups" # Dossier des sauvegardes
TZ: Europe/Paris
volumes: volumes:
# Volume persistant pour la base SQLite - ./data:/app/data # Dossier local data/ vers /app/data
- sqlite_data:/app/data
# Monter ta DB locale (décommente pour utiliser tes données locales)
- ./prisma/dev.db:/app/data/prod.db
- ./backups:/app/backups
restart: unless-stopped restart: unless-stopped
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health || exit 1"] test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 40s
# Service de développement (optionnel)
towercontrol-dev: towercontrol-dev:
build: build:
context: . context: .
@@ -34,20 +30,29 @@ services:
ports: ports:
- "3005:3000" - "3005:3000"
environment: environment:
- NODE_ENV=development NODE_ENV: development
- DATABASE_URL=file:/app/data/dev.db 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
volumes: volumes:
- .:/app - .:/app # code en live
- /app/node_modules - /app/node_modules # vol anonyme pour ne pas écraser ceux du conteneur
- /app/.next - /app/.next
- sqlite_data_dev:/app/data - ./data:/app/data # Dossier local data/ vers /app/data
command: sh -c "npm install && npx prisma generate && npx prisma migrate deploy && npm run dev" command: >
sh -c "npm install &&
npx prisma generate &&
npx prisma migrate deploy &&
npm run dev"
profiles: profiles:
- dev - dev
volumes: # 📁 Structure des données :
sqlite_data: # ./data/ -> /app/data (bind mount)
driver: local # ├── prod.db -> Base de données production
sqlite_data_dev: # ├── dev.db -> Base de données développement
driver: local # └── backups/ -> Sauvegardes automatiques
#
# 🔧 Configuration via .env.docker
# 📚 Documentation : ./data/README.md

View File

@@ -1,5 +1,13 @@
# Base de données (requis) # Base de données (requis)
DATABASE_URL="file:./dev.db" DATABASE_URL="file:../data/dev.db"
# Chemin de la base de données pour les backups (optionnel)
# Si non défini, utilise DATABASE_URL ou le chemin par défaut
BACKUP_DATABASE_PATH="./data/dev.db"
# Dossier de stockage des sauvegardes (optionnel)
# Par défaut: ./backups en local, ./data/backups en production
BACKUP_STORAGE_PATH="./backups"
# Intégration Jira (optionnel) # Intégration Jira (optionnel)
JIRA_BASE_URL="" # https://votre-domaine.atlassian.net JIRA_BASE_URL="" # https://votre-domaine.atlassian.net

View File

@@ -101,6 +101,10 @@ model UserPreferences {
// Configuration Jira (JSON) // Configuration Jira (JSON)
jiraConfig Json? jiraConfig Json?
// Configuration du scheduler Jira
jiraAutoSync Boolean @default(false)
jiraSyncInterval String @default("daily") // hourly, daily, weekly
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

@@ -4,8 +4,9 @@
* Usage: tsx scripts/backup-manager.ts [command] [options] * Usage: tsx scripts/backup-manager.ts [command] [options]
*/ */
import { backupService, BackupConfig } from '../services/backup'; import { backupService, BackupConfig } from '../src/services/backup';
import { backupScheduler } from '../services/backup-scheduler'; import { backupScheduler } from '../src/services/backup-scheduler';
import { formatDateForDisplay } from '../src/lib/date-utils';
interface CliOptions { interface CliOptions {
command: string; command: string;
@@ -21,7 +22,7 @@ class BackupManagerCLI {
🔧 TowerControl Backup Manager 🔧 TowerControl Backup Manager
COMMANDES: COMMANDES:
create Créer une nouvelle sauvegarde create [--force] Créer une nouvelle sauvegarde (--force pour ignorer la détection de changements)
list Lister toutes les sauvegardes list Lister toutes les sauvegardes
delete <filename> Supprimer une sauvegarde delete <filename> Supprimer une sauvegarde
restore <filename> Restaurer une sauvegarde restore <filename> Restaurer une sauvegarde
@@ -35,6 +36,7 @@ COMMANDES:
EXEMPLES: EXEMPLES:
tsx backup-manager.ts create tsx backup-manager.ts create
tsx backup-manager.ts create --force
tsx backup-manager.ts list tsx backup-manager.ts list
tsx backup-manager.ts delete towercontrol_2025-01-15T10-30-00-000Z.db tsx backup-manager.ts delete towercontrol_2025-01-15T10-30-00-000Z.db
tsx backup-manager.ts restore towercontrol_2025-01-15T10-30-00-000Z.db.gz tsx backup-manager.ts restore towercontrol_2025-01-15T10-30-00-000Z.db.gz
@@ -91,7 +93,7 @@ OPTIONS:
} }
private formatDate(date: Date): string { private formatDate(date: Date): string {
return new Date(date).toLocaleString('fr-FR'); return formatDateForDisplay(date, 'DISPLAY_LONG');
} }
async run(args: string[]): Promise<void> { async run(args: string[]): Promise<void> {
@@ -105,7 +107,7 @@ OPTIONS:
try { try {
switch (options.command) { switch (options.command) {
case 'create': case 'create':
await this.createBackup(); await this.createBackup(options.force || false);
break; break;
case 'list': case 'list':
@@ -167,13 +169,22 @@ OPTIONS:
} }
} }
private async createBackup(): Promise<void> { private async createBackup(force: boolean = false): Promise<void> {
console.log('🔄 Création d\'une sauvegarde...'); console.log('🔄 Création d\'une sauvegarde...');
const result = await backupService.createBackup('manual'); 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');
return;
}
if (result.status === 'success') { if (result.status === 'success') {
console.log(`✅ Sauvegarde créée: ${result.filename}`); console.log(`✅ Sauvegarde créée: ${result.filename}`);
console.log(` Taille: ${this.formatFileSize(result.size)}`); console.log(` Taille: ${this.formatFileSize(result.size)}`);
if (result.databaseHash) {
console.log(` Hash: ${result.databaseHash.substring(0, 12)}...`);
}
} else { } else {
console.error(`❌ Échec de la sauvegarde: ${result.error}`); console.error(`❌ Échec de la sauvegarde: ${result.error}`);
process.exit(1); process.exit(1);

View File

@@ -1,4 +1,4 @@
import { prisma } from '../services/database'; import { prisma } from '../src/services/database';
/** /**
* Script pour reset la base de données et supprimer les anciennes données * Script pour reset la base de données et supprimer les anciennes données

View File

@@ -1,5 +1,5 @@
import { tasksService } from '../services/tasks'; import { tasksService } from '../src/services/tasks';
import { TaskStatus, TaskPriority } from '../lib/types'; import { TaskStatus, TaskPriority } from '../src/lib/types';
/** /**
* Script pour ajouter des données de test avec tags et variété * Script pour ajouter des données de test avec tags et variété

View File

@@ -1,4 +1,4 @@
import { tagsService } from '../services/tags'; import { tagsService } from '../src/services/tags';
async function seedTags() { async function seedTags() {
console.log('🏷️ Création des tags de test...'); console.log('🏷️ Création des tags de test...');

View File

@@ -1,415 +0,0 @@
import { promises as fs } from 'fs';
import { exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';
import { prisma } from './database';
import { userPreferencesService } from './user-preferences';
const execAsync = promisify(exec);
export interface BackupConfig {
enabled: boolean;
interval: 'hourly' | 'daily' | 'weekly';
maxBackups: number;
backupPath: string;
includeUploads?: boolean;
compression?: boolean;
}
export interface BackupInfo {
id: string;
filename: string;
size: number;
createdAt: Date;
type: 'manual' | 'automatic';
status: 'success' | 'failed' | 'in_progress';
error?: string;
}
export class BackupService {
private defaultConfig: BackupConfig = {
enabled: true,
interval: 'hourly',
maxBackups: 5,
backupPath: path.join(process.cwd(), 'backups'),
includeUploads: true,
compression: true,
};
private config: BackupConfig;
constructor(config?: Partial<BackupConfig>) {
this.config = { ...this.defaultConfig, ...config };
// Charger la config depuis la DB de manière asynchrone
this.loadConfigFromDB().catch(() => {
// Ignorer les erreurs de chargement initial
});
}
/**
* Charge la configuration depuis la base de données
*/
private async loadConfigFromDB(): Promise<void> {
try {
const preferences = await userPreferencesService.getAllPreferences();
if (preferences.viewPreferences && typeof preferences.viewPreferences === 'object') {
const backupConfig = (preferences.viewPreferences as Record<string, unknown>).backupConfig;
if (backupConfig) {
this.config = { ...this.defaultConfig, ...backupConfig };
}
}
} catch (error) {
console.warn('Could not load backup config from DB, using defaults:', error);
}
}
/**
* Sauvegarde la configuration dans la base de données
*/
private async saveConfigToDB(): Promise<void> {
try {
// Pour l'instant, on stocke la config backup en tant que JSON dans viewPreferences
// TODO: Ajouter un champ dédié dans le schéma pour la config backup
await prisma.userPreferences.upsert({
where: { id: 'default' },
update: {
viewPreferences: JSON.parse(JSON.stringify({
...(await userPreferencesService.getViewPreferences()),
backupConfig: this.config
}))
},
create: {
id: 'default',
kanbanFilters: {},
viewPreferences: JSON.parse(JSON.stringify({ backupConfig: this.config })),
columnVisibility: {},
jiraConfig: {}
}
});
} catch (error) {
console.error('Failed to save backup config to DB:', error);
}
}
/**
* Crée une sauvegarde complète de la base de données
*/
async createBackup(type: 'manual' | 'automatic' = 'manual'): Promise<BackupInfo> {
const backupId = `backup_${Date.now()}`;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `towercontrol_${timestamp}.db`;
const backupPath = path.join(this.config.backupPath, filename);
console.log(`🔄 Starting ${type} backup: ${filename}`);
try {
// Créer le dossier de backup si nécessaire
await this.ensureBackupDirectory();
// Vérifier l'état de la base de données
await this.verifyDatabaseHealth();
// Créer la sauvegarde SQLite
await this.createSQLiteBackup(backupPath);
// Compresser si activé
let finalPath = backupPath;
if (this.config.compression) {
finalPath = await this.compressBackup(backupPath);
await fs.unlink(backupPath); // Supprimer le fichier non compressé
}
// Obtenir les stats du fichier
const stats = await fs.stat(finalPath);
const backupInfo: BackupInfo = {
id: backupId,
filename: path.basename(finalPath),
size: stats.size,
createdAt: new Date(),
type,
status: 'success',
};
// Nettoyer les anciennes sauvegardes
await this.cleanOldBackups();
console.log(`✅ Backup completed: ${backupInfo.filename} (${this.formatFileSize(backupInfo.size)})`);
return backupInfo;
} catch (error) {
console.error(`❌ Backup failed:`, error);
return {
id: backupId,
filename,
size: 0,
createdAt: new Date(),
type,
status: 'failed',
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Crée une sauvegarde SQLite en utilisant la commande .backup
*/
private async createSQLiteBackup(backupPath: string): Promise<void> {
const dbPath = path.resolve(process.env.DATABASE_URL?.replace('file:', '') || './prisma/dev.db');
// Méthode 1: Utiliser sqlite3 CLI (plus fiable)
try {
const command = `sqlite3 "${dbPath}" ".backup '${backupPath}'"`;
await execAsync(command);
console.log(`✅ SQLite backup created using CLI: ${backupPath}`);
return;
} catch (cliError) {
console.warn(`⚠️ SQLite CLI backup failed, trying copy method:`, cliError);
}
// Méthode 2: Copie simple du fichier (fallback)
try {
await fs.copyFile(dbPath, backupPath);
console.log(`✅ SQLite backup created using file copy: ${backupPath}`);
} catch (copyError) {
throw new Error(`Failed to create SQLite backup: ${copyError}`);
}
}
/**
* Compresse une sauvegarde
*/
private async compressBackup(filePath: string): Promise<string> {
const compressedPath = `${filePath}.gz`;
try {
const command = `gzip -c "${filePath}" > "${compressedPath}"`;
await execAsync(command);
console.log(`✅ Backup compressed: ${compressedPath}`);
return compressedPath;
} catch (error) {
console.warn(`⚠️ Compression failed, keeping uncompressed backup:`, error);
return filePath;
}
}
/**
* Restaure une sauvegarde
*/
async restoreBackup(filename: string): Promise<void> {
const backupPath = path.join(this.config.backupPath, filename);
const dbPath = path.resolve(process.env.DATABASE_URL?.replace('file:', '') || './prisma/dev.db');
console.log(`🔄 Restore paths - backup: ${backupPath}, target: ${dbPath}`);
console.log(`🔄 Starting restore from: ${filename}`);
try {
// Vérifier que le fichier de sauvegarde existe
await fs.access(backupPath);
// Décompresser si nécessaire
let sourceFile = backupPath;
if (filename.endsWith('.gz')) {
const tempFile = backupPath.replace('.gz', '');
console.log(`🔄 Decompressing ${backupPath} to ${tempFile}`);
try {
await execAsync(`gunzip -c "${backupPath}" > "${tempFile}"`);
console.log(`✅ Decompression successful`);
// Vérifier que le fichier décompressé existe
await fs.access(tempFile);
console.log(`✅ Decompressed file exists: ${tempFile}`);
sourceFile = tempFile;
} catch (decompError) {
console.error(`❌ Decompression failed:`, decompError);
throw decompError;
}
}
// Créer une sauvegarde de la base actuelle avant restauration
const currentBackup = await this.createBackup('manual');
console.log(`✅ Current database backed up as: ${currentBackup.filename}`);
// Fermer toutes les connexions
await prisma.$disconnect();
// Vérifier que le fichier source existe
await fs.access(sourceFile);
console.log(`✅ Source file verified: ${sourceFile}`);
// Remplacer la base de données
console.log(`🔄 Copying ${sourceFile} to ${dbPath}`);
await fs.copyFile(sourceFile, dbPath);
console.log(`✅ Database file copied successfully`);
// Nettoyer le fichier temporaire si décompressé
if (sourceFile !== backupPath) {
await fs.unlink(sourceFile);
}
// Reconnecter à la base
await prisma.$connect();
// Vérifier l'intégrité après restauration
await this.verifyDatabaseHealth();
console.log(`✅ Database restored from: ${filename}`);
} catch (error) {
console.error(`❌ Restore failed:`, error);
throw new Error(`Failed to restore backup: ${error}`);
}
}
/**
* Liste toutes les sauvegardes disponibles
*/
async listBackups(): Promise<BackupInfo[]> {
try {
await this.ensureBackupDirectory();
const files = await fs.readdir(this.config.backupPath);
const backups: BackupInfo[] = [];
for (const file of files) {
if (file.startsWith('towercontrol_') && (file.endsWith('.db') || file.endsWith('.db.gz'))) {
const filePath = path.join(this.config.backupPath, file);
const stats = await fs.stat(filePath);
// Extraire la date du nom de fichier
const dateMatch = file.match(/towercontrol_(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)/);
let createdAt = stats.birthtime;
if (dateMatch) {
// Convertir le format de fichier vers ISO string valide
// Format: 2025-09-18T14-12-05-737Z -> 2025-09-18T14:12:05.737Z
const isoString = dateMatch[1]
.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/, 'T$1:$2:$3.$4Z');
createdAt = new Date(isoString);
}
backups.push({
id: file,
filename: file,
size: stats.size,
createdAt,
type: 'automatic', // On ne peut pas déterminer le type depuis le nom
status: 'success',
});
}
}
return backups.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
} catch (error) {
console.error('Error listing backups:', error);
return [];
}
}
/**
* Supprime une sauvegarde
*/
async deleteBackup(filename: string): Promise<void> {
const backupPath = path.join(this.config.backupPath, filename);
try {
await fs.unlink(backupPath);
console.log(`✅ Backup deleted: ${filename}`);
} catch (error) {
console.error(`❌ Failed to delete backup ${filename}:`, error);
throw error;
}
}
/**
* Vérifie l'intégrité de la base de données
*/
async verifyDatabaseHealth(): Promise<void> {
try {
// Test de connexion simple
await prisma.$queryRaw`SELECT 1`;
// Vérification de l'intégrité SQLite
const result = await prisma.$queryRaw<{integrity_check: string}[]>`PRAGMA integrity_check`;
if (result.length > 0 && result[0].integrity_check !== 'ok') {
throw new Error(`Database integrity check failed: ${result[0].integrity_check}`);
}
console.log('✅ Database health check passed');
} catch (error) {
console.error('❌ Database health check failed:', error);
throw error;
}
}
/**
* Nettoie les anciennes sauvegardes selon la configuration
*/
private async cleanOldBackups(): Promise<void> {
try {
const backups = await this.listBackups();
if (backups.length > this.config.maxBackups) {
const toDelete = backups.slice(this.config.maxBackups);
for (const backup of toDelete) {
await this.deleteBackup(backup.filename);
}
console.log(`🧹 Cleaned ${toDelete.length} old backups`);
}
} catch (error) {
console.error('Error cleaning old backups:', error);
}
}
/**
* S'assure que le dossier de backup existe
*/
private async ensureBackupDirectory(): Promise<void> {
try {
await fs.access(this.config.backupPath);
} catch {
await fs.mkdir(this.config.backupPath, { recursive: true });
console.log(`📁 Created backup directory: ${this.config.backupPath}`);
}
}
/**
* Formate la taille de fichier
*/
private formatFileSize(bytes: number): string {
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]}`;
}
/**
* Met à jour la configuration
*/
async updateConfig(newConfig: Partial<BackupConfig>): Promise<void> {
this.config = { ...this.config, ...newConfig };
await this.saveConfigToDB();
}
/**
* Obtient la configuration actuelle
*/
getConfig(): BackupConfig {
return { ...this.config };
}
}
// Instance singleton
export const backupService = new BackupService();

View File

@@ -3,6 +3,7 @@
import { dailyService } from '@/services/daily'; import { dailyService } from '@/services/daily';
import { UpdateDailyCheckboxData, DailyCheckbox, CreateDailyCheckboxData } from '@/lib/types'; import { UpdateDailyCheckboxData, DailyCheckbox, CreateDailyCheckboxData } from '@/lib/types';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { getToday, getPreviousWorkday, parseDate, normalizeDate } from '@/lib/date-utils';
/** /**
* Toggle l'état d'une checkbox * Toggle l'état d'une checkbox
@@ -19,7 +20,7 @@ export async function toggleCheckbox(checkboxId: string): Promise<{
// (le front-end gère déjà l'état optimiste) // (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 // Récupérer toutes les checkboxes d'aujourd'hui et hier pour trouver celle à toggle
const today = new Date(); const today = getToday();
const dailyView = await dailyService.getDailyView(today); const dailyView = await dailyService.getDailyView(today);
let checkbox = dailyView.today.find(cb => cb.id === checkboxId); let checkbox = dailyView.today.find(cb => cb.id === checkboxId);
@@ -57,7 +58,7 @@ export async function addCheckboxToDaily(dailyId: string, content: string, taskI
}> { }> {
try { try {
// Le dailyId correspond à la date au format YYYY-MM-DD // Le dailyId correspond à la date au format YYYY-MM-DD
const date = new Date(dailyId); const date = parseDate(dailyId);
const newCheckbox = await dailyService.addCheckbox({ const newCheckbox = await dailyService.addCheckbox({
date, date,
@@ -86,7 +87,7 @@ export async function addTodayCheckbox(content: string, type?: 'task' | 'meeting
}> { }> {
try { try {
const newCheckbox = await dailyService.addCheckbox({ const newCheckbox = await dailyService.addCheckbox({
date: new Date(), date: getToday(),
text: content, text: content,
type: type || 'task', type: type || 'task',
taskId taskId
@@ -112,8 +113,7 @@ export async function addYesterdayCheckbox(content: string, type?: 'task' | 'mee
error?: string; error?: string;
}> { }> {
try { try {
const yesterday = new Date(); const yesterday = getPreviousWorkday(getToday());
yesterday.setDate(yesterday.getDate() - 1);
const newCheckbox = await dailyService.addCheckbox({ const newCheckbox = await dailyService.addCheckbox({
date: yesterday, date: yesterday,
@@ -209,8 +209,7 @@ export async function addTodoToTask(taskId: string, text: string, date?: Date):
error?: string; error?: string;
}> { }> {
try { try {
const targetDate = date || new Date(); const targetDate = normalizeDate(date || getToday());
targetDate.setHours(0, 0, 0, 0);
const checkboxData: CreateDailyCheckboxData = { const checkboxData: CreateDailyCheckboxData = {
date: targetDate, date: targetDate,
@@ -243,7 +242,7 @@ export async function reorderCheckboxes(dailyId: string, checkboxIds: string[]):
}> { }> {
try { try {
// Le dailyId correspond à la date au format YYYY-MM-DD // Le dailyId correspond à la date au format YYYY-MM-DD
const date = new Date(dailyId); const date = parseDate(dailyId);
await dailyService.reorderCheckboxes(date, checkboxIds); await dailyService.reorderCheckboxes(date, checkboxIds);

View File

@@ -1,6 +1,7 @@
'use server'; 'use server';
import { getJiraAnalytics } from './jira-analytics'; import { getJiraAnalytics } from './jira-analytics';
import { formatDateForDisplay, getToday } from '@/lib/date-utils';
export type ExportFormat = 'csv' | 'json'; export type ExportFormat = 'csv' | 'json';
@@ -103,7 +104,7 @@ export async function exportJiraAnalytics(format: ExportFormat = 'csv'): Promise
} }
const analytics = analyticsResult.data; const analytics = analyticsResult.data;
const timestamp = new Date().toISOString().slice(0, 16).replace(/:/g, '-'); const timestamp = getToday().toISOString().slice(0, 16).replace(/:/g, '-');
const projectKey = analytics.project.key; const projectKey = analytics.project.key;
if (format === 'json') { if (format === 'json') {
@@ -142,7 +143,7 @@ function generateCSV(analytics: JiraAnalytics): string {
// Header du rapport // Header du rapport
lines.push('# Rapport Analytics Jira'); lines.push('# Rapport Analytics Jira');
lines.push(`# Projet: ${analytics.project.name} (${analytics.project.key})`); lines.push(`# Projet: ${analytics.project.name} (${analytics.project.key})`);
lines.push(`# Généré le: ${new Date().toLocaleString('fr-FR')}`); lines.push(`# Généré le: ${formatDateForDisplay(getToday(), 'DISPLAY_LONG')}`);
lines.push(`# Total tickets: ${analytics.project.totalIssues}`); lines.push(`# Total tickets: ${analytics.project.totalIssues}`);
lines.push(''); lines.push('');

View File

@@ -4,6 +4,7 @@ import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/jira-analy
import { userPreferencesService } from '@/services/user-preferences'; import { userPreferencesService } from '@/services/user-preferences';
import { SprintDetails } from '@/components/jira/SprintDetailModal'; import { SprintDetails } from '@/components/jira/SprintDetailModal';
import { JiraTask, AssigneeDistribution, StatusDistribution, SprintVelocity } from '@/lib/types'; import { JiraTask, AssigneeDistribution, StatusDistribution, SprintVelocity } from '@/lib/types';
import { parseDate } from '@/lib/date-utils';
export interface SprintDetailsResult { export interface SprintDetailsResult {
success: boolean; success: boolean;
@@ -48,11 +49,11 @@ export async function getSprintDetails(sprintName: string): Promise<SprintDetail
// Filtrer les issues pour ce sprint spécifique // 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 // 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 // Pour simplifier, on prend les issues dans la période du sprint
const sprintStart = new Date(sprint.startDate); const sprintStart = parseDate(sprint.startDate);
const sprintEnd = new Date(sprint.endDate); const sprintEnd = parseDate(sprint.endDate);
const sprintIssues = allIssues.filter(issue => { const sprintIssues = allIssues.filter(issue => {
const issueDate = new Date(issue.created); const issueDate = parseDate(issue.created);
return issueDate >= sprintStart && issueDate <= sprintEnd; return issueDate >= sprintStart && issueDate <= sprintEnd;
}); });
@@ -116,8 +117,8 @@ function calculateSprintMetrics(issues: JiraTask[], sprint: SprintVelocity) {
let averageCycleTime = 0; let averageCycleTime = 0;
if (completedIssuesWithDates.length > 0) { if (completedIssuesWithDates.length > 0) {
const totalCycleTime = completedIssuesWithDates.reduce((total, issue) => { const totalCycleTime = completedIssuesWithDates.reduce((total, issue) => {
const created = new Date(issue.created); const created = parseDate(issue.created);
const updated = new Date(issue.updated); 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; return total + cycleTime;
}, 0); }, 0);

View File

@@ -1,6 +1,7 @@
'use server'; 'use server';
import { MetricsService, WeeklyMetricsOverview, VelocityTrend } from '@/services/metrics'; import { MetricsService, WeeklyMetricsOverview, VelocityTrend } from '@/services/metrics';
import { getToday } from '@/lib/date-utils';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
/** /**
@@ -12,7 +13,7 @@ export async function getWeeklyMetrics(date?: Date): Promise<{
error?: string; error?: string;
}> { }> {
try { try {
const targetDate = date || new Date(); const targetDate = date || getToday();
const metrics = await MetricsService.getWeeklyMetrics(targetDate); const metrics = await MetricsService.getWeeklyMetrics(targetDate);
return { return {

View File

@@ -0,0 +1,16 @@
'use server';
import { SystemInfoService } from '@/services/system-info';
export async function getSystemInfo() {
try {
const systemInfo = await SystemInfoService.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'
};
}
}

View File

@@ -0,0 +1,94 @@
import { NextRequest, NextResponse } from 'next/server';
import { backupService } from '@/services/backup';
interface RouteParams {
params: Promise<{
filename: string;
}>;
}
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'))) {
return NextResponse.json(
{ success: false, error: 'Invalid backup filename' },
{ status: 400 }
);
}
await backupService.deleteBackup(filename);
return NextResponse.json({
success: true,
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'
},
{ status: 500 }
);
}
}
export async function POST(
request: NextRequest,
{ params }: RouteParams
) {
try {
const { filename } = await params;
const body = await request.json();
const { action } = body;
if (action === 'restore') {
// Vérification de sécurité
if (!filename.startsWith('towercontrol_') ||
(!filename.endsWith('.db') && !filename.endsWith('.db.gz'))) {
return NextResponse.json(
{ success: false, error: 'Invalid backup filename' },
{ status: 400 }
);
}
// Protection environnement de production
if (process.env.NODE_ENV === 'production') {
return NextResponse.json(
{ 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}`
});
}
return NextResponse.json(
{ success: false, error: 'Invalid action' },
{ status: 400 }
);
} catch (error) {
console.error('Error in backup operation:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Operation failed'
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,126 @@
import { NextRequest, NextResponse } from 'next/server';
import { backupService } from '@/services/backup';
import { backupScheduler } from '@/services/backup-scheduler';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const action = searchParams.get('action');
if (action === 'logs') {
const maxLines = parseInt(searchParams.get('maxLines') || '100');
const logs = await backupService.getBackupLogs(maxLines);
return NextResponse.json({
success: true,
data: { logs }
});
}
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');
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch backups',
details: error instanceof Error ? error.stack : undefined
},
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { action, ...params } = body;
switch (action) {
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, data: backup });
case 'verify':
await backupService.verifyDatabaseHealth();
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) {
backupScheduler.restart();
}
return NextResponse.json({
success: true,
message: 'Configuration updated',
data: backupService.getConfig()
});
case 'scheduler':
if (params.enabled) {
backupScheduler.start();
} else {
backupScheduler.stop();
}
return NextResponse.json({
success: true,
data: backupScheduler.getStatus()
});
default:
return NextResponse.json(
{ success: false, error: 'Invalid action' },
{ status: 400 }
);
}
} catch (error) {
console.error('Error in backup operation:', error);
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
);
}
}

View File

@@ -1,5 +1,6 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { dailyService } from '@/services/daily'; import { dailyService } from '@/services/daily';
import { getToday, parseDate, isValidAPIDate, createDateFromParts } from '@/lib/date-utils';
/** /**
* API route pour récupérer la vue daily (hier + aujourd'hui) * API route pour récupérer la vue daily (hier + aujourd'hui)
@@ -32,14 +33,19 @@ export async function GET(request: Request) {
} }
// Vue daily pour une date donnée (ou aujourd'hui par défaut) // Vue daily pour une date donnée (ou aujourd'hui par défaut)
const targetDate = date ? new Date(date) : new Date(); let targetDate: Date;
if (date && isNaN(targetDate.getTime())) { if (date) {
if (!isValidAPIDate(date)) {
return NextResponse.json( return NextResponse.json(
{ error: 'Format de date invalide. Utilisez YYYY-MM-DD' }, { error: 'Format de date invalide. Utilisez YYYY-MM-DD' },
{ status: 400 } { status: 400 }
); );
} }
targetDate = parseDate(date);
} else {
targetDate = getToday();
}
const dailyView = await dailyService.getDailyView(targetDate); const dailyView = await dailyService.getDailyView(targetDate);
return NextResponse.json(dailyView); return NextResponse.json(dailyView);
@@ -73,9 +79,9 @@ export async function POST(request: Request) {
if (typeof body.date === 'string') { if (typeof body.date === 'string') {
// Si c'est une string YYYY-MM-DD, créer une date locale // Si c'est une string YYYY-MM-DD, créer une date locale
const [year, month, day] = body.date.split('-').map(Number); const [year, month, day] = body.date.split('-').map(Number);
date = new Date(year, month - 1, day); // month est 0-indexé date = createDateFromParts(year, month, day);
} else { } else {
date = new Date(body.date); date = parseDate(body.date);
} }
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {

View File

@@ -1,14 +1,55 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { createJiraService, JiraService } from '@/services/jira'; import { createJiraService, JiraService } from '@/services/jira';
import { userPreferencesService } from '@/services/user-preferences'; import { userPreferencesService } from '@/services/user-preferences';
import { jiraScheduler } from '@/services/jira-scheduler';
/** /**
* Route POST /api/jira/sync * Route POST /api/jira/sync
* Synchronise les tickets Jira avec la base locale * Synchronise les tickets Jira avec la base locale
* Supporte aussi les actions du scheduler
*/ */
export async function POST() { export async function POST(request: Request) {
try { try {
// Essayer d'abord la config depuis la base de données // Vérifier s'il y a des actions spécifiques (scheduler)
const body = await request.json().catch(() => ({}));
const { action, ...params } = body;
// Actions du scheduler
if (action) {
switch (action) {
case 'scheduler':
if (params.enabled) {
await jiraScheduler.start();
} else {
jiraScheduler.stop();
}
return NextResponse.json({
success: true,
data: await jiraScheduler.getStatus()
});
case 'config':
await userPreferencesService.saveJiraSchedulerConfig(
params.jiraAutoSync,
params.jiraSyncInterval
);
// Redémarrer le scheduler si la config a changé
await jiraScheduler.restart();
return NextResponse.json({
success: true,
message: 'Configuration scheduler mise à jour',
data: await jiraScheduler.getStatus()
});
default:
return NextResponse.json(
{ success: false, error: 'Action inconnue' },
{ status: 400 }
);
}
}
// Synchronisation normale (manuelle)
const jiraConfig = await userPreferencesService.getJiraConfig(); const jiraConfig = await userPreferencesService.getJiraConfig();
let jiraService: JiraService | null = null; let jiraService: JiraService | null = null;
@@ -34,7 +75,7 @@ export async function POST() {
); );
} }
console.log('🔄 Début de la synchronisation Jira...'); console.log('🔄 Début de la synchronisation Jira manuelle...');
// Tester la connexion d'abord // Tester la connexion d'abord
const connectionOk = await jiraService.testConnection(); const connectionOk = await jiraService.testConnection();
@@ -118,6 +159,9 @@ export async function GET() {
projectValidation = await jiraService.validateProject(jiraConfig.projectKey); projectValidation = await jiraService.validateProject(jiraConfig.projectKey);
} }
// Récupérer aussi le statut du scheduler
const schedulerStatus = await jiraScheduler.getStatus();
return NextResponse.json({ return NextResponse.json({
connected, connected,
message: connected ? 'Connexion Jira OK' : 'Impossible de se connecter à Jira', message: connected ? 'Connexion Jira OK' : 'Impossible de se connecter à Jira',
@@ -126,7 +170,8 @@ export async function GET() {
exists: projectValidation.exists, exists: projectValidation.exists,
name: projectValidation.name, name: projectValidation.name,
error: projectValidation.error error: projectValidation.error
} : null } : null,
scheduler: schedulerStatus
}); });
} catch (error) { } catch (error) {

View File

@@ -10,6 +10,7 @@ import { DailyCalendar } from '@/components/daily/DailyCalendar';
import { DailySection } from '@/components/daily/DailySection'; import { DailySection } from '@/components/daily/DailySection';
import { dailyClient } from '@/clients/daily-client'; import { dailyClient } from '@/clients/daily-client';
import { Header } from '@/components/ui/Header'; import { Header } from '@/components/ui/Header';
import { getPreviousWorkday, formatDateLong, isToday, generateDateTitle, formatDateShort, isYesterday } from '@/lib/date-utils';
interface DailyPageClientProps { interface DailyPageClientProps {
initialDailyView?: DailyView; initialDailyView?: DailyView;
@@ -99,9 +100,7 @@ export function DailyPageClient({
}; };
const getYesterdayDate = () => { const getYesterdayDate = () => {
const yesterday = new Date(currentDate); return getPreviousWorkday(currentDate);
yesterday.setDate(yesterday.getDate() - 1);
return yesterday;
}; };
const getTodayDate = () => { const getTodayDate = () => {
@@ -113,17 +112,23 @@ export function DailyPageClient({
}; };
const formatCurrentDate = () => { const formatCurrentDate = () => {
return currentDate.toLocaleDateString('fr-FR', { return formatDateLong(currentDate);
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}; };
const isToday = () => { const isTodayDate = () => {
const today = new Date(); return isToday(currentDate);
return currentDate.toDateString() === today.toDateString(); };
const getTodayTitle = () => {
return generateDateTitle(currentDate, '🎯');
};
const getYesterdayTitle = () => {
const yesterdayDate = getYesterdayDate();
if (isYesterday(yesterdayDate)) {
return "📋 Hier";
}
return `📋 ${formatDateShort(yesterdayDate)}`;
}; };
if (loading) { if (loading) {
@@ -179,7 +184,7 @@ export function DailyPageClient({
<div className="text-sm font-bold text-[var(--foreground)] font-mono"> <div className="text-sm font-bold text-[var(--foreground)] font-mono">
{formatCurrentDate()} {formatCurrentDate()}
</div> </div>
{!isToday() && ( {!isTodayDate() && (
<button <button
onClick={goToToday} onClick={goToToday}
className="text-xs text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono" className="text-xs text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono"
@@ -218,7 +223,7 @@ export function DailyPageClient({
<div className="xl:col-span-2 grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="xl:col-span-2 grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Section Hier */} {/* Section Hier */}
<DailySection <DailySection
title="📋 Hier" title={getYesterdayTitle()}
date={getYesterdayDate()} date={getYesterdayDate()}
checkboxes={dailyView.yesterday} checkboxes={dailyView.yesterday}
onAddCheckbox={handleAddYesterdayCheckbox} onAddCheckbox={handleAddYesterdayCheckbox}
@@ -233,7 +238,7 @@ export function DailyPageClient({
{/* Section Aujourd'hui */} {/* Section Aujourd'hui */}
<DailySection <DailySection
title="🎯 Aujourd'hui" title={getTodayTitle()}
date={getTodayDate()} date={getTodayDate()}
checkboxes={dailyView.today} checkboxes={dailyView.today}
onAddCheckbox={handleAddTodayCheckbox} onAddCheckbox={handleAddTodayCheckbox}

View File

@@ -1,6 +1,7 @@
import { Metadata } from 'next'; import { Metadata } from 'next';
import { DailyPageClient } from './DailyPageClient'; import { DailyPageClient } from './DailyPageClient';
import { dailyService } from '@/services/daily'; import { dailyService } from '@/services/daily';
import { getToday } from '@/lib/date-utils';
// Force dynamic rendering (no static generation) // Force dynamic rendering (no static generation)
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -12,7 +13,7 @@ export const metadata: Metadata = {
export default async function DailyPage() { export default async function DailyPage() {
// Récupérer les données côté serveur // Récupérer les données côté serveur
const today = new Date(); const today = getToday();
try { try {
const [dailyView, dailyDates] = await Promise.all([ const [dailyView, dailyDates] = await Promise.all([

View File

@@ -470,11 +470,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">📉 Burndown Chart</h3> <h3 className="font-semibold">📉 Burndown Chart</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full h-96 overflow-hidden">
<BurndownChart <BurndownChart
sprintHistory={analytics.velocityMetrics.sprintHistory} sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-96" className="h-full w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
@@ -482,11 +484,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">📈 Throughput</h3> <h3 className="font-semibold">📈 Throughput</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full h-96 overflow-hidden">
<ThroughputChart <ThroughputChart
sprintHistory={analytics.velocityMetrics.sprintHistory} sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-96" className="h-full w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -496,11 +500,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">🎯 Métriques de qualité</h3> <h3 className="font-semibold">🎯 Métriques de qualité</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full overflow-hidden">
<QualityMetrics <QualityMetrics
analytics={analytics} analytics={analytics}
className="min-h-96" className="min-h-96 w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
@@ -509,11 +515,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">📊 Predictabilité</h3> <h3 className="font-semibold">📊 Predictabilité</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full overflow-hidden">
<PredictabilityMetrics <PredictabilityMetrics
sprintHistory={analytics.velocityMetrics.sprintHistory} sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-auto" className="h-auto w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
@@ -522,11 +530,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">🤝 Matrice de collaboration</h3> <h3 className="font-semibold">🤝 Matrice de collaboration</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full overflow-hidden">
<CollaborationMatrix <CollaborationMatrix
analytics={analytics} analytics={analytics}
className="h-auto" className="h-auto w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
@@ -535,11 +545,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">📊 Comparaison inter-sprints</h3> <h3 className="font-semibold">📊 Comparaison inter-sprints</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full overflow-hidden">
<SprintComparison <SprintComparison
sprintHistory={analytics.velocityMetrics.sprintHistory} sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-auto" className="h-auto w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
@@ -548,12 +560,14 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">🔥 Heatmap d&apos;activité de l&apos;équipe</h3> <h3 className="font-semibold">🔥 Heatmap d&apos;activité de l&apos;équipe</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full overflow-hidden">
<TeamActivityHeatmap <TeamActivityHeatmap
workloadByAssignee={analytics.workInProgress.byAssignee} workloadByAssignee={analytics.workInProgress.byAssignee}
statusDistribution={analytics.workInProgress.byStatus} statusDistribution={analytics.workInProgress.byStatus}
className="min-h-96" className="min-h-96 w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -566,12 +580,14 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">🚀 Vélocité des sprints</h3> <h3 className="font-semibold">🚀 Vélocité des sprints</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<VelocityChart <VelocityChart
sprintHistory={analytics.velocityMetrics.sprintHistory} sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-64" className="h-full w-full"
onSprintClick={handleSprintClick} onSprintClick={handleSprintClick}
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
@@ -581,11 +597,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">📉 Burndown Chart</h3> <h3 className="font-semibold">📉 Burndown Chart</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full h-96 overflow-hidden">
<BurndownChart <BurndownChart
sprintHistory={analytics.velocityMetrics.sprintHistory} sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-96" className="h-full w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
@@ -593,11 +611,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">📊 Throughput</h3> <h3 className="font-semibold">📊 Throughput</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full h-96 overflow-hidden">
<ThroughputChart <ThroughputChart
sprintHistory={analytics.velocityMetrics.sprintHistory} sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-96" className="h-full w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -607,11 +627,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">📊 Comparaison des sprints</h3> <h3 className="font-semibold">📊 Comparaison des sprints</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full overflow-hidden">
<SprintComparison <SprintComparison
sprintHistory={analytics.velocityMetrics.sprintHistory} sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-auto" className="h-auto w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -625,11 +647,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold"> Cycle Time par type</h3> <h3 className="font-semibold"> Cycle Time par type</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<CycleTimeChart <CycleTimeChart
cycleTimeByType={analytics.cycleTimeMetrics.cycleTimeByType} cycleTimeByType={analytics.cycleTimeMetrics.cycleTimeByType}
className="h-64" className="h-full w-full"
/> />
</div>
<div className="mt-4 text-center"> <div className="mt-4 text-center">
<div className="text-2xl font-bold text-[var(--primary)]"> <div className="text-2xl font-bold text-[var(--primary)]">
{analytics.cycleTimeMetrics.averageCycleTime.toFixed(1)} {analytics.cycleTimeMetrics.averageCycleTime.toFixed(1)}
@@ -645,12 +669,14 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">🔥 Heatmap d&apos;activité</h3> <h3 className="font-semibold">🔥 Heatmap d&apos;activité</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<TeamActivityHeatmap <TeamActivityHeatmap
workloadByAssignee={analytics.workInProgress.byAssignee} workloadByAssignee={analytics.workInProgress.byAssignee}
statusDistribution={analytics.workInProgress.byStatus} statusDistribution={analytics.workInProgress.byStatus}
className="h-64" className="h-full w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -661,11 +687,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">🎯 Métriques de qualité</h3> <h3 className="font-semibold">🎯 Métriques de qualité</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<QualityMetrics <QualityMetrics
analytics={analytics} analytics={analytics}
className="h-64" className="h-full w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
@@ -673,11 +701,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">📈 Predictabilité</h3> <h3 className="font-semibold">📈 Predictabilité</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<PredictabilityMetrics <PredictabilityMetrics
sprintHistory={analytics.velocityMetrics.sprintHistory} sprintHistory={analytics.velocityMetrics.sprintHistory}
className="h-64" className="h-full w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -692,11 +722,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">👥 Répartition de l&apos;équipe</h3> <h3 className="font-semibold">👥 Répartition de l&apos;équipe</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<TeamDistributionChart <TeamDistributionChart
distribution={analytics.teamMetrics.issuesDistribution} distribution={analytics.teamMetrics.issuesDistribution}
className="h-64" className="h-full w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
@@ -704,11 +736,13 @@ export function JiraDashboardPageClient({ initialJiraConfig }: JiraDashboardPage
<CardHeader> <CardHeader>
<h3 className="font-semibold">🤝 Matrice de collaboration</h3> <h3 className="font-semibold">🤝 Matrice de collaboration</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4">
<div className="w-full h-64 overflow-hidden">
<CollaborationMatrix <CollaborationMatrix
analytics={analytics} analytics={analytics}
className="h-64" className="h-full w-full"
/> />
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View File

@@ -1,12 +1,21 @@
import { userPreferencesService } from '@/services/user-preferences'; import { userPreferencesService } from '@/services/user-preferences';
import { SystemInfoService } from '@/services/system-info';
import { SettingsIndexPageClient } from '@/components/settings/SettingsIndexPageClient'; import { SettingsIndexPageClient } from '@/components/settings/SettingsIndexPageClient';
// Force dynamic rendering (no static generation) // Force dynamic rendering (no static generation)
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export default async function SettingsPage() { export default async function SettingsPage() {
// Fetch basic data for the index page // Fetch data in parallel for better performance
const preferences = await userPreferencesService.getAllPreferences(); const [preferences, systemInfo] = await Promise.all([
userPreferencesService.getAllPreferences(),
SystemInfoService.getSystemInfo()
]);
return <SettingsIndexPageClient initialPreferences={preferences} />; return (
<SettingsIndexPageClient
initialPreferences={preferences}
initialSystemInfo={systemInfo}
/>
);
} }

View File

@@ -28,11 +28,17 @@ export class BackupClient {
/** /**
* Crée une nouvelle sauvegarde manuelle * Crée une nouvelle sauvegarde manuelle
*/ */
async createBackup(): Promise<BackupInfo> { async createBackup(force: boolean = false): Promise<BackupInfo | null> {
const response = await httpClient.post<{ data: BackupInfo }>(this.baseUrl, { const response = await httpClient.post<{ data?: BackupInfo; skipped?: boolean; message?: string }>(this.baseUrl, {
action: 'create' action: 'create',
force
}); });
return response.data;
if (response.skipped) {
return null; // Backup was skipped
}
return response.data!;
} }
/** /**
@@ -95,6 +101,14 @@ export class BackupClient {
action: 'restore' action: 'restore'
}); });
} }
/**
* Récupère les logs de backup
*/
async getBackupLogs(maxLines: number = 100): Promise<string[]> {
const response = await httpClient.get<{ data: { logs: string[] } }>(`${this.baseUrl}?action=logs&maxLines=${maxLines}`);
return response.data.logs;
}
} }
export const backupClient = new BackupClient(); export const backupClient = new BackupClient();

View File

@@ -1,5 +1,6 @@
import { httpClient } from './base/http-client'; import { httpClient } from './base/http-client';
import { DailyCheckbox, DailyView, Task } from '@/lib/types'; import { DailyCheckbox, DailyView, Task } from '@/lib/types';
import { formatDateForAPI, parseDate, getToday, addDays, subtractDays } from '@/lib/date-utils';
// Types pour les réponses API (avec dates en string) // Types pour les réponses API (avec dates en string)
interface ApiCheckbox { interface ApiCheckbox {
@@ -73,7 +74,7 @@ export class DailyClient {
const result = await httpClient.get<ApiHistoryItem[]>(`/daily?${params}`); const result = await httpClient.get<ApiHistoryItem[]>(`/daily?${params}`);
return result.map(item => ({ return result.map(item => ({
date: new Date(item.date), date: parseDate(item.date),
checkboxes: item.checkboxes.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb)) checkboxes: item.checkboxes.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb))
})); }));
} }
@@ -97,10 +98,7 @@ export class DailyClient {
* Formate une date pour l'API (évite les décalages timezone) * Formate une date pour l'API (évite les décalages timezone)
*/ */
formatDateForAPI(date: Date): string { formatDateForAPI(date: Date): string {
const year = date.getFullYear(); return formatDateForAPI(date);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`; // YYYY-MM-DD
} }
/** /**
@@ -109,9 +107,9 @@ export class DailyClient {
private transformCheckboxDates(checkbox: ApiCheckbox): DailyCheckbox { private transformCheckboxDates(checkbox: ApiCheckbox): DailyCheckbox {
return { return {
...checkbox, ...checkbox,
date: new Date(checkbox.date), date: parseDate(checkbox.date),
createdAt: new Date(checkbox.createdAt), createdAt: parseDate(checkbox.createdAt),
updatedAt: new Date(checkbox.updatedAt) updatedAt: parseDate(checkbox.updatedAt)
}; };
} }
@@ -120,7 +118,7 @@ export class DailyClient {
*/ */
private transformDailyViewDates(view: ApiDailyView): DailyView { private transformDailyViewDates(view: ApiDailyView): DailyView {
return { return {
date: new Date(view.date), date: parseDate(view.date),
yesterday: view.yesterday.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb)), yesterday: view.yesterday.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb)),
today: view.today.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb)) today: view.today.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb))
}; };
@@ -130,16 +128,19 @@ export class DailyClient {
* Récupère la vue daily d'une date relative (hier, aujourd'hui, demain) * Récupère la vue daily d'une date relative (hier, aujourd'hui, demain)
*/ */
async getDailyViewByRelativeDate(relative: 'yesterday' | 'today' | 'tomorrow'): Promise<DailyView> { async getDailyViewByRelativeDate(relative: 'yesterday' | 'today' | 'tomorrow'): Promise<DailyView> {
const date = new Date(); let date: Date;
switch (relative) { switch (relative) {
case 'yesterday': case 'yesterday':
date.setDate(date.getDate() - 1); date = subtractDays(getToday(), 1);
break; break;
case 'tomorrow': case 'tomorrow':
date.setDate(date.getDate() + 1); date = addDays(getToday(), 1);
break;
case 'today':
default:
date = getToday();
break; break;
// 'today' ne change rien
} }
return this.getDailyView(date); return this.getDailyView(date);

View File

@@ -0,0 +1,68 @@
/**
* Client pour l'API Jira
*/
import { HttpClient } from './base/http-client';
import { JiraSyncResult } from '@/services/jira';
export interface JiraConnectionStatus {
connected: boolean;
message: string;
details?: string;
scheduler?: JiraSchedulerStatus;
}
export interface JiraSchedulerStatus {
isRunning: boolean;
isEnabled: boolean;
interval: 'hourly' | 'daily' | 'weekly';
nextSync: string | null;
jiraConfigured: boolean;
}
export class JiraClient extends HttpClient {
constructor() {
super('/api/jira');
}
/**
* Teste la connexion à Jira
*/
async testConnection(): Promise<JiraConnectionStatus> {
return this.get<JiraConnectionStatus>('/sync');
}
/**
* Lance la synchronisation manuelle des tickets Jira
*/
async syncTasks(): Promise<JiraSyncResult> {
const response = await this.post<{ data: JiraSyncResult }>('/sync');
return response.data;
}
/**
* Active/désactive le scheduler automatique
*/
async toggleScheduler(enabled: boolean): Promise<JiraSchedulerStatus> {
const response = await this.post<{ data: JiraSchedulerStatus }>('/sync', {
action: 'scheduler',
enabled
});
return response.data;
}
/**
* Met à jour la configuration du scheduler
*/
async updateSchedulerConfig(jiraAutoSync: boolean, jiraSyncInterval: 'hourly' | 'daily' | 'weekly'): Promise<JiraSchedulerStatus> {
const response = await this.post<{ data: JiraSchedulerStatus }>('/sync', {
action: 'config',
jiraAutoSync,
jiraSyncInterval
});
return response.data;
}
}
// Instance singleton
export const jiraClient = new JiraClient();

View File

@@ -2,6 +2,7 @@
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { parseDate, formatDateShort } from '@/lib/date-utils';
interface CompletionTrendData { interface CompletionTrendData {
date: string; date: string;
@@ -18,11 +19,11 @@ interface CompletionTrendChartProps {
export function CompletionTrendChart({ data, title = "Tendance de Completion" }: CompletionTrendChartProps) { export function CompletionTrendChart({ data, title = "Tendance de Completion" }: CompletionTrendChartProps) {
// Formatter pour les dates // Formatter pour les dates
const formatDate = (dateStr: string) => { const formatDate = (dateStr: string) => {
const date = new Date(dateStr); try {
return date.toLocaleDateString('fr-FR', { return formatDateShort(parseDate(dateStr));
day: 'numeric', } catch {
month: 'short' return dateStr;
}); }
}; };
// Tooltip personnalisé // Tooltip personnalisé

View File

@@ -3,6 +3,9 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { formatDateForAPI, createDate, getToday } from '@/lib/date-utils';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
interface DailyCalendarProps { interface DailyCalendarProps {
currentDate: Date; currentDate: Date;
@@ -15,33 +18,30 @@ export function DailyCalendar({
onDateSelect, onDateSelect,
dailyDates, dailyDates,
}: DailyCalendarProps) { }: DailyCalendarProps) {
const [viewDate, setViewDate] = useState(new Date(currentDate)); const [viewDate, setViewDate] = useState(createDate(currentDate));
// Formatage des dates pour comparaison (éviter le décalage timezone) // Formatage des dates pour comparaison (éviter le décalage timezone)
const formatDateKey = (date: Date) => { const formatDateKey = (date: Date) => {
const year = date.getFullYear(); return formatDateForAPI(date);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}; };
const currentDateKey = formatDateKey(currentDate); const currentDateKey = formatDateKey(currentDate);
// Navigation mois // Navigation mois
const goToPreviousMonth = () => { const goToPreviousMonth = () => {
const newDate = new Date(viewDate); const newDate = createDate(viewDate);
newDate.setMonth(newDate.getMonth() - 1); newDate.setMonth(newDate.getMonth() - 1);
setViewDate(newDate); setViewDate(newDate);
}; };
const goToNextMonth = () => { const goToNextMonth = () => {
const newDate = new Date(viewDate); const newDate = createDate(viewDate);
newDate.setMonth(newDate.getMonth() + 1); newDate.setMonth(newDate.getMonth() + 1);
setViewDate(newDate); setViewDate(newDate);
}; };
const goToToday = () => { const goToToday = () => {
const today = new Date(); const today = getToday();
setViewDate(today); setViewDate(today);
onDateSelect(today); onDateSelect(today);
}; };
@@ -57,18 +57,18 @@ export function DailyCalendar({
const lastDay = new Date(year, month + 1, 0); const lastDay = new Date(year, month + 1, 0);
// Premier lundi de la semaine contenant le premier jour // Premier lundi de la semaine contenant le premier jour
const startDate = new Date(firstDay); const startDate = createDate(firstDay);
const dayOfWeek = firstDay.getDay(); const dayOfWeek = firstDay.getDay();
const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Lundi = 0 const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Lundi = 0
startDate.setDate(firstDay.getDate() - daysToSubtract); startDate.setDate(firstDay.getDate() - daysToSubtract);
// Générer toutes les dates du calendrier (6 semaines) // Générer toutes les dates du calendrier (6 semaines)
const days = []; const days = [];
const currentDay = new Date(startDate); const currentDay = createDate(startDate);
for (let i = 0; i < 42; i++) { for (let i = 0; i < 42; i++) {
// 6 semaines × 7 jours // 6 semaines × 7 jours
days.push(new Date(currentDay)); days.push(createDate(currentDay));
currentDay.setDate(currentDay.getDate() + 1); currentDay.setDate(currentDay.getDate() + 1);
} }
@@ -81,8 +81,8 @@ export function DailyCalendar({
onDateSelect(date); onDateSelect(date);
}; };
const isToday = (date: Date) => { const isTodayDate = (date: Date) => {
const today = new Date(); const today = getToday();
return formatDateKey(date) === formatDateKey(today); return formatDateKey(date) === formatDateKey(today);
}; };
@@ -99,10 +99,7 @@ export function DailyCalendar({
}; };
const formatMonthYear = () => { const formatMonthYear = () => {
return viewDate.toLocaleDateString('fr-FR', { return format(viewDate, 'MMMM yyyy', { locale: fr });
month: 'long',
year: 'numeric',
});
}; };
const weekDays = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim']; const weekDays = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
@@ -157,7 +154,7 @@ export function DailyCalendar({
<div className="grid grid-cols-7 gap-1"> <div className="grid grid-cols-7 gap-1">
{days.map((date, index) => { {days.map((date, index) => {
const isCurrentMonthDay = isCurrentMonth(date); const isCurrentMonthDay = isCurrentMonth(date);
const isTodayDay = isToday(date); const isTodayDay = isTodayDate(date);
const hasCheckboxes = hasDaily(date); const hasCheckboxes = hasDaily(date);
const isSelectedDay = isSelected(date); const isSelectedDay = isSelected(date);

View File

@@ -40,13 +40,6 @@ export function DailySection({
}: DailySectionProps) { }: DailySectionProps) {
const [activeId, setActiveId] = useState<string | null>(null); const [activeId, setActiveId] = useState<string | null>(null);
const [items, setItems] = useState(checkboxes); const [items, setItems] = useState(checkboxes);
const formatShortDate = (date: Date) => {
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
// Mettre à jour les items quand les checkboxes changent // Mettre à jour les items quand les checkboxes changent
React.useEffect(() => { React.useEffect(() => {
@@ -99,7 +92,7 @@ export function DailySection({
<div className="p-4 pb-0"> <div className="p-4 pb-0">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-[var(--foreground)] font-mono flex items-center gap-2"> <h2 className="text-lg font-bold text-[var(--foreground)] font-mono flex items-center gap-2">
{title} <span className="text-sm font-normal text-[var(--muted-foreground)]">({formatShortDate(date)})</span> {title} <span className="text-sm font-normal text-[var(--muted-foreground)]"></span>
{refreshing && ( {refreshing && (
<div className="w-4 h-4 border-2 border-[var(--primary)] border-t-transparent rounded-full animate-spin"></div> <div className="w-4 h-4 border-2 border-[var(--primary)] border-t-transparent rounded-full animate-spin"></div>
)} )}

View File

@@ -2,6 +2,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { useWeeklyMetrics, useVelocityTrends } from '@/hooks/use-metrics'; import { useWeeklyMetrics, useVelocityTrends } from '@/hooks/use-metrics';
import { getToday } from '@/lib/date-utils';
import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { DailyStatusChart } from './charts/DailyStatusChart'; import { DailyStatusChart } from './charts/DailyStatusChart';
@@ -19,7 +20,7 @@ interface MetricsTabProps {
} }
export function MetricsTab({ className }: MetricsTabProps) { export function MetricsTab({ className }: MetricsTabProps) {
const [selectedDate] = useState<Date>(new Date()); const [selectedDate] = useState<Date>(getToday());
const [weeksBack, setWeeksBack] = useState(4); const [weeksBack, setWeeksBack] = useState(4);
const { metrics, loading: metricsLoading, error: metricsError, refetch: refetchMetrics } = useWeeklyMetrics(selectedDate); const { metrics, loading: metricsLoading, error: metricsError, refetch: refetchMetrics } = useWeeklyMetrics(selectedDate);
@@ -93,7 +94,7 @@ export function MetricsTab({ className }: MetricsTabProps) {
</div> </div>
</div> </div>
{metricsLoading || trendsLoading ? ( {metricsLoading ? (
<Card> <Card>
<CardContent className="p-6 text-center"> <CardContent className="p-6 text-center">
<div className="animate-pulse"> <div className="animate-pulse">
@@ -207,7 +208,6 @@ export function MetricsTab({ className }: MetricsTabProps) {
</div> </div>
{/* Tendances de vélocité */} {/* Tendances de vélocité */}
{trends.length > 0 && (
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -216,6 +216,7 @@ export function MetricsTab({ className }: MetricsTabProps) {
value={weeksBack} value={weeksBack}
onChange={(e) => setWeeksBack(parseInt(e.target.value))} onChange={(e) => setWeeksBack(parseInt(e.target.value))}
className="text-sm border border-[var(--border)] rounded px-2 py-1 bg-[var(--background)]" className="text-sm border border-[var(--border)] rounded px-2 py-1 bg-[var(--background)]"
disabled={trendsLoading}
> >
<option value={4}>4 semaines</option> <option value={4}>4 semaines</option>
<option value={8}>8 semaines</option> <option value={8}>8 semaines</option>
@@ -224,10 +225,22 @@ export function MetricsTab({ className }: MetricsTabProps) {
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{trendsLoading ? (
<div className="h-[300px] flex items-center justify-center">
<div className="animate-pulse text-center">
<div className="h-4 bg-[var(--border)] rounded w-32 mx-auto mb-2"></div>
<div className="h-48 bg-[var(--border)] rounded"></div>
</div>
</div>
) : trends.length > 0 ? (
<VelocityTrendChart data={trends} /> <VelocityTrendChart data={trends} />
) : (
<div className="h-[300px] flex items-center justify-center text-[var(--muted-foreground)]">
Aucune donnée de vélocité disponible
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
)}
{/* Analyses de productivité */} {/* Analyses de productivité */}
<Card> <Card>

View File

@@ -3,6 +3,7 @@
import { Task } from '@/lib/types'; import { Task } from '@/lib/types';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { TagDisplay } from '@/components/ui/TagDisplay'; import { TagDisplay } from '@/components/ui/TagDisplay';
import { formatDateShort } from '@/lib/date-utils';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { useTasksContext } from '@/contexts/TasksContext'; import { useTasksContext } from '@/contexts/TasksContext';
import { getPriorityConfig, getPriorityColorHex, getStatusBadgeClasses, getStatusLabel } from '@/lib/status-config'; import { getPriorityConfig, getPriorityColorHex, getStatusBadgeClasses, getStatusLabel } from '@/lib/status-config';
@@ -18,7 +19,7 @@ export function RecentTasks({ tasks }: RecentTasksProps) {
// Prendre les 5 tâches les plus récentes (créées ou modifiées) // Prendre les 5 tâches les plus récentes (créées ou modifiées)
const recentTasks = tasks const recentTasks = tasks
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
.slice(0, 5); .slice(0, 5);
// Fonctions simplifiées utilisant la configuration centralisée // Fonctions simplifiées utilisant la configuration centralisée
@@ -116,10 +117,7 @@ export function RecentTasks({ tasks }: RecentTasksProps) {
</div> </div>
<div className="text-xs text-[var(--muted-foreground)] whitespace-nowrap"> <div className="text-xs text-[var(--muted-foreground)] whitespace-nowrap">
{new Date(task.updatedAt).toLocaleDateString('fr-FR', { {formatDateShort(task.updatedAt)}
day: 'numeric',
month: 'short'
})}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,6 +2,7 @@
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { DailyMetrics } from '@/services/metrics'; import { DailyMetrics } from '@/services/metrics';
import { parseDate, formatDateShort } from '@/lib/date-utils';
interface CompletionRateChartProps { interface CompletionRateChartProps {
data: DailyMetrics[]; data: DailyMetrics[];
@@ -12,7 +13,7 @@ export function CompletionRateChart({ data, className }: CompletionRateChartProp
// Transformer les données pour le graphique // Transformer les données pour le graphique
const chartData = data.map(day => ({ const chartData = data.map(day => ({
day: day.dayName.substring(0, 3), // Lun, Mar, etc. day: day.dayName.substring(0, 3), // Lun, Mar, etc.
date: new Date(day.date).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' }), date: formatDateShort(parseDate(day.date)),
completionRate: day.completionRate, completionRate: day.completionRate,
completed: day.completed, completed: day.completed,
total: day.totalTasks total: day.totalTasks

View File

@@ -2,6 +2,7 @@
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { DailyMetrics } from '@/services/metrics'; import { DailyMetrics } from '@/services/metrics';
import { parseDate, formatDateShort } from '@/lib/date-utils';
interface DailyStatusChartProps { interface DailyStatusChartProps {
data: DailyMetrics[]; data: DailyMetrics[];
@@ -12,7 +13,7 @@ export function DailyStatusChart({ data, className }: DailyStatusChartProps) {
// Transformer les données pour le graphique // Transformer les données pour le graphique
const chartData = data.map(day => ({ const chartData = data.map(day => ({
day: day.dayName.substring(0, 3), // Lun, Mar, etc. day: day.dayName.substring(0, 3), // Lun, Mar, etc.
date: new Date(day.date).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' }), date: formatDateShort(parseDate(day.date)),
'Complétées': day.completed, 'Complétées': day.completed,
'En cours': day.inProgress, 'En cours': day.inProgress,
'Bloquées': day.blocked, 'Bloquées': day.blocked,

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import { DailyMetrics } from '@/services/metrics'; import { DailyMetrics } from '@/services/metrics';
import { parseDate, isToday } from '@/lib/date-utils';
interface WeeklyActivityHeatmapProps { interface WeeklyActivityHeatmapProps {
data: DailyMetrics[]; data: DailyMetrics[];
@@ -67,7 +68,7 @@ export function WeeklyActivityHeatmap({ data, className }: WeeklyActivityHeatmap
</div> </div>
{/* Indicator si jour actuel */} {/* Indicator si jour actuel */}
{new Date(day.date).toDateString() === new Date().toDateString() && ( {isToday(parseDate(day.date)) && (
<div className="w-2 h-2 bg-blue-500 rounded-full"></div> <div className="w-2 h-2 bg-blue-500 rounded-full"></div>
)} )}
</div> </div>

View File

@@ -8,6 +8,7 @@ import { TagInput } from '@/components/ui/TagInput';
import { TaskPriority, TaskStatus } from '@/lib/types'; import { TaskPriority, TaskStatus } from '@/lib/types';
import { CreateTaskData } from '@/clients/tasks-client'; import { CreateTaskData } from '@/clients/tasks-client';
import { getAllStatuses, getAllPriorities } from '@/lib/status-config'; import { getAllStatuses, getAllPriorities } from '@/lib/status-config';
import { formatDateForDateTimeInput, parseDateTimeInput } from '@/lib/date-utils';
interface CreateTaskFormProps { interface CreateTaskFormProps {
isOpen: boolean; isOpen: boolean;
@@ -151,10 +152,10 @@ export function CreateTaskForm({ isOpen, onClose, onSubmit, loading = false }: C
<Input <Input
label="Date d'échéance" label="Date d'échéance"
type="datetime-local" type="datetime-local"
value={formData.dueDate ? new Date(formData.dueDate.getTime() - formData.dueDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''} value={formData.dueDate ? formatDateForDateTimeInput(formData.dueDate) : ''}
onChange={(e) => setFormData((prev: CreateTaskData) => ({ onChange={(e) => setFormData((prev: CreateTaskData) => ({
...prev, ...prev,
dueDate: e.target.value ? new Date(e.target.value) : undefined dueDate: e.target.value ? parseDateTimeInput(e.target.value) : undefined
}))} }))}
disabled={loading} disabled={loading}
/> />

View File

@@ -11,6 +11,7 @@ import { Task, TaskPriority, TaskStatus } from '@/lib/types';
import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { useUserPreferences } from '@/contexts/UserPreferencesContext';
// UpdateTaskData removed - using Server Actions directly // UpdateTaskData removed - using Server Actions directly
import { getAllStatuses, getAllPriorities } from '@/lib/status-config'; import { getAllStatuses, getAllPriorities } from '@/lib/status-config';
import { formatDateForDateTimeInput, parseDateTimeInput } from '@/lib/date-utils';
interface EditTaskFormProps { interface EditTaskFormProps {
isOpen: boolean; isOpen: boolean;
@@ -56,7 +57,7 @@ export function EditTaskForm({ isOpen, onClose, onSubmit, task, loading = false
status: task.status, status: task.status,
priority: task.priority, priority: task.priority,
tags: task.tags || [], tags: task.tags || [],
dueDate: task.dueDate ? new Date(task.dueDate) : undefined dueDate: task.dueDate
}); });
} }
}, [task]); }, [task]);
@@ -181,10 +182,10 @@ export function EditTaskForm({ isOpen, onClose, onSubmit, task, loading = false
<Input <Input
label="Date d'échéance" label="Date d'échéance"
type="datetime-local" type="datetime-local"
value={formData.dueDate ? new Date(formData.dueDate.getTime() - formData.dueDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''} value={formData.dueDate ? formatDateForDateTimeInput(formData.dueDate) : ''}
onChange={(e) => setFormData(prev => ({ onChange={(e) => setFormData(prev => ({
...prev, ...prev,
dueDate: e.target.value ? new Date(e.target.value) : undefined dueDate: e.target.value ? parseDateTimeInput(e.target.value) : undefined
}))} }))}
disabled={loading} disabled={loading}
/> />

View File

@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useTransition } from 'react';
import { DailyCheckbox } from '@/lib/types'; import { DailyCheckbox } from '@/lib/types';
import { tasksClient } from '@/clients/tasks-client'; import { tasksClient } from '@/clients/tasks-client';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { formatDateSmart, parseDate } from '@/lib/date-utils';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { addTodoToTask, toggleCheckbox } from '@/actions/daily'; import { addTodoToTask, toggleCheckbox } from '@/actions/daily';
@@ -41,7 +42,7 @@ export function RelatedTodos({ taskId }: RelatedTodosProps) {
startTransition(async () => { startTransition(async () => {
try { try {
// Si une date est spécifiée, l'utiliser, sinon undefined (aujourd'hui par défaut) // Si une date est spécifiée, l'utiliser, sinon undefined (aujourd'hui par défaut)
const targetDate = newTodoDate ? new Date(newTodoDate) : undefined; const targetDate = newTodoDate ? parseDate(newTodoDate) : undefined;
const result = await addTodoToTask(taskId, newTodoText, targetDate); const result = await addTodoToTask(taskId, newTodoText, targetDate);
@@ -78,15 +79,11 @@ export function RelatedTodos({ taskId }: RelatedTodosProps) {
const formatDate = (date: Date | string) => { const formatDate = (date: Date | string) => {
try { try {
const dateObj = typeof date === 'string' ? new Date(date) : date; const dateObj = typeof date === 'string' ? parseDate(date) : date;
if (isNaN(dateObj.getTime())) { if (isNaN(dateObj.getTime())) {
return 'Date invalide'; return 'Date invalide';
} }
return new Intl.DateTimeFormat('fr-FR', { return formatDateSmart(dateObj);
day: 'numeric',
month: 'short',
year: 'numeric'
}).format(dateObj);
} catch (error) { } catch (error) {
console.error('Erreur formatage date:', error, date); console.error('Erreur formatage date:', error, date);
return 'Date invalide'; return 'Date invalide';

View File

@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { Modal } from '@/components/ui/Modal'; import { Modal } from '@/components/ui/Modal';
import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { formatDateForDisplay, getToday } from '@/lib/date-utils';
interface AnomalyDetectionPanelProps { interface AnomalyDetectionPanelProps {
className?: string; className?: string;
@@ -42,7 +43,7 @@ export default function AnomalyDetectionPanel({ className = '' }: AnomalyDetecti
if (result.success && result.data) { if (result.success && result.data) {
setAnomalies(result.data); setAnomalies(result.data);
setLastUpdate(new Date().toLocaleString('fr-FR')); setLastUpdate(formatDateForDisplay(getToday(), 'DISPLAY_LONG'));
} else { } else {
setError(result.error || 'Erreur lors de la détection'); setError(result.error || 'Erreur lors de la détection');
} }

View File

@@ -6,6 +6,7 @@ import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { fr } from 'date-fns/locale'; import { fr } from 'date-fns/locale';
import { parseDate } from '@/lib/date-utils';
interface SyncLog { interface SyncLog {
id: string; id: string;
@@ -111,7 +112,7 @@ export function JiraLogs({ className = "" }: JiraLogsProps) {
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
{getStatusBadge(log.status)} {getStatusBadge(log.status)}
<span className="text-xs text-[var(--muted-foreground)]"> <span className="text-xs text-[var(--muted-foreground)]">
{formatDistanceToNow(new Date(log.createdAt), { {formatDistanceToNow(parseDate(log.createdAt), {
addSuffix: true, addSuffix: true,
locale: fr locale: fr
})} })}

View File

@@ -0,0 +1,209 @@
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { jiraClient, JiraSchedulerStatus } from '@/clients/jira-client';
import { parseDate, getToday } from '@/lib/date-utils';
interface JiraSchedulerConfigProps {
className?: string;
}
export function JiraSchedulerConfig({ className = "" }: JiraSchedulerConfigProps) {
const [schedulerStatus, setSchedulerStatus] = useState<JiraSchedulerStatus | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Charger le statut initial
useEffect(() => {
loadSchedulerStatus();
}, []);
const loadSchedulerStatus = async () => {
try {
const status = await jiraClient.testConnection();
if (status.scheduler) {
setSchedulerStatus(status.scheduler);
}
} catch (err) {
console.error('Erreur lors du chargement du statut scheduler:', err);
}
};
const toggleScheduler = async () => {
if (!schedulerStatus) return;
setIsLoading(true);
setError(null);
try {
// Utiliser isEnabled au lieu de isRunning pour l'activation
const newStatus = await jiraClient.updateSchedulerConfig(!schedulerStatus.isEnabled, schedulerStatus.interval);
setSchedulerStatus(newStatus);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur lors du toggle scheduler');
} finally {
setIsLoading(false);
}
};
const updateInterval = async (interval: 'hourly' | 'daily' | 'weekly') => {
if (!schedulerStatus) return;
setIsLoading(true);
setError(null);
try {
const newStatus = await jiraClient.updateSchedulerConfig(true, interval);
setSchedulerStatus(newStatus);
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour');
} finally {
setIsLoading(false);
}
};
const getStatusBadge = () => {
if (!schedulerStatus) return null;
if (!schedulerStatus.jiraConfigured) {
return <Badge variant="warning" size="sm"> Jira non configuré</Badge>;
}
if (!schedulerStatus.isEnabled) {
return <Badge variant="default" size="sm"> Désactivé</Badge>;
}
return schedulerStatus.isRunning ? (
<Badge variant="success" size="sm"> Actif</Badge>
) : (
<Badge variant="danger" size="sm"> Arrêté</Badge>
);
};
const getNextSyncText = () => {
if (!schedulerStatus?.nextSync) return 'Aucune synchronisation planifiée';
const nextSync = parseDate(schedulerStatus.nextSync);
const now = getToday();
const diffMs = nextSync.getTime() - now.getTime();
if (diffMs <= 0) return 'Synchronisation en cours...';
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
if (diffHours > 0) {
return `Dans ${diffHours}h ${diffMinutes}min`;
} else {
return `Dans ${diffMinutes}min`;
}
};
const getIntervalText = (interval: string) => {
switch (interval) {
case 'hourly': return 'Toutes les heures';
case 'daily': return 'Quotidienne';
case 'weekly': return 'Hebdomadaire';
default: return interval;
}
};
if (!schedulerStatus) {
return (
<Card className={className}>
<CardHeader>
<h3 className="text-lg font-semibold"> Synchronisation automatique</h3>
</CardHeader>
<CardContent>
<p className="text-gray-500">Chargement...</p>
</CardContent>
</Card>
);
}
return (
<Card className={className}>
<CardHeader>
<div className="flex items-center justify-between gap-3">
<h3 className="text-base sm:text-lg font-semibold flex-1 min-w-0 truncate"> Synchronisation automatique</h3>
<div className="flex-shrink-0">
{getStatusBadge()}
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-700 text-sm">{error}</p>
</div>
)}
{/* Statut actuel */}
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium text-gray-600">Statut:</span>
<p className="mt-1">
{schedulerStatus.isEnabled && schedulerStatus.isRunning ? '🟢 Actif' : '🔴 Arrêté'}
</p>
</div>
<div>
<span className="font-medium text-gray-600">Fréquence:</span>
<p className="mt-1">{getIntervalText(schedulerStatus.interval)}</p>
</div>
<div className="col-span-2">
<span className="font-medium text-gray-600">Prochaine synchronisation:</span>
<p className="mt-1">{getNextSyncText()}</p>
</div>
</div>
{/* Contrôles */}
<div className="flex flex-col gap-3">
{/* Toggle scheduler */}
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Synchronisation automatique</span>
<Button
variant={schedulerStatus.isEnabled ? "danger" : "primary"}
size="sm"
onClick={toggleScheduler}
disabled={isLoading || !schedulerStatus.jiraConfigured}
>
{schedulerStatus.isEnabled ? 'Désactiver' : 'Activer'}
</Button>
</div>
{/* Sélecteur d'intervalle */}
{schedulerStatus.isEnabled && (
<div>
<span className="text-sm font-medium text-gray-600 block mb-2">Fréquence de synchronisation</span>
<div className="flex gap-2">
{(['hourly', 'daily', 'weekly'] as const).map((interval) => (
<Button
key={interval}
variant={schedulerStatus.interval === interval ? "primary" : "secondary"}
size="sm"
onClick={() => updateInterval(interval)}
disabled={isLoading}
>
{getIntervalText(interval)}
</Button>
))}
</div>
</div>
)}
</div>
{/* Avertissement si Jira non configuré */}
{!schedulerStatus.jiraConfigured && (
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-yellow-700 text-sm">
Configurez d&apos;abord votre connexion Jira pour activer la synchronisation automatique.
</p>
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -4,6 +4,7 @@ import { useState } from 'react';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { getToday } from '@/lib/date-utils';
import { Modal } from '@/components/ui/Modal'; import { Modal } from '@/components/ui/Modal';
import { jiraClient } from '@/clients/jira-client'; import { jiraClient } from '@/clients/jira-client';
import { JiraSyncResult, JiraSyncAction } from '@/services/jira'; import { JiraSyncResult, JiraSyncAction } from '@/services/jira';
@@ -79,7 +80,7 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
{success ? "✓ Succès" : "⚠ Erreurs"} {success ? "✓ Succès" : "⚠ Erreurs"}
</Badge> </Badge>
<span className="text-[var(--muted-foreground)] text-xs"> <span className="text-[var(--muted-foreground)] text-xs">
{new Date().toLocaleTimeString()} {getToday().toLocaleTimeString()}
</span> </span>
</div> </div>
<div className="text-xs text-[var(--muted-foreground)]"> <div className="text-xs text-[var(--muted-foreground)]">

View File

@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import { SprintVelocity, JiraTask, AssigneeDistribution, StatusDistribution } from '@/lib/types'; import { SprintVelocity, JiraTask, AssigneeDistribution, StatusDistribution } from '@/lib/types';
import { Modal } from '@/components/ui/Modal'; import { Modal } from '@/components/ui/Modal';
import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { parseDate, formatDateForDisplay } from '@/lib/date-utils';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
@@ -144,7 +145,7 @@ export default function SprintDetailModal({
<div className="text-center"> <div className="text-center">
<div className="text-sm text-gray-600">Période</div> <div className="text-sm text-gray-600">Période</div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
{new Date(sprint.startDate).toLocaleDateString('fr-FR')} - {new Date(sprint.endDate).toLocaleDateString('fr-FR')} {formatDateForDisplay(parseDate(sprint.startDate))} - {formatDateForDisplay(parseDate(sprint.endDate))}
</div> </div>
</div> </div>
</div> </div>
@@ -318,7 +319,7 @@ export default function SprintDetailModal({
<div className="flex items-center gap-4 text-xs text-gray-500"> <div className="flex items-center gap-4 text-xs text-gray-500">
<span>📋 {issue.issuetype.name}</span> <span>📋 {issue.issuetype.name}</span>
<span>👤 {issue.assignee?.displayName || 'Non assigné'}</span> <span>👤 {issue.assignee?.displayName || 'Non assigné'}</span>
<span>📅 {new Date(issue.created).toLocaleDateString('fr-FR')}</span> <span>📅 {formatDateForDisplay(parseDate(issue.created))}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@ import { TagInput } from '@/components/ui/TagInput';
import { TaskStatus, TaskPriority } from '@/lib/types'; import { TaskStatus, TaskPriority } from '@/lib/types';
import { CreateTaskData } from '@/clients/tasks-client'; import { CreateTaskData } from '@/clients/tasks-client';
import { getAllPriorities } from '@/lib/status-config'; import { getAllPriorities } from '@/lib/status-config';
import { formatDateForDateTimeInput, parseDateTimeInput } from '@/lib/date-utils';
interface QuickAddTaskProps { interface QuickAddTaskProps {
status: TaskStatus; status: TaskStatus;
@@ -189,10 +190,10 @@ export function QuickAddTask({ status, onSubmit, onCancel, swimlaneContext }: Qu
<div className="flex items-center justify-between text-xs min-w-0"> <div className="flex items-center justify-between text-xs min-w-0">
<input <input
type="datetime-local" type="datetime-local"
value={formData.dueDate ? new Date(formData.dueDate.getTime() - formData.dueDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''} value={formData.dueDate ? formatDateForDateTimeInput(formData.dueDate) : ''}
onChange={(e) => setFormData(prev => ({ onChange={(e) => setFormData(prev => ({
...prev, ...prev,
dueDate: e.target.value ? new Date(e.target.value) : undefined dueDate: e.target.value ? parseDateTimeInput(e.target.value) : undefined
}))} }))}
onFocus={() => setActiveField('date')} onFocus={() => setActiveField('date')}
disabled={isSubmitting} disabled={isSubmitting}

Some files were not shown because too many files have changed in this diff Show More