import { prisma } from '@/services/core/database'; import { Prisma } from '@prisma/client'; import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData, BusinessError, DailyCheckboxType, TaskStatus, TaskPriority, TaskSource, Task, } from '@/lib/types'; import { getPreviousWorkday, normalizeDate, formatDateForAPI, getToday, getYesterday, } from '@/lib/date-utils'; /** * Service pour la gestion des checkboxes daily */ export class DailyService { /** * Récupère la vue daily pour une date donnée (checkboxes d'hier et d'aujourd'hui) */ async getDailyView(date: Date, userId: string): Promise { // Normaliser la date (début de journée) const today = normalizeDate(date); // Utiliser la logique de jour de travail précédent au lieu de jour-1 const yesterday = getPreviousWorkday(today); // Récupérer les checkboxes des deux jours const [yesterdayCheckboxes, todayCheckboxes] = await Promise.all([ this.getCheckboxesByDate(yesterday, userId), this.getCheckboxesByDate(today, userId), ]); return { date: today, yesterday: yesterdayCheckboxes, today: todayCheckboxes, }; } /** * Récupère toutes les checkboxes d'une date donnée pour un utilisateur */ async getCheckboxesByDate( date: Date, userId: string ): Promise { // Normaliser la date (début de journée) const normalizedDate = normalizeDate(date); const checkboxes = await prisma.dailyCheckbox.findMany({ where: { date: normalizedDate, userId: userId, }, include: { task: { include: { taskTags: { include: { tag: true, }, }, primaryTag: true, }, }, user: true, }, orderBy: { order: 'asc' }, }); return checkboxes.map(this.mapPrismaCheckbox); } /** * Ajoute une checkbox à une date donnée */ async addCheckbox(data: CreateDailyCheckboxData): Promise { // Normaliser la date const normalizedDate = normalizeDate(data.date); // Calculer l'ordre suivant pour cette date const maxOrder = await prisma.dailyCheckbox.aggregate({ where: { date: normalizedDate }, _max: { order: true }, }); const order = data.order ?? (maxOrder._max.order ?? -1) + 1; const checkbox = await prisma.dailyCheckbox.create({ data: { date: normalizedDate, text: data.text.trim(), type: data.type ?? 'task', taskId: data.taskId, userId: data.userId, order, isChecked: data.isChecked ?? false, }, include: { task: { include: { taskTags: { include: { tag: true, }, }, primaryTag: true, }, }, user: true, }, }); return this.mapPrismaCheckbox(checkbox); } /** * Met à jour une checkbox */ async updateCheckbox( checkboxId: string, data: UpdateDailyCheckboxData ): Promise { const updateData: Prisma.DailyCheckboxUpdateInput = {}; if (data.text !== undefined) updateData.text = data.text.trim(); if (data.isChecked !== undefined) updateData.isChecked = data.isChecked; if (data.type !== undefined) updateData.type = data.type; if (data.taskId !== undefined) { if (data.taskId === null) { updateData.task = { disconnect: true }; } else { updateData.task = { connect: { id: data.taskId } }; } } if (data.order !== undefined) updateData.order = data.order; if (data.date !== undefined) updateData.date = normalizeDate(data.date); const checkbox = await prisma.dailyCheckbox.update({ where: { id: checkboxId }, data: updateData, include: { task: { include: { taskTags: { include: { tag: true, }, }, primaryTag: true, }, }, user: true, }, }); return this.mapPrismaCheckbox(checkbox); } /** * Toggle l'état d'une checkbox par son ID (indépendant de la date) */ async toggleCheckbox(checkboxId: string): Promise { const existing = await prisma.dailyCheckbox.findUnique({ where: { id: checkboxId }, }); if (!existing) { throw new BusinessError('Checkbox non trouvée'); } const updated = await prisma.dailyCheckbox.update({ where: { id: checkboxId }, data: { isChecked: !existing.isChecked, updatedAt: new Date() }, include: { task: { include: { taskTags: { include: { tag: true, }, }, primaryTag: true, }, }, user: true, }, }); return this.mapPrismaCheckbox(updated); } /** * Supprime une checkbox */ async deleteCheckbox(checkboxId: string): Promise { const checkbox = await prisma.dailyCheckbox.findUnique({ where: { id: checkboxId }, }); if (!checkbox) { throw new BusinessError('Checkbox non trouvée'); } await prisma.dailyCheckbox.delete({ where: { id: checkboxId }, }); } /** * Réordonne les checkboxes d'une date donnée */ async reorderCheckboxes(date: Date, checkboxIds: string[]): Promise { await prisma.$transaction(async (prisma) => { for (let i = 0; i < checkboxIds.length; i++) { await prisma.dailyCheckbox.update({ where: { id: checkboxIds[i] }, data: { order: i }, }); } }); } /** * Recherche dans les checkboxes */ async searchCheckboxes( query: string, limit: number = 20 ): Promise { const checkboxes = await prisma.dailyCheckbox.findMany({ where: { text: { contains: query, }, }, include: { task: { include: { taskTags: { include: { tag: true, }, }, primaryTag: true, }, }, user: true, }, orderBy: { date: 'desc' }, take: limit, }); return checkboxes.map(this.mapPrismaCheckbox); } /** * Récupère l'historique des checkboxes (groupées par date) */ async getCheckboxHistory( userId: string, limit: number = 30 ): Promise<{ date: Date; checkboxes: DailyCheckbox[] }[]> { // Récupérer les dates distinctes des dernières checkboxes pour cet utilisateur const distinctDates = await prisma.dailyCheckbox.findMany({ where: { userId }, select: { date: true }, distinct: ['date'], orderBy: { date: 'desc' }, take: limit, }); const history = []; for (const { date } of distinctDates) { const checkboxes = await this.getCheckboxesByDate(date, userId); if (checkboxes.length > 0) { history.push({ date, checkboxes }); } } return history; } /** * Récupère la vue daily d'aujourd'hui */ async getTodaysDailyView(userId: string): Promise { return this.getDailyView(getToday(), userId); } /** * Ajoute une checkbox pour aujourd'hui */ async addTodayCheckbox( userId: string, text: string, taskId?: string ): Promise { return this.addCheckbox({ date: getToday(), userId, text, taskId, }); } /** * Ajoute une checkbox pour hier */ async addYesterdayCheckbox( userId: string, text: string, taskId?: string ): Promise { return this.addCheckbox({ date: getYesterday(), userId, text, taskId, }); } /** * Mappe une checkbox Prisma vers notre interface * Accepte les checkboxes avec ou sans les relations taskTags et primaryTag */ private mapPrismaCheckbox( checkbox: Prisma.DailyCheckboxGetPayload<{ include: { task: | true | { include: { taskTags: { include: { tag: true; }; }; primaryTag: true; }; }; user: true; }; }> ): DailyCheckbox { // Extraire les tags de la tâche si elle existe let taskTags: string[] = []; let taskTagDetails: | Array<{ id: string; name: string; color: string; isPinned?: boolean }> | undefined = undefined; let taskPrimaryTag: | { id: string; name: string; color: string; isPinned?: boolean } | undefined = undefined; if (checkbox.task) { // Vérifier si taskTags est disponible (peut être true ou un objet avec include) const taskWithTags = checkbox.task as unknown as { taskTags?: Array<{ tag: { id: string; name: string; color: string; isPinned: boolean }; }>; primaryTag?: { id: string; name: string; color: string; isPinned: boolean; } | null; }; if ( 'taskTags' in taskWithTags && taskWithTags.taskTags && Array.isArray(taskWithTags.taskTags) ) { // Utiliser les relations Prisma pour récupérer les noms et détails des tags taskTags = taskWithTags.taskTags.map((tt) => tt.tag.name); taskTagDetails = taskWithTags.taskTags.map((tt) => ({ id: tt.tag.id, name: tt.tag.name, color: tt.tag.color, isPinned: tt.tag.isPinned, })); } // Extraire le primaryTag si disponible if ('primaryTag' in taskWithTags && taskWithTags.primaryTag) { taskPrimaryTag = { id: taskWithTags.primaryTag.id, name: taskWithTags.primaryTag.name, color: taskWithTags.primaryTag.color, isPinned: taskWithTags.primaryTag.isPinned, }; } } return { id: checkbox.id, date: checkbox.date, text: checkbox.text, isChecked: checkbox.isChecked, type: checkbox.type as DailyCheckboxType, order: checkbox.order, taskId: checkbox.taskId || undefined, userId: checkbox.userId, task: checkbox.task ? { id: checkbox.task.id, title: checkbox.task.title, description: checkbox.task.description || undefined, status: checkbox.task.status as TaskStatus, priority: checkbox.task.priority as TaskPriority, source: checkbox.task.source as TaskSource, sourceId: checkbox.task.sourceId || undefined, tags: taskTags, tagDetails: taskTagDetails, primaryTagId: checkbox.task.primaryTagId || undefined, primaryTag: taskPrimaryTag, dueDate: checkbox.task.dueDate || undefined, completedAt: checkbox.task.completedAt || undefined, createdAt: checkbox.task.createdAt, updatedAt: checkbox.task.updatedAt, jiraProject: checkbox.task.jiraProject || undefined, jiraKey: checkbox.task.jiraKey || undefined, assignee: checkbox.task.assignee || undefined, ownerId: (checkbox.task as unknown as { ownerId: string }).ownerId, // Cast temporaire jusqu'à ce que Prisma soit mis à jour } : undefined, isArchived: checkbox.text.includes('[ARCHIVÉ]'), createdAt: checkbox.createdAt, updatedAt: checkbox.updatedAt, }; } /** * Récupère toutes les dates qui ont des checkboxes (pour le calendrier) */ async getDailyDates(userId?: string): Promise { const whereConditions: Prisma.DailyCheckboxWhereInput = {}; // Filtrer par utilisateur si spécifié if (userId) { whereConditions.userId = userId; } const checkboxes = await prisma.dailyCheckbox.findMany({ where: whereConditions, select: { date: true, }, distinct: ['date'], orderBy: { date: 'desc', }, }); return checkboxes.map((checkbox) => { return formatDateForAPI(checkbox.date); }); } /** * Récupère toutes les dates de fin (dueDate) des tâches non terminées (pour le calendrier) * Retourne un objet avec les dates comme clés et les tâches associées */ async getTaskDeadlineDates( userId: string ): Promise> { const tasks = await prisma.task.findMany({ where: { ownerId: userId, dueDate: { not: null, }, status: { notIn: ['done', 'cancelled', 'archived'], }, }, select: { id: true, title: true, dueDate: true, }, orderBy: { dueDate: 'asc', }, }); const datesMap: Record = {}; tasks.forEach((task) => { if (!task.dueDate) return; // Normaliser la date pour éviter les problèmes de timezone const normalizedDate = normalizeDate(task.dueDate); const dateKey = formatDateForAPI(normalizedDate); if (!datesMap[dateKey]) { datesMap[dateKey] = []; } datesMap[dateKey].push(task.title); }); return datesMap; } /** * Récupère les tâches avec deadline pour une date donnée * Retourne un tableau de tâches complètes */ async getTasksByDeadlineDate(userId: string, date: Date): Promise { const normalizedDate = normalizeDate(date); const dateKey = formatDateForAPI(normalizedDate); const tasks = await prisma.task.findMany({ where: { ownerId: userId, dueDate: { not: null, }, status: { notIn: ['done', 'cancelled', 'archived'], }, }, include: { taskTags: { include: { tag: true, }, }, primaryTag: true, _count: { select: { dailyCheckboxes: true, }, }, }, orderBy: { dueDate: 'asc', }, }); // Filtrer les tâches dont la date de fin correspond à la date demandée const filteredTasks = tasks .filter((task) => { if (!task.dueDate) return false; const taskDateKey = formatDateForAPI(normalizeDate(task.dueDate)); return taskDateKey === dateKey; }) .map((task) => ({ id: task.id, title: task.title, description: task.description ?? undefined, status: task.status as TaskStatus, priority: task.priority as TaskPriority, source: task.source as TaskSource, sourceId: task.sourceId ?? undefined, tags: task.taskTags.map((tt) => tt.tag.name), tagDetails: task.taskTags.map((tt) => ({ id: tt.tag.id, name: tt.tag.name, color: tt.tag.color, isPinned: tt.tag.isPinned, })), primaryTagId: task.primaryTagId ?? undefined, primaryTag: task.primaryTag ? { id: task.primaryTag.id, name: task.primaryTag.name, color: task.primaryTag.color, isPinned: task.primaryTag.isPinned, } : undefined, dueDate: task.dueDate ?? undefined, completedAt: task.completedAt ?? undefined, createdAt: task.createdAt, updatedAt: task.updatedAt, jiraProject: task.jiraProject ?? undefined, jiraKey: task.jiraKey ?? undefined, jiraType: task.jiraType ?? undefined, tfsProject: task.tfsProject ?? undefined, tfsPullRequestId: task.tfsPullRequestId ?? undefined, tfsRepository: task.tfsRepository ?? undefined, tfsSourceBranch: task.tfsSourceBranch ?? undefined, tfsTargetBranch: task.tfsTargetBranch ?? undefined, assignee: task.assignee ?? undefined, ownerId: (task as unknown as { ownerId: string }).ownerId, todosCount: task._count.dailyCheckboxes, })); return filteredTasks; } /** * Récupère toutes les checkboxes non cochées (tâches en attente) */ async getPendingCheckboxes(options?: { maxDays?: number; excludeToday?: boolean; type?: DailyCheckboxType; limit?: number; userId?: string; // Filtrer par utilisateur }): Promise { const today = normalizeDate(getToday()); const maxDays = options?.maxDays ?? 30; const excludeToday = options?.excludeToday ?? true; // Calculer la date limite (maxDays jours en arrière) const limitDate = new Date(today); limitDate.setDate(limitDate.getDate() - maxDays); // Construire les conditions de filtrage const whereConditions: Prisma.DailyCheckboxWhereInput = { isChecked: false, date: { gte: limitDate, ...(excludeToday ? { lt: today } : { lte: today }), }, }; // Filtrer par type si spécifié if (options?.type) { whereConditions.type = options.type; } // Filtrer par utilisateur si spécifié if (options?.userId) { whereConditions.userId = options.userId; // S'assurer que si le todo est lié à une tâche, cette tâche appartient bien à l'utilisateur whereConditions.OR = [ { task: null }, // Todos standalone (sans tâche associée) { task: { ownerId: options.userId } }, // Todos liés à une tâche de l'utilisateur ]; } const checkboxes = await prisma.dailyCheckbox.findMany({ where: whereConditions, include: { task: { include: { taskTags: { include: { tag: true, }, }, primaryTag: true, }, }, user: true, }, orderBy: [{ date: 'desc' }, { order: 'asc' }], ...(options?.limit ? { take: options.limit } : {}), }); return checkboxes.map(this.mapPrismaCheckbox); } /** * Archive une checkbox (marque comme archivée sans la cocher) */ async archiveCheckbox(checkboxId: string): Promise { // Pour l'instant, on utilise un champ text pour marquer comme archivé // Plus tard on pourra ajouter un champ dédié dans la DB const checkbox = await prisma.dailyCheckbox.update({ where: { id: checkboxId }, data: { text: (await prisma.dailyCheckbox.findUnique({ where: { id: checkboxId } })) ?.text + ' [ARCHIVÉ]', updatedAt: new Date(), }, include: { task: { include: { taskTags: { include: { tag: true, }, }, primaryTag: true, }, }, user: true, }, }); return this.mapPrismaCheckbox(checkbox); } /** * Déplace une checkbox non cochée à aujourd'hui */ async moveCheckboxToToday(checkboxId: string): Promise { const checkbox = await prisma.dailyCheckbox.findUnique({ where: { id: checkboxId }, }); if (!checkbox) { throw new BusinessError('Checkbox non trouvée'); } if (checkbox.isChecked) { throw new BusinessError('Impossible de déplacer une tâche déjà cochée'); } const today = normalizeDate(getToday()); // Vérifier si la checkbox est déjà pour aujourd'hui if (normalizeDate(checkbox.date).getTime() === today.getTime()) { throw new BusinessError("La tâche est déjà programmée pour aujourd'hui"); } // Calculer l'ordre suivant pour aujourd'hui const maxOrder = await prisma.dailyCheckbox.aggregate({ where: { date: today }, _max: { order: true }, }); const newOrder = (maxOrder._max.order ?? -1) + 1; const updatedCheckbox = await prisma.dailyCheckbox.update({ where: { id: checkboxId }, data: { date: today, order: newOrder, updatedAt: new Date(), }, include: { task: { include: { taskTags: { include: { tag: true, }, }, primaryTag: true, }, }, user: true, }, }); return this.mapPrismaCheckbox(updatedCheckbox); } } // Instance singleton du service export const dailyService = new DailyService();