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, include: { taskTags: { include: { tag: true } } }, take: filters?.limit, // Pas de limite par défaut - récupère toutes les tâches 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', dueDate: taskData.dueDate, source: 'manual', // Source manuelle sourceId: `manual-${Date.now()}` // ID unique }, include: { taskTags: { include: { tag: true } } } }); // Créer les relations avec les tags if (taskData.tags && taskData.tags.length > 0) { await this.createTaskTagRelations(task.id, taskData.tags); } // Récupérer la tâche avec les tags pour le retour const taskWithTags = await prisma.task.findUnique({ where: { id: task.id }, include: { taskTags: { include: { tag: true } } } }); return this.mapPrismaTaskToTask(taskWithTags!); } /** * 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 = { title: updates.title, description: updates.description, status: updates.status, priority: updates.priority, dueDate: updates.dueDate, updatedAt: new Date() }; if (updates.status === 'done' && !task.completedAt) { updateData.completedAt = new Date(); } else if (updates.status && updates.status !== 'done' && task.completedAt) { updateData.completedAt = null; } await prisma.task.update({ where: { id: taskId }, data: updateData }); // Mettre à jour les relations avec les tags if (updates.tags !== undefined) { await this.updateTaskTagRelations(taskId, updates.tags); } // Récupérer la tâche avec les tags pour le retour const taskWithTags = await prisma.task.findUnique({ where: { id: taskId }, include: { taskTags: { include: { tag: true } } } }); return this.mapPrismaTaskToTask(taskWithTags!); } /** * 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 }; } /** * Crée les relations TaskTag pour une tâche */ private async createTaskTagRelations(taskId: string, tagNames: string[]): Promise { for (const tagName of tagNames) { try { // Créer ou récupérer le tag const tag = await prisma.tag.upsert({ where: { name: tagName }, update: {}, // Pas de mise à jour nécessaire create: { name: tagName, color: this.generateTagColor(tagName) } }); // Créer la relation TaskTag si elle n'existe pas await prisma.taskTag.upsert({ where: { taskId_tagId: { taskId: taskId, tagId: tag.id } }, update: {}, // Pas de mise à jour nécessaire create: { taskId: taskId, tagId: tag.id } }); } catch (error) { console.error(`Erreur lors de la création de la relation tag ${tagName}:`, error); } } } /** * Met à jour les relations TaskTag pour une tâche */ private async updateTaskTagRelations(taskId: string, tagNames: string[]): Promise { // Supprimer toutes les relations existantes await prisma.taskTag.deleteMany({ where: { taskId: taskId } }); // Créer les nouvelles relations if (tagNames.length > 0) { await this.createTaskTagRelations(taskId, tagNames); } } /** * 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<{ include: { taskTags: { include: { tag: true } } } }> | Prisma.TaskGetPayload): Task { // Extraire les tags depuis les relations TaskTag ou fallback sur tagsJson let tags: string[] = []; if ('taskTags' in prismaTask && prismaTask.taskTags && Array.isArray(prismaTask.taskTags)) { // Utiliser les relations Prisma tags = prismaTask.taskTags.map((tt) => tt.tag.name); } 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: tags, 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();