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

@@ -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,54 +392,58 @@ 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) => (
<div
key={index}
className="p-2 bg-[var(--muted)]/10 rounded border border-[var(--border)]"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-2">
<span className="font-mono text-sm font-bold text-[var(--foreground)] shrink-0">
PR #{action.pullRequestId}
</span>
<span className="text-sm text-[var(--muted-foreground)] truncate">
{action.prTitle}
</span>
</div>
</div>
<Badge variant="outline" size="sm" className="shrink-0">
{getActionLabel(action.type)}
</Badge>
</div>
{action.reason && (
<div className="mt-1 text-xs text-[var(--muted-foreground)] italic">
💡 {action.reason}
</div>
)}
{action.changes && action.changes.length > 0 && (
<div className="mt-1 space-y-0.5">
<div className="text-xs font-medium text-[var(--muted-foreground)]">
Modifications:
</div>
{action.changes.map((change, changeIndex) => (
<div
key={changeIndex}
className="text-xs font-mono text-[var(--foreground)] pl-2 border-l-2 border-purple-400/30"
>
{change}
{(typeActions as TfsSyncAction[]).map(
(action: TfsSyncAction, index: number) => (
<div
key={index}
className="p-2 bg-[var(--muted)]/10 rounded border border-[var(--border)]"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-baseline gap-2">
<span className="font-mono text-sm font-bold text-[var(--foreground)] shrink-0">
PR #{action.pullRequestId}
</span>
<span className="text-sm text-[var(--muted-foreground)] truncate">
{action.prTitle}
</span>
</div>
))}
</div>
<Badge variant="outline" size="sm" className="shrink-0">
{getActionLabel(action.type)}
</Badge>
</div>
)}
</div>
))}
{action.reason && (
<div className="mt-1 text-xs text-[var(--muted-foreground)] italic">
💡 {action.reason}
</div>
)}
{action.changes && action.changes.length > 0 && (
<div className="mt-1 space-y-0.5">
<div className="text-xs font-medium text-[var(--muted-foreground)]">
Modifications:
</div>
{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,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'],
},
},
},
},
],
},
],
},

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?: {
status?: TaskStatus[];
search?: string;
limit?: number;
offset?: number;
}): Promise<Task[]> {
const where: Prisma.TaskWhereInput = {};
async getTasks(
userId: string,
filters?: {
status?: TaskStatus[];
search?: string;
limit?: number;
offset?: number;
}
): 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,
};
}