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:
@@ -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");
|
||||
@@ -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");
|
||||
@@ -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?
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('/');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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é
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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é
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user