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();