+
{label}
-
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