From 124e8baee885d24f9952aaca846d3a4ae871255b Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sun, 14 Sep 2025 08:15:22 +0200 Subject: [PATCH] feat: overhaul TODO.md and enhance Kanban components - Updated TODO.md to reflect the new project structure and phases, marking several tasks as completed. - Enhanced Kanban components with a tech-inspired design, including new styles for columns and task cards. - Removed the obsolete reminders service and task processor, streamlining the codebase for better maintainability. - Introduced a modern API for task management, including CRUD operations and improved error handling. - Updated global styles for a cohesive dark theme and added custom scrollbar styles. --- TODO.md | 183 +++++++-------- components/kanban/Board.tsx | 23 +- components/kanban/Column.tsx | 78 +++++-- components/kanban/TaskCard.tsx | 109 +++++---- components/ui/Header.tsx | 92 +++++--- lib/config.ts | 74 ++----- scripts/reset-database.ts | 87 ++++++++ scripts/seed-data.ts | 100 +++++++++ services/database.ts | 2 +- services/reminders.ts | 332 ---------------------------- services/task-processor.ts | 310 -------------------------- services/tasks.ts | 237 ++++++++++++++++++++ src/app/api/config/route.ts | 100 --------- src/app/api/sync/reminders/route.ts | 60 ----- src/app/api/tasks/route.ts | 105 ++++++++- src/app/api/test/route.ts | 59 ----- src/app/globals.css | 43 +++- src/app/page.tsx | 17 +- 18 files changed, 857 insertions(+), 1154 deletions(-) create mode 100644 scripts/reset-database.ts create mode 100644 scripts/seed-data.ts delete mode 100644 services/reminders.ts delete mode 100644 services/task-processor.ts create mode 100644 services/tasks.ts delete mode 100644 src/app/api/config/route.ts delete mode 100644 src/app/api/sync/reminders/route.ts delete mode 100644 src/app/api/test/route.ts diff --git a/TODO.md b/TODO.md index 19ea599..5e2a4d5 100644 --- a/TODO.md +++ b/TODO.md @@ -1,131 +1,138 @@ -# TowerControl - Plan de développement +# TowerControl v2.0 - Gestionnaire de tâches moderne -## Phase 1: Setup initial et architecture backend (Priorité 1) +## ✅ 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 pour commencer) +- [x] Configurer base de données (SQLite local) - [x] Setup Prisma ORM -### 1.2 Architecture backend - Services de base +### 1.2 Architecture backend standalone - [x] Créer `services/database.ts` - Pool de connexion DB -- [x] Créer `services/reminders.ts` - Service pour récupérer les rappels macOS -- [x] Créer `lib/types.ts` - Types partagés (Task, Tag, Project, etc.) -- [x] Créer `services/task-processor.ts` - Logique métier des tâches +- [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 Intégration Rappels macOS (Focus principal Phase 1) -- [x] Rechercher comment accéder aux rappels macOS en local (SQLite, AppleScript, ou API) -- [x] Créer script d'extraction des rappels depuis la DB locale macOS -- [x] Parser les tags et catégories des rappels -- [x] Mapper les données vers le modèle interne -- [x] Créer service de synchronisation périodique +### 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` -### 1.4 API Routes essentielles (terminé) -- [x] `app/api/tasks/route.ts` - CRUD tâches -- [x] `app/api/sync/reminders/route.ts` - Synchronisation rappels -- [x] `app/api/config/route.ts` - Configuration et test des listes -- [x] `app/api/test/route.ts` - Tests des services +**Architecture finale** : App standalone avec backend propre et API REST moderne -**Note** : Privilégier SSR avec appels directs aux services plutôt que créer plus de routes API +## 🎯 Phase 2: Interface utilisateur moderne (EN COURS) -## Phase 2: Interface utilisateur Kanban (Priorité 2) +### 2.1 Système de design et composants UI +- [ ] Créer les composants UI de base (Button, Input, Card, Modal, etc.) +- [ ] Implémenter le système de design (couleurs, typographie, spacing) +- [ ] Setup Tailwind CSS avec design tokens personnalisés +- [ ] Créer une palette de couleurs moderne et accessible -### 2.1 Composants de base +### 2.2 Composants Kanban existants (à améliorer) - [x] `components/kanban/Board.tsx` - Tableau Kanban principal -- [x] `components/kanban/Column.tsx` - Colonnes du Kanban +- [x] `components/kanban/Column.tsx` - Colonnes du Kanban - [x] `components/kanban/TaskCard.tsx` - Cartes de tâches - [x] `components/ui/Header.tsx` - Header avec statistiques +- [ ] Améliorer le design et l'UX des composants existants -### 2.2 Clients HTTP +### 2.3 Clients HTTP et hooks - [ ] `clients/tasks-client.ts` - Client pour les tâches - [ ] `clients/base/http-client.ts` - Client HTTP de base +- [ ] `hooks/useTasks.ts` - Hook pour la gestion des tâches +- [ ] `hooks/useKanban.ts` - Hook pour drag & drop - [ ] Gestion des erreurs et loading states -### 2.3 Hooks React -- [ ] `hooks/useTasks.ts` - Hook pour la gestion des tâches -- [ ] `hooks/useKanban.ts` - Hook pour la logique Kanban (drag & drop) -- [ ] `hooks/useSync.ts` - Hook pour la synchronisation +### 2.4 Fonctionnalités Kanban avancées +- [ ] Drag & drop entre colonnes (react-beautiful-dnd) +- [ ] Formulaires de création/édition de tâches +- [ ] Filtrage par tags/statut/priorité +- [ ] Recherche en temps réel dans les tâches +- [ ] Gestion des tags avec couleurs -### 2.4 Interface Kanban -- [ ] Affichage des tâches par statut/tag -- [ ] Drag & drop entre colonnes -- [ ] Filtrage par tags/projets -- [ ] Recherche dans les tâches +## 📊 Phase 3: Dashboard et analytics (Priorité 3) -## Phase 3: Intégration Jira (Priorité 3) +### 3.1 Page d'accueil/dashboard +- [ ] Créer une page d'accueil moderne avec vue d'ensemble +- [ ] Widgets de statistiques (tâches par statut, priorité, etc.) +- [ ] Graphiques de productivité (tâches complétées par jour/semaine) +- [ ] Indicateurs de performance personnels -### 3.1 Services Jira -- [ ] `services/jira-client.ts` - Client Jira API -- [ ] `services/jira-sync.ts` - Synchronisation des tâches Jira -- [ ] Gestion multi-projets Jira -- [ ] Mapping des statuts Jira vers Kanban interne +### 3.2 Analytics et métriques +- [ ] `services/analytics.ts` - Calculs statistiques +- [ ] Métriques de productivité (vélocité, temps moyen, etc.) +- [ ] Graphiques avec Chart.js ou Recharts +- [ ] Export des données en CSV/JSON -### 3.2 API Routes Jira -- [ ] `app/api/jira/projects/route.ts` - Liste des projets -- [ ] `app/api/jira/tasks/route.ts` - Tâches Jira -- [ ] `app/api/jira/sync/route.ts` - Synchronisation -- [ ] Configuration des credentials Jira +## 🔧 Phase 4: Fonctionnalités avancées (Priorité 4) -### 3.3 Interface Jira -- [ ] Sélecteur de projets Jira -- [ ] Affichage mixte rappels + Jira dans le Kanban -- [ ] Indicateurs visuels pour différencier les sources +### 4.1 Gestion avancée des tâches +- [ ] Sous-tâches et hiérarchie +- [ ] Dates d'échéance et rappels +- [ ] Assignation et collaboration +- [ ] Templates de tâches -## Phase 4: Statistiques équipe (Priorité 4) +### 4.2 Personnalisation et thèmes +- [ ] Mode sombre/clair +- [ ] Personnalisation des couleurs +- [ ] Configuration des colonnes Kanban +- [ ] Préférences utilisateur -### 4.1 Services analytics -- [ ] `services/team-analytics.ts` - Calculs statistiques équipe -- [ ] `services/jira-team-sync.ts` - Récupération données équipe -- [ ] Agrégation des métriques (vélocité, burndown, etc.) +## 🚀 Phase 5: Intégrations futures (Priorité 5) -### 4.2 Dashboard équipe -- [ ] `components/dashboard/TeamStats.tsx` - Statistiques équipe -- [ ] `components/charts/` - Graphiques (vélocité, burndown, etc.) -- [ ] `app/team/page.tsx` - Page dédiée équipe -- [ ] Filtres par période, membre, projet +### 5.1 Intégrations externes (optionnel) +- [ ] Import/Export depuis d'autres outils +- [ ] API webhooks pour intégrations +- [ ] Synchronisation cloud (optionnel) +- [ ] Notifications push -## Phase 5: Outils additionnels (Priorité 5) - -### 5.1 Intégrations futures -- [ ] Calendrier (événements, deadlines) -- [ ] Notifications (rappels, alertes) -- [ ] Export/Import de données -- [ ] Thèmes et personnalisation - -### 5.2 Optimisations -- [ ] Cache Redis pour les données Jira +### 5.2 Optimisations et performance - [ ] Optimisation des requêtes DB -- [ ] Pagination des tâches -- [ ] Mode offline basique +- [ ] Pagination et virtualisation +- [ ] Cache côté client +- [ ] PWA et mode offline -## Configuration technique +## 🛠️ Configuration technique -### Stack +### Stack moderne - **Frontend**: Next.js 14, React, TypeScript, Tailwind CSS -- **Backend**: Next.js API Routes, Prisma ORM -- **Database**: SQLite (local) → PostgreSQL (production) -- **Intégrations**: macOS Reminders, Jira API -- **UI**: Shadcn/ui, React DnD pour le Kanban +- **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 -### Structure respectée +### Architecture respectée ``` -/services/ # Accès DB et logique métier -/app/api/ # Routes API utilisant les services -/clients/ # Clients HTTP frontend -/components/ # Composants React (pas de logique métier) -/hooks/ # Hooks React -/lib/ # Types et utilitaires partagés +src/app/ +├── api/tasks/ # API CRUD complète +├── page.tsx # Page principale +└── layout.tsx + +services/ +├── database.ts # Pool Prisma +└── tasks.ts # Service tâches standalone + +components/ +├── kanban/ # Board Kanban +├── ui/ # Composants UI de base +└── dashboard/ # Widgets dashboard (futur) + +clients/ # Clients HTTP (à créer) +hooks/ # Hooks React (à créer) +lib/ +├── types.ts # Types TypeScript +└── config.ts # Config app moderne ``` -## Prochaines étapes immédiates +## 🎯 Prochaines étapes immédiates -1. **Setup Next.js** avec la structure de dossiers -2. **Recherche technique** : Comment accéder aux rappels macOS localement -3. **Créer le service `reminders.ts`** pour l'extraction des données -4. **API de base** pour les tâches et synchronisation +1. **Créer les composants UI de base** (Button, Input, Card, Modal) +2. **Implémenter le système de design** avec Tailwind +3. **Améliorer le Kanban** avec un design moderne +4. **Ajouter drag & drop** entre les colonnes +5. **Créer les formulaires** de tâches --- -*Ce plan se concentre d'abord sur le backend et les rappels macOS comme demandé. Chaque phase peut être développée indépendamment.* +*Focus sur l'expérience utilisateur et le design moderne. App standalone prête pour évoluer.* diff --git a/components/kanban/Board.tsx b/components/kanban/Board.tsx index 56c559f..3d7f250 100644 --- a/components/kanban/Board.tsx +++ b/components/kanban/Board.tsx @@ -56,16 +56,19 @@ export function KanbanBoard({ tasks }: KanbanBoardProps) { ]; return ( -
- {columns.map((column) => ( - - ))} +
+ {/* Board tech dark */} +
+ {columns.map((column) => ( + + ))} +
); } diff --git a/components/kanban/Column.tsx b/components/kanban/Column.tsx index fe01c9e..b1803fb 100644 --- a/components/kanban/Column.tsx +++ b/components/kanban/Column.tsx @@ -9,41 +9,73 @@ interface KanbanColumnProps { } export function KanbanColumn({ id, title, color, tasks }: KanbanColumnProps) { - const colorClasses = { - gray: 'border-gray-300 bg-gray-50 dark:border-gray-600 dark:bg-gray-800', - blue: 'border-blue-300 bg-blue-50 dark:border-blue-600 dark:bg-blue-900/20', - green: 'border-green-300 bg-green-50 dark:border-green-600 dark:bg-green-900/20', - red: 'border-red-300 bg-red-50 dark:border-red-600 dark:bg-red-900/20' + // Couleurs tech/cyberpunk + const techStyles = { + gray: { + border: 'border-slate-700', + glow: 'shadow-slate-500/20', + accent: 'text-slate-400', + badge: 'bg-slate-800 text-slate-300 border border-slate-600' + }, + blue: { + border: 'border-cyan-500/30', + glow: 'shadow-cyan-500/20', + accent: 'text-cyan-400', + badge: 'bg-cyan-950 text-cyan-300 border border-cyan-500/30' + }, + green: { + border: 'border-emerald-500/30', + glow: 'shadow-emerald-500/20', + accent: 'text-emerald-400', + badge: 'bg-emerald-950 text-emerald-300 border border-emerald-500/30' + }, + red: { + border: 'border-red-500/30', + glow: 'shadow-red-500/20', + accent: 'text-red-400', + badge: 'bg-red-950 text-red-300 border border-red-500/30' + } }; - const headerColorClasses = { - gray: 'text-gray-700 dark:text-gray-300', - blue: 'text-blue-700 dark:text-blue-300', - green: 'text-green-700 dark:text-green-300', - red: 'text-red-700 dark:text-red-300' + const style = techStyles[color as keyof typeof techStyles]; + + // Icônes tech + const techIcons = { + todo: '⚡', + in_progress: '🔄', + done: '✓', + cancelled: '✕' }; return ( -
- {/* En-tête de colonne */} -
+
+ {/* Header tech avec glow */} +
-

- {title} -

- - {tasks.length} +
+
+

+ {title} +

+
+ + {String(tasks.length).padStart(2, '0')}
- {/* Zone de contenu */} -
+ {/* Zone de contenu tech */} +
{tasks.length === 0 ? ( -
-
📝
-

Aucune tâche

+
+
+ {techIcons[id]} +
+

NO DATA

+
+
+
) : ( tasks.map((task) => ( diff --git a/components/kanban/TaskCard.tsx b/components/kanban/TaskCard.tsx index e682176..fb66aa5 100644 --- a/components/kanban/TaskCard.tsx +++ b/components/kanban/TaskCard.tsx @@ -27,83 +27,74 @@ export function TaskCard({ task }: TaskCardProps) { }; return ( -
- {/* En-tête avec emojis */} - {emojis.length > 0 && ( -
- {emojis.map((emoji, index) => ( - - {emoji} - - ))} -
- )} +
+ {/* Header tech avec titre et status */} +
+ {emojis.length > 0 && ( +
+ {emojis.slice(0, 2).map((emoji, index) => ( + + {emoji} + + ))} +
+ )} + +

+ {titleWithoutEmojis} +

+ + {/* Indicateur de priorité tech */} +
+
- {/* Titre */} -

- {titleWithoutEmojis} -

- - {/* Description si présente */} + {/* Description tech */} {task.description && ( -

+

{task.description}

)} - {/* Tags */} -
- {/* Priorité */} - - {task.priority} - - - {/* Source */} - - {task.source} - - - {/* Tags personnalisés */} - {task.tags && task.tags.length > 0 && ( - task.tags.slice(0, 2).map((tag, index) => ( + {/* Tags tech style */} + {task.tags && task.tags.length > 0 && ( +
+ {task.tags.slice(0, 3).map((tag, index) => ( - #{tag} + {tag} - )) - )} -
- - {/* Footer avec dates */} -
-
- {task.dueDate && ( - - 📅 {formatDistanceToNow(new Date(task.dueDate), { - addSuffix: true, - locale: fr - })} + ))} + {task.tags.length > 3 && ( + + +{task.tags.length - 3} )}
- -
- {task.completedAt ? ( - - ✅ {formatDistanceToNow(new Date(task.completedAt), { + )} + + {/* Footer tech avec séparateur néon */} +
+
+ {task.dueDate ? ( + + + {formatDistanceToNow(new Date(task.dueDate), { addSuffix: true, locale: fr })} ) : ( - - Créé {formatDistanceToNow(new Date(task.createdAt), { - addSuffix: true, - locale: fr - })} - + --:-- + )} + + {task.completedAt && ( + ✓ DONE )}
diff --git a/components/ui/Header.tsx b/components/ui/Header.tsx index 156de42..11736d9 100644 --- a/components/ui/Header.tsx +++ b/components/ui/Header.tsx @@ -12,43 +12,46 @@ interface HeaderProps { export function Header({ title, subtitle, stats }: HeaderProps) { return ( -
-
-
- {/* Titre et sous-titre */} -
-

- {title} -

-

- {subtitle} -

+
+
+
+ {/* Titre tech avec glow */} +
+
+
+

+ {title} +

+

+ {subtitle} +

+
- {/* Statistiques */} -
+ {/* Stats tech dashboard */} +
@@ -66,20 +69,47 @@ interface StatCardProps { } function StatCard({ label, value, color }: StatCardProps) { - const colorClasses = { - blue: 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800', - green: 'bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-300 dark:border-green-800', - yellow: 'bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-300 dark:border-yellow-800', - gray: 'bg-gray-50 text-gray-700 border-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700', - purple: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-900/20 dark:text-purple-300 dark:border-purple-800' + const techStyles = { + blue: { + bg: 'bg-slate-800/50', + border: 'border-cyan-500/30', + text: 'text-cyan-300', + glow: 'shadow-cyan-500/20' + }, + green: { + bg: 'bg-slate-800/50', + border: 'border-emerald-500/30', + text: 'text-emerald-300', + glow: 'shadow-emerald-500/20' + }, + yellow: { + bg: 'bg-slate-800/50', + border: 'border-yellow-500/30', + text: 'text-yellow-300', + glow: 'shadow-yellow-500/20' + }, + gray: { + bg: 'bg-slate-800/50', + border: 'border-slate-500/30', + text: 'text-slate-300', + glow: 'shadow-slate-500/20' + }, + purple: { + bg: 'bg-slate-800/50', + border: 'border-purple-500/30', + text: 'text-purple-300', + glow: 'shadow-purple-500/20' + } }; + const style = techStyles[color]; + return ( -
-
+
+
{label}
-
+
{value}
diff --git a/lib/config.ts b/lib/config.ts index b11b44a..114270f 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -1,41 +1,37 @@ /** - * Configuration de l'application TowerControl + * Configuration de l'application TowerControl (version standalone) */ export interface AppConfig { - reminders: { - targetList: string; - syncInterval: number; // en minutes - enabledLists: string[]; + app: { + name: string; + version: string; }; - jira: { - baseUrl?: string; - username?: string; - apiToken?: string; - projects: string[]; + ui: { + theme: 'light' | 'dark' | 'system'; + itemsPerPage: number; }; - sync: { - autoSync: boolean; - batchSize: number; + features: { + enableDragAndDrop: boolean; + enableNotifications: boolean; + autoSave: boolean; }; } // Configuration par défaut const defaultConfig: AppConfig = { - reminders: { - targetList: process.env.REMINDERS_TARGET_LIST || 'Boulot', - syncInterval: parseInt(process.env.REMINDERS_SYNC_INTERVAL || '15'), - enabledLists: (process.env.REMINDERS_ENABLED_LISTS || 'Boulot').split(',') + app: { + name: 'TowerControl', + version: '2.0.0' }, - jira: { - baseUrl: process.env.JIRA_BASE_URL, - username: process.env.JIRA_USERNAME, - apiToken: process.env.JIRA_API_TOKEN, - projects: (process.env.JIRA_PROJECTS || '').split(',').filter(p => p.length > 0) + ui: { + theme: (process.env.NEXT_PUBLIC_THEME as 'light' | 'dark' | 'system') || 'system', + itemsPerPage: parseInt(process.env.NEXT_PUBLIC_ITEMS_PER_PAGE || '50') }, - sync: { - autoSync: process.env.AUTO_SYNC === 'true', - batchSize: parseInt(process.env.SYNC_BATCH_SIZE || '50') + features: { + enableDragAndDrop: process.env.NEXT_PUBLIC_ENABLE_DRAG_DROP !== 'false', + enableNotifications: process.env.NEXT_PUBLIC_ENABLE_NOTIFICATIONS === 'true', + autoSave: process.env.NEXT_PUBLIC_AUTO_SAVE !== 'false' } }; @@ -46,33 +42,11 @@ export function getConfig(): AppConfig { return defaultConfig; } -/** - * Récupère la liste cible des rappels - */ -export function getTargetRemindersList(): string { - return getConfig().reminders.targetList; -} - -/** - * Récupère les listes autorisées pour la synchronisation - */ -export function getEnabledRemindersLists(): string[] { - return getConfig().reminders.enabledLists; -} - -/** - * Vérifie si une liste est autorisée pour la synchronisation - */ -export function isListEnabled(listName: string): boolean { - const enabledLists = getEnabledRemindersLists(); - return enabledLists.includes(listName); -} - /** * Configuration pour le développement/debug */ export const DEBUG_CONFIG = { - logAppleScript: process.env.NODE_ENV === 'development', - mockData: process.env.USE_MOCK_DATA === 'true', - verboseLogging: process.env.VERBOSE_LOGGING === 'true' + isDevelopment: process.env.NODE_ENV === 'development', + verboseLogging: process.env.VERBOSE_LOGGING === 'true', + enableDevTools: process.env.NODE_ENV === 'development' }; diff --git a/scripts/reset-database.ts b/scripts/reset-database.ts new file mode 100644 index 0000000..e29670d --- /dev/null +++ b/scripts/reset-database.ts @@ -0,0 +1,87 @@ +import { prisma } from '../services/database'; + +/** + * Script pour reset la base de données et supprimer les anciennes données + */ +async function resetDatabase() { + console.log('🗑️ Reset de la base de données...'); + console.log('==================================='); + + try { + // Compter les tâches avant suppression + const beforeCount = await prisma.task.count(); + const manualCount = await prisma.task.count({ where: { source: 'manual' } }); + const remindersCount = await prisma.task.count({ where: { source: 'reminders' } }); + + console.log(`📊 État actuel:`); + console.log(` Total: ${beforeCount} tâches`); + console.log(` Manuelles: ${manualCount} tâches`); + console.log(` Rappels: ${remindersCount} tâches`); + console.log(''); + + // Supprimer toutes les tâches de synchronisation + const deletedTasks = await prisma.task.deleteMany({ + where: { + source: 'reminders' + } + }); + + console.log(`✅ Supprimé ${deletedTasks.count} tâches de synchronisation`); + + // Supprimer les logs de sync + const deletedLogs = await prisma.syncLog.deleteMany(); + console.log(`✅ Supprimé ${deletedLogs.count} logs de synchronisation`); + + // Supprimer les tags orphelins (optionnel) + const deletedTags = await prisma.tag.deleteMany(); + console.log(`✅ Supprimé ${deletedTags.count} tags`); + + // Compter après nettoyage + const afterCount = await prisma.task.count(); + + console.log(''); + console.log('🎉 Base de données nettoyée !'); + console.log(`📊 Résultat: ${afterCount} tâches restantes`); + + // Afficher les tâches restantes + if (afterCount > 0) { + console.log(''); + console.log('📋 Tâches restantes:'); + const remainingTasks = await prisma.task.findMany({ + orderBy: { createdAt: 'desc' } + }); + + remainingTasks.forEach((task, index) => { + const statusEmoji = { + 'todo': '⏳', + 'in_progress': '🔄', + 'done': '✅', + 'cancelled': '❌' + }[task.status] || '❓'; + + const tags = JSON.parse(task.tagsJson || '[]'); + const tagsStr = tags.length > 0 ? ` [${tags.join(', ')}]` : ''; + + console.log(` ${index + 1}. ${statusEmoji} ${task.title}${tagsStr}`); + }); + } + + } catch (error) { + console.error('❌ Erreur lors du reset:', error); + throw error; + } +} + +// Exécuter le script +if (require.main === module) { + resetDatabase().then(() => { + console.log(''); + console.log('✨ Reset terminé avec succès !'); + process.exit(0); + }).catch((error) => { + console.error('💥 Erreur fatale:', error); + process.exit(1); + }); +} + +export { resetDatabase }; diff --git a/scripts/seed-data.ts b/scripts/seed-data.ts new file mode 100644 index 0000000..e6c987e --- /dev/null +++ b/scripts/seed-data.ts @@ -0,0 +1,100 @@ +import { tasksService } from '../services/tasks'; +import { TaskStatus, TaskPriority } from '../lib/types'; + +/** + * Script pour ajouter des données de test avec tags et variété + */ +async function seedTestData() { + console.log('🌱 Ajout de données de test...'); + console.log('================================'); + + const testTasks = [ + { + title: '🎨 Redesign du dashboard', + description: 'Créer une interface moderne et intuitive pour le tableau de bord principal', + status: 'in_progress' as TaskStatus, + priority: 'high' as TaskPriority, + tags: ['design', 'ui', 'frontend'], + dueDate: new Date('2025-01-20') + }, + { + title: '🔧 Optimiser les performances API', + description: 'Améliorer les temps de réponse des endpoints et ajouter la pagination', + status: 'todo' as TaskStatus, + priority: 'medium' as TaskPriority, + tags: ['backend', 'performance', 'api'], + dueDate: new Date('2025-01-25') + }, + { + title: '✅ Tests unitaires composants', + description: 'Ajouter des tests Jest/RTL pour les composants principaux', + status: 'done' as TaskStatus, + priority: 'medium' as TaskPriority, + tags: ['testing', 'jest', 'quality'], + dueDate: new Date('2025-01-10') + } + ]; + + let createdCount = 0; + let errorCount = 0; + + for (const taskData of testTasks) { + try { + const task = await tasksService.createTask(taskData); + + const statusEmoji = { + 'todo': '⏳', + 'in_progress': '🔄', + 'done': '✅', + 'cancelled': '❌' + }[task.status]; + + const priorityEmoji = { + 'low': '🔵', + 'medium': '🟡', + 'high': '🔴' + }[task.priority]; + + console.log(` ${statusEmoji} ${priorityEmoji} ${task.title}`); + console.log(` Tags: ${task.tags?.join(', ') || 'aucun'}`); + if (task.dueDate) { + console.log(` Échéance: ${task.dueDate.toLocaleDateString('fr-FR')}`); + } + console.log(''); + + createdCount++; + } catch (error) { + console.error(` ❌ Erreur pour "${taskData.title}":`, error instanceof Error ? error.message : error); + errorCount++; + } + } + + console.log('📊 Résumé:'); + console.log(` ✅ Tâches créées: ${createdCount}`); + console.log(` ❌ Erreurs: ${errorCount}`); + + // Afficher les stats finales + const stats = await tasksService.getTaskStats(); + console.log(''); + console.log('📈 Statistiques finales:'); + console.log(` Total: ${stats.total} tâches`); + console.log(` À faire: ${stats.todo}`); + console.log(` En cours: ${stats.inProgress}`); + console.log(` Terminées: ${stats.completed}`); + console.log(` Annulées: ${stats.cancelled}`); + console.log(` Taux de completion: ${stats.completionRate}%`); +} + +// Exécuter le script +if (require.main === module) { + seedTestData().then(() => { + console.log(''); + console.log('✨ Données de test ajoutées avec succès !'); + process.exit(0); + }).catch((error) => { + console.error('💥 Erreur fatale:', error); + process.exit(1); + }); +} + +export { seedTestData }; diff --git a/services/database.ts b/services/database.ts index 761e6d0..20d90a1 100644 --- a/services/database.ts +++ b/services/database.ts @@ -39,7 +39,7 @@ export async function closeDatabaseConnection(): Promise { // Fonction utilitaire pour les transactions export async function withTransaction( - callback: (tx: PrismaClient) => Promise + callback: (tx: Omit) => Promise ): Promise { return await prisma.$transaction(async (tx) => { return await callback(tx); diff --git a/services/reminders.ts b/services/reminders.ts deleted file mode 100644 index 386f127..0000000 --- a/services/reminders.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { exec } from 'child_process'; -import { promisify } from 'util'; -import { MacOSReminder } from '@/lib/types'; -import { getTargetRemindersList, getEnabledRemindersLists, isListEnabled, DEBUG_CONFIG } from '@/lib/config'; - -const execAsync = promisify(exec); - -/** - * Service pour récupérer les rappels macOS via AppleScript - * Approche sécurisée qui utilise l'API officielle d'Apple - */ -export class RemindersService { - - /** - * Récupère tous les rappels depuis l'app Rappels macOS - * Utilise la configuration pour filtrer les listes autorisées - */ - async getAllReminders(): Promise { - try { - if (DEBUG_CONFIG.mockData) { - console.log('🔧 Mode mock activé - utilisation des données de test'); - return this.getMockReminders(); - } - - // Récupérer uniquement les listes autorisées - return await this.getRemindersFromEnabledLists(); - } catch (error) { - console.error('Erreur lors de la récupération des rappels:', error); - return this.getMockReminders(); - } - } - - /** - * Récupère les rappels uniquement des listes autorisées en configuration - */ - async getRemindersFromEnabledLists(): Promise { - try { - const reminders: MacOSReminder[] = []; - const enabledLists = getEnabledRemindersLists(); - - console.log(`📋 Synchronisation des listes autorisées: ${enabledLists.join(', ')}`); - - for (const listName of enabledLists) { - try { - console.log(`🔄 Traitement de la liste: ${listName}`); - const listReminders = await this.getRemindersFromListSimple(listName); - - if (listReminders.length > 0) { - console.log(`✅ ${listReminders.length} rappels trouvés dans "${listName}"`); - reminders.push(...listReminders); - } else { - console.log(`ℹ️ Aucun rappel dans "${listName}"`); - } - } catch (error) { - console.error(`❌ Erreur pour la liste ${listName}:`, error); - } - } - - console.log(`📊 Total: ${reminders.length} rappels récupérés`); - return reminders; - } catch (error) { - console.error('Erreur getRemindersFromEnabledLists:', error); - return this.getMockReminders(); - } - } - - /** - * Récupère les rappels de la liste cible principale uniquement - */ - async getTargetListReminders(): Promise { - try { - const targetList = getTargetRemindersList(); - console.log(`🎯 Récupération de la liste cible: ${targetList}`); - - return await this.getRemindersFromListSimple(targetList); - } catch (error) { - console.error('Erreur getTargetListReminders:', error); - return []; - } - } - - /** - * Récupère les rappels d'une liste spécifique - */ - async getRemindersByList(listName: string): Promise { - try { - const script = ` - tell application "Reminders" - set remindersList to {} - set targetList to list "${listName}" - - repeat with reminder in reminders of targetList - set reminderRecord to {id:(id of reminder as string), title:(name of reminder), notes:(body of reminder), completed:(completed of reminder), dueDate:missing value, completionDate:missing value, priority:(priority of reminder), list:"${listName}"} - - -- Gérer la date d'échéance - try - set dueDate of reminderRecord to (due date of reminder as string) - end try - - -- Gérer la date de completion - try - if completed of reminder then - set completionDate of reminderRecord to (completion date of reminder as string) - end if - end try - - set end of remindersList to reminderRecord - end repeat - - return remindersList - end tell - `; - - const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}' 2>/dev/null || echo "[]"`); - - return this.parseAppleScriptOutput(stdout); - } catch (error) { - console.error(`Erreur lors de la récupération des rappels de la liste ${listName}:`, error); - return []; - } - } - - /** - * Récupère la liste des listes de rappels - */ - async getReminderLists(): Promise { - try { - const script = ` - tell application "Reminders" - set listNames to {} - repeat with reminderList in lists - set end of listNames to name of reminderList - end repeat - return listNames - end tell - `; - - const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}' 2>/dev/null || echo ""`); - - // Parse la sortie AppleScript pour extraire les noms de listes - const lists = stdout.trim().split(', ').filter(list => list.length > 0); - return lists; - } catch (error) { - console.error('Erreur lors de la récupération des listes:', error); - return []; - } - } - - /** - * Test si l'app Rappels est accessible - */ - async testRemindersAccess(): Promise { - try { - const script = ` - tell application "Reminders" - return count of lists - end tell - `; - - await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}' 2>/dev/null`); - return true; - } catch (error) { - console.error('Impossible d\'accéder à l\'app Rappels:', error); - return false; - } - } - - /** - * Parse la sortie AppleScript en objets MacOSReminder - */ - private parseAppleScriptOutput(output: string): MacOSReminder[] { - try { - console.log('Sortie AppleScript brute:', output); - - // Si pas de sortie ou sortie vide, retourner tableau vide - if (!output || output.trim() === '' || output.trim() === '{}') { - return []; - } - - // Pour l'instant, on utilise une approche simple avec des données réelles - // TODO: Implémenter le parsing complet de la sortie AppleScript - return this.getRemindersFromEnabledLists(); - } catch (error) { - console.error('Erreur lors du parsing AppleScript:', error); - return []; - } - } - - - /** - * Récupère les rappels d'une liste avec une approche simple - */ - private async getRemindersFromListSimple(listName: string): Promise { - try { - if (DEBUG_CONFIG.verboseLogging) { - console.log(`🔍 Récupération des rappels de la liste: ${listName}`); - } - - // Script simple pour récupérer les infos de base - const script = ` - tell application "Reminders" - set remindersList to {} - try - set targetList to (first list whose name is "${listName}") - - repeat with r in reminders of targetList - try - set reminderInfo to (name of r) & "|" & (completed of r) & "|" & (priority of r) & "|" & "${listName}" - set end of remindersList to reminderInfo - end try - end repeat - on error errMsg - return "ERROR: " & errMsg - end try - - return remindersList - end tell - `; - - const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}' 2>/dev/null || echo ""`); - - if (DEBUG_CONFIG.logAppleScript) { - console.log(`📝 Sortie AppleScript pour ${listName}:`, stdout.substring(0, 200)); - } - - // Vérifier si il y a une erreur dans la sortie - if (stdout.includes('ERROR:')) { - console.error(`❌ Erreur AppleScript pour ${listName}:`, stdout); - return []; - } - - return this.parseSimpleReminderOutput(stdout, listName); - } catch (error) { - console.error(`❌ Erreur getRemindersFromListSimple pour ${listName}:`, error); - return []; - } - } - - /** - * Parse la sortie simple des rappels - */ - private parseSimpleReminderOutput(output: string, listName: string): MacOSReminder[] { - try { - if (!output || output.trim() === '') return []; - - // Nettoyer la sortie AppleScript - const cleanOutput = output.trim().replace(/^{|}$/g, ''); - if (!cleanOutput) return []; - - const reminderStrings = cleanOutput.split(', '); - const reminders: MacOSReminder[] = []; - - for (let i = 0; i < reminderStrings.length; i++) { - const reminderStr = reminderStrings[i].replace(/"/g, ''); - const parts = reminderStr.split('|'); - - if (parts.length >= 4) { - const [title, completed, priority, list] = parts; - - reminders.push({ - id: `${listName}-${i}`, - title: title.trim(), - completed: completed.trim() === 'true', - priority: parseInt(priority.trim()) || 0, - list: list.trim(), - tags: this.extractTagsFromTitle(title.trim()) - }); - } - } - - return reminders; - } catch (error) { - console.error('Erreur parseSimpleReminderOutput:', error); - return []; - } - } - - /** - * Extrait les tags du titre (format #tag) - */ - private extractTagsFromTitle(title: string): string[] { - const tagRegex = /#(\w+)/g; - const tags: string[] = []; - let match; - - while ((match = tagRegex.exec(title)) !== null) { - tags.push(match[1]); - } - - return tags; - } - - /** - * Données de test pour le développement - */ - private getMockReminders(): MacOSReminder[] { - return [ - { - id: 'mock-1', - title: 'Finir le service reminders', - notes: 'Implémenter la récupération des rappels macOS', - completed: false, - dueDate: new Date('2025-01-16'), - priority: 5, - list: 'Travail', - tags: ['dev', 'backend'] - }, - { - id: 'mock-2', - title: 'Tester l\'intégration Jira', - notes: 'Configurer l\'API Jira pour récupérer les tâches', - completed: false, - dueDate: new Date('2025-01-18'), - priority: 9, - list: 'Projets', - tags: ['jira', 'api'] - }, - { - id: 'mock-3', - title: 'Créer le Kanban board', - completed: true, - completionDate: new Date('2025-01-10'), - priority: 5, - list: 'Travail', - tags: ['ui', 'frontend'] - } - ]; - } -} - -// Instance singleton -export const remindersService = new RemindersService(); diff --git a/services/task-processor.ts b/services/task-processor.ts deleted file mode 100644 index 1a333f0..0000000 --- a/services/task-processor.ts +++ /dev/null @@ -1,310 +0,0 @@ -import { prisma } from './database'; -import { remindersService } from './reminders'; -import { Task, TaskStatus, TaskPriority, MacOSReminder, SyncLog, BusinessError } from '@/lib/types'; -import { Prisma } from '@prisma/client'; - -/** - * Service pour traiter et synchroniser les tâches - * Contient toute la logique métier pour les tâches - */ -export class TaskProcessorService { - - /** - * Synchronise les rappels macOS avec la base de données - */ - async syncRemindersToDatabase(): Promise { - const startTime = Date.now(); - let tasksSync = 0; - - try { - // Récupérer les rappels depuis macOS - const reminders = await remindersService.getAllReminders(); - - // Traiter chaque rappel - for (const reminder of reminders) { - await this.processReminder(reminder); - tasksSync++; - } - - // Créer le log de synchronisation - const syncLog = await prisma.syncLog.create({ - data: { - source: 'reminders', - status: 'success', - message: `Synchronisé ${tasksSync} rappels en ${Date.now() - startTime}ms`, - tasksSync - } - }); - - console.log(`✅ Sync reminders terminée: ${tasksSync} tâches`); - return syncLog; - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue'; - - const syncLog = await prisma.syncLog.create({ - data: { - source: 'reminders', - status: 'error', - message: `Erreur de sync: ${errorMessage}`, - tasksSync - } - }); - - console.error('❌ Erreur sync reminders:', error); - return syncLog; - } - } - - /** - * Traite un rappel macOS et le sauvegarde/met à jour en base - */ - private async processReminder(reminder: MacOSReminder): Promise { - const taskData = this.mapReminderToTask(reminder); - - try { - // Upsert (insert ou update) de la tâche - await prisma.task.upsert({ - where: { - source_sourceId: { - source: 'reminders', - sourceId: reminder.id - } - }, - update: { - title: taskData.title, - description: taskData.description, - status: taskData.status, - priority: taskData.priority, - tagsJson: JSON.stringify(taskData.tags || []), - dueDate: taskData.dueDate, - completedAt: taskData.completedAt, - updatedAt: new Date() - }, - create: { - title: taskData.title, - description: taskData.description, - status: taskData.status, - priority: taskData.priority, - source: 'reminders', - sourceId: reminder.id, - tagsJson: JSON.stringify(taskData.tags || []), - dueDate: taskData.dueDate, - completedAt: taskData.completedAt - } - }); - - // Gérer les tags - if (taskData.tags && taskData.tags.length > 0) { - await this.processTags(taskData.tags); - } - - } catch (error) { - console.error(`Erreur lors du traitement du rappel ${reminder.id}:`, error); - throw error; - } - } - - /** - * Convertit un rappel macOS en objet Task - */ - private mapReminderToTask(reminder: MacOSReminder): Partial { - return { - title: reminder.title, - description: reminder.notes || undefined, - status: this.mapReminderStatus(reminder), - priority: this.mapReminderPriority(reminder.priority), - tags: reminder.tags || [], - dueDate: reminder.dueDate || undefined, - completedAt: reminder.completionDate || undefined - }; - } - - /** - * Convertit le statut d'un rappel macOS en TaskStatus - */ - private mapReminderStatus(reminder: MacOSReminder): TaskStatus { - if (reminder.completed) { - return 'done'; - } - - // Si la tâche a une date d'échéance passée, elle est en retard - if (reminder.dueDate && reminder.dueDate < new Date()) { - return 'todo'; // On garde 'todo' mais on pourrait ajouter un statut 'overdue' - } - - return 'todo'; - } - - /** - * Convertit la priorité macOS (0-9) en TaskPriority - */ - private mapReminderPriority(macosPriority: number): TaskPriority { - switch (macosPriority) { - case 0: return 'low'; - case 1: return 'low'; - case 5: return 'medium'; - case 9: return 'high'; - default: return 'medium'; - } - } - - /** - * Traite et crée les tags s'ils n'existent pas - */ - private async processTags(tagNames: string[]): Promise { - for (const tagName of tagNames) { - try { - await prisma.tag.upsert({ - where: { name: tagName }, - update: {}, // Pas de mise à jour nécessaire - create: { - name: tagName, - color: this.generateTagColor(tagName) - } - }); - } catch (error) { - console.error(`Erreur lors de la création du tag ${tagName}:`, error); - } - } - } - - /** - * Génère une couleur pour un tag basée sur son nom - */ - private generateTagColor(tagName: string): string { - const colors = [ - '#ef4444', '#f97316', '#f59e0b', '#eab308', - '#84cc16', '#22c55e', '#10b981', '#14b8a6', - '#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1', - '#8b5cf6', '#a855f7', '#d946ef', '#ec4899' - ]; - - // Hash simple du nom pour choisir une couleur - let hash = 0; - for (let i = 0; i < tagName.length; i++) { - hash = tagName.charCodeAt(i) + ((hash << 5) - hash); - } - - return colors[Math.abs(hash) % colors.length]; - } - - /** - * Récupère toutes les tâches avec filtres optionnels - */ - async getTasks(filters?: { - status?: TaskStatus[]; - source?: string[]; - search?: string; - limit?: number; - offset?: number; - }): Promise { - const where: Prisma.TaskWhereInput = {}; - - if (filters?.status) { - where.status = { in: filters.status }; - } - - if (filters?.source) { - where.source = { in: filters.source }; - } - - if (filters?.search) { - where.OR = [ - { title: { contains: filters.search, mode: 'insensitive' } }, - { description: { contains: filters.search, mode: 'insensitive' } } - ]; - } - - const tasks = await prisma.task.findMany({ - where, - take: filters?.limit || 100, - skip: filters?.offset || 0, - orderBy: [ - { completedAt: 'desc' }, - { dueDate: 'asc' }, - { createdAt: 'desc' } - ] - }); - - return tasks.map(this.mapPrismaTaskToTask); - } - - /** - * Met à jour le statut d'une tâche - */ - async updateTaskStatus(taskId: string, newStatus: TaskStatus): Promise { - const task = await prisma.task.findUnique({ - where: { id: taskId } - }); - - if (!task) { - throw new BusinessError(`Tâche ${taskId} introuvable`); - } - - // Logique métier : si on marque comme terminé, on ajoute la date - const updateData: Prisma.TaskUpdateInput = { - status: newStatus, - updatedAt: new Date() - }; - - if (newStatus === 'done' && !task.completedAt) { - updateData.completedAt = new Date(); - } else if (newStatus !== 'done' && task.completedAt) { - updateData.completedAt = null; - } - - const updatedTask = await prisma.task.update({ - where: { id: taskId }, - data: updateData - }); - - return this.mapPrismaTaskToTask(updatedTask); - } - - /** - * Convertit une tâche Prisma en objet Task - */ - private mapPrismaTaskToTask(prismaTask: any): Task { - return { - id: prismaTask.id, - title: prismaTask.title, - description: prismaTask.description, - status: prismaTask.status as TaskStatus, - priority: prismaTask.priority as TaskPriority, - source: prismaTask.source, - sourceId: prismaTask.sourceId, - tags: JSON.parse(prismaTask.tagsJson || '[]'), - dueDate: prismaTask.dueDate, - completedAt: prismaTask.completedAt, - createdAt: prismaTask.createdAt, - updatedAt: prismaTask.updatedAt, - jiraProject: prismaTask.jiraProject, - jiraKey: prismaTask.jiraKey, - assignee: prismaTask.assignee - }; - } - - /** - * Récupère les statistiques des tâches - */ - async getTaskStats() { - const [total, completed, inProgress, todo] = await Promise.all([ - prisma.task.count(), - prisma.task.count({ where: { status: 'done' } }), - prisma.task.count({ where: { status: 'in_progress' } }), - prisma.task.count({ where: { status: 'todo' } }) - ]); - - return { - total, - completed, - inProgress, - todo, - completionRate: total > 0 ? Math.round((completed / total) * 100) : 0 - }; - } -} - -// Instance singleton -export const taskProcessorService = new TaskProcessorService(); diff --git a/services/tasks.ts b/services/tasks.ts new file mode 100644 index 0000000..f9c0da7 --- /dev/null +++ b/services/tasks.ts @@ -0,0 +1,237 @@ +import { prisma } from './database'; +import { Task, TaskStatus, TaskPriority, TaskSource, BusinessError } from '@/lib/types'; +import { Prisma } from '@prisma/client'; + +/** + * Service pour la gestion des tâches (version standalone) + */ +export class TasksService { + + /** + * Récupère toutes les tâches avec filtres optionnels + */ + async getTasks(filters?: { + status?: TaskStatus[]; + search?: string; + limit?: number; + offset?: number; + }): Promise { + const where: Prisma.TaskWhereInput = {}; + + if (filters?.status) { + where.status = { in: filters.status }; + } + + if (filters?.search) { + where.OR = [ + { title: { contains: filters.search } }, + { description: { contains: filters.search } } + ]; + } + + const tasks = await prisma.task.findMany({ + where, + take: filters?.limit || 100, + skip: filters?.offset || 0, + orderBy: [ + { completedAt: 'desc' }, + { dueDate: 'asc' }, + { createdAt: 'desc' } + ] + }); + + return tasks.map(this.mapPrismaTaskToTask); + } + + /** + * Crée une nouvelle tâche + */ + async createTask(taskData: { + title: string; + description?: string; + status?: TaskStatus; + priority?: TaskPriority; + tags?: string[]; + dueDate?: Date; + }): Promise { + const task = await prisma.task.create({ + data: { + title: taskData.title, + description: taskData.description, + status: taskData.status || 'todo', + priority: taskData.priority || 'medium', + tagsJson: JSON.stringify(taskData.tags || []), + dueDate: taskData.dueDate, + source: 'manual', // Source manuelle + sourceId: `manual-${Date.now()}` // ID unique + } + }); + + // Gérer les tags + if (taskData.tags && taskData.tags.length > 0) { + await this.processTags(taskData.tags); + } + + return this.mapPrismaTaskToTask(task); + } + + /** + * Met à jour une tâche + */ + async updateTask(taskId: string, updates: { + title?: string; + description?: string; + status?: TaskStatus; + priority?: TaskPriority; + tags?: string[]; + dueDate?: Date; + }): Promise { + const task = await prisma.task.findUnique({ + where: { id: taskId } + }); + + if (!task) { + throw new BusinessError(`Tâche ${taskId} introuvable`); + } + + // Logique métier : si on marque comme terminé, on ajoute la date + const updateData: Prisma.TaskUpdateInput = { + ...updates, + updatedAt: new Date() + }; + + if (updates.tags) { + updateData.tagsJson = JSON.stringify(updates.tags); + } + + if (updates.status === 'done' && !task.completedAt) { + updateData.completedAt = new Date(); + } else if (updates.status && updates.status !== 'done' && task.completedAt) { + updateData.completedAt = null; + } + + const updatedTask = await prisma.task.update({ + where: { id: taskId }, + data: updateData + }); + + // Gérer les tags + if (updates.tags && updates.tags.length > 0) { + await this.processTags(updates.tags); + } + + return this.mapPrismaTaskToTask(updatedTask); + } + + /** + * Supprime une tâche + */ + async deleteTask(taskId: string): Promise { + const task = await prisma.task.findUnique({ + where: { id: taskId } + }); + + if (!task) { + throw new BusinessError(`Tâche ${taskId} introuvable`); + } + + await prisma.task.delete({ + where: { id: taskId } + }); + } + + /** + * Met à jour le statut d'une tâche + */ + async updateTaskStatus(taskId: string, newStatus: TaskStatus): Promise { + return this.updateTask(taskId, { status: newStatus }); + } + + /** + * Récupère les statistiques des tâches + */ + async getTaskStats() { + const [total, completed, inProgress, todo, cancelled] = await Promise.all([ + prisma.task.count(), + prisma.task.count({ where: { status: 'done' } }), + prisma.task.count({ where: { status: 'in_progress' } }), + prisma.task.count({ where: { status: 'todo' } }), + prisma.task.count({ where: { status: 'cancelled' } }) + ]); + + return { + total, + completed, + inProgress, + todo, + cancelled, + completionRate: total > 0 ? Math.round((completed / total) * 100) : 0 + }; + } + + /** + * Traite et crée les tags s'ils n'existent pas + */ + private async processTags(tagNames: string[]): Promise { + for (const tagName of tagNames) { + try { + await prisma.tag.upsert({ + where: { name: tagName }, + update: {}, // Pas de mise à jour nécessaire + create: { + name: tagName, + color: this.generateTagColor(tagName) + } + }); + } catch (error) { + console.error(`Erreur lors de la création du tag ${tagName}:`, error); + } + } + } + + /** + * Génère une couleur pour un tag basée sur son nom + */ + private generateTagColor(tagName: string): string { + const colors = [ + '#ef4444', '#f97316', '#f59e0b', '#eab308', + '#84cc16', '#22c55e', '#10b981', '#14b8a6', + '#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1', + '#8b5cf6', '#a855f7', '#d946ef', '#ec4899' + ]; + + // Hash simple du nom pour choisir une couleur + let hash = 0; + for (let i = 0; i < tagName.length; i++) { + hash = tagName.charCodeAt(i) + ((hash << 5) - hash); + } + + return colors[Math.abs(hash) % colors.length]; + } + + /** + * Convertit une tâche Prisma en objet Task + */ + private mapPrismaTaskToTask(prismaTask: Prisma.TaskGetPayload): Task { + return { + id: prismaTask.id, + title: prismaTask.title, + description: prismaTask.description ?? undefined, + status: prismaTask.status as TaskStatus, + priority: prismaTask.priority as TaskPriority, + source: prismaTask.source as TaskSource, + sourceId: prismaTask.sourceId?? undefined, + tags: JSON.parse(prismaTask.tagsJson || '[]'), + dueDate: prismaTask.dueDate ?? undefined, + completedAt: prismaTask.completedAt ?? undefined, + createdAt: prismaTask.createdAt, + updatedAt: prismaTask.updatedAt, + jiraProject: prismaTask.jiraProject ?? undefined, + jiraKey: prismaTask.jiraKey ?? undefined, + assignee: prismaTask.assignee ?? undefined + }; + } +} + +// Instance singleton +export const tasksService = new TasksService(); diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts deleted file mode 100644 index a445956..0000000 --- a/src/app/api/config/route.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getConfig, getTargetRemindersList, getEnabledRemindersLists } from '@/lib/config'; -import { remindersService } from '@/services/reminders'; - -/** - * API route pour récupérer la configuration actuelle - */ -export async function GET() { - try { - const config = getConfig(); - const availableLists = await remindersService.getReminderLists(); - - return NextResponse.json({ - success: true, - config, - availableLists, - currentTarget: getTargetRemindersList(), - enabledLists: getEnabledRemindersLists() - }); - - } catch (error) { - console.error('❌ Erreur lors de la récupération de la config:', error); - - return NextResponse.json({ - success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' - }, { status: 500 }); - } -} - -/** - * API route pour tester l'accès à une liste spécifique - */ -export async function POST(request: Request) { - try { - const body = await request.json(); - const { listName, action } = body; - - if (!listName) { - return NextResponse.json({ - success: false, - error: 'listName est requis' - }, { status: 400 }); - } - - let result: any = {}; - - switch (action) { - case 'test': - // Tester l'accès à une liste spécifique - const reminders = await remindersService.getRemindersByList(listName); - result = { - listName, - accessible: true, - reminderCount: reminders.length, - sample: reminders.slice(0, 3).map(r => ({ - title: r.title, - completed: r.completed, - priority: r.priority - })) - }; - break; - - case 'preview': - // Prévisualiser les rappels d'une liste - const previewReminders = await remindersService.getRemindersByList(listName); - result = { - listName, - reminders: previewReminders.map(r => ({ - id: r.id, - title: r.title, - completed: r.completed, - priority: r.priority, - tags: r.tags || [] - })) - }; - break; - - default: - return NextResponse.json({ - success: false, - error: 'Action non supportée. Utilisez "test" ou "preview"' - }, { status: 400 }); - } - - return NextResponse.json({ - success: true, - action, - result - }); - - } catch (error) { - console.error('❌ Erreur lors du test de liste:', error); - - return NextResponse.json({ - success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' - }, { status: 500 }); - } -} diff --git a/src/app/api/sync/reminders/route.ts b/src/app/api/sync/reminders/route.ts deleted file mode 100644 index a542a6f..0000000 --- a/src/app/api/sync/reminders/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { NextResponse } from 'next/server'; -import { taskProcessorService } from '@/services/task-processor'; - -/** - * API route pour synchroniser les rappels macOS avec la base de données - */ -export async function POST() { - try { - console.log('🔄 Début de la synchronisation des rappels...'); - - const syncResult = await taskProcessorService.syncRemindersToDatabase(); - - return NextResponse.json({ - success: true, - message: 'Synchronisation des rappels terminée', - syncLog: syncResult - }); - - } catch (error) { - console.error('❌ Erreur lors de la synchronisation:', error); - - return NextResponse.json({ - success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue lors de la synchronisation' - }, { status: 500 }); - } -} - -/** - * API route pour obtenir le statut de la dernière synchronisation - */ -export async function GET() { - try { - // Récupérer les derniers logs de sync - const { prisma } = await import('@/services/database'); - - const lastSyncLogs = await prisma.syncLog.findMany({ - where: { source: 'reminders' }, - orderBy: { createdAt: 'desc' }, - take: 5 - }); - - const taskStats = await taskProcessorService.getTaskStats(); - - return NextResponse.json({ - success: true, - lastSyncLogs, - taskStats, - message: 'Statut de synchronisation récupéré' - }); - - } catch (error) { - console.error('❌ Erreur lors de la récupération du statut:', error); - - return NextResponse.json({ - success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' - }, { status: 500 }); - } -} diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index e7638a3..16b05dc 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server'; -import { taskProcessorService } from '@/services/task-processor'; -import { TaskStatus } from '@/lib/types'; +import { tasksService } from '@/services/tasks'; +import { TaskStatus, TaskPriority } from '@/lib/types'; /** * API route pour récupérer les tâches avec filtres optionnels @@ -10,7 +10,13 @@ export async function GET(request: Request) { const { searchParams } = new URL(request.url); // Extraire les paramètres de filtre - const filters: any = {}; + const filters: { + status?: TaskStatus[]; + source?: string[]; + search?: string; + limit?: number; + offset?: number; + } = {}; const status = searchParams.get('status'); if (status) { @@ -38,8 +44,8 @@ export async function GET(request: Request) { } // Récupérer les tâches - const tasks = await taskProcessorService.getTasks(filters); - const stats = await taskProcessorService.getTaskStats(); + const tasks = await tasksService.getTasks(filters); + const stats = await tasksService.getTaskStats(); return NextResponse.json({ success: true, @@ -60,26 +66,71 @@ export async function GET(request: Request) { } /** - * API route pour mettre à jour le statut d'une tâche + * API route pour créer une nouvelle tâche + */ +export async function POST(request: Request) { + try { + const body = await request.json(); + const { title, description, status, priority, tags, dueDate } = body; + + if (!title) { + return NextResponse.json({ + success: false, + error: 'Le titre est requis' + }, { status: 400 }); + } + + const task = await tasksService.createTask({ + title, + description, + status: status as TaskStatus, + priority: priority as TaskPriority, + tags, + dueDate: dueDate ? new Date(dueDate) : undefined + }); + + return NextResponse.json({ + success: true, + data: task, + message: 'Tâche créée avec succès' + }); + + } catch (error) { + console.error('❌ Erreur lors de la création de la tâche:', error); + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }, { status: 500 }); + } +} + +/** + * API route pour mettre à jour une tâche */ export async function PATCH(request: Request) { try { const body = await request.json(); - const { taskId, status } = body; + const { taskId, ...updates } = body; - if (!taskId || !status) { + if (!taskId) { return NextResponse.json({ success: false, - error: 'taskId et status sont requis' + error: 'taskId est requis' }, { status: 400 }); } - const updatedTask = await taskProcessorService.updateTaskStatus(taskId, status); + // Convertir dueDate si présent + if (updates.dueDate) { + updates.dueDate = new Date(updates.dueDate); + } + + const updatedTask = await tasksService.updateTask(taskId, updates); return NextResponse.json({ success: true, data: updatedTask, - message: `Tâche ${taskId} mise à jour avec le statut ${status}` + message: 'Tâche mise à jour avec succès' }); } catch (error) { @@ -91,3 +142,35 @@ export async function PATCH(request: Request) { }, { status: 500 }); } } + +/** + * API route pour supprimer une tâche + */ +export async function DELETE(request: Request) { + try { + const { searchParams } = new URL(request.url); + const taskId = searchParams.get('taskId'); + + if (!taskId) { + return NextResponse.json({ + success: false, + error: 'taskId est requis' + }, { status: 400 }); + } + + await tasksService.deleteTask(taskId); + + return NextResponse.json({ + success: true, + message: 'Tâche supprimée avec succès' + }); + + } catch (error) { + console.error('❌ Erreur lors de la suppression de la tâche:', error); + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }, { status: 500 }); + } +} diff --git a/src/app/api/test/route.ts b/src/app/api/test/route.ts deleted file mode 100644 index e1ccdea..0000000 --- a/src/app/api/test/route.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { NextResponse } from 'next/server'; -import { testDatabaseConnection } from '@/services/database'; -import { remindersService } from '@/services/reminders'; -import { taskProcessorService } from '@/services/task-processor'; - -/** - * API route de test pour vérifier que tous les services fonctionnent - */ -export async function GET() { - try { - const results = { - timestamp: new Date().toISOString(), - database: false, - reminders: false, - taskProcessor: false, - reminderLists: [] as string[], - taskStats: null as any - }; - - // Test de la base de données - try { - results.database = await testDatabaseConnection(); - } catch (error) { - console.error('Test DB failed:', error); - } - - // Test de l'accès aux rappels - try { - results.reminders = await remindersService.testRemindersAccess(); - if (results.reminders) { - results.reminderLists = await remindersService.getReminderLists(); - } - } catch (error) { - console.error('Test Reminders failed:', error); - } - - // Test du service de traitement des tâches - try { - results.taskStats = await taskProcessorService.getTaskStats(); - results.taskProcessor = true; - } catch (error) { - console.error('Test TaskProcessor failed:', error); - } - - return NextResponse.json({ - success: true, - message: 'Tests des services terminés', - results - }); - - } catch (error) { - console.error('Erreur dans l\'API de test:', error); - - return NextResponse.json({ - success: false, - error: error instanceof Error ? error.message : 'Erreur inconnue' - }, { status: 500 }); - } -} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..dd6e888 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,8 +1,8 @@ @import "tailwindcss"; :root { - --background: #ffffff; - --foreground: #171717; + --background: #020617; /* slate-950 */ + --foreground: #f1f5f9; /* slate-100 */ } @theme inline { @@ -12,15 +12,38 @@ --font-mono: var(--font-geist-mono); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - body { background: var(--background); color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; + font-family: var(--font-geist-mono), 'Courier New', monospace; + overflow-x: hidden; +} + +/* Scrollbar tech style */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #1e293b; /* slate-800 */ +} + +::-webkit-scrollbar-thumb { + background: #475569; /* slate-600 */ + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #06b6d4; /* cyan-500 */ +} + +/* Animations tech */ +@keyframes glow { + 0%, 100% { box-shadow: 0 0 5px rgba(6, 182, 212, 0.3); } + 50% { box-shadow: 0 0 20px rgba(6, 182, 212, 0.6); } +} + +.animate-glow { + animation: glow 2s ease-in-out infinite; } diff --git a/src/app/page.tsx b/src/app/page.tsx index c6dd65b..67a60ef 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,26 +1,23 @@ -import { taskProcessorService } from '@/services/task-processor'; -import { getTargetRemindersList } from '@/lib/config'; +import { tasksService } from '@/services/tasks'; import { KanbanBoard } from '../../components/kanban/Board'; import { Header } from '../../components/ui/Header'; export default async function HomePage() { - // SSR - Récupération des données côté serveur + // SSR - Récupération des données côté serveur (focus sur les tâches récentes) const [tasks, stats] = await Promise.all([ - taskProcessorService.getTasks({ limit: 100 }), - taskProcessorService.getTaskStats() + tasksService.getTasks({ limit: 20 }), // Réduire pour voir les nouvelles tâches + tasksService.getTaskStats() ]); - const targetList = getTargetRemindersList(); - return ( -
+
-
+