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.
This commit is contained in:
Julien Froidefond
2025-10-10 08:54:52 +02:00
parent 6748799a90
commit 6bfcd1f100
9 changed files with 142 additions and 36 deletions

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 (
<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-4">
Non autorisé
</h1>
<p className="text-[var(--muted-foreground)]">
Vous devez être connecté pour accéder à la page daily.
</p>
</div>
</div>
);
}
// 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

View File

@@ -17,6 +17,7 @@ interface ApiCheckbox {
type: 'task' | 'meeting';
order: number;
taskId?: string;
userId: string;
task?: Task;
createdAt: string;
updatedAt: string;

View File

@@ -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;

View File

@@ -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<DailyView> {
async getDailyView(date: Date, userId: string): Promise<DailyView> {
// 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<DailyCheckbox[]> {
async getCheckboxesByDate(
date: Date,
userId: string
): Promise<DailyCheckbox[]> {
// 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<DailyView> {
return this.getDailyView(getToday());
async getTodaysDailyView(userId: string): Promise<DailyView> {
return this.getDailyView(getToday(), userId);
}
/**
* Ajoute une checkbox pour aujourd'hui
*/
async addTodayCheckbox(
userId: string,
text: string,
taskId?: string
): Promise<DailyCheckbox> {
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<DailyCheckbox> {
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);

View File

@@ -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,