feat: complete tag management and UI integration

- Marked multiple tasks as completed in TODO.md related to tag management features.
- Replaced manual tag input with `TagInput` component in `CreateTaskForm`, `EditTaskForm`, and `QuickAddTask` for better UX.
- Updated `TaskCard` to display tags using `TagDisplay` with color support.
- Enhanced `TasksService` to manage task-tag relationships with CRUD operations.
- Integrated tag management into the global context for better accessibility across components.
This commit is contained in:
Julien Froidefond
2025-09-14 16:44:22 +02:00
parent edbd82e8ac
commit c5a7d16425
27 changed files with 2055 additions and 224 deletions

View File

@@ -31,6 +31,13 @@ export class TasksService {
const tasks = await prisma.task.findMany({
where,
include: {
taskTags: {
include: {
tag: true
}
}
},
take: filters?.limit || 100,
skip: filters?.offset || 0,
orderBy: [
@@ -60,19 +67,37 @@ export class TasksService {
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
},
include: {
taskTags: {
include: {
tag: true
}
}
}
});
// Gérer les tags
// Créer les relations avec les tags
if (taskData.tags && taskData.tags.length > 0) {
await this.processTags(taskData.tags);
await this.createTaskTagRelations(task.id, taskData.tags);
}
return this.mapPrismaTaskToTask(task);
// 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!);
}
/**
@@ -104,9 +129,6 @@ export class TasksService {
updatedAt: new Date()
};
if (updates.tags) {
updateData.tagsJson = JSON.stringify(updates.tags);
}
if (updates.status === 'done' && !task.completedAt) {
updateData.completedAt = new Date();
@@ -114,17 +136,29 @@ export class TasksService {
updateData.completedAt = null;
}
const updatedTask = await prisma.task.update({
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);
// Mettre à jour les relations avec les tags
if (updates.tags !== undefined) {
await this.updateTaskTagRelations(taskId, updates.tags);
}
return this.mapPrismaTaskToTask(updatedTask);
// 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!);
}
/**
@@ -174,12 +208,13 @@ export class TasksService {
}
/**
* Traite et crée les tags s'ils n'existent pas
* Crée les relations TaskTag pour une tâche
*/
private async processTags(tagNames: string[]): Promise<void> {
private async createTaskTagRelations(taskId: string, tagNames: string[]): Promise<void> {
for (const tagName of tagNames) {
try {
await prisma.tag.upsert({
// 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: {
@@ -187,12 +222,42 @@ export class TasksService {
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 du tag ${tagName}:`, 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
*/
@@ -216,7 +281,23 @@ export class TasksService {
/**
* Convertit une tâche Prisma en objet Task
*/
private mapPrismaTaskToTask(prismaTask: Prisma.TaskGetPayload<object>): 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,
@@ -224,8 +305,8 @@ export class TasksService {
status: prismaTask.status as TaskStatus,
priority: prismaTask.priority as TaskPriority,
source: prismaTask.source as TaskSource,
sourceId: prismaTask.sourceId?? undefined,
tags: JSON.parse(prismaTask.tagsJson || '[]'),
sourceId: prismaTask.sourceId ?? undefined,
tags: tags,
dueDate: prismaTask.dueDate ?? undefined,
completedAt: prismaTask.completedAt ?? undefined,
createdAt: prismaTask.createdAt,