Files
towercontrol/services/tasks.ts
Julien Froidefond 02f59caf08 feat: add related todos functionality in task editing
- Implemented `RelatedTodos` component in `EditTaskForm` to display and manage related todos for a task.
- Updated `TasksClient` to fetch daily checkboxes related to a task.
- Enhanced `TasksService` with a method to retrieve related daily checkboxes from the database.
- Added functionality to create new todos linked to tasks in the daily actions.
- Marked the task for displaying related todos in the task editing interface as complete in TODO.md.
2025-09-18 15:26:07 +02:00

374 lines
10 KiB
TypeScript

import { prisma } from './database';
import { Task, TaskStatus, TaskPriority, TaskSource, BusinessError, DailyCheckbox, DailyCheckboxType } 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<Task[]> {
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<Task> {
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<Task> {
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<void> {
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<Task> {
return this.updateTask(taskId, { status: newStatus });
}
/**
* Récupère les daily checkboxes liées à une tâche
*/
async getTaskRelatedCheckboxes(taskId: string): Promise<DailyCheckbox[]> {
const checkboxes = await prisma.dailyCheckbox.findMany({
where: { taskId: taskId },
include: { task: true },
orderBy: [
{ date: 'desc' },
{ order: 'asc' }
]
});
return checkboxes.map(checkbox => ({
id: checkbox.id,
date: checkbox.date,
text: checkbox.text,
isChecked: checkbox.isChecked,
type: checkbox.type as DailyCheckboxType,
order: checkbox.order,
taskId: checkbox.taskId ?? undefined,
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: [], // Les tags ne sont pas nécessaires dans ce contexte
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,
jiraType: checkbox.task.jiraType ?? undefined,
assignee: checkbox.task.assignee ?? undefined
} : undefined,
createdAt: checkbox.createdAt,
updatedAt: checkbox.updatedAt
}));
}
/**
* Récupère les statistiques des tâches
*/
async getTaskStats() {
const [total, completed, inProgress, todo, backlog, cancelled, freeze, archived] = 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: 'backlog' } }),
prisma.task.count({ where: { status: 'cancelled' } }),
prisma.task.count({ where: { status: 'freeze' } }),
prisma.task.count({ where: { status: 'archived' } })
]);
return {
total,
completed,
inProgress,
todo,
backlog,
cancelled,
freeze,
archived,
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<void> {
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<void> {
// 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<object>): 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,
jiraType: prismaTask.jiraType ?? undefined,
assignee: prismaTask.assignee ?? undefined
};
}
}
// Instance singleton
export const tasksService = new TasksService();