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.
This commit is contained in:
@@ -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";
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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<T = void> = {
|
||||
success: boolean;
|
||||
@@ -18,7 +20,16 @@ export async function createTag(
|
||||
color: string
|
||||
): Promise<ActionResult<Tag>> {
|
||||
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<ActionResult<Tag>> {
|
||||
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<ActionResult> {
|
||||
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('/');
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
<NotesPageClient initialNotes={initialNotes} initialTags={initialTags} />
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen bg-[var(--background)] flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-bold text-[var(--foreground)] mb-2">
|
||||
Accès non autorisé
|
||||
</h1>
|
||||
<p className="text-[var(--muted-foreground)]">
|
||||
Vous devez être connecté pour accéder aux paramètres.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch data server-side
|
||||
const tags = await tagsService.getTags();
|
||||
const tags = await tagsService.getTags(session.user.id);
|
||||
|
||||
return <GeneralSettingsPageClient initialTags={tags} />;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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<void> {
|
||||
private async ensureJiraTagExists(userId: string): Promise<void> {
|
||||
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<void> {
|
||||
private async assignJiraTag(taskId: string, userId: string): Promise<void> {
|
||||
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`);
|
||||
|
||||
@@ -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<void> {
|
||||
private async ensureTfsTagExists(userId: string): Promise<void> {
|
||||
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<void> {
|
||||
private async assignTfsTag(taskId: string, userId: string): Promise<void> {
|
||||
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({
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Tag | null> {
|
||||
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<Tag | null> {
|
||||
async getTagById(id: string, userId: string): Promise<Tag | null> {
|
||||
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<Tag | null> {
|
||||
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<Tag> {
|
||||
// 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<Tag | null> {
|
||||
// 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<void> {
|
||||
// Vérifier que le tag existe
|
||||
const existing = await this.getTagById(id);
|
||||
async deleteTag(id: string, userId: string): Promise<void> {
|
||||
// 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<Array<Tag & { usage: number }>> {
|
||||
// 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<Tag[]> {
|
||||
async searchTags(
|
||||
query: string,
|
||||
userId: string,
|
||||
limit: number = 10
|
||||
): Promise<Tag[]> {
|
||||
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<Tag[]> {
|
||||
async ensureTagsExist(tagNames: string[], userId: string): Promise<Tag[]> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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),
|
||||
},
|
||||
});
|
||||
// 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<void> {
|
||||
// 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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user