diff --git a/prisma/migrations/20250115140000_add_task_owner_relation/migration.sql b/prisma/migrations/20250115140000_add_task_owner_relation/migration.sql new file mode 100644 index 0000000..e111ceb --- /dev/null +++ b/prisma/migrations/20250115140000_add_task_owner_relation/migration.sql @@ -0,0 +1,54 @@ +-- Add ownerId column to tasks table +ALTER TABLE "tasks" ADD COLUMN "ownerId" TEXT NOT NULL DEFAULT ''; + +-- Get the first user ID to assign all existing tasks +-- We'll use a subquery to get the first user's ID +UPDATE "tasks" +SET "ownerId" = ( + SELECT "id" FROM "users" + ORDER BY "createdAt" ASC + LIMIT 1 +) +WHERE "ownerId" = ''; + +-- Now make ownerId NOT NULL without default +-- First, we need to recreate the table since SQLite doesn't support ALTER COLUMN +CREATE TABLE "tasks_new" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "description" TEXT, + "status" TEXT NOT NULL DEFAULT 'todo', + "priority" TEXT NOT NULL DEFAULT 'medium', + "source" TEXT NOT NULL, + "sourceId" TEXT, + "dueDate" DATETIME, + "completedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "jiraProject" TEXT, + "jiraKey" TEXT, + "assignee" TEXT, + "ownerId" TEXT NOT NULL, + "jiraType" TEXT, + "tfsProject" TEXT, + "tfsPullRequestId" INTEGER, + "tfsRepository" TEXT, + "tfsSourceBranch" TEXT, + "tfsTargetBranch" TEXT, + "primaryTagId" TEXT, + CONSTRAINT "tasks_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "tasks_primaryTagId_fkey" FOREIGN KEY ("primaryTagId") REFERENCES "tags" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- Copy data from old table to new table +INSERT INTO "tasks_new" SELECT * FROM "tasks"; + +-- Drop old table +DROP TABLE "tasks"; + +-- Rename new table +ALTER TABLE "tasks_new" RENAME TO "tasks"; + +-- Recreate indexes +CREATE UNIQUE INDEX "tasks_source_sourceId_key" ON "tasks"("source", "sourceId"); +CREATE INDEX "tasks_ownerId_idx" ON "tasks"("ownerId"); diff --git a/prisma/migrations/20250115140001_fix_task_owner_assignment/migration.sql b/prisma/migrations/20250115140001_fix_task_owner_assignment/migration.sql new file mode 100644 index 0000000..bddc14b --- /dev/null +++ b/prisma/migrations/20250115140001_fix_task_owner_assignment/migration.sql @@ -0,0 +1,56 @@ +-- Add ownerId column to tasks table if it doesn't exist +ALTER TABLE "tasks" ADD COLUMN "ownerId" TEXT; + +-- Create a temporary user if no users exist +INSERT OR IGNORE INTO "users" ("id", "email", "name", "password", "createdAt", "updatedAt") +VALUES ('temp-user', 'temp@example.com', 'Temporary User', '$2b$10$temp', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +-- Assign all existing tasks to the first user (or temp user if none exist) +UPDATE "tasks" +SET "ownerId" = ( + SELECT "id" FROM "users" + ORDER BY "createdAt" ASC + LIMIT 1 +) +WHERE "ownerId" IS NULL; + +-- Now make ownerId NOT NULL by recreating the table +CREATE TABLE "tasks_new" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "description" TEXT, + "status" TEXT NOT NULL DEFAULT 'todo', + "priority" TEXT NOT NULL DEFAULT 'medium', + "source" TEXT NOT NULL, + "sourceId" TEXT, + "dueDate" DATETIME, + "completedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "jiraProject" TEXT, + "jiraKey" TEXT, + "assignee" TEXT, + "ownerId" TEXT NOT NULL, + "jiraType" TEXT, + "tfsProject" TEXT, + "tfsPullRequestId" INTEGER, + "tfsRepository" TEXT, + "tfsSourceBranch" TEXT, + "tfsTargetBranch" TEXT, + "primaryTagId" TEXT, + CONSTRAINT "tasks_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "tasks_primaryTagId_fkey" FOREIGN KEY ("primaryTagId") REFERENCES "tags" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- Copy data from old table to new table +INSERT INTO "tasks_new" SELECT * FROM "tasks"; + +-- Drop old table +DROP TABLE "tasks"; + +-- Rename new table +ALTER TABLE "tasks_new" RENAME TO "tasks"; + +-- Recreate indexes +CREATE UNIQUE INDEX "tasks_source_sourceId_key" ON "tasks"("source", "sourceId"); +CREATE INDEX "tasks_ownerId_idx" ON "tasks"("ownerId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a42fd95..0b46fe9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -23,6 +23,7 @@ model User { preferences UserPreferences? notes Note[] dailyCheckboxes DailyCheckbox[] + tasks Task[] @relation("TaskOwner") @@map("users") } @@ -41,7 +42,9 @@ model Task { updatedAt DateTime @updatedAt jiraProject String? jiraKey String? - assignee String? + assignee String? // Legacy field - keep for Jira/TFS compatibility + ownerId String // Required - chaque tâche appartient à un user + owner User @relation("TaskOwner", fields: [ownerId], references: [id], onDelete: Cascade) jiraType String? tfsProject String? tfsPullRequestId Int? diff --git a/scripts/seed-data.ts b/scripts/seed-data.ts index 64683a5..86ce126 100644 --- a/scripts/seed-data.ts +++ b/scripts/seed-data.ts @@ -1,5 +1,6 @@ import { tasksService } from '../src/services/task-management/tasks'; import { TaskStatus, TaskPriority } from '../src/lib/types'; +import { prisma } from '../src/services/core/database'; /** * Script pour ajouter des données de test avec tags et variété @@ -8,6 +9,28 @@ async function seedTestData() { console.log('🌱 Ajout de données de test...'); console.log('================================'); + // Récupérer le premier user ou créer un user temporaire + let userId: string; + const firstUser = await prisma.user.findFirst({ + orderBy: { createdAt: 'asc' }, + }); + + if (firstUser) { + userId = firstUser.id; + console.log(`👤 Utilisation du user existant: ${firstUser.email}`); + } else { + // Créer un user temporaire pour les tests + const tempUser = await prisma.user.create({ + data: { + email: 'test@example.com', + name: 'Test User', + password: '$2b$10$temp', // Mot de passe temporaire + }, + }); + userId = tempUser.id; + console.log(`👤 User temporaire créé: ${tempUser.email}`); + } + const testTasks = [ { title: '🎨 Design System Implementation', @@ -58,7 +81,10 @@ async function seedTestData() { for (const taskData of testTasks) { try { - const task = await tasksService.createTask(taskData); + const task = await tasksService.createTask({ + ...taskData, + ownerId: userId, // Ajouter l'ownerId + }); const statusEmoji = { backlog: '📋', @@ -101,7 +127,7 @@ async function seedTestData() { console.log(` ❌ Erreurs: ${errorCount}`); // Afficher les stats finales - const stats = await tasksService.getTaskStats(); + const stats = await tasksService.getTaskStats(userId); console.log(''); console.log('📈 Statistiques finales:'); console.log(` Total: ${stats.total} tâches`); diff --git a/src/actions/metrics.ts b/src/actions/metrics.ts index 9b6894f..24aca3f 100644 --- a/src/actions/metrics.ts +++ b/src/actions/metrics.ts @@ -6,6 +6,8 @@ import { VelocityTrend, } from '@/services/analytics/metrics'; import { getToday } from '@/lib/date-utils'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; /** * Récupère les métriques hebdomadaires pour une date donnée @@ -16,8 +18,20 @@ export async function getWeeklyMetrics(date?: Date): Promise<{ error?: string; }> { try { + // Récupérer l'utilisateur connecté + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { + success: false, + error: 'Utilisateur non authentifié', + }; + } + const targetDate = date || getToday(); - const metrics = await MetricsService.getWeeklyMetrics(targetDate); + const metrics = await MetricsService.getWeeklyMetrics( + session.user.id, + targetDate + ); return { success: true, @@ -44,6 +58,15 @@ export async function getVelocityTrends(weeksBack: number = 4): Promise<{ error?: string; }> { try { + // Récupérer l'utilisateur connecté + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { + success: false, + error: 'Utilisateur non authentifié', + }; + } + if (weeksBack < 1 || weeksBack > 12) { return { success: false, @@ -51,7 +74,10 @@ export async function getVelocityTrends(weeksBack: number = 4): Promise<{ }; } - const trends = await MetricsService.getVelocityTrends(weeksBack); + const trends = await MetricsService.getVelocityTrends( + session.user.id, + weeksBack + ); return { success: true, diff --git a/src/actions/tasks.ts b/src/actions/tasks.ts index f5456b9..e8efeaa 100644 --- a/src/actions/tasks.ts +++ b/src/actions/tasks.ts @@ -3,6 +3,8 @@ import { tasksService } from '@/services/task-management/tasks'; import { revalidatePath } from 'next/cache'; import { TaskStatus, TaskPriority } from '@/lib/types'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; export type ActionResult = { success: boolean; @@ -10,6 +12,30 @@ export type ActionResult = { error?: string; }; +/** + * Helper pour vérifier l'authentification + */ +async function getAuthenticatedUser() { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + throw new Error('Non authentifié'); + } + return session.user.id; +} + +/** + * Helper pour vérifier qu'une tâche appartient au user connecté + */ +async function verifyTaskOwnership(taskId: string): Promise { + try { + const userId = await getAuthenticatedUser(); + const tasks = await tasksService.getTasks(userId); + return tasks.some((t) => t.id === taskId); + } catch { + return false; + } +} + /** * Server Action pour mettre à jour le statut d'une tâche */ @@ -18,13 +44,24 @@ export async function updateTaskStatus( status: TaskStatus ): Promise { try { - const task = await tasksService.updateTask(taskId, { status }); + // Vérifier l'authentification et récupérer l'ID du user + const userId = await getAuthenticatedUser(); + + // Vérifier que la tâche appartient au user connecté + const isOwner = await verifyTaskOwnership(taskId); + if (!isOwner) { + return { success: false, error: 'Tâche non trouvée ou non autorisée' }; + } + + const updatedTask = await tasksService.updateTask(userId, taskId, { + status, + }); // Revalidation automatique du cache revalidatePath('/'); revalidatePath('/tasks'); - return { success: true, data: task }; + return { success: true, data: updatedTask }; } catch (error) { console.error('Error updating task status:', error); return { @@ -47,7 +84,18 @@ export async function updateTaskTitle( return { success: false, error: 'Title cannot be empty' }; } - const task = await tasksService.updateTask(taskId, { title: title.trim() }); + // Vérifier l'authentification et récupérer l'ID du user + const userId = await getAuthenticatedUser(); + + // Vérifier que la tâche appartient au user connecté + const isOwner = await verifyTaskOwnership(taskId); + if (!isOwner) { + return { success: false, error: 'Tâche non trouvée ou non autorisée' }; + } + + const task = await tasksService.updateTask(userId, taskId, { + title: title.trim(), + }); // Revalidation automatique du cache revalidatePath('/'); @@ -69,7 +117,16 @@ export async function updateTaskTitle( */ export async function deleteTask(taskId: string): Promise { try { - await tasksService.deleteTask(taskId); + // Vérifier l'authentification et récupérer l'ID du user + const userId = await getAuthenticatedUser(); + + // Vérifier que la tâche appartient au user connecté + const isOwner = await verifyTaskOwnership(taskId); + if (!isOwner) { + return { success: false, error: 'Tâche non trouvée ou non autorisée' }; + } + + await tasksService.deleteTask(userId, taskId); // Revalidation automatique du cache revalidatePath('/'); @@ -99,6 +156,15 @@ export async function updateTask(data: { dueDate?: Date; }): Promise { try { + // Vérifier l'authentification et récupérer l'ID du user + const userId = await getAuthenticatedUser(); + + // Vérifier que la tâche appartient au user connecté + const isOwner = await verifyTaskOwnership(data.taskId); + if (!isOwner) { + return { success: false, error: 'Tâche non trouvée ou non autorisée' }; + } + const updateData: Record = {}; if (data.title !== undefined) { @@ -117,7 +183,7 @@ export async function updateTask(data: { updateData.primaryTagId = data.primaryTagId; if (data.dueDate !== undefined) updateData.dueDate = data.dueDate; - const task = await tasksService.updateTask(data.taskId, updateData); + const task = await tasksService.updateTask(userId, data.taskId, updateData); // Revalidation automatique du cache revalidatePath('/'); @@ -149,6 +215,9 @@ export async function createTask(data: { return { success: false, error: 'Title is required' }; } + // Vérifier l'authentification et récupérer l'ID du user + const userId = await getAuthenticatedUser(); + const task = await tasksService.createTask({ title: data.title.trim(), description: data.description?.trim() || '', @@ -156,6 +225,7 @@ export async function createTask(data: { priority: data.priority || 'medium', tags: data.tags || [], primaryTagId: data.primaryTagId, + ownerId: userId, // Assigner la tâche au user connecté }); // Revalidation automatique du cache diff --git a/src/actions/tfs.ts b/src/actions/tfs.ts index 0309711..600d72d 100644 --- a/src/actions/tfs.ts +++ b/src/actions/tfs.ts @@ -108,8 +108,17 @@ export async function saveTfsSchedulerConfig( */ export async function syncTfsPullRequests() { try { + // Récupérer l'utilisateur connecté + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return { + success: false, + error: 'Utilisateur non authentifié', + }; + } + // Lancer la synchronisation via le service singleton - const result = await tfsService.syncTasks(); + const result = await tfsService.syncTasks(session.user.id); if (result.success) { revalidatePath('/'); diff --git a/src/app/api/daily/pending/route.ts b/src/app/api/daily/pending/route.ts index a32bb1e..3fbc2e2 100644 --- a/src/app/api/daily/pending/route.ts +++ b/src/app/api/daily/pending/route.ts @@ -1,9 +1,17 @@ import { NextRequest, NextResponse } from 'next/server'; import { dailyService } from '@/services/task-management/daily'; import { DailyCheckboxType } from '@/lib/types'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; 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 maxDays = searchParams.get('maxDays') @@ -20,6 +28,7 @@ export async function GET(request: NextRequest) { excludeToday, type, limit, + userId: session.user.id, // Filtrer par user connecté }); return NextResponse.json(pendingCheckboxes); diff --git a/src/app/api/jira/sync/route.ts b/src/app/api/jira/sync/route.ts index cf13fb7..21c36a4 100644 --- a/src/app/api/jira/sync/route.ts +++ b/src/app/api/jira/sync/route.ts @@ -115,7 +115,7 @@ export async function POST(request: Request) { } // Effectuer la synchronisation - const syncResult = await jiraService.syncTasks(); + const syncResult = await jiraService.syncTasks(session.user.id); // Convertir SyncResult en JiraSyncResult pour le client const jiraSyncResult = { diff --git a/src/app/api/tasks/[id]/checkboxes/route.ts b/src/app/api/tasks/[id]/checkboxes/route.ts index 7f1916a..5b21fd9 100644 --- a/src/app/api/tasks/[id]/checkboxes/route.ts +++ b/src/app/api/tasks/[id]/checkboxes/route.ts @@ -1,5 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { tasksService } from '@/services/task-management/tasks'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; export async function GET( request: NextRequest, @@ -15,7 +17,16 @@ export async function GET( ); } - const checkboxes = await tasksService.getTaskRelatedCheckboxes(id); + // Vérifier l'authentification + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); + } + + const checkboxes = await tasksService.getTaskRelatedCheckboxes( + session.user.id, + id + ); return NextResponse.json({ data: checkboxes }); } catch (error) { diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 4f7346b..b0d4b41 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -1,12 +1,23 @@ import { NextResponse } from 'next/server'; import { tasksService } from '@/services/task-management/tasks'; import { TaskStatus } from '@/lib/types'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '@/lib/auth'; /** * API route pour récupérer les tâches avec filtres optionnels */ export async function GET(request: Request) { try { + // Vérifier l'authentification + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { success: false, error: 'Non authentifié' }, + { status: 401 } + ); + } + const { searchParams } = new URL(request.url); // Extraire les paramètres de filtre @@ -16,6 +27,7 @@ export async function GET(request: Request) { search?: string; limit?: number; offset?: number; + ownerId?: string; // Filtre par propriétaire } = {}; const status = searchParams.get('status'); @@ -44,8 +56,8 @@ export async function GET(request: Request) { } // Récupérer les tâches - const tasks = await tasksService.getTasks(filters); - const stats = await tasksService.getTaskStats(); + const tasks = await tasksService.getTasks(session.user.id, filters); + const stats = await tasksService.getTaskStats(session.user.id); return NextResponse.json({ success: true, diff --git a/src/app/daily/page.tsx b/src/app/daily/page.tsx index a8156f1..59dbb94 100644 --- a/src/app/daily/page.tsx +++ b/src/app/daily/page.tsx @@ -40,13 +40,16 @@ export default async function DailyPage() { const [dailyView, dailyDates, deadlineMetrics, pendingTasks] = await Promise.all([ dailyService.getDailyView(today, session.user.id), - dailyService.getDailyDates(), - DeadlineAnalyticsService.getDeadlineMetrics().catch(() => null), // Graceful fallback + dailyService.getDailyDates(session.user.id), + DeadlineAnalyticsService.getDeadlineMetrics(session.user.id).catch( + () => null + ), // Graceful fallback dailyService .getPendingCheckboxes({ maxDays: 7, excludeToday: true, limit: 50, + userId: session.user.id, }) .catch(() => []), // Graceful fallback ]); diff --git a/src/app/kanban/page.tsx b/src/app/kanban/page.tsx index d016468..a89572f 100644 --- a/src/app/kanban/page.tsx +++ b/src/app/kanban/page.tsx @@ -1,14 +1,34 @@ import { tasksService } from '@/services/task-management/tasks'; import { tagsService } from '@/services/task-management/tags'; import { KanbanPageClient } from './KanbanPageClient'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; // Force dynamic rendering (no static generation) export const dynamic = 'force-dynamic'; export default async function KanbanPage() { + // Récupérer l'utilisateur connecté + const session = await getServerSession(authOptions); + const userId = session?.user?.id; + + // Si pas d'utilisateur connecté, retourner une page vide + if (!userId) { + return ( +
+
+

Connexion requise

+

+ Veuillez vous connecter pour accéder au tableau Kanban. +

+
+
+ ); + } + // SSR - Récupération des données côté serveur const [initialTasks, initialTags] = await Promise.all([ - tasksService.getTasks(), + tasksService.getTasks(userId), tagsService.getTags(), ]); diff --git a/src/app/page.tsx b/src/app/page.tsx index 1783121..2f78a17 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,11 +4,31 @@ import { AnalyticsService } from '@/services/analytics/analytics'; import { DeadlineAnalyticsService } from '@/services/analytics/deadline-analytics'; import { TagAnalyticsService } from '@/services/analytics/tag-analytics'; import { HomePageClient } from '@/components/HomePageClient'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; // Force dynamic rendering (no static generation) export const dynamic = 'force-dynamic'; export default async function HomePage() { + // Récupérer l'utilisateur connecté + const session = await getServerSession(authOptions); + const userId = session?.user?.id; + + // Si pas d'utilisateur connecté, retourner une page vide ou rediriger + if (!userId) { + return ( +
+
+

Connexion requise

+

+ Veuillez vous connecter pour accéder à votre tableau de bord. +

+
+
+ ); + } + // SSR - Récupération des données côté serveur const [ initialTasks, @@ -18,12 +38,12 @@ export default async function HomePage() { deadlineMetrics, tagMetrics, ] = await Promise.all([ - tasksService.getTasks(), + tasksService.getTasks(userId), tagsService.getTags(), - tasksService.getTaskStats(), - AnalyticsService.getProductivityMetrics(), - DeadlineAnalyticsService.getDeadlineMetrics(), - TagAnalyticsService.getTagDistributionMetrics(), + tasksService.getTaskStats(userId), + AnalyticsService.getProductivityMetrics(userId), + DeadlineAnalyticsService.getDeadlineMetrics(userId), + TagAnalyticsService.getTagDistributionMetrics(userId), ]); return ( diff --git a/src/app/settings/advanced/page.tsx b/src/app/settings/advanced/page.tsx index e81b435..244ecd3 100644 --- a/src/app/settings/advanced/page.tsx +++ b/src/app/settings/advanced/page.tsx @@ -3,14 +3,34 @@ import { tagsService } from '@/services/task-management/tags'; import { backupService } from '@/services/data-management/backup'; import { backupScheduler } from '@/services/data-management/backup-scheduler'; import { AdvancedSettingsPageClient } from '@/components/settings/AdvancedSettingsPageClient'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; // Force dynamic rendering for real-time data export const dynamic = 'force-dynamic'; export default async function AdvancedSettingsPage() { + // Récupérer l'utilisateur connecté + const session = await getServerSession(authOptions); + const userId = session?.user?.id; + + // Si pas d'utilisateur connecté, retourner une page vide + if (!userId) { + return ( +
+
+

Connexion requise

+

+ Veuillez vous connecter pour accéder aux paramètres avancés. +

+
+
+ ); + } + // Fetch all data server-side const [taskStats, tags] = await Promise.all([ - tasksService.getTaskStats(), + tasksService.getTaskStats(userId), tagsService.getTags(), ]); diff --git a/src/app/weekly-manager/page.tsx b/src/app/weekly-manager/page.tsx index 42d78f8..0db7879 100644 --- a/src/app/weekly-manager/page.tsx +++ b/src/app/weekly-manager/page.tsx @@ -3,15 +3,35 @@ import { ManagerSummaryService } from '@/services/analytics/manager-summary'; import { tasksService } from '@/services/task-management/tasks'; import { tagsService } from '@/services/task-management/tags'; import { WeeklyManagerPageClient } from './WeeklyManagerPageClient'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; // Force dynamic rendering (no static generation) export const dynamic = 'force-dynamic'; export default async function WeeklyManagerPage() { + // Récupérer l'utilisateur connecté + const session = await getServerSession(authOptions); + const userId = session?.user?.id; + + // Si pas d'utilisateur connecté, retourner une page vide + if (!userId) { + return ( +
+
+

Connexion requise

+

+ Veuillez vous connecter pour accéder au gestionnaire hebdomadaire. +

+
+
+ ); + } + // SSR - Récupération des données côté serveur const [summary, initialTasks, initialTags] = await Promise.all([ - ManagerSummaryService.getManagerSummary(), - tasksService.getTasks(), + ManagerSummaryService.getManagerSummary(userId), + tasksService.getTasks(userId), tagsService.getTags(), ]); diff --git a/src/components/tfs/TfsSync.tsx b/src/components/tfs/TfsSync.tsx index 1fff511..6c2b79d 100644 --- a/src/components/tfs/TfsSync.tsx +++ b/src/components/tfs/TfsSync.tsx @@ -6,7 +6,7 @@ import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { getToday } from '@/lib/date-utils'; import { Modal } from '@/components/ui/Modal'; -import { TfsSyncResult, TfsSyncAction } from '@/services/integrations/tfs'; +import { TfsSyncResult, TfsSyncAction } from '@/services/integrations/tfs/tfs'; interface TfsSyncProps { onSyncComplete?: () => void; @@ -201,7 +201,7 @@ export function TfsSync({ onSyncComplete, className = '' }: TfsSyncProps) { Erreurs ({errors.length}):
- {errors.map((err, i) => ( + {errors.map((err: string, i: number) => (
{getActionIcon(type as TfsSyncAction['type'])} {getActionLabel(type as TfsSyncAction['type'])} ( - {typeActions.length}) + {(typeActions as TfsSyncAction[]).length})
- {typeActions.map((action, index) => ( -
-
-
-
- - PR #{action.pullRequestId} - - - {action.prTitle} - -
-
- - {getActionLabel(action.type)} - -
- - {action.reason && ( -
- 💡 {action.reason} -
- )} - - {action.changes && action.changes.length > 0 && ( -
-
- Modifications: -
- {action.changes.map((change, changeIndex) => ( -
- {change} + {(typeActions as TfsSyncAction[]).map( + (action: TfsSyncAction, index: number) => ( +
+
+
+
+ + PR #{action.pullRequestId} + + + {action.prTitle} +
- ))} +
+ + {getActionLabel(action.type)} +
- )} -
- ))} + + {action.reason && ( +
+ 💡 {action.reason} +
+ )} + + {action.changes && action.changes.length > 0 && ( +
+
+ Modifications: +
+ {action.changes.map( + (change: string, changeIndex: number) => ( +
+ {change} +
+ ) + )} +
+ )} +
+ ) + )}
))} diff --git a/src/components/ui-showcase/sections/TaskSelectorSection.tsx b/src/components/ui-showcase/sections/TaskSelectorSection.tsx index cd3464d..f1e139e 100644 --- a/src/components/ui-showcase/sections/TaskSelectorSection.tsx +++ b/src/components/ui-showcase/sections/TaskSelectorSection.tsx @@ -20,6 +20,7 @@ const mockTasks: Task[] = [ source: 'manual', sourceId: '1', tagDetails: [], + ownerId: 'mock-user-1', }, { id: '2', @@ -34,6 +35,7 @@ const mockTasks: Task[] = [ source: 'manual', sourceId: '2', tagDetails: [], + ownerId: 'mock-user-1', }, { id: '3', @@ -48,6 +50,7 @@ const mockTasks: Task[] = [ source: 'manual', sourceId: '3', tagDetails: [], + ownerId: 'mock-user-1', }, { id: '4', @@ -62,6 +65,7 @@ const mockTasks: Task[] = [ source: 'manual', sourceId: '4', tagDetails: [], + ownerId: 'mock-user-1', }, { id: '5', @@ -76,6 +80,7 @@ const mockTasks: Task[] = [ source: 'manual', sourceId: '5', tagDetails: [], + ownerId: 'mock-user-1', }, ]; diff --git a/src/lib/types.ts b/src/lib/types.ts index c5fbb60..bbc834c 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -64,6 +64,7 @@ export interface Task { tfsTargetBranch?: string; assignee?: string; + ownerId: string; // ID du propriétaire de la tâche todosCount?: number; // Nombre de todos reliés à cette tâche } diff --git a/src/services/analytics/analytics.ts b/src/services/analytics/analytics.ts index 075d1df..871389b 100644 --- a/src/services/analytics/analytics.ts +++ b/src/services/analytics/analytics.ts @@ -44,6 +44,7 @@ export class AnalyticsService { * Calcule les métriques de productivité pour une période donnée */ static async getProductivityMetrics( + userId: string, timeRange?: TimeRange, sources?: string[] ): Promise { @@ -56,6 +57,9 @@ export class AnalyticsService { // Récupérer toutes les tâches depuis la base de données avec leurs tags const dbTasks = await prisma.task.findMany({ + where: { + ownerId: userId, + }, include: { taskTags: { include: { @@ -83,6 +87,7 @@ export class AnalyticsService { jiraKey: task.jiraKey || undefined, jiraType: task.jiraType || undefined, assignee: task.assignee || undefined, + ownerId: task.ownerId, })); // Filtrer par sources si spécifié diff --git a/src/services/analytics/deadline-analytics.ts b/src/services/analytics/deadline-analytics.ts index 41d513f..09caa8d 100644 --- a/src/services/analytics/deadline-analytics.ts +++ b/src/services/analytics/deadline-analytics.ts @@ -34,6 +34,7 @@ export class DeadlineAnalyticsService { * Analyse les tâches selon leurs échéances */ static async getDeadlineMetrics( + userId: string, sources?: string[] ): Promise { try { @@ -42,6 +43,7 @@ export class DeadlineAnalyticsService { // Récupérer toutes les tâches non terminées avec échéance const dbTasks = await prisma.task.findMany({ where: { + ownerId: userId, dueDate: { not: null, }, @@ -137,9 +139,10 @@ export class DeadlineAnalyticsService { * Retourne les tâches les plus critiques (en retard + échéance dans 48h) */ static async getCriticalDeadlines( + userId: string, sources?: string[] ): Promise { - const metrics = await this.getDeadlineMetrics(sources); + const metrics = await this.getDeadlineMetrics(userId, sources); return [...metrics.overdue, ...metrics.critical].slice(0, 10); // Limite à 10 tâches les plus critiques } diff --git a/src/services/analytics/manager-summary.ts b/src/services/analytics/manager-summary.ts index 42d1f5c..27088c7 100644 --- a/src/services/analytics/manager-summary.ts +++ b/src/services/analytics/manager-summary.ts @@ -87,6 +87,7 @@ export class ManagerSummaryService { * Génère un résumé orienté manager pour les 7 derniers jours */ static async getManagerSummary( + userId: string, date: Date = getToday() ): Promise { // Fenêtre glissante de 7 jours au lieu de semaine calendaire @@ -96,8 +97,8 @@ export class ManagerSummaryService { // Récupérer les données de base const [tasks, checkboxes] = await Promise.all([ - this.getCompletedTasks(weekStart, weekEnd), - this.getCompletedCheckboxes(weekStart, weekEnd), + this.getCompletedTasks(userId, weekStart, weekEnd), + this.getCompletedCheckboxes(userId, weekStart, weekEnd), ]); // Analyser et extraire les accomplissements clés @@ -107,7 +108,7 @@ export class ManagerSummaryService { ); // Identifier les défis à venir - const upcomingChallenges = await this.identifyUpcomingChallenges(); + const upcomingChallenges = await this.identifyUpcomingChallenges(userId); // Calculer les métriques const metrics = this.calculateMetrics(tasks, checkboxes); @@ -130,9 +131,14 @@ export class ManagerSummaryService { /** * Récupère les tâches complétées de la semaine */ - private static async getCompletedTasks(startDate: Date, endDate: Date) { + private static async getCompletedTasks( + userId: string, + startDate: Date, + endDate: Date + ) { const tasks = await prisma.task.findMany({ where: { + ownerId: userId, OR: [ // Tâches avec completedAt dans la période (priorité) { @@ -172,14 +178,24 @@ export class ManagerSummaryService { /** * Récupère les checkboxes complétées de la semaine */ - private static async getCompletedCheckboxes(startDate: Date, endDate: Date) { + private static async getCompletedCheckboxes( + userId: string, + startDate: Date, + endDate: Date + ) { const checkboxes = await prisma.dailyCheckbox.findMany({ where: { + userId: userId, isChecked: true, date: { gte: startDate, lte: endDate, }, + // S'assurer que si le todo est lié à une tâche, cette tâche appartient bien à l'utilisateur + OR: [ + { task: null }, // Todos standalone (sans tâche associée) + { task: { ownerId: userId } }, // Todos liés à une tâche de l'utilisateur + ], }, select: { id: true, @@ -297,12 +313,13 @@ export class ManagerSummaryService { /** * Identifie les défis et enjeux à venir */ - private static async identifyUpcomingChallenges(): Promise< - UpcomingChallenge[] - > { + private static async identifyUpcomingChallenges( + userId: string + ): Promise { // Récupérer les tâches à venir (priorité high/medium en premier) const upcomingTasks = await prisma.task.findMany({ where: { + ownerId: userId, completedAt: null, }, orderBy: [ @@ -331,18 +348,30 @@ export class ManagerSummaryService { // Récupérer les checkboxes récurrentes non complétées (meetings + tâches prioritaires) const upcomingCheckboxes = await prisma.dailyCheckbox.findMany({ where: { + userId: userId, // Filtrer par utilisateur isChecked: false, date: { gte: getToday(), }, - OR: [ - { type: 'meeting' }, + // S'assurer que si le todo est lié à une tâche, cette tâche appartient bien à l'utilisateur + AND: [ { - task: { - priority: { - in: ['high', 'medium'], + OR: [ + { task: null }, // Todos standalone (sans tâche associée) + { task: { ownerId: userId } }, // Todos liés à une tâche de l'utilisateur + ], + }, + { + OR: [ + { type: 'meeting' }, + { + task: { + priority: { + in: ['high', 'medium'], + }, + }, }, - }, + ], }, ], }, diff --git a/src/services/analytics/metrics.ts b/src/services/analytics/metrics.ts index dd3f8fc..8894a74 100644 --- a/src/services/analytics/metrics.ts +++ b/src/services/analytics/metrics.ts @@ -72,6 +72,7 @@ export class MetricsService { * Récupère les métriques journalières des 7 derniers jours */ static async getWeeklyMetrics( + userId: string, date: Date = getToday() ): Promise { // Fenêtre glissante de 7 jours au lieu de semaine calendaire @@ -84,7 +85,7 @@ export class MetricsService { // Récupérer les données pour chaque jour const dailyBreakdown = await Promise.all( - daysOfWeek.map((day) => this.getDailyMetrics(day)) + daysOfWeek.map((day) => this.getDailyMetrics(userId, day)) ); // Calculer les métriques de résumé @@ -114,7 +115,10 @@ export class MetricsService { /** * Récupère les métriques pour un jour donné */ - private static async getDailyMetrics(date: Date): Promise { + private static async getDailyMetrics( + userId: string, + date: Date + ): Promise { const dayStart = startOfDay(date); const dayEnd = endOfDay(date); @@ -124,6 +128,7 @@ export class MetricsService { // Tâches complétées ce jour prisma.task.count({ where: { + ownerId: userId, OR: [ { completedAt: { @@ -145,6 +150,7 @@ export class MetricsService { // Tâches en cours (status = in_progress à ce moment) prisma.task.count({ where: { + ownerId: userId, status: 'in_progress', createdAt: { lte: dayEnd }, }, @@ -153,6 +159,7 @@ export class MetricsService { // Tâches bloquées prisma.task.count({ where: { + ownerId: userId, status: 'blocked', createdAt: { lte: dayEnd }, }, @@ -161,6 +168,7 @@ export class MetricsService { // Tâches en attente prisma.task.count({ where: { + ownerId: userId, status: 'pending', createdAt: { lte: dayEnd }, }, @@ -169,6 +177,7 @@ export class MetricsService { // Nouvelles tâches créées ce jour prisma.task.count({ where: { + ownerId: userId, createdAt: { gte: dayStart, lte: dayEnd, @@ -179,6 +188,7 @@ export class MetricsService { // Total des tâches existantes ce jour prisma.task.count({ where: { + ownerId: userId, createdAt: { lte: dayEnd }, }, }), @@ -375,6 +385,7 @@ export class MetricsService { * Récupère les métriques de vélocité d'équipe (pour graphiques de tendance) */ static async getVelocityTrends( + userId: string, weeksBack: number = 4 ): Promise { const trends = []; @@ -388,6 +399,7 @@ export class MetricsService { const [completed, created] = await Promise.all([ prisma.task.count({ where: { + ownerId: userId, completedAt: { gte: weekStart, lte: weekEnd, @@ -396,6 +408,7 @@ export class MetricsService { }), prisma.task.count({ where: { + ownerId: userId, createdAt: { gte: weekStart, lte: weekEnd, diff --git a/src/services/analytics/tag-analytics.ts b/src/services/analytics/tag-analytics.ts index 19e02b7..312f816 100644 --- a/src/services/analytics/tag-analytics.ts +++ b/src/services/analytics/tag-analytics.ts @@ -47,6 +47,7 @@ export class TagAnalyticsService { * Calcule les métriques de distribution par tags */ static async getTagDistributionMetrics( + userId: string, timeRange?: TimeRange, sources?: string[] ): Promise { @@ -60,6 +61,7 @@ export class TagAnalyticsService { // Récupérer toutes les tâches avec leurs tags const dbTasks = await prisma.task.findMany({ where: { + ownerId: userId, createdAt: { gte: start, lte: end, @@ -102,6 +104,7 @@ export class TagAnalyticsService { jiraKey: task.jiraKey || undefined, jiraType: task.jiraType || undefined, assignee: task.assignee || undefined, + ownerId: task.ownerId, })); // Filtrer par sources si spécifié diff --git a/src/services/integrations/jira/jira.ts b/src/services/integrations/jira/jira.ts index cc46b04..94054c9 100644 --- a/src/services/integrations/jira/jira.ts +++ b/src/services/integrations/jira/jira.ts @@ -355,7 +355,7 @@ export class JiraService { /** * Synchronise les tickets Jira avec la base locale */ - async syncTasks(): Promise { + async syncTasks(userId: string): Promise { const result: SyncResult = { success: false, totalItems: 0, @@ -395,7 +395,7 @@ export class JiraService { // Synchroniser chaque ticket for (const jiraTask of filteredTasks) { try { - const syncAction = await this.syncSingleTask(jiraTask); + const syncAction = await this.syncSingleTask(jiraTask, userId); // Convertir JiraSyncAction vers SyncAction const standardAction: SyncAction = { @@ -467,7 +467,10 @@ export class JiraService { /** * Synchronise un ticket Jira unique */ - private async syncSingleTask(jiraTask: JiraTask): Promise { + private async syncSingleTask( + jiraTask: JiraTask, + userId: string + ): Promise { // Chercher la tâche existante const existingTask = await prisma.task.findUnique({ where: { @@ -496,6 +499,7 @@ export class JiraService { jiraType: this.mapJiraTypeToDisplay(jiraTask.issuetype.name), assignee: jiraTask.assignee?.displayName || null, updatedAt: parseDate(jiraTask.updated), + ownerId: userId, }; if (!existingTask) { diff --git a/src/services/integrations/jira/scheduler.ts b/src/services/integrations/jira/scheduler.ts index 9820b95..b9d4069 100644 --- a/src/services/integrations/jira/scheduler.ts +++ b/src/services/integrations/jira/scheduler.ts @@ -127,7 +127,7 @@ export class JiraScheduler { } // Effectuer la synchronisation - const result = await jiraService.syncTasks(); + const result = await jiraService.syncTasks(userId); if (result.success) { console.log( diff --git a/src/services/integrations/tfs.ts b/src/services/integrations/tfs.ts deleted file mode 100644 index 9af09e7..0000000 --- a/src/services/integrations/tfs.ts +++ /dev/null @@ -1,1093 +0,0 @@ -/** - * Service de gestion TFS/Azure DevOps - * Intégration unidirectionnelle Azure DevOps → TowerControl - * Focus sur les Pull Requests comme tâches - */ - -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'; - -export interface TfsConfig { - enabled: boolean; - organizationUrl?: string; // https://dev.azure.com/myorg - projectName?: string; // Optionnel: pour filtrer un projet spécifique - personalAccessToken?: string; - repositories?: string[]; // Liste des repos à surveiller - ignoredRepositories?: string[]; // Liste des repos à ignorer -} - -export interface TfsSyncAction { - type: 'created' | 'updated' | 'skipped' | 'deleted'; - pullRequestId: number; - prTitle: string; - reason?: string; - changes?: string[]; -} - -// Types génériques pour compatibilité avec d'autres services -export interface SyncAction { - type: 'created' | 'updated' | 'skipped' | 'deleted'; - itemId: string | number; - title: string; - message?: string; -} - -export interface SyncResult { - success: boolean; - totalItems: number; - actions: SyncAction[]; - errors: string[]; - stats: { - created: number; - updated: number; - skipped: number; - deleted: number; - }; -} - -export interface TfsSyncResult { - success: boolean; - totalPullRequests: number; - pullRequestsCreated: number; - pullRequestsUpdated: number; - pullRequestsSkipped: number; - pullRequestsDeleted: number; - errors: string[]; - actions: TfsSyncAction[]; -} - -export class TfsService { - readonly config: TfsConfig; - - constructor(config: TfsConfig) { - this.config = config; - } - - /** - * Teste la connexion à Azure DevOps - */ - async testConnection(): Promise { - try { - // Tester avec l'endpoint des projets pour valider l'accès à l'organisation - const response = await this.makeApiRequest( - '/_apis/projects?api-version=6.0&$top=1' - ); - - if (response.ok) { - console.log('✓ Connexion TFS réussie et organisation accessible'); - return true; - } else if (response.status === 401) { - console.error( - '❗️ Erreur TFS: Authentification échouée (token invalide)' - ); - return false; - } else if (response.status === 403) { - console.error( - '❗️ Erreur TFS: Accès refusé (permissions insuffisantes)' - ); - return false; - } else { - console.error( - `❗️ Erreur TFS: ${response.status} ${response.statusText}` - ); - return false; - } - } catch (error) { - console.error('❗️ Erreur connexion TFS:', error); - return false; - } - } - - /** - * Valide la configuration TFS - */ - async validateConfig(): Promise<{ valid: boolean; error?: string }> { - if (!this.config.enabled) { - return { valid: false, error: 'TFS désactivé' }; - } - if (!this.config.organizationUrl) { - return { valid: false, error: "URL de l'organisation manquante" }; - } - if (!this.config.personalAccessToken) { - return { valid: false, error: "Token d'accès personnel manquant" }; - } - - // Tester la connexion pour validation complète - const connectionOk = await this.testConnection(); - if (!connectionOk) { - return { - valid: false, - error: 'Impossible de se connecter avec ces paramètres', - }; - } - - return { valid: true }; - } - - /** - * Valide l'existence d'un projet Azure DevOps - */ - async validateProject( - projectName: string - ): Promise<{ exists: boolean; name?: string; error?: string }> { - try { - const response = await this.makeApiRequest( - `/_apis/projects/${encodeURIComponent(projectName)}?api-version=6.0` - ); - - if (response.ok) { - const projectData = await response.json(); - return { - exists: true, - name: projectData.name, - }; - } else if (response.status === 404) { - return { - exists: false, - error: `Projet "${projectName}" non trouvé`, - }; - } else { - const errorData = await response.json().catch(() => ({})); - return { - exists: false, - error: errorData.message || `Erreur ${response.status}`, - }; - } - } catch (error) { - console.error('❌ Erreur validation projet TFS:', error); - return { - exists: false, - error: error instanceof Error ? error.message : 'Erreur de connexion', - }; - } - } - - /** - * Récupère la liste des repositories d'un projet (ou de toute l'organisation si pas de projet spécifié) - */ - async getRepositories(): Promise< - Array<{ id: string; name: string; project?: string }> - > { - try { - // Si un projet spécifique est configuré, récupérer uniquement ses repos - let endpoint: string; - if (this.config.projectName) { - endpoint = `/_apis/git/repositories?api-version=6.0&$top=1000`; - } else { - // Récupérer tous les repositories de l'organisation - endpoint = `/_apis/git/repositories?api-version=6.0&includeAllProjects=true&$top=1000`; - } - - const response = await this.makeApiRequest(endpoint); - - if (!response.ok) { - throw new Error(`Erreur API: ${response.status}`); - } - - const data = await response.json(); - return ( - data.value?.map( - (repo: { id: string; name: string; project?: { name: string } }) => ({ - id: repo.id, - name: repo.name, - project: repo.project?.name, - }) - ) || [] - ); - } catch (error) { - console.error('❗️ Erreur récupération repositories TFS:', error); - return []; - } - } - - /** - * Récupère toutes les Pull Requests assignées à l'utilisateur actuel dans l'organisation - */ - async getMyPullRequests(): Promise { - try { - // Uniquement les PRs créées par l'utilisateur (simplifié) - const createdPrs = await this.getPullRequestsByCreator(); - - // Filtrer les PRs selon la configuration - const filteredPrs = this.filterPullRequests(createdPrs); - - return filteredPrs; - } catch (error) { - console.error('❗️ Erreur récupération PRs utilisateur:', error); - return []; - } - } - - /** - * Récupère les PRs créées par l'utilisateur - */ - private async getPullRequestsByCreator(): Promise { - try { - // Récupérer l'ID utilisateur réel pour le filtrage - const currentUserId = await this.getCurrentUserId(); - if (!currentUserId) { - console.error( - "❌ Impossible de récupérer l'ID utilisateur pour filtrer les PRs" - ); - return []; - } - - console.log( - `🎯 Recherche des PRs créées par l'utilisateur ID: ${currentUserId}` - ); - - const searchParams = new URLSearchParams({ - 'api-version': '6.0', - 'searchCriteria.creatorId': currentUserId, // Utiliser l'ID réel au lieu de @me - 'searchCriteria.status': 'all', // Inclut active, completed, abandoned - $top: '1000', - }); - - const url = `/_apis/git/pullrequests?${searchParams.toString()}`; - const response = await this.makeApiRequest(url); - - if (!response.ok) { - const errorText = await response.text(); - console.error('❌ Erreur API créateur:', response.status, errorText); - throw new Error( - `Erreur API créateur: ${response.status} - ${errorText}` - ); - } - - const data = await response.json(); - - const prs = data.value || []; - return prs; - } catch (error) { - console.error('❗️ Erreur récupération PRs créateur:', error); - return []; - } - } - - /** - * Récupère l'ID de l'utilisateur courant - */ - private async getCurrentUserId(): Promise { - try { - // Essayer d'abord avec l'endpoint ConnectionData (plus fiable) - const response = await this.makeApiRequest('/_apis/connectionData'); - - if (response.ok) { - const connectionData = await response.json(); - const userId = connectionData?.authenticatedUser?.id; - if (userId) { - console.log('✅ ID utilisateur récupéré via ConnectionData:', userId); - return userId; - } - } - - console.error( - "❌ Impossible de récupérer l'ID utilisateur par aucune méthode" - ); - return null; - } catch (error) { - console.error('❌ Erreur récupération ID utilisateur:', error); - return null; - } - } - - /** - * Filtre les Pull Requests selon la configuration - */ - private filterPullRequests(pullRequests: TfsPullRequest[]): TfsPullRequest[] { - console.log('🗺 Configuration de filtrage:', { - projectName: this.config.projectName, - repositories: this.config.repositories, - ignoredRepositories: this.config.ignoredRepositories, - }); - - // console.log( - // '📋 PRs avant filtrage:', - // pullRequests.map((pr) => ({ - // id: pr.pullRequestId, - // title: pr.title, - // project: pr.repository.project.name, - // repository: pr.repository.name, - // status: pr.status, - // closedDate: pr.closedDate, - // })) - // ); - - let filtered = pullRequests; - const initialCount = filtered.length; - - // 1. Filtrer par statut pertinent (exclure les abandoned, limiter les completed récentes) - const beforeStatusFilter = filtered.length; - filtered = this.filterByRelevantStatus(filtered); - console.log( - `📋 Filtrage statut pertinent: ${beforeStatusFilter} -> ${filtered.length}` - ); - - // 2. Filtrer par projet si spécifié - if (this.config.projectName) { - const beforeProjectFilter = filtered.length; - filtered = filtered.filter( - (pr) => pr.repository.project.name === this.config.projectName - ); - console.log( - `🎯 Filtrage projet "${this.config.projectName}": ${beforeProjectFilter} -> ${filtered.length}` - ); - } - - // Filtrer par repositories autorisés - if (this.config.repositories?.length) { - const beforeRepoFilter = filtered.length; - filtered = filtered.filter((pr) => - this.config.repositories!.includes(pr.repository.name) - ); - console.log( - `📋 Filtrage repositories autorisés ${JSON.stringify(this.config.repositories)}: ${beforeRepoFilter} -> ${filtered.length}` - ); - } - - // Exclure les repositories ignorés - if (this.config.ignoredRepositories?.length) { - const beforeIgnoreFilter = filtered.length; - filtered = filtered.filter( - (pr) => !this.config.ignoredRepositories!.includes(pr.repository.name) - ); - console.log( - `❌ Exclusion repositories ignorés ${JSON.stringify(this.config.ignoredRepositories)}: ${beforeIgnoreFilter} -> ${filtered.length}` - ); - } - - console.log( - `🎟️ Résultat filtrage final: ${initialCount} -> ${filtered.length}` - ); - // console.log( - // '📋 PRs après filtrage:', - // filtered.map((pr) => ({ - // id: pr.pullRequestId, - // title: pr.title, - // project: pr.repository.project.name, - // repository: pr.repository.name, - // status: pr.status, - // })) - // ); - - return filtered; - } - - /** - * Filtre les PRs par statut pertinent - * - Garde toutes les PRs actives créées dans les 90 derniers jours - * - Garde les PRs completed récentes (moins de 30 jours) - * - Exclut les PRs abandoned - * - Exclut les PRs trop anciennes - * - Exclut les PRs automatiques (Renovate, etc.) - */ - private filterByRelevantStatus( - pullRequests: TfsPullRequest[] - ): TfsPullRequest[] { - const now = new Date(); - const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); - - return pullRequests.filter((pr) => { - // Exclure les PRs automatiques (Renovate, Dependabot, etc.) - if (this.isAutomaticPR(pr)) { - console.log( - `🤖 PR ${pr.pullRequestId} (${pr.title}): PR automatique - EXCLUE` - ); - return false; - } - - // Filtrer d'abord par âge - exclure les PRs trop anciennes - // const createdDate = parseDate(pr.creationDate); - // if (createdDate < ninetyDaysAgo) { - // console.log( - // `🗺 PR ${pr.pullRequestId} (${pr.title}): Trop ancienne (${formatDateForDisplay(createdDate)}) - EXCLUE` - // ); - // return false; - // } - - switch (pr.status.toLowerCase()) { - case 'active': - // PRs actives récentes - console.log( - `✅ PR ${pr.pullRequestId} (${pr.title}): Active récente - INCLUSE` - ); - return true; - - case 'completed': - // PRs completed récentes (moins de 30 jours) - if (pr.closedDate) { - const closedDate = parseDate(pr.closedDate); - const isRecent = closedDate >= thirtyDaysAgo; - return isRecent; - } else { - // Si pas de date de fermeture, on l'inclut par sécurité - return true; - } - - case 'abandoned': - // PRs abandonnées ne sont pas pertinentes - return false; - - default: - // Statut inconnu, on l'inclut par précaution - return true; - } - }); - } - - /** - * Détermine si une PR est automatique (bot, renovate, dependabot, etc.) - */ - private isAutomaticPR(pr: TfsPullRequest): boolean { - // Patterns dans le titre - const automaticTitlePatterns = [ - /configure renovate/i, - /update dependency/i, - /bump .+ from .+ to/i, - /\[dependabot\]/i, - /\[renovate\]/i, - /automated pr/i, - /auto.update/i, - /security update/i, - ]; - - // Vérifier le titre - for (const pattern of automaticTitlePatterns) { - if (pattern.test(pr.title)) { - return true; - } - } - - // Patterns dans la description - const automaticDescPatterns = [ - /this pr was automatically created/i, - /renovate bot/i, - /dependabot/i, - /automated dependency update/i, - ]; - - // Vérifier la description - if (pr.description) { - for (const pattern of automaticDescPatterns) { - if (pattern.test(pr.description)) { - return true; - } - } - } - - // Vérifier l'auteur (noms de bots courants) - const botAuthors = [ - 'renovate[bot]', - 'dependabot[bot]', - 'dependabot', - 'renovate', - 'greenkeeper[bot]', - 'snyk-bot', - ]; - - const authorName = pr.createdBy.displayName?.toLowerCase() || ''; - for (const botName of botAuthors) { - if (authorName.includes(botName.toLowerCase())) { - return true; - } - } - - // Vérifier la branche source (patterns de bots) - const automaticBranchPatterns = [ - /renovate\//i, - /dependabot\//i, - /update\/.+dependency/i, - /bump\//i, - ]; - - const sourceBranch = pr.sourceRefName.replace('refs/heads/', ''); - for (const pattern of automaticBranchPatterns) { - if (pattern.test(sourceBranch)) { - return true; - } - } - - return false; - } - - /** - * Synchronise les Pull Requests avec les tâches locales - */ - async syncTasks(): Promise { - const result: TfsSyncResult = { - success: true, - totalPullRequests: 0, - pullRequestsCreated: 0, - pullRequestsUpdated: 0, - pullRequestsSkipped: 0, - pullRequestsDeleted: 0, - actions: [], - errors: [], - }; - - try { - console.log('🔄 Début synchronisation TFS Pull Requests...'); - - // S'assurer que le tag TFS existe - await this.ensureTfsTagExists(); - - // Récupérer toutes les PRs assignées à l'utilisateur - const allPullRequests = await this.getMyPullRequests(); - result.totalPullRequests = allPullRequests.length; - - if (allPullRequests.length === 0) { - return result; - } - - // Récupérer les IDs des PRs actuelles pour le nettoyage - const currentPrIds = new Set( - allPullRequests.map((pr) => pr.pullRequestId) - ); - - // Synchroniser chaque PR - for (const pr of allPullRequests) { - try { - const syncAction = await this.syncSinglePullRequest(pr); - result.actions.push(syncAction); - - // Compter les actions - if (syncAction.type === 'created') { - result.pullRequestsCreated++; - } else if (syncAction.type === 'updated') { - result.pullRequestsUpdated++; - } else { - result.pullRequestsSkipped++; - } - } catch (error) { - const errorMsg = `Erreur sync PR ${pr.pullRequestId}: ${error instanceof Error ? error.message : 'Erreur inconnue'}`; - result.errors.push(errorMsg); - console.error('❌', errorMsg); - } - } - - // Nettoyer les tâches TFS qui ne sont plus actives - const deletedActions = - await this.cleanupInactivePullRequests(currentPrIds); - result.pullRequestsDeleted = deletedActions.length; - result.actions.push(...deletedActions); - - console.log(`✅ Synchronisation TFS terminée:`, { - créées: result.pullRequestsCreated, - mises_a_jour: result.pullRequestsUpdated, - ignorées: result.pullRequestsSkipped, - supprimées: result.pullRequestsDeleted, - }); - - result.success = result.errors.length === 0; - } catch (error) { - result.success = false; - const errorMsg = - error instanceof Error ? error.message : 'Erreur inconnue'; - result.errors.push(errorMsg); - console.error('❌ Erreur sync TFS:', errorMsg); - } - - return result; - } - - /** - * Synchronise une Pull Request unique - */ - private async syncSinglePullRequest( - pr: TfsPullRequest - ): Promise { - const pullRequestId = pr.pullRequestId; - const sourceId = `tfs-pr-${pullRequestId}`; - - // Chercher la tâche existante - const existingTask = await prisma.task.findFirst({ - where: { sourceId }, - }); - - const taskData = this.mapPullRequestToTask(pr); - - if (!existingTask) { - // Créer nouvelle tâche - const newTask = await prisma.task.create({ - data: { - ...taskData, - sourceId, - createdAt: new Date(), - updatedAt: new Date(), - }, - }); - - // Assigner le tag TFS - await this.assignTfsTag(newTask.id); - - return { - type: 'created', - pullRequestId, - prTitle: pr.title, - }; - } else { - // Détecter les changements - const changes: string[] = []; - - if (existingTask.title !== taskData.title) { - changes.push(`Titre: ${existingTask.title} → ${taskData.title}`); - } - if (existingTask.status !== taskData.status) { - changes.push(`Statut: ${existingTask.status} → ${taskData.status}`); - } - if (existingTask.description !== taskData.description) { - changes.push('Description modifiée'); - } - if (existingTask.assignee !== taskData.assignee) { - changes.push( - `Assigné: ${existingTask.assignee} → ${taskData.assignee}` - ); - } - - if (changes.length === 0) { - // S'assurer que le tag TFS est assigné (pour les anciennes tâches) - await this.assignTfsTag(existingTask.id); - - return { - type: 'skipped', - pullRequestId, - prTitle: pr.title, - reason: 'Aucun changement détecté', - }; - } - - // Mettre à jour la tâche - await prisma.task.update({ - where: { id: existingTask.id }, - data: { - ...taskData, - updatedAt: new Date(), - }, - }); - - return { - type: 'updated', - pullRequestId, - prTitle: pr.title, - changes, - }; - } - } - - /** - * S'assure que le tag TFS existe - */ - private async ensureTfsTagExists(): 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éé'); - } - } catch (error) { - console.warn('Erreur création tag TFS:', error); - } - } - - /** - * Assigne automatiquement le tag "TFS" aux tâches importées - */ - private async assignTfsTag(taskId: 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, - }, - }); - } - - // Vérifier si la relation existe déjà - const existingRelation = await prisma.taskTag.findFirst({ - where: { taskId, tagId: tfsTag.id }, - }); - - if (!existingRelation) { - await prisma.taskTag.create({ - data: { taskId, tagId: tfsTag.id }, - }); - } - } catch (error) { - console.error('❌ Erreur assignation tag TFS:', error); - // Ne pas faire échouer la sync pour un problème de tag - } - } - - /** - * Mappe une Pull Request TFS vers le format Task - */ - private mapPullRequestToTask(pr: TfsPullRequest) { - const status = this.mapTfsStatusToInternal(pr.status); - const sourceBranch = pr.sourceRefName.replace('refs/heads/', ''); - const targetBranch = pr.targetRefName.replace('refs/heads/', ''); - - return { - title: `PR: ${pr.title}`, - description: this.formatPullRequestDescription(pr), - status, - priority: this.determinePrPriority(pr), - source: 'tfs' as const, - dueDate: null, - completedAt: - pr.status === 'completed' && pr.closedDate - ? parseDate(pr.closedDate) - : null, - - // Métadonnées TFS - tfsProject: pr.repository.project.name, - tfsPullRequestId: pr.pullRequestId, - tfsRepository: pr.repository.name, - tfsSourceBranch: sourceBranch, - tfsTargetBranch: targetBranch, - assignee: pr.createdBy.displayName, - }; - } - - /** - * Formate la description d'une Pull Request - */ - private formatPullRequestDescription(pr: TfsPullRequest): string { - const parts = []; - - if (pr.description) { - parts.push(pr.description); - } - - parts.push(`**Repository:** ${pr.repository.name}`); - parts.push( - `**Branch:** ${pr.sourceRefName.replace('refs/heads/', '')} → ${pr.targetRefName.replace('refs/heads/', '')}` - ); - parts.push(`**Auteur:** ${pr.createdBy.displayName}`); - parts.push( - `**Créé le:** ${formatDateForDisplay(parseDate(pr.creationDate))}` - ); - - if (pr.reviewers && pr.reviewers.length > 0) { - const reviewersInfo = pr.reviewers.map((r) => { - let status = ''; - switch (r.vote) { - case 10: - status = '✅ Approuvé avec suggestions'; - break; - case 5: - status = '✅ Approuvé'; - break; - case -5: - status = "⏳ En attente de l'auteur"; - break; - case -10: - status = '❌ Rejeté'; - break; - default: - status = '⏳ Pas de vote'; - } - return `${r.displayName}: ${status}`; - }); - parts.push(`**Reviewers:**\n${reviewersInfo.join('\n')}`); - } - - if (pr.isDraft) { - parts.push('**🚧 Draft**'); - } - - return parts.join('\n\n'); - } - - /** - * Mappe les statuts TFS vers les statuts internes - */ - private mapTfsStatusToInternal(tfsStatus: string): string { - switch (tfsStatus.toLowerCase()) { - case 'active': - return 'in_progress'; - case 'completed': - return 'done'; - case 'abandoned': - return 'cancelled'; - default: - return 'todo'; - } - } - - /** - * Détermine la priorité d'une PR basée sur divers critères - */ - private determinePrPriority(pr: TfsPullRequest): string { - // PR en Draft = Low - if (pr.isDraft) return 'low'; - - // PR avec des conflits = High - if (pr.mergeStatus === 'conflicts' || pr.mergeStatus === 'failed') - return 'high'; - - // PR vers main/master = Medium par défaut - const targetBranch = pr.targetRefName.replace('refs/heads/', ''); - if (['main', 'master', 'production'].includes(targetBranch)) - return 'medium'; - - // Défaut - return 'low'; - } - - /** - * Nettoie les tâches TFS qui ne correspondent plus aux PRs actives - */ - private async cleanupInactivePullRequests( - currentPrIds: Set - ): Promise { - const deletedActions: TfsSyncAction[] = []; - - try { - console.log('🧹 Nettoyage des tâches TFS inactives...'); - - // Récupérer toutes les tâches TFS existantes - const existingTfsTasks = await prisma.task.findMany({ - where: { source: 'tfs' }, - select: { - id: true, - sourceId: true, - tfsPullRequestId: true, - title: true, - }, - }); - - // Identifier les tâches à supprimer - const tasksToDelete = existingTfsTasks.filter((task) => { - const prId = task.tfsPullRequestId; - if (!prId) { - return true; - } - - const shouldKeep = currentPrIds.has(prId); - return !shouldKeep; - }); - - // Supprimer les tâches obsolètes - for (const task of tasksToDelete) { - try { - await prisma.task.delete({ where: { id: task.id } }); - - deletedActions.push({ - type: 'deleted', - pullRequestId: task.tfsPullRequestId || 0, - prTitle: task.title || `Tâche ${task.id}`, - reason: 'Pull Request plus active ou supprimée', - }); - } catch (error) { - console.error(`❌ Erreur suppression tâche ${task.id}:`, error); - // Continue avec les autres tâches - } - } - - if (tasksToDelete.length > 0) { - console.log( - `✨ ${tasksToDelete.length} tâches TFS obsolètes supprimées` - ); - } - } catch (error) { - console.error('❌ Erreur nettoyage tâches TFS:', error); - } - - return deletedActions; - } - - /** - * Supprime toutes les tâches TFS de la base de données locale - */ - async deleteAllTasks(): Promise<{ - success: boolean; - deletedCount: number; - error?: string; - }> { - try { - // Récupérer toutes les tâches TFS - const tfsTasks = await prisma.task.findMany({ - where: { source: 'tfs' }, - select: { id: true, title: true }, - }); - - if (tfsTasks.length === 0) { - return { - success: true, - deletedCount: 0, - }; - } - - // Supprimer toutes les tâches TFS en une seule opération - const deleteResult = await prisma.task.deleteMany({ - where: { source: 'tfs' }, - }); - - console.log(`✅ ${deleteResult.count} tâches TFS supprimées avec succès`); - - return { - success: true, - deletedCount: deleteResult.count, - }; - } catch (error) { - console.error('❌ Erreur suppression tâches TFS:', error); - return { - success: false, - deletedCount: 0, - error: error instanceof Error ? error.message : 'Erreur inconnue', - }; - } - } - - /** - * Récupère les métadonnées du projet (repositories, branches, etc.) - */ - async getMetadata(): Promise<{ - repositories: Array<{ id: string; name: string }>; - }> { - const repositories = await this.getRepositories(); - return { repositories }; - } - - /** - * Effectue une requête vers l'API Azure DevOps - */ - private async makeApiRequest(endpoint: string): Promise { - if (!this.config.organizationUrl || !this.config.personalAccessToken) { - throw new Error('Configuration TFS manquante'); - } - - // Si l'endpoint commence par /_apis, c'est un endpoint organisation - // Sinon, on peut inclure le projet si spécifié - let url: string; - if (endpoint.startsWith('/_apis')) { - url = `${this.config.organizationUrl}${endpoint}`; - } else { - // Pour compatibilité avec d'autres endpoints - const project = this.config.projectName - ? `/${this.config.projectName}` - : ''; - url = `${this.config.organizationUrl}${project}${endpoint}`; - } - - const headers: Record = { - Authorization: `Basic ${Buffer.from(`:${this.config.personalAccessToken}`).toString('base64')}`, - 'Content-Type': 'application/json', - Accept: 'application/json', - }; - - // console.log('🌐 Requête API Azure DevOps:', { - // url, - // method: 'GET', - // headers: { - // ...headers, - // 'Authorization': 'Basic [MASQUÉ]' // Masquer le token pour la sécurité - // } - // }); - - const response = await fetch(url, { headers }); - - // console.log('🔄 Réponse brute Azure DevOps:', { - // status: response.status, - // statusText: response.statusText, - // url: response.url, - // headers: Object.fromEntries(response.headers.entries()) - // }); - - return response; - } -} - -/** - * Instance TFS préconfigurée avec les préférences utilisateur - */ -class TfsServiceInstance extends TfsService { - constructor() { - super({ enabled: false }); // Config vide par défaut - } - - private async getConfig(userId?: string): Promise { - const targetUserId = userId || 'default'; - const userConfig = await userPreferencesService.getTfsConfig(targetUserId); - return userConfig; - } - - async testConnection(userId?: string): Promise { - const config = await this.getConfig(userId); - if ( - !config.enabled || - !config.organizationUrl || - !config.personalAccessToken - ) { - return false; - } - - const service = new TfsService(config); - return service.testConnection(); - } - - async validateConfig( - userId?: string - ): Promise<{ valid: boolean; error?: string }> { - const config = await this.getConfig(userId); - const service = new TfsService(config); - return service.validateConfig(); - } - - async syncTasks(userId?: string): Promise { - const config = await this.getConfig(userId); - const service = new TfsService(config); - return service.syncTasks(); - } - - async deleteAllTasks(): Promise<{ - success: boolean; - deletedCount: number; - error?: string; - }> { - const config = await this.getConfig(); - const service = new TfsService(config); - return service.deleteAllTasks(); - } - - async getMetadata(): Promise<{ - repositories: Array<{ id: string; name: string }>; - }> { - const config = await this.getConfig(); - const service = new TfsService(config); - return service.getMetadata(); - } - - async validateProject( - projectName: string - ): Promise<{ exists: boolean; name?: string; error?: string }> { - const config = await this.getConfig(); - const service = new TfsService(config); - return service.validateProject(projectName); - } - - reset(): void { - // Pas besoin de reset, la config est récupérée à chaque fois - } -} - -/** - * Service TFS préconfiguré avec récupération automatique des préférences - */ -export const tfsService = new TfsServiceInstance(); diff --git a/src/services/integrations/tfs/scheduler.ts b/src/services/integrations/tfs/scheduler.ts index 33f205b..2ef6ba7 100644 --- a/src/services/integrations/tfs/scheduler.ts +++ b/src/services/integrations/tfs/scheduler.ts @@ -1,6 +1,7 @@ import { userPreferencesService } from '@/services/core/user-preferences'; import { TfsService } from './tfs'; import { addMinutes, getToday } from '@/lib/date-utils'; +import { prisma } from '@/services/core/database'; export interface TfsSchedulerConfig { enabled: boolean; @@ -124,8 +125,18 @@ export class TfsScheduler { return; } + // Récupérer le premier utilisateur pour la synchronisation automatique + const firstUser = await prisma.user.findFirst({ + orderBy: { createdAt: 'asc' }, + }); + + if (!firstUser) { + console.error('❌ Scheduled TFS sync failed: no user found'); + return; + } + // Effectuer la synchronisation - const result = await tfsService.syncTasks(); + const result = await tfsService.syncTasks(firstUser.id); if (result.success) { console.log( diff --git a/src/services/integrations/tfs/tfs.ts b/src/services/integrations/tfs/tfs.ts index f070a6f..99e18a2 100644 --- a/src/services/integrations/tfs/tfs.ts +++ b/src/services/integrations/tfs/tfs.ts @@ -558,7 +558,7 @@ export class TfsService { /** * Synchronise les Pull Requests avec les tâches locales */ - async syncTasks(): Promise { + async syncTasks(userId: string): Promise { const result: TfsSyncResult = { success: true, totalPullRequests: 0, @@ -592,7 +592,7 @@ export class TfsService { // Synchroniser chaque PR for (const pr of allPullRequests) { try { - const syncAction = await this.syncSinglePullRequest(pr); + const syncAction = await this.syncSinglePullRequest(pr, userId); result.actions.push(syncAction); // Compter les actions @@ -639,7 +639,8 @@ export class TfsService { * Synchronise une Pull Request unique */ private async syncSinglePullRequest( - pr: TfsPullRequest + pr: TfsPullRequest, + userId: string ): Promise { const pullRequestId = pr.pullRequestId; const sourceId = `tfs-pr-${pullRequestId}`; @@ -659,6 +660,7 @@ export class TfsService { sourceId, createdAt: new Date(), updatedAt: new Date(), + ownerId: userId, }, }); @@ -1109,10 +1111,10 @@ class TfsServiceInstance extends TfsService { return service.validateConfig(); } - async syncTasks(userId?: string): Promise { + async syncTasks(userId: string): Promise { const config = await this.getConfig(userId); const service = new TfsService(config); - return service.syncTasks(); + return service.syncTasks(userId); } async deleteAllTasks(): Promise<{ diff --git a/src/services/notes.ts b/src/services/notes.ts index 633e7c4..bbf7fac 100644 --- a/src/services/notes.ts +++ b/src/services/notes.ts @@ -93,6 +93,7 @@ export class NotesService { tfsRepository: prismaTask.tfsRepository || undefined, tfsSourceBranch: prismaTask.tfsSourceBranch || undefined, tfsTargetBranch: prismaTask.tfsTargetBranch || undefined, + ownerId: (prismaTask as unknown as { ownerId: string }).ownerId, // Cast temporaire }; } /** diff --git a/src/services/task-management/daily.ts b/src/services/task-management/daily.ts index cd43b11..d92af09 100644 --- a/src/services/task-management/daily.ts +++ b/src/services/task-management/daily.ts @@ -282,6 +282,7 @@ export class DailyService { jiraProject: checkbox.task.jiraProject || undefined, jiraKey: checkbox.task.jiraKey || undefined, assignee: checkbox.task.assignee || undefined, + ownerId: (checkbox.task as unknown as { ownerId: string }).ownerId, // Cast temporaire jusqu'à ce que Prisma soit mis à jour } : undefined, isArchived: checkbox.text.includes('[ARCHIVÉ]'), @@ -293,8 +294,16 @@ export class DailyService { /** * Récupère toutes les dates qui ont des checkboxes (pour le calendrier) */ - async getDailyDates(): Promise { + async getDailyDates(userId?: string): Promise { + const whereConditions: Prisma.DailyCheckboxWhereInput = {}; + + // Filtrer par utilisateur si spécifié + if (userId) { + whereConditions.userId = userId; + } + const checkboxes = await prisma.dailyCheckbox.findMany({ + where: whereConditions, select: { date: true, }, @@ -317,6 +326,7 @@ export class DailyService { excludeToday?: boolean; type?: DailyCheckboxType; limit?: number; + userId?: string; // Filtrer par utilisateur }): Promise { const today = normalizeDate(getToday()); const maxDays = options?.maxDays ?? 30; @@ -327,15 +337,7 @@ export class DailyService { limitDate.setDate(limitDate.getDate() - maxDays); // Construire les conditions de filtrage - const whereConditions: { - isChecked: boolean; - date: { - gte: Date; - lt?: Date; - lte?: Date; - }; - type?: DailyCheckboxType; - } = { + const whereConditions: Prisma.DailyCheckboxWhereInput = { isChecked: false, date: { gte: limitDate, @@ -348,6 +350,17 @@ export class DailyService { whereConditions.type = options.type; } + // Filtrer par utilisateur si spécifié + if (options?.userId) { + whereConditions.userId = options.userId; + + // S'assurer que si le todo est lié à une tâche, cette tâche appartient bien à l'utilisateur + whereConditions.OR = [ + { task: null }, // Todos standalone (sans tâche associée) + { task: { ownerId: options.userId } }, // Todos liés à une tâche de l'utilisateur + ]; + } + const checkboxes = await prisma.dailyCheckbox.findMany({ where: whereConditions, include: { task: true, user: true }, diff --git a/src/services/task-management/tasks.ts b/src/services/task-management/tasks.ts index c5e9b8b..5054168 100644 --- a/src/services/task-management/tasks.ts +++ b/src/services/task-management/tasks.ts @@ -20,13 +20,18 @@ export class TasksService { /** * Récupère toutes les tâches avec filtres optionnels */ - async getTasks(filters?: { - status?: TaskStatus[]; - search?: string; - limit?: number; - offset?: number; - }): Promise { - const where: Prisma.TaskWhereInput = {}; + async getTasks( + userId: string, + filters?: { + status?: TaskStatus[]; + search?: string; + limit?: number; + offset?: number; + } + ): Promise { + const where: Prisma.TaskWhereInput = { + ownerId: userId, // Toujours filtrer par propriétaire + }; if (filters?.status) { where.status = { in: filters.status }; @@ -77,6 +82,7 @@ export class TasksService { tags?: string[]; primaryTagId?: string; dueDate?: Date; + ownerId: string; // Requis - chaque tâche doit avoir un propriétaire }): Promise { const status = taskData.status || 'todo'; const task = await prisma.task.create({ @@ -87,6 +93,7 @@ export class TasksService { priority: taskData.priority || 'medium', dueDate: taskData.dueDate, primaryTagId: taskData.primaryTagId, + ownerId: taskData.ownerId, // Assigner le propriétaire source: 'manual', // Source manuelle sourceId: `manual-${Date.now()}`, // ID unique // Si créée directement en done/archived, définir completedAt @@ -128,6 +135,7 @@ export class TasksService { * Met à jour une tâche */ async updateTask( + userId: string, taskId: string, updates: { title?: string; @@ -139,12 +147,14 @@ export class TasksService { dueDate?: Date; } ): Promise { - const task = await prisma.task.findUnique({ - where: { id: taskId }, + const task = await prisma.task.findFirst({ + where: { id: taskId, ownerId: userId }, }); if (!task) { - throw new BusinessError(`Tâche ${taskId} introuvable`); + throw new BusinessError( + `Tâche ${taskId} introuvable ou accès non autorisé` + ); } // Logique métier : si on marque comme terminé, on ajoute la date @@ -198,13 +208,15 @@ export class TasksService { /** * Supprime une tâche */ - async deleteTask(taskId: string): Promise { - const task = await prisma.task.findUnique({ - where: { id: taskId }, + async deleteTask(userId: string, taskId: string): Promise { + const task = await prisma.task.findFirst({ + where: { id: taskId, ownerId: userId }, }); if (!task) { - throw new BusinessError(`Tâche ${taskId} introuvable`); + throw new BusinessError( + `Tâche ${taskId} introuvable ou accès non autorisé` + ); } await prisma.task.delete({ @@ -215,14 +227,30 @@ export class TasksService { /** * Met à jour le statut d'une tâche */ - async updateTaskStatus(taskId: string, newStatus: TaskStatus): Promise { - return this.updateTask(taskId, { status: newStatus }); + async updateTaskStatus( + userId: string, + taskId: string, + newStatus: TaskStatus + ): Promise { + return this.updateTask(userId, taskId, { status: newStatus }); } /** * Récupère les daily checkboxes liées à une tâche */ - async getTaskRelatedCheckboxes(taskId: string): Promise { + async getTaskRelatedCheckboxes( + userId: string, + taskId: string + ): Promise { + // Vérifier que la tâche appartient à l'utilisateur + const task = await prisma.task.findFirst({ + where: { id: taskId, ownerId: userId }, + }); + + if (!task) { + throw new Error('Tâche non trouvée ou accès non autorisé'); + } + const checkboxes = await prisma.dailyCheckbox.findMany({ where: { taskId: taskId }, include: { task: true }, @@ -256,6 +284,7 @@ export class TasksService { jiraKey: checkbox.task.jiraKey ?? undefined, jiraType: checkbox.task.jiraType ?? undefined, assignee: checkbox.task.assignee ?? undefined, + ownerId: (checkbox.task as unknown as { ownerId: string }).ownerId, // Cast temporaire } : undefined, isArchived: checkbox.text.includes('[ARCHIVÉ]'), @@ -267,7 +296,7 @@ export class TasksService { /** * Récupère les statistiques des tâches */ - async getTaskStats() { + async getTaskStats(userId: string) { const [ total, done, @@ -278,14 +307,14 @@ export class TasksService { cancelled, freeze, ] = await Promise.all([ - prisma.task.count(), - prisma.task.count({ where: { status: 'done' } }), - prisma.task.count({ where: { status: 'archived' } }), - prisma.task.count({ where: { status: 'in_progress' } }), - prisma.task.count({ where: { status: 'todo' } }), - prisma.task.count({ where: { status: 'backlog' } }), - prisma.task.count({ where: { status: 'cancelled' } }), - prisma.task.count({ where: { status: 'freeze' } }), + prisma.task.count({ where: { ownerId: userId } }), + prisma.task.count({ where: { ownerId: userId, status: 'done' } }), + prisma.task.count({ where: { ownerId: userId, status: 'archived' } }), + prisma.task.count({ where: { ownerId: userId, status: 'in_progress' } }), + prisma.task.count({ where: { ownerId: userId, status: 'todo' } }), + prisma.task.count({ where: { ownerId: userId, status: 'backlog' } }), + prisma.task.count({ where: { ownerId: userId, status: 'cancelled' } }), + prisma.task.count({ where: { ownerId: userId, status: 'freeze' } }), ]); const completed = done + archived; // Terminées = Done + Archived @@ -451,6 +480,7 @@ export class TasksService { tfsSourceBranch: prismaTask.tfsSourceBranch ?? undefined, tfsTargetBranch: prismaTask.tfsTargetBranch ?? undefined, assignee: prismaTask.assignee ?? undefined, + ownerId: prismaTask.ownerId, todosCount: todosCount, }; }