From 7952459b42b408c93177018920c41c0dc87f0318 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sat, 11 Oct 2025 15:03:59 +0200 Subject: [PATCH] feat(Tags): implement user-specific tag management and enhance related services - Added ownerId field to Tag model to associate tags with users. - Updated tagsService methods to enforce user ownership in tag operations. - Enhanced API routes to include user authentication and ownership checks for tag retrieval and management. - Modified seeding script to assign tags to the first user found in the database. - Updated various components and services to ensure user-specific tag handling throughout the application. --- .../migration.sql | 59 ++++++++++ prisma/schema.prisma | 6 +- scripts/seed-tags.ts | 27 ++++- src/actions/tags.ts | 27 ++++- src/app/api/tags/[id]/route.ts | 10 +- src/app/api/tags/route.ts | 20 +++- src/app/kanban/page.tsx | 2 +- src/app/notes/page.tsx | 2 +- src/app/page.tsx | 2 +- src/app/settings/advanced/page.tsx | 2 +- src/app/settings/general/page.tsx | 21 +++- src/app/weekly-manager/page.tsx | 2 +- src/services/integrations/jira/jira.ts | 38 ++---- src/services/integrations/tfs/tfs.ts | 43 ++----- src/services/notes.ts | 98 ++++++++++------ src/services/task-management/tags.ts | 110 +++++++++++------- src/services/task-management/tasks.ts | 37 +++--- 17 files changed, 329 insertions(+), 177 deletions(-) create mode 100644 prisma/migrations/20250115120000_add_tag_owner_relation/migration.sql diff --git a/prisma/migrations/20250115120000_add_tag_owner_relation/migration.sql b/prisma/migrations/20250115120000_add_tag_owner_relation/migration.sql new file mode 100644 index 0000000..da6db52 --- /dev/null +++ b/prisma/migrations/20250115120000_add_tag_owner_relation/migration.sql @@ -0,0 +1,59 @@ +-- Migration pour ajouter ownerId aux tags +-- Les tags existants seront assignés au premier utilisateur +-- Cette version préserve les relations TaskTag existantes + +-- Étape 1: Ajouter la colonne ownerId temporairement nullable +ALTER TABLE "tags" ADD COLUMN "ownerId" TEXT; + +-- Étape 2: Assigner tous les tags existants au premier utilisateur +UPDATE "tags" +SET "ownerId" = ( + SELECT "id" FROM "users" + ORDER BY "createdAt" ASC + LIMIT 1 +) +WHERE "ownerId" IS NULL; + +-- Étape 3: Sauvegarder les relations TaskTag existantes avec les noms des tags +CREATE TEMPORARY TABLE "temp_task_tag_names" AS +SELECT tt."taskId", t."name" as "tagName" +FROM "task_tags" tt +JOIN "tags" t ON tt."tagId" = t."id"; + +-- Étape 4: Supprimer les anciennes relations TaskTag +DELETE FROM "task_tags"; + +-- Étape 5: Créer la nouvelle table avec ownerId non-nullable +CREATE TABLE "new_tags" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "color" TEXT NOT NULL DEFAULT '#6b7280', + "isPinned" BOOLEAN NOT NULL DEFAULT false, + "ownerId" TEXT NOT NULL, + CONSTRAINT "new_tags_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- Étape 6: Copier les données des tags +INSERT INTO "new_tags" ("id", "name", "color", "isPinned", "ownerId") +SELECT "id", "name", "color", "isPinned", "ownerId" FROM "tags"; + +-- Étape 7: Supprimer l'ancienne table +DROP TABLE "tags"; + +-- Étape 8: Renommer la nouvelle table +ALTER TABLE "new_tags" RENAME TO "tags"; + +-- Étape 9: Créer l'index unique pour (name, ownerId) +CREATE UNIQUE INDEX "tags_name_ownerId_key" ON "tags"("name", "ownerId"); + +-- Étape 10: Restaurer les relations TaskTag en utilisant les noms des tags +INSERT INTO "task_tags" ("taskId", "tagId") +SELECT tt."taskId", t."id" as "tagId" +FROM "temp_task_tag_names" tt +JOIN "tags" t ON tt."tagName" = t."name" +WHERE EXISTS ( + SELECT 1 FROM "tasks" WHERE "tasks"."id" = tt."taskId" +); + +-- Étape 11: Nettoyer la table temporaire +DROP TABLE "temp_task_tag_names"; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0b46fe9..b3756fa 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,6 +24,7 @@ model User { notes Note[] dailyCheckboxes DailyCheckbox[] tasks Task[] @relation("TaskOwner") + tags Tag[] @relation("TagOwner") @@map("users") } @@ -63,13 +64,16 @@ model Task { model Tag { id String @id @default(cuid()) - name String @unique + name String color String @default("#6b7280") isPinned Boolean @default(false) + ownerId String // Chaque tag appartient à un utilisateur + owner User @relation("TagOwner", fields: [ownerId], references: [id], onDelete: Cascade) taskTags TaskTag[] primaryTasks Task[] @relation("PrimaryTag") noteTags NoteTag[] + @@unique([name, ownerId]) // Un nom de tag unique par utilisateur @@map("tags") } diff --git a/scripts/seed-tags.ts b/scripts/seed-tags.ts index 07abf9e..c562076 100644 --- a/scripts/seed-tags.ts +++ b/scripts/seed-tags.ts @@ -1,7 +1,22 @@ +import { PrismaClient } from '@prisma/client'; import { tagsService } from '../src/services/task-management/tags'; +const prisma = new PrismaClient(); + async function seedTags() { - console.log('🏷️ Création des tags de test...'); + console.log('🌱 Début du seeding des tags...'); + + // Récupérer le premier utilisateur pour assigner les tags + const firstUser = await prisma.user.findFirst({ + orderBy: { createdAt: 'asc' }, + }); + + if (!firstUser) { + console.log("❌ Aucun utilisateur trouvé. Créez d'abord un utilisateur."); + return; + } + + console.log(`👤 Assignation des tags à: ${firstUser.email}`); const testTags = [ { name: 'frontend', color: '#3B82F6' }, @@ -19,9 +34,15 @@ async function seedTags() { for (const tagData of testTags) { try { - const existing = await tagsService.getTagByName(tagData.name); + const existing = await tagsService.getTagByName( + tagData.name, + firstUser.id + ); if (!existing) { - const tag = await tagsService.createTag(tagData); + const tag = await tagsService.createTag({ + ...tagData, + userId: firstUser.id, + }); console.log(`✅ Tag créé: ${tag.name} (${tag.color})`); } else { console.log(`⚠️ Tag existe déjà: ${tagData.name}`); diff --git a/src/actions/tags.ts b/src/actions/tags.ts index 3cf2c92..db4566d 100644 --- a/src/actions/tags.ts +++ b/src/actions/tags.ts @@ -3,6 +3,8 @@ import { tagsService } from '@/services/task-management/tags'; import { revalidatePath } from 'next/cache'; import { Tag } from '@/lib/types'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; export type ActionResult = { success: boolean; @@ -18,7 +20,16 @@ export async function createTag( color: string ): Promise> { try { - const tag = await tagsService.createTag({ name, color }); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'User not authenticated' }; + } + + const tag = await tagsService.createTag({ + name, + color, + userId: session.user.id, + }); // Revalider les pages qui utilisent les tags revalidatePath('/'); @@ -43,7 +54,12 @@ export async function updateTag( data: { name?: string; color?: string; isPinned?: boolean } ): Promise> { try { - const tag = await tagsService.updateTag(tagId, data); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'User not authenticated' }; + } + + const tag = await tagsService.updateTag(tagId, session.user.id, data); if (!tag) { return { success: false, error: 'Tag non trouvé' }; @@ -69,7 +85,12 @@ export async function updateTag( */ export async function deleteTag(tagId: string): Promise { try { - await tagsService.deleteTag(tagId); + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'User not authenticated' }; + } + + await tagsService.deleteTag(tagId, session.user.id); // Revalider les pages qui utilisent les tags revalidatePath('/'); diff --git a/src/app/api/tags/[id]/route.ts b/src/app/api/tags/[id]/route.ts index 52a7c13..e812bfc 100644 --- a/src/app/api/tags/[id]/route.ts +++ b/src/app/api/tags/[id]/route.ts @@ -1,5 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { tagsService } from '@/services/task-management/tags'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; /** * GET /api/tags/[id] - Récupère un tag par son ID @@ -9,8 +11,14 @@ export async function GET( { params }: { params: Promise<{ id: string }> } ) { try { + // Vérifier l'authentification + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); + } + const { id } = await params; - const tag = await tagsService.getTagById(id); + const tag = await tagsService.getTagById(id, session.user.id); if (!tag) { return NextResponse.json({ error: 'Tag non trouvé' }, { status: 404 }); diff --git a/src/app/api/tags/route.ts b/src/app/api/tags/route.ts index 23d2f77..abd09e5 100644 --- a/src/app/api/tags/route.ts +++ b/src/app/api/tags/route.ts @@ -1,11 +1,19 @@ import { NextRequest, NextResponse } from 'next/server'; import { tagsService } from '@/services/task-management/tags'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; /** * GET /api/tags - Récupère tous les tags ou recherche par query */ export async function GET(request: NextRequest) { try { + // Vérifier l'authentification + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); + } + const { searchParams } = new URL(request.url); const query = searchParams.get('q'); const popular = searchParams.get('popular'); @@ -14,14 +22,14 @@ export async function GET(request: NextRequest) { let tags; if (popular === 'true') { - // Récupérer les tags les plus utilisés - tags = await tagsService.getPopularTags(limit); + // Récupérer les tags les plus utilisés pour cet utilisateur + tags = await tagsService.getPopularTags(session.user.id, limit); } else if (query) { - // Recherche par nom (pour autocomplete) - tags = await tagsService.searchTags(query, limit); + // Recherche par nom (pour autocomplete) pour cet utilisateur + tags = await tagsService.searchTags(query, session.user.id, limit); } else { - // Récupérer tous les tags - tags = await tagsService.getTags(); + // Récupérer tous les tags de cet utilisateur + tags = await tagsService.getTags(session.user.id); } return NextResponse.json({ diff --git a/src/app/kanban/page.tsx b/src/app/kanban/page.tsx index a89572f..71b8702 100644 --- a/src/app/kanban/page.tsx +++ b/src/app/kanban/page.tsx @@ -29,7 +29,7 @@ export default async function KanbanPage() { // SSR - Récupération des données côté serveur const [initialTasks, initialTags] = await Promise.all([ tasksService.getTasks(userId), - tagsService.getTags(), + tagsService.getTags(userId), ]); return ( diff --git a/src/app/notes/page.tsx b/src/app/notes/page.tsx index 604f9dd..732b749 100644 --- a/src/app/notes/page.tsx +++ b/src/app/notes/page.tsx @@ -33,7 +33,7 @@ export default async function NotesPage() { // SSR - Récupération des données côté serveur const initialNotes = await notesService.getNotes(session.user.id); - const initialTags = await tagsService.getTags(); + const initialTags = await tagsService.getTags(session.user.id); return ( diff --git a/src/app/page.tsx b/src/app/page.tsx index 2f78a17..190b38a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -39,7 +39,7 @@ export default async function HomePage() { tagMetrics, ] = await Promise.all([ tasksService.getTasks(userId), - tagsService.getTags(), + tagsService.getTags(userId), tasksService.getTaskStats(userId), AnalyticsService.getProductivityMetrics(userId), DeadlineAnalyticsService.getDeadlineMetrics(userId), diff --git a/src/app/settings/advanced/page.tsx b/src/app/settings/advanced/page.tsx index 244ecd3..2947fa0 100644 --- a/src/app/settings/advanced/page.tsx +++ b/src/app/settings/advanced/page.tsx @@ -31,7 +31,7 @@ export default async function AdvancedSettingsPage() { // Fetch all data server-side const [taskStats, tags] = await Promise.all([ tasksService.getTaskStats(userId), - tagsService.getTags(), + tagsService.getTags(userId), ]); // Compose backup data like the API does diff --git a/src/app/settings/general/page.tsx b/src/app/settings/general/page.tsx index 7fd2e08..7acb9a7 100644 --- a/src/app/settings/general/page.tsx +++ b/src/app/settings/general/page.tsx @@ -1,12 +1,31 @@ import { tagsService } from '@/services/task-management/tags'; import { GeneralSettingsPageClient } from '@/components/settings/GeneralSettingsPageClient'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; // Force dynamic rendering for real-time data export const dynamic = 'force-dynamic'; export default async function GeneralSettingsPage() { + const session = await getServerSession(authOptions); + + if (!session?.user?.id) { + return ( +
+
+

+ Accès non autorisé +

+

+ Vous devez être connecté pour accéder aux paramètres. +

+
+
+ ); + } + // Fetch data server-side - const tags = await tagsService.getTags(); + const tags = await tagsService.getTags(session.user.id); return ; } diff --git a/src/app/weekly-manager/page.tsx b/src/app/weekly-manager/page.tsx index 0db7879..4070c61 100644 --- a/src/app/weekly-manager/page.tsx +++ b/src/app/weekly-manager/page.tsx @@ -32,7 +32,7 @@ export default async function WeeklyManagerPage() { const [summary, initialTasks, initialTags] = await Promise.all([ ManagerSummaryService.getManagerSummary(userId), tasksService.getTasks(userId), - tagsService.getTags(), + tagsService.getTags(userId), ]); return ( diff --git a/src/services/integrations/jira/jira.ts b/src/services/integrations/jira/jira.ts index 94054c9..eccd533 100644 --- a/src/services/integrations/jira/jira.ts +++ b/src/services/integrations/jira/jira.ts @@ -6,6 +6,7 @@ import { JiraTask } from '@/lib/types'; import { prisma } from '@/services/core/database'; import { parseDate, formatDateForDisplay } from '@/lib/date-utils'; +import { tagsService } from '../../task-management/tags'; export interface JiraConfig { enabled: boolean; @@ -326,26 +327,13 @@ export class JiraService { /** * S'assure que le tag "🔗 From Jira" existe dans la base */ - private async ensureJiraTagExists(): Promise { + private async ensureJiraTagExists(userId: string): Promise { try { const tagName = '🔗 From Jira'; - // Vérifier si le tag existe déjà - const existingTag = await prisma.tag.findUnique({ - where: { name: tagName }, - }); - - if (!existingTag) { - // Créer le tag s'il n'existe pas - await prisma.tag.create({ - data: { - name: tagName, - color: '#0082C9', // Bleu Jira - isPinned: false, - }, - }); - console.log(`✅ Tag "${tagName}" créé automatiquement`); - } + // Utiliser le service tags pour créer ou récupérer le tag + await tagsService.ensureTagsExist([tagName], userId); + console.log(`✅ Tag "${tagName}" créé/récupéré automatiquement`); } catch (error) { console.error('Erreur lors de la création du tag Jira:', error); // Ne pas faire échouer la sync pour un problème de tag @@ -375,7 +363,7 @@ export class JiraService { this.unknownStatuses.clear(); // S'assurer que le tag "From Jira" existe - await this.ensureJiraTagExists(); + await this.ensureJiraTagExists(userId); // Récupérer les tickets Jira actuellement assignés const jiraTasks = await this.getAssignedIssues(); @@ -526,7 +514,7 @@ export class JiraService { }); // Assigner le tag Jira - await this.assignJiraTag(newTask.id); + await this.assignJiraTag(newTask.id, userId); console.log( `➕ Nouvelle tâche créée: ${jiraTask.key} (status: ${taskData.status})` @@ -598,7 +586,7 @@ export class JiraService { ); // S'assurer que le tag Jira est assigné (pour les anciennes tâches) même en skip - await this.assignJiraTag(existingTask.id); + await this.assignJiraTag(existingTask.id, userId); return { type: 'skipped', @@ -647,7 +635,7 @@ export class JiraService { }); // S'assurer que le tag Jira est assigné (pour les anciennes tâches) - await this.assignJiraTag(existingTask.id); + await this.assignJiraTag(existingTask.id, userId); console.log( `🔄 Tâche mise à jour (titre/priorité préservés): ${jiraTask.key} (${changes.length} changement${changes.length > 1 ? 's' : ''})` @@ -756,14 +744,12 @@ export class JiraService { /** * Assigne le tag "🔗 From Jira" à une tâche si pas déjà assigné */ - private async assignJiraTag(taskId: string): Promise { + private async assignJiraTag(taskId: string, userId: string): Promise { try { const tagName = '🔗 From Jira'; - // Récupérer le tag - const jiraTag = await prisma.tag.findUnique({ - where: { name: tagName }, - }); + // Utiliser le service tags pour récupérer le tag + const jiraTag = await tagsService.getTagByName(tagName, userId); if (!jiraTag) { console.warn(`⚠️ Tag "${tagName}" introuvable lors de l'assignation`); diff --git a/src/services/integrations/tfs/tfs.ts b/src/services/integrations/tfs/tfs.ts index 99e18a2..b5b03ec 100644 --- a/src/services/integrations/tfs/tfs.ts +++ b/src/services/integrations/tfs/tfs.ts @@ -8,6 +8,7 @@ import { TfsPullRequest } from '@/lib/types'; import { prisma } from '@/services/core/database'; import { parseDate, formatDateForDisplay } from '@/lib/date-utils'; import { userPreferencesService } from '@/services/core/user-preferences'; +import { tagsService } from '../../task-management/tags'; export interface TfsConfig { enabled: boolean; @@ -574,7 +575,7 @@ export class TfsService { console.log('🔄 Début synchronisation TFS Pull Requests...'); // S'assurer que le tag TFS existe - await this.ensureTfsTagExists(); + await this.ensureTfsTagExists(userId); // Récupérer toutes les PRs assignées à l'utilisateur const allPullRequests = await this.getMyPullRequests(); @@ -665,7 +666,7 @@ export class TfsService { }); // Assigner le tag TFS - await this.assignTfsTag(newTask.id); + await this.assignTfsTag(newTask.id, userId); return { type: 'created', @@ -693,7 +694,7 @@ export class TfsService { if (changes.length === 0) { // S'assurer que le tag TFS est assigné (pour les anciennes tâches) - await this.assignTfsTag(existingTask.id); + await this.assignTfsTag(existingTask.id, userId); return { type: 'skipped', @@ -724,21 +725,11 @@ export class TfsService { /** * S'assure que le tag TFS existe */ - private async ensureTfsTagExists(): Promise { + private async ensureTfsTagExists(userId: string): Promise { try { - const existingTag = await prisma.tag.findFirst({ - where: { name: '🧑‍💻 TFS' }, - }); - - if (!existingTag) { - await prisma.tag.create({ - data: { - name: '🧑‍💻 TFS', - color: '#0066cc', // Bleu Azure DevOps - }, - }); - console.log('✅ Tag TFS créé'); - } + // Utiliser le service tags pour créer ou récupérer le tag + await tagsService.ensureTagsExist(['🧑‍💻 TFS'], userId); + console.log('✅ Tag TFS créé/récupéré'); } catch (error) { console.warn('Erreur création tag TFS:', error); } @@ -747,21 +738,11 @@ export class TfsService { /** * Assigne automatiquement le tag "TFS" aux tâches importées */ - private async assignTfsTag(taskId: string): Promise { + private async assignTfsTag(taskId: string, userId: string): Promise { try { - let tfsTag = await prisma.tag.findFirst({ - where: { name: '🧑‍💻 TFS' }, - }); - - if (!tfsTag) { - tfsTag = await prisma.tag.create({ - data: { - name: '🧑‍💻 TFS', - color: '#0078d4', // Couleur Azure - isPinned: false, - }, - }); - } + // Utiliser le service tags pour créer ou récupérer le tag + const tags = await tagsService.ensureTagsExist(['🧑‍💻 TFS'], userId); + const tfsTag = tags[0]; // Vérifier si la relation existe déjà const existingRelation = await prisma.taskTag.findFirst({ diff --git a/src/services/notes.ts b/src/services/notes.ts index bbf7fac..5edba66 100644 --- a/src/services/notes.ts +++ b/src/services/notes.ts @@ -1,6 +1,7 @@ import { prisma } from '@/services/core/database'; import { Task } from '@/lib/types'; import { Prisma } from '@prisma/client'; +import { tagsService } from './task-management/tags'; export interface Note { id: string; @@ -178,19 +179,38 @@ export class NotesService { content: data.content, userId: data.userId, taskId: data.taskId, // Ajouter le taskId - noteTags: data.tags - ? { - create: data.tags.map((tagName) => ({ - tag: { - connectOrCreate: { - where: { name: tagName }, - create: { name: tagName }, - }, - }, - })), - } - : undefined, }, + include: { + task: { + include: { + taskTags: { + include: { + tag: true, + }, + }, + primaryTag: true, + }, + }, + }, + }); + + // Créer les relations avec les tags si fournis + if (data.tags && data.tags.length > 0) { + // Créer ou récupérer tous les tags + const tags = await tagsService.ensureTagsExist(data.tags, data.userId); + + // Créer les relations NoteTag + await prisma.noteTag.createMany({ + data: tags.map((tag) => ({ + noteId: note.id, + tagId: tag.id, + })), + }); + } + + // Récupérer la note avec les tags + const noteWithTags = await prisma.note.findUnique({ + where: { id: note.id }, include: { noteTags: { include: { @@ -211,10 +231,10 @@ export class NotesService { }); return { - ...note, - taskId: note.taskId || undefined, // Convertir null en undefined - task: this.mapPrismaTaskToTask(note.task), // Mapper correctement l'objet Task - tags: note.noteTags.map((nt) => nt.tag.name), + ...noteWithTags!, + taskId: noteWithTags!.taskId || undefined, // Convertir null en undefined + task: this.mapPrismaTaskToTask(noteWithTags!.task), // Mapper correctement l'objet Task + tags: noteWithTags!.noteTags.map((nt) => nt.tag.name), }; } @@ -272,22 +292,34 @@ export class NotesService { // Gérer les tags si fournis if (data.tags !== undefined) { - updateData.noteTags = { - deleteMany: {}, // Supprimer tous les tags existants - create: data.tags.map((tagName) => ({ - tag: { - connectOrCreate: { - where: { name: tagName }, - create: { name: tagName }, - }, - }, - })), - }; + // Supprimer toutes les relations existantes + await prisma.noteTag.deleteMany({ + where: { noteId: noteId }, + }); + + // Créer les nouvelles relations si des tags sont fournis + if (data.tags.length > 0) { + // Créer ou récupérer tous les tags + const tags = await tagsService.ensureTagsExist(data.tags, userId); + + // Créer les relations NoteTag + await prisma.noteTag.createMany({ + data: tags.map((tag) => ({ + noteId: noteId, + tagId: tag.id, + })), + }); + } } - const note = await prisma.note.update({ + await prisma.note.update({ + where: { id: noteId }, + data: updateData as Prisma.NoteUpdateInput, + }); + + // Récupérer la note avec les tags après la mise à jour + const noteWithTags = await prisma.note.findUnique({ where: { id: noteId }, - data: updateData, include: { noteTags: { include: { @@ -308,10 +340,10 @@ export class NotesService { }); return { - ...note, - taskId: note.taskId || undefined, // Convertir null en undefined - task: this.mapPrismaTaskToTask(note.task), // Mapper correctement l'objet Task - tags: note.noteTags.map((nt) => nt.tag.name), + ...noteWithTags!, + taskId: noteWithTags!.taskId || undefined, // Convertir null en undefined + task: this.mapPrismaTaskToTask(noteWithTags!.task), // Mapper correctement l'objet Task + tags: noteWithTags!.noteTags.map((nt) => nt.tag.name), }; } diff --git a/src/services/task-management/tags.ts b/src/services/task-management/tags.ts index f1af094..79075fc 100644 --- a/src/services/task-management/tags.ts +++ b/src/services/task-management/tags.ts @@ -7,10 +7,11 @@ import { Tag } from '@/lib/types'; */ export const tagsService = { /** - * Récupère tous les tags avec leur nombre d'utilisations + * Récupère tous les tags d'un utilisateur avec leur nombre d'utilisations */ - async getTags(): Promise<(Tag & { usage: number })[]> { + async getTags(userId: string): Promise<(Tag & { usage: number })[]> { const tags = await prisma.tag.findMany({ + where: { ownerId: userId }, include: { _count: { select: { @@ -31,32 +32,13 @@ export const tagsService = { }, /** - * Récupère un tag par son ID + * Récupère un tag par son ID (vérifie que l'utilisateur en est propriétaire) */ - async getTagById(id: string): Promise { - const tag = await prisma.tag.findUnique({ - where: { id }, - }); - - if (!tag) return null; - - return { - id: tag.id, - name: tag.name, - color: tag.color, - isPinned: tag.isPinned, - }; - }, - - /** - * Récupère un tag par son nom - */ - async getTagByName(name: string): Promise { + async getTagById(id: string, userId: string): Promise { const tag = await prisma.tag.findFirst({ where: { - name: { - equals: name, - }, + id, + ownerId: userId, }, }); @@ -71,15 +53,39 @@ export const tagsService = { }, /** - * Crée un nouveau tag + * Récupère un tag par son nom pour un utilisateur spécifique + */ + async getTagByName(name: string, userId: string): Promise { + const tag = await prisma.tag.findFirst({ + where: { + name: { + equals: name, + }, + ownerId: userId, + }, + }); + + if (!tag) return null; + + return { + id: tag.id, + name: tag.name, + color: tag.color, + isPinned: tag.isPinned, + }; + }, + + /** + * Crée un nouveau tag pour un utilisateur */ async createTag(data: { name: string; color: string; isPinned?: boolean; + userId: string; }): Promise { - // Vérifier si le tag existe déjà - const existing = await this.getTagByName(data.name); + // Vérifier si le tag existe déjà pour cet utilisateur + const existing = await this.getTagByName(data.name, data.userId); if (existing) { throw new Error(`Un tag avec le nom "${data.name}" existe déjà`); } @@ -89,6 +95,7 @@ export const tagsService = { name: data.name.trim(), color: data.color, isPinned: data.isPinned || false, + ownerId: data.userId, }, }); @@ -101,21 +108,24 @@ export const tagsService = { }, /** - * Met à jour un tag + * Met à jour un tag (vérifie que l'utilisateur en est propriétaire) */ async updateTag( id: string, + userId: string, data: { name?: string; color?: string; isPinned?: boolean } ): Promise { - // Vérifier que le tag existe - const existing = await this.getTagById(id); + // Vérifier que le tag existe et appartient à l'utilisateur + const existing = await this.getTagById(id, userId); if (!existing) { - throw new Error(`Tag avec l'ID "${id}" non trouvé`); + throw new Error( + `Tag avec l'ID "${id}" non trouvé ou vous n'en êtes pas propriétaire` + ); } - // Si on change le nom, vérifier qu'il n'existe pas déjà + // Si on change le nom, vérifier qu'il n'existe pas déjà pour cet utilisateur if (data.name && data.name !== existing.name) { - const nameExists = await this.getTagByName(data.name); + const nameExists = await this.getTagByName(data.name, userId); if (nameExists && nameExists.id !== id) { throw new Error(`Un tag avec le nom "${data.name}" existe déjà`); } @@ -150,13 +160,15 @@ export const tagsService = { }, /** - * Supprime un tag et toutes ses relations + * Supprime un tag et toutes ses relations (vérifie que l'utilisateur en est propriétaire) */ - async deleteTag(id: string): Promise { - // Vérifier que le tag existe - const existing = await this.getTagById(id); + async deleteTag(id: string, userId: string): Promise { + // Vérifier que le tag existe et appartient à l'utilisateur + const existing = await this.getTagById(id, userId); if (!existing) { - throw new Error(`Tag avec l'ID "${id}" non trouvé`); + throw new Error( + `Tag avec l'ID "${id}" non trouvé ou vous n'en êtes pas propriétaire` + ); } // Supprimer d'abord toutes les relations TaskTag @@ -171,9 +183,10 @@ export const tagsService = { }, /** - * Récupère les tags les plus utilisés + * Récupère les tags les plus utilisés pour un utilisateur */ async getPopularTags( + userId: string, limit: number = 10 ): Promise> { // Utiliser une requête SQL brute pour compter les usages @@ -188,6 +201,7 @@ export const tagsService = { SELECT t.id, t.name, t.color, COUNT(tt.tagId) as usage FROM tags t LEFT JOIN task_tags tt ON t.id = tt.tagId + WHERE t.ownerId = ${userId} GROUP BY t.id, t.name, t.color ORDER BY usage DESC, t.name ASC LIMIT ${limit} @@ -202,14 +216,19 @@ export const tagsService = { }, /** - * Recherche des tags par nom (pour autocomplete) + * Recherche des tags par nom pour un utilisateur (pour autocomplete) */ - async searchTags(query: string, limit: number = 10): Promise { + async searchTags( + query: string, + userId: string, + limit: number = 10 + ): Promise { const tags = await prisma.tag.findMany({ where: { name: { contains: query, }, + ownerId: userId, }, orderBy: { name: 'asc' }, take: limit, @@ -224,15 +243,15 @@ export const tagsService = { }, /** - * Crée automatiquement des tags manquants à partir d'une liste de noms + * Crée automatiquement des tags manquants à partir d'une liste de noms pour un utilisateur */ - async ensureTagsExist(tagNames: string[]): Promise { + async ensureTagsExist(tagNames: string[], userId: string): Promise { const results: Tag[] = []; for (const name of tagNames) { if (!name.trim()) continue; - let tag = await this.getTagByName(name.trim()); + let tag = await this.getTagByName(name.trim(), userId); if (!tag) { // Générer une couleur aléatoirement @@ -251,6 +270,7 @@ export const tagsService = { tag = await this.createTag({ name: name.trim(), color: randomColor, + userId, }); } diff --git a/src/services/task-management/tasks.ts b/src/services/task-management/tasks.ts index 5054168..06b0af2 100644 --- a/src/services/task-management/tasks.ts +++ b/src/services/task-management/tasks.ts @@ -11,7 +11,7 @@ import { } from '@/lib/types'; import { Prisma } from '@prisma/client'; import { getToday } from '@/lib/date-utils'; -import { generateTagColor } from '@/lib/tag-colors'; +import { tagsService } from './tags'; /** * Service pour la gestion des tâches (version standalone) @@ -112,7 +112,11 @@ export class TasksService { // Créer les relations avec les tags if (taskData.tags && taskData.tags.length > 0) { - await this.createTaskTagRelations(task.id, taskData.tags); + await this.createTaskTagRelations( + task.id, + taskData.tags, + taskData.ownerId + ); } // Récupérer la tâche avec les tags pour le retour @@ -186,7 +190,7 @@ export class TasksService { // Mettre à jour les relations avec les tags if (updates.tags !== undefined) { - await this.updateTaskTagRelations(taskId, updates.tags); + await this.updateTaskTagRelations(taskId, updates.tags, userId); } // Récupérer la tâche avec les tags pour le retour @@ -337,19 +341,14 @@ export class TasksService { */ private async createTaskTagRelations( taskId: string, - tagNames: string[] + tagNames: string[], + userId: 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), - }, - }); + // Utiliser le service tags pour créer ou récupérer le tag + const tags = await tagsService.ensureTagsExist([tagName], userId); + const tag = tags[0]; // Créer la relation TaskTag si elle n'existe pas await prisma.taskTag.upsert({ @@ -379,7 +378,8 @@ export class TasksService { */ private async updateTaskTagRelations( taskId: string, - tagNames: string[] + tagNames: string[], + userId: string ): Promise { // Supprimer toutes les relations existantes await prisma.taskTag.deleteMany({ @@ -388,17 +388,10 @@ export class TasksService { // Créer les nouvelles relations if (tagNames.length > 0) { - await this.createTaskTagRelations(taskId, tagNames); + await this.createTaskTagRelations(taskId, tagNames, userId); } } - /** - * Génère une couleur pour un tag basée sur son nom - */ - private generateTagColor(tagName: string): string { - return generateTagColor(tagName); - } - /** * Convertit une tâche Prisma en objet Task */