feat(Task): implement user ownership for tasks and enhance related services

- Added ownerId field to Task model to associate tasks with users.
- Updated TaskService methods to enforce user ownership in task operations.
- Enhanced API routes to include user authentication and ownership checks.
- Modified DailyService and analytics services to filter tasks by user.
- Integrated user session handling in various components for personalized task management.
This commit is contained in:
Julien Froidefond
2025-10-10 11:36:10 +02:00
parent 6bfcd1f100
commit 8cb0dcf3af
32 changed files with 617 additions and 1227 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<T = unknown> = {
success: boolean;
@@ -10,6 +12,30 @@ export type ActionResult<T = unknown> = {
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<boolean> {
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<ActionResult> {
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<ActionResult> {
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<ActionResult> {
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<string, unknown> = {};
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

View File

@@ -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('/');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">Connexion requise</h1>
<p className="text-gray-600">
Veuillez vous connecter pour accéder au tableau Kanban.
</p>
</div>
</div>
);
}
// SSR - Récupération des données côté serveur
const [initialTasks, initialTags] = await Promise.all([
tasksService.getTasks(),
tasksService.getTasks(userId),
tagsService.getTags(),
]);

View File

@@ -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 (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">Connexion requise</h1>
<p className="text-gray-600">
Veuillez vous connecter pour accéder à votre tableau de bord.
</p>
</div>
</div>
);
}
// 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 (

View File

@@ -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 (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">Connexion requise</h1>
<p className="text-gray-600">
Veuillez vous connecter pour accéder aux paramètres avancés.
</p>
</div>
</div>
);
}
// Fetch all data server-side
const [taskStats, tags] = await Promise.all([
tasksService.getTaskStats(),
tasksService.getTaskStats(userId),
tagsService.getTags(),
]);

View File

@@ -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 (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">Connexion requise</h1>
<p className="text-gray-600">
Veuillez vous connecter pour accéder au gestionnaire hebdomadaire.
</p>
</div>
</div>
);
}
// 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(),
]);

View File

@@ -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}):
</div>
<div className="space-y-1 max-h-20 overflow-y-auto">
{errors.map((err, i) => (
{errors.map((err: string, i: number) => (
<div
key={i}
className="text-[var(--destructive)] font-mono text-xs"
@@ -392,11 +392,12 @@ function SyncActionsList({ actions }: { actions: TfsSyncAction[] }) {
>
{getActionIcon(type as TfsSyncAction['type'])}
{getActionLabel(type as TfsSyncAction['type'])} (
{typeActions.length})
{(typeActions as TfsSyncAction[]).length})
</h4>
<div className="space-y-2">
{typeActions.map((action, index) => (
{(typeActions as TfsSyncAction[]).map(
(action: TfsSyncAction, index: number) => (
<div
key={index}
className="p-2 bg-[var(--muted)]/10 rounded border border-[var(--border)]"
@@ -428,18 +429,21 @@ function SyncActionsList({ actions }: { actions: TfsSyncAction[] }) {
<div className="text-xs font-medium text-[var(--muted-foreground)]">
Modifications:
</div>
{action.changes.map((change, changeIndex) => (
{action.changes.map(
(change: string, changeIndex: number) => (
<div
key={changeIndex}
className="text-xs font-mono text-[var(--foreground)] pl-2 border-l-2 border-purple-400/30"
>
{change}
</div>
))}
)
)}
</div>
)}
</div>
))}
)
)}
</div>
</div>
))}

View File

@@ -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',
},
];

View File

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

View File

@@ -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<ProductivityMetrics> {
@@ -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é

View File

@@ -34,6 +34,7 @@ export class DeadlineAnalyticsService {
* Analyse les tâches selon leurs échéances
*/
static async getDeadlineMetrics(
userId: string,
sources?: string[]
): Promise<DeadlineMetrics> {
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<DeadlineTask[]> {
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
}

View File

@@ -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<ManagerSummary> {
// 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<UpcomingChallenge[]> {
// 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,10 +348,20 @@ 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(),
},
// S'assurer que si le todo est lié à une tâche, cette tâche appartient bien à l'utilisateur
AND: [
{
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' },
{
@@ -346,6 +373,8 @@ export class ManagerSummaryService {
},
],
},
],
},
select: {
id: true,
text: true,

View File

@@ -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<WeeklyMetricsOverview> {
// 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<DailyMetrics> {
private static async getDailyMetrics(
userId: string,
date: Date
): Promise<DailyMetrics> {
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<VelocityTrend[]> {
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,

View File

@@ -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<TagDistributionMetrics> {
@@ -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é

View File

@@ -355,7 +355,7 @@ export class JiraService {
/**
* Synchronise les tickets Jira avec la base locale
*/
async syncTasks(): Promise<SyncResult> {
async syncTasks(userId: string): Promise<SyncResult> {
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<JiraSyncAction> {
private async syncSingleTask(
jiraTask: JiraTask,
userId: string
): Promise<JiraSyncAction> {
// 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) {

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -558,7 +558,7 @@ export class TfsService {
/**
* Synchronise les Pull Requests avec les tâches locales
*/
async syncTasks(): Promise<TfsSyncResult> {
async syncTasks(userId: string): Promise<TfsSyncResult> {
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<TfsSyncAction> {
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<TfsSyncResult> {
async syncTasks(userId: string): Promise<TfsSyncResult> {
const config = await this.getConfig(userId);
const service = new TfsService(config);
return service.syncTasks();
return service.syncTasks(userId);
}
async deleteAllTasks(): Promise<{

View File

@@ -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
};
}
/**

View File

@@ -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<string[]> {
async getDailyDates(userId?: string): Promise<string[]> {
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<DailyCheckbox[]> {
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 },

View File

@@ -20,13 +20,18 @@ export class TasksService {
/**
* Récupère toutes les tâches avec filtres optionnels
*/
async getTasks(filters?: {
async getTasks(
userId: string,
filters?: {
status?: TaskStatus[];
search?: string;
limit?: number;
offset?: number;
}): Promise<Task[]> {
const where: Prisma.TaskWhereInput = {};
}
): Promise<Task[]> {
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<Task> {
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<Task> {
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<void> {
const task = await prisma.task.findUnique({
where: { id: taskId },
async deleteTask(userId: string, taskId: string): Promise<void> {
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<Task> {
return this.updateTask(taskId, { status: newStatus });
async updateTaskStatus(
userId: string,
taskId: string,
newStatus: TaskStatus
): Promise<Task> {
return this.updateTask(userId, taskId, { status: newStatus });
}
/**
* Récupère les daily checkboxes liées à une tâche
*/
async getTaskRelatedCheckboxes(taskId: string): Promise<DailyCheckbox[]> {
async getTaskRelatedCheckboxes(
userId: string,
taskId: string
): Promise<DailyCheckbox[]> {
// 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,
};
}