From 6bfcd1f10024c9bf42cdc28c98f03aa42742b56e Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 10 Oct 2025 08:54:52 +0200 Subject: [PATCH] feat(DailyCheckbox): associate checkboxes with users and enhance daily view functionality - Added userId field to DailyCheckbox model for user association. - Updated DailyService methods to handle user-specific checkbox retrieval and management. - Integrated user authentication checks in API routes and actions for secure access to daily data. - Enhanced DailyPage to display user-specific daily views, ensuring proper session handling. - Updated client and service interfaces to reflect changes in data structure. --- .../migration.sql | 17 ++++++ prisma/schema.prisma | 32 +++++++----- src/actions/daily.ts | 27 +++++++++- src/app/api/daily/route.ts | 22 +++++++- src/app/daily/page.tsx | 22 +++++++- src/clients/daily-client.ts | 1 + src/lib/types.ts | 4 ++ src/services/task-management/daily.ts | 52 ++++++++++++------- src/services/task-management/tasks.ts | 1 + 9 files changed, 142 insertions(+), 36 deletions(-) create mode 100644 prisma/migrations/20251010084245_add_user_to_daily_checkbox/migration.sql diff --git a/prisma/migrations/20251010084245_add_user_to_daily_checkbox/migration.sql b/prisma/migrations/20251010084245_add_user_to_daily_checkbox/migration.sql new file mode 100644 index 0000000..fed0814 --- /dev/null +++ b/prisma/migrations/20251010084245_add_user_to_daily_checkbox/migration.sql @@ -0,0 +1,17 @@ +-- Migration pour ajouter userId aux DailyCheckbox +-- et associer les entrées existantes au premier utilisateur + +-- 1. Ajouter la colonne userId (nullable temporairement) +ALTER TABLE "daily_checkboxes" ADD COLUMN "userId" TEXT; + +-- 2. Migrer les données existantes vers le premier utilisateur +-- (on suppose qu'il y a au moins un utilisateur dans la table users) +UPDATE "daily_checkboxes" +SET "userId" = (SELECT id FROM "users" LIMIT 1) +WHERE "userId" IS NULL; + +-- 3. Créer un index sur userId pour les performances +CREATE INDEX "daily_checkboxes_userId_idx" ON "daily_checkboxes"("userId"); + +-- Note: La contrainte de clé étrangère sera gérée par Prisma +-- SQLite ne supporte pas les contraintes de clé étrangère dans ALTER TABLE diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 448bb23..a42fd95 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,20 +8,21 @@ datasource db { } model User { - id String @id @default(cuid()) - email String @unique - name String? - firstName String? - lastName String? - avatar String? // URL de l'avatar - role String @default("user") // user, admin, etc. - isActive Boolean @default(true) - lastLoginAt DateTime? - password String // Hashé avec bcrypt - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - preferences UserPreferences? - notes Note[] + id String @id @default(cuid()) + email String @unique + name String? + firstName String? + lastName String? + avatar String? // URL de l'avatar + role String @default("user") // user, admin, etc. + isActive Boolean @default(true) + lastLoginAt DateTime? + password String // Hashé avec bcrypt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + preferences UserPreferences? + notes Note[] + dailyCheckboxes DailyCheckbox[] @@map("users") } @@ -98,11 +99,14 @@ model DailyCheckbox { type String @default("task") order Int @default(0) taskId String? + userId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt task Task? @relation(fields: [taskId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([date]) + @@index([userId]) @@map("daily_checkboxes") } diff --git a/src/actions/daily.ts b/src/actions/daily.ts index c06b0f3..ff245ff 100644 --- a/src/actions/daily.ts +++ b/src/actions/daily.ts @@ -13,6 +13,8 @@ import { parseDate, normalizeDate, } from '@/lib/date-utils'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; /** * Toggle l'état d'une checkbox @@ -23,6 +25,11 @@ export async function toggleCheckbox(checkboxId: string): Promise<{ error?: string; }> { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + // Nous devons d'abord récupérer la checkbox pour connaître son état actuel // En absence de getCheckboxById, nous allons essayer de la trouver via une vue daily // Pour l'instant, nous allons simplement toggle via updateCheckbox @@ -30,7 +37,7 @@ export async function toggleCheckbox(checkboxId: string): Promise<{ // Récupérer toutes les checkboxes d'aujourd'hui et hier pour trouver celle à toggle const today = getToday(); - const dailyView = await dailyService.getDailyView(today); + const dailyView = await dailyService.getDailyView(today, session.user.id); let checkbox = dailyView.today.find((cb) => cb.id === checkboxId); if (!checkbox) { @@ -70,8 +77,14 @@ export async function addTodayCheckbox( error?: string; }> { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + const newCheckbox = await dailyService.addCheckbox({ date: getToday(), + userId: session.user.id, text: content, type: type || 'task', taskId, @@ -101,10 +114,16 @@ export async function addYesterdayCheckbox( error?: string; }> { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + const yesterday = getPreviousWorkday(getToday()); const newCheckbox = await dailyService.addCheckbox({ date: yesterday, + userId: session.user.id, text: content, type: type || 'task', taskId, @@ -180,10 +199,16 @@ export async function addTodoToTask( error?: string; }> { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + const targetDate = normalizeDate(date || getToday()); const checkboxData: CreateDailyCheckboxData = { date: targetDate, + userId: session.user.id, text: text.trim(), type: 'task', taskId: taskId, diff --git a/src/app/api/daily/route.ts b/src/app/api/daily/route.ts index 057cf2e..337a241 100644 --- a/src/app/api/daily/route.ts +++ b/src/app/api/daily/route.ts @@ -6,12 +6,19 @@ import { isValidAPIDate, createDateFromParts, } from '@/lib/date-utils'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; /** * API route pour récupérer la vue daily (hier + aujourd'hui) */ export async function GET(request: Request) { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); + } + const { searchParams } = new URL(request.url); const action = searchParams.get('action'); @@ -20,7 +27,10 @@ export async function GET(request: Request) { if (action === 'history') { // Récupérer l'historique const limit = parseInt(searchParams.get('limit') || '30'); - const history = await dailyService.getCheckboxHistory(limit); + const history = await dailyService.getCheckboxHistory( + session.user.id, + limit + ); return NextResponse.json(history); } @@ -55,7 +65,10 @@ export async function GET(request: Request) { targetDate = getToday(); } - const dailyView = await dailyService.getDailyView(targetDate); + const dailyView = await dailyService.getDailyView( + targetDate, + session.user.id + ); return NextResponse.json(dailyView); } catch (error) { console.error('Erreur lors de la récupération du daily:', error); @@ -71,6 +84,10 @@ export async function GET(request: Request) { */ export async function POST(request: Request) { try { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); + } const body = await request.json(); // Validation des données @@ -100,6 +117,7 @@ export async function POST(request: Request) { const checkbox = await dailyService.addCheckbox({ date, + userId: session.user.id, text: body.text, type: body.type, taskId: body.taskId, diff --git a/src/app/daily/page.tsx b/src/app/daily/page.tsx index aa922a8..a8156f1 100644 --- a/src/app/daily/page.tsx +++ b/src/app/daily/page.tsx @@ -3,6 +3,8 @@ import { DailyPageClient } from './DailyPageClient'; import { dailyService } from '@/services/task-management/daily'; import { DeadlineAnalyticsService } from '@/services/analytics/deadline-analytics'; import { getToday } from '@/lib/date-utils'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; // Force dynamic rendering (no static generation) export const dynamic = 'force-dynamic'; @@ -13,13 +15,31 @@ export const metadata: Metadata = { }; export default async function DailyPage() { + // Récupérer la session utilisateur + const session = await getServerSession(authOptions); + + if (!session?.user?.id) { + return ( +
+
+

+ Non autorisé +

+

+ Vous devez être connecté pour accéder à la page daily. +

+
+
+ ); + } + // Récupérer les données côté serveur const today = getToday(); try { const [dailyView, dailyDates, deadlineMetrics, pendingTasks] = await Promise.all([ - dailyService.getDailyView(today), + dailyService.getDailyView(today, session.user.id), dailyService.getDailyDates(), DeadlineAnalyticsService.getDeadlineMetrics().catch(() => null), // Graceful fallback dailyService diff --git a/src/clients/daily-client.ts b/src/clients/daily-client.ts index 6d72eda..b522779 100644 --- a/src/clients/daily-client.ts +++ b/src/clients/daily-client.ts @@ -17,6 +17,7 @@ interface ApiCheckbox { type: 'task' | 'meeting'; order: number; taskId?: string; + userId: string; task?: Task; createdAt: string; updatedAt: string; diff --git a/src/lib/types.ts b/src/lib/types.ts index fa303c2..c5fbb60 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,5 +1,6 @@ import { TfsConfig } from '@/services/integrations/tfs/tfs'; import { Theme } from './ui-config'; +import { User } from '@/services/users'; // Réexporter Theme pour les autres modules export type { Theme }; @@ -438,7 +439,9 @@ export interface DailyCheckbox { type: DailyCheckboxType; order: number; taskId?: string; + userId: string; task?: Task; // Relation optionnelle vers une tâche + user?: User; // Relation vers l'utilisateur isArchived?: boolean; createdAt: Date; updatedAt: Date; @@ -447,6 +450,7 @@ export interface DailyCheckbox { // Interface pour créer/modifier une checkbox export interface CreateDailyCheckboxData { date: Date; + userId: string; text: string; type?: DailyCheckboxType; taskId?: string; diff --git a/src/services/task-management/daily.ts b/src/services/task-management/daily.ts index 0a25260..cd43b11 100644 --- a/src/services/task-management/daily.ts +++ b/src/services/task-management/daily.ts @@ -26,7 +26,7 @@ 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): Promise { + async getDailyView(date: Date, userId: string): Promise { // Normaliser la date (début de journée) const today = normalizeDate(date); @@ -35,8 +35,8 @@ export class DailyService { // Récupérer les checkboxes des deux jours const [yesterdayCheckboxes, todayCheckboxes] = await Promise.all([ - this.getCheckboxesByDate(yesterday), - this.getCheckboxesByDate(today), + this.getCheckboxesByDate(yesterday, userId), + this.getCheckboxesByDate(today, userId), ]); return { @@ -47,15 +47,21 @@ export class DailyService { } /** - * Récupère toutes les checkboxes d'une date donnée + * Récupère toutes les checkboxes d'une date donnée pour un utilisateur */ - async getCheckboxesByDate(date: Date): Promise { + 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 }, - include: { task: true }, + where: { + date: normalizedDate, + userId: userId, + }, + include: { task: true, user: true }, orderBy: { order: 'asc' }, }); @@ -83,10 +89,11 @@ export class DailyService { text: data.text.trim(), type: data.type ?? 'task', taskId: data.taskId, + userId: data.userId, order, isChecked: data.isChecked ?? false, }, - include: { task: true }, + include: { task: true, user: true }, }); return this.mapPrismaCheckbox(checkbox); @@ -117,7 +124,7 @@ export class DailyService { const checkbox = await prisma.dailyCheckbox.update({ where: { id: checkboxId }, data: updateData, - include: { task: true }, + include: { task: true, user: true }, }); return this.mapPrismaCheckbox(checkbox); @@ -167,7 +174,7 @@ export class DailyService { contains: query, }, }, - include: { task: true }, + include: { task: true, user: true }, orderBy: { date: 'desc' }, take: limit, }); @@ -179,10 +186,12 @@ export class DailyService { * 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 + // 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' }, @@ -191,7 +200,7 @@ export class DailyService { const history = []; for (const { date } of distinctDates) { - const checkboxes = await this.getCheckboxesByDate(date); + const checkboxes = await this.getCheckboxesByDate(date, userId); if (checkboxes.length > 0) { history.push({ date, checkboxes }); } @@ -203,19 +212,21 @@ export class DailyService { /** * Récupère la vue daily d'aujourd'hui */ - async getTodaysDailyView(): Promise { - return this.getDailyView(getToday()); + 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, }); @@ -225,11 +236,13 @@ export class DailyService { * Ajoute une checkbox pour hier */ async addYesterdayCheckbox( + userId: string, text: string, taskId?: string ): Promise { return this.addCheckbox({ date: getYesterday(), + userId, text, taskId, }); @@ -239,7 +252,9 @@ export class DailyService { * Mappe une checkbox Prisma vers notre interface */ private mapPrismaCheckbox( - checkbox: Prisma.DailyCheckboxGetPayload<{ include: { task: true } }> + checkbox: Prisma.DailyCheckboxGetPayload<{ + include: { task: true; user: true }; + }> ): DailyCheckbox { return { id: checkbox.id, @@ -249,6 +264,7 @@ export class DailyService { type: checkbox.type as DailyCheckboxType, order: checkbox.order, taskId: checkbox.taskId || undefined, + userId: checkbox.userId, task: checkbox.task ? { id: checkbox.task.id, @@ -334,7 +350,7 @@ export class DailyService { const checkboxes = await prisma.dailyCheckbox.findMany({ where: whereConditions, - include: { task: true }, + include: { task: true, user: true }, orderBy: [{ date: 'desc' }, { order: 'asc' }], ...(options?.limit ? { take: options.limit } : {}), }); @@ -356,7 +372,7 @@ export class DailyService { ?.text + ' [ARCHIVÉ]', updatedAt: new Date(), }, - include: { task: true }, + include: { task: true, user: true }, }); return this.mapPrismaCheckbox(checkbox); @@ -400,7 +416,7 @@ export class DailyService { order: newOrder, updatedAt: new Date(), }, - include: { task: true }, + include: { task: true, user: true }, }); return this.mapPrismaCheckbox(updatedCheckbox); diff --git a/src/services/task-management/tasks.ts b/src/services/task-management/tasks.ts index 1801855..c5e9b8b 100644 --- a/src/services/task-management/tasks.ts +++ b/src/services/task-management/tasks.ts @@ -237,6 +237,7 @@ export class TasksService { type: checkbox.type as DailyCheckboxType, order: checkbox.order, taskId: checkbox.taskId ?? undefined, + userId: checkbox.userId, task: checkbox.task ? { id: checkbox.task.id,