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?
|
preferences UserPreferences?
|
||||||
notes Note[]
|
notes Note[]
|
||||||
dailyCheckboxes DailyCheckbox[]
|
dailyCheckboxes DailyCheckbox[]
|
||||||
|
tasks Task[] @relation("TaskOwner")
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
@@ -41,7 +42,9 @@ model Task {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
jiraProject String?
|
jiraProject String?
|
||||||
jiraKey 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?
|
jiraType String?
|
||||||
tfsProject String?
|
tfsProject String?
|
||||||
tfsPullRequestId Int?
|
tfsPullRequestId Int?
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { tasksService } from '../src/services/task-management/tasks';
|
import { tasksService } from '../src/services/task-management/tasks';
|
||||||
import { TaskStatus, TaskPriority } from '../src/lib/types';
|
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é
|
* 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('🌱 Ajout de données de test...');
|
||||||
console.log('================================');
|
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 = [
|
const testTasks = [
|
||||||
{
|
{
|
||||||
title: '🎨 Design System Implementation',
|
title: '🎨 Design System Implementation',
|
||||||
@@ -58,7 +81,10 @@ async function seedTestData() {
|
|||||||
|
|
||||||
for (const taskData of testTasks) {
|
for (const taskData of testTasks) {
|
||||||
try {
|
try {
|
||||||
const task = await tasksService.createTask(taskData);
|
const task = await tasksService.createTask({
|
||||||
|
...taskData,
|
||||||
|
ownerId: userId, // Ajouter l'ownerId
|
||||||
|
});
|
||||||
|
|
||||||
const statusEmoji = {
|
const statusEmoji = {
|
||||||
backlog: '📋',
|
backlog: '📋',
|
||||||
@@ -101,7 +127,7 @@ async function seedTestData() {
|
|||||||
console.log(` ❌ Erreurs: ${errorCount}`);
|
console.log(` ❌ Erreurs: ${errorCount}`);
|
||||||
|
|
||||||
// Afficher les stats finales
|
// Afficher les stats finales
|
||||||
const stats = await tasksService.getTaskStats();
|
const stats = await tasksService.getTaskStats(userId);
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('📈 Statistiques finales:');
|
console.log('📈 Statistiques finales:');
|
||||||
console.log(` Total: ${stats.total} tâches`);
|
console.log(` Total: ${stats.total} tâches`);
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
VelocityTrend,
|
VelocityTrend,
|
||||||
} from '@/services/analytics/metrics';
|
} from '@/services/analytics/metrics';
|
||||||
import { getToday } from '@/lib/date-utils';
|
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
|
* 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;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
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 targetDate = date || getToday();
|
||||||
const metrics = await MetricsService.getWeeklyMetrics(targetDate);
|
const metrics = await MetricsService.getWeeklyMetrics(
|
||||||
|
session.user.id,
|
||||||
|
targetDate
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -44,6 +58,15 @@ export async function getVelocityTrends(weeksBack: number = 4): Promise<{
|
|||||||
error?: string;
|
error?: string;
|
||||||
}> {
|
}> {
|
||||||
try {
|
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) {
|
if (weeksBack < 1 || weeksBack > 12) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { tasksService } from '@/services/task-management/tasks';
|
import { tasksService } from '@/services/task-management/tasks';
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
import { TaskStatus, TaskPriority } from '@/lib/types';
|
import { TaskStatus, TaskPriority } from '@/lib/types';
|
||||||
|
import { getServerSession } from 'next-auth/next';
|
||||||
|
import { authOptions } from '@/lib/auth';
|
||||||
|
|
||||||
export type ActionResult<T = unknown> = {
|
export type ActionResult<T = unknown> = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -10,6 +12,30 @@ export type ActionResult<T = unknown> = {
|
|||||||
error?: string;
|
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
|
* Server Action pour mettre à jour le statut d'une tâche
|
||||||
*/
|
*/
|
||||||
@@ -18,13 +44,24 @@ export async function updateTaskStatus(
|
|||||||
status: TaskStatus
|
status: TaskStatus
|
||||||
): Promise<ActionResult> {
|
): Promise<ActionResult> {
|
||||||
try {
|
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
|
// Revalidation automatique du cache
|
||||||
revalidatePath('/');
|
revalidatePath('/');
|
||||||
revalidatePath('/tasks');
|
revalidatePath('/tasks');
|
||||||
|
|
||||||
return { success: true, data: task };
|
return { success: true, data: updatedTask };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating task status:', error);
|
console.error('Error updating task status:', error);
|
||||||
return {
|
return {
|
||||||
@@ -47,7 +84,18 @@ export async function updateTaskTitle(
|
|||||||
return { success: false, error: 'Title cannot be empty' };
|
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
|
// Revalidation automatique du cache
|
||||||
revalidatePath('/');
|
revalidatePath('/');
|
||||||
@@ -69,7 +117,16 @@ export async function updateTaskTitle(
|
|||||||
*/
|
*/
|
||||||
export async function deleteTask(taskId: string): Promise<ActionResult> {
|
export async function deleteTask(taskId: string): Promise<ActionResult> {
|
||||||
try {
|
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
|
// Revalidation automatique du cache
|
||||||
revalidatePath('/');
|
revalidatePath('/');
|
||||||
@@ -99,6 +156,15 @@ export async function updateTask(data: {
|
|||||||
dueDate?: Date;
|
dueDate?: Date;
|
||||||
}): Promise<ActionResult> {
|
}): Promise<ActionResult> {
|
||||||
try {
|
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> = {};
|
const updateData: Record<string, unknown> = {};
|
||||||
|
|
||||||
if (data.title !== undefined) {
|
if (data.title !== undefined) {
|
||||||
@@ -117,7 +183,7 @@ export async function updateTask(data: {
|
|||||||
updateData.primaryTagId = data.primaryTagId;
|
updateData.primaryTagId = data.primaryTagId;
|
||||||
if (data.dueDate !== undefined) updateData.dueDate = data.dueDate;
|
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
|
// Revalidation automatique du cache
|
||||||
revalidatePath('/');
|
revalidatePath('/');
|
||||||
@@ -149,6 +215,9 @@ export async function createTask(data: {
|
|||||||
return { success: false, error: 'Title is required' };
|
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({
|
const task = await tasksService.createTask({
|
||||||
title: data.title.trim(),
|
title: data.title.trim(),
|
||||||
description: data.description?.trim() || '',
|
description: data.description?.trim() || '',
|
||||||
@@ -156,6 +225,7 @@ export async function createTask(data: {
|
|||||||
priority: data.priority || 'medium',
|
priority: data.priority || 'medium',
|
||||||
tags: data.tags || [],
|
tags: data.tags || [],
|
||||||
primaryTagId: data.primaryTagId,
|
primaryTagId: data.primaryTagId,
|
||||||
|
ownerId: userId, // Assigner la tâche au user connecté
|
||||||
});
|
});
|
||||||
|
|
||||||
// Revalidation automatique du cache
|
// Revalidation automatique du cache
|
||||||
|
|||||||
@@ -108,8 +108,17 @@ export async function saveTfsSchedulerConfig(
|
|||||||
*/
|
*/
|
||||||
export async function syncTfsPullRequests() {
|
export async function syncTfsPullRequests() {
|
||||||
try {
|
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
|
// Lancer la synchronisation via le service singleton
|
||||||
const result = await tfsService.syncTasks();
|
const result = await tfsService.syncTasks(session.user.id);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
revalidatePath('/');
|
revalidatePath('/');
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { dailyService } from '@/services/task-management/daily';
|
import { dailyService } from '@/services/task-management/daily';
|
||||||
import { DailyCheckboxType } from '@/lib/types';
|
import { DailyCheckboxType } from '@/lib/types';
|
||||||
|
import { getServerSession } from 'next-auth/next';
|
||||||
|
import { authOptions } from '@/lib/auth';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
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 { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
const maxDays = searchParams.get('maxDays')
|
const maxDays = searchParams.get('maxDays')
|
||||||
@@ -20,6 +28,7 @@ export async function GET(request: NextRequest) {
|
|||||||
excludeToday,
|
excludeToday,
|
||||||
type,
|
type,
|
||||||
limit,
|
limit,
|
||||||
|
userId: session.user.id, // Filtrer par user connecté
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json(pendingCheckboxes);
|
return NextResponse.json(pendingCheckboxes);
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Effectuer la synchronisation
|
// Effectuer la synchronisation
|
||||||
const syncResult = await jiraService.syncTasks();
|
const syncResult = await jiraService.syncTasks(session.user.id);
|
||||||
|
|
||||||
// Convertir SyncResult en JiraSyncResult pour le client
|
// Convertir SyncResult en JiraSyncResult pour le client
|
||||||
const jiraSyncResult = {
|
const jiraSyncResult = {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { tasksService } from '@/services/task-management/tasks';
|
import { tasksService } from '@/services/task-management/tasks';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth';
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
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 });
|
return NextResponse.json({ data: checkboxes });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { tasksService } from '@/services/task-management/tasks';
|
import { tasksService } from '@/services/task-management/tasks';
|
||||||
import { TaskStatus } from '@/lib/types';
|
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
|
* API route pour récupérer les tâches avec filtres optionnels
|
||||||
*/
|
*/
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
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);
|
const { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
// Extraire les paramètres de filtre
|
// Extraire les paramètres de filtre
|
||||||
@@ -16,6 +27,7 @@ export async function GET(request: Request) {
|
|||||||
search?: string;
|
search?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
ownerId?: string; // Filtre par propriétaire
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
const status = searchParams.get('status');
|
const status = searchParams.get('status');
|
||||||
@@ -44,8 +56,8 @@ export async function GET(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer les tâches
|
// Récupérer les tâches
|
||||||
const tasks = await tasksService.getTasks(filters);
|
const tasks = await tasksService.getTasks(session.user.id, filters);
|
||||||
const stats = await tasksService.getTaskStats();
|
const stats = await tasksService.getTaskStats(session.user.id);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -40,13 +40,16 @@ export default async function DailyPage() {
|
|||||||
const [dailyView, dailyDates, deadlineMetrics, pendingTasks] =
|
const [dailyView, dailyDates, deadlineMetrics, pendingTasks] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
dailyService.getDailyView(today, session.user.id),
|
dailyService.getDailyView(today, session.user.id),
|
||||||
dailyService.getDailyDates(),
|
dailyService.getDailyDates(session.user.id),
|
||||||
DeadlineAnalyticsService.getDeadlineMetrics().catch(() => null), // Graceful fallback
|
DeadlineAnalyticsService.getDeadlineMetrics(session.user.id).catch(
|
||||||
|
() => null
|
||||||
|
), // Graceful fallback
|
||||||
dailyService
|
dailyService
|
||||||
.getPendingCheckboxes({
|
.getPendingCheckboxes({
|
||||||
maxDays: 7,
|
maxDays: 7,
|
||||||
excludeToday: true,
|
excludeToday: true,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
|
userId: session.user.id,
|
||||||
})
|
})
|
||||||
.catch(() => []), // Graceful fallback
|
.catch(() => []), // Graceful fallback
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,14 +1,34 @@
|
|||||||
import { tasksService } from '@/services/task-management/tasks';
|
import { tasksService } from '@/services/task-management/tasks';
|
||||||
import { tagsService } from '@/services/task-management/tags';
|
import { tagsService } from '@/services/task-management/tags';
|
||||||
import { KanbanPageClient } from './KanbanPageClient';
|
import { KanbanPageClient } from './KanbanPageClient';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth';
|
||||||
|
|
||||||
// Force dynamic rendering (no static generation)
|
// Force dynamic rendering (no static generation)
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default async function KanbanPage() {
|
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
|
// SSR - Récupération des données côté serveur
|
||||||
const [initialTasks, initialTags] = await Promise.all([
|
const [initialTasks, initialTags] = await Promise.all([
|
||||||
tasksService.getTasks(),
|
tasksService.getTasks(userId),
|
||||||
tagsService.getTags(),
|
tagsService.getTags(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,31 @@ import { AnalyticsService } from '@/services/analytics/analytics';
|
|||||||
import { DeadlineAnalyticsService } from '@/services/analytics/deadline-analytics';
|
import { DeadlineAnalyticsService } from '@/services/analytics/deadline-analytics';
|
||||||
import { TagAnalyticsService } from '@/services/analytics/tag-analytics';
|
import { TagAnalyticsService } from '@/services/analytics/tag-analytics';
|
||||||
import { HomePageClient } from '@/components/HomePageClient';
|
import { HomePageClient } from '@/components/HomePageClient';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth';
|
||||||
|
|
||||||
// Force dynamic rendering (no static generation)
|
// Force dynamic rendering (no static generation)
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default async function HomePage() {
|
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
|
// SSR - Récupération des données côté serveur
|
||||||
const [
|
const [
|
||||||
initialTasks,
|
initialTasks,
|
||||||
@@ -18,12 +38,12 @@ export default async function HomePage() {
|
|||||||
deadlineMetrics,
|
deadlineMetrics,
|
||||||
tagMetrics,
|
tagMetrics,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
tasksService.getTasks(),
|
tasksService.getTasks(userId),
|
||||||
tagsService.getTags(),
|
tagsService.getTags(),
|
||||||
tasksService.getTaskStats(),
|
tasksService.getTaskStats(userId),
|
||||||
AnalyticsService.getProductivityMetrics(),
|
AnalyticsService.getProductivityMetrics(userId),
|
||||||
DeadlineAnalyticsService.getDeadlineMetrics(),
|
DeadlineAnalyticsService.getDeadlineMetrics(userId),
|
||||||
TagAnalyticsService.getTagDistributionMetrics(),
|
TagAnalyticsService.getTagDistributionMetrics(userId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,14 +3,34 @@ import { tagsService } from '@/services/task-management/tags';
|
|||||||
import { backupService } from '@/services/data-management/backup';
|
import { backupService } from '@/services/data-management/backup';
|
||||||
import { backupScheduler } from '@/services/data-management/backup-scheduler';
|
import { backupScheduler } from '@/services/data-management/backup-scheduler';
|
||||||
import { AdvancedSettingsPageClient } from '@/components/settings/AdvancedSettingsPageClient';
|
import { AdvancedSettingsPageClient } from '@/components/settings/AdvancedSettingsPageClient';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth';
|
||||||
|
|
||||||
// Force dynamic rendering for real-time data
|
// Force dynamic rendering for real-time data
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default async function AdvancedSettingsPage() {
|
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
|
// Fetch all data server-side
|
||||||
const [taskStats, tags] = await Promise.all([
|
const [taskStats, tags] = await Promise.all([
|
||||||
tasksService.getTaskStats(),
|
tasksService.getTaskStats(userId),
|
||||||
tagsService.getTags(),
|
tagsService.getTags(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,35 @@ import { ManagerSummaryService } from '@/services/analytics/manager-summary';
|
|||||||
import { tasksService } from '@/services/task-management/tasks';
|
import { tasksService } from '@/services/task-management/tasks';
|
||||||
import { tagsService } from '@/services/task-management/tags';
|
import { tagsService } from '@/services/task-management/tags';
|
||||||
import { WeeklyManagerPageClient } from './WeeklyManagerPageClient';
|
import { WeeklyManagerPageClient } from './WeeklyManagerPageClient';
|
||||||
|
import { getServerSession } from 'next-auth';
|
||||||
|
import { authOptions } from '@/lib/auth';
|
||||||
|
|
||||||
// Force dynamic rendering (no static generation)
|
// Force dynamic rendering (no static generation)
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
export default async function WeeklyManagerPage() {
|
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
|
// SSR - Récupération des données côté serveur
|
||||||
const [summary, initialTasks, initialTags] = await Promise.all([
|
const [summary, initialTasks, initialTags] = await Promise.all([
|
||||||
ManagerSummaryService.getManagerSummary(),
|
ManagerSummaryService.getManagerSummary(userId),
|
||||||
tasksService.getTasks(),
|
tasksService.getTasks(userId),
|
||||||
tagsService.getTags(),
|
tagsService.getTags(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
|||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { getToday } from '@/lib/date-utils';
|
import { getToday } from '@/lib/date-utils';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import { TfsSyncResult, TfsSyncAction } from '@/services/integrations/tfs';
|
import { TfsSyncResult, TfsSyncAction } from '@/services/integrations/tfs/tfs';
|
||||||
|
|
||||||
interface TfsSyncProps {
|
interface TfsSyncProps {
|
||||||
onSyncComplete?: () => void;
|
onSyncComplete?: () => void;
|
||||||
@@ -201,7 +201,7 @@ export function TfsSync({ onSyncComplete, className = '' }: TfsSyncProps) {
|
|||||||
Erreurs ({errors.length}):
|
Erreurs ({errors.length}):
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1 max-h-20 overflow-y-auto">
|
<div className="space-y-1 max-h-20 overflow-y-auto">
|
||||||
{errors.map((err, i) => (
|
{errors.map((err: string, i: number) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-[var(--destructive)] font-mono text-xs"
|
className="text-[var(--destructive)] font-mono text-xs"
|
||||||
@@ -392,54 +392,58 @@ function SyncActionsList({ actions }: { actions: TfsSyncAction[] }) {
|
|||||||
>
|
>
|
||||||
{getActionIcon(type as TfsSyncAction['type'])}
|
{getActionIcon(type as TfsSyncAction['type'])}
|
||||||
{getActionLabel(type as TfsSyncAction['type'])} (
|
{getActionLabel(type as TfsSyncAction['type'])} (
|
||||||
{typeActions.length})
|
{(typeActions as TfsSyncAction[]).length})
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{typeActions.map((action, index) => (
|
{(typeActions as TfsSyncAction[]).map(
|
||||||
<div
|
(action: TfsSyncAction, index: number) => (
|
||||||
key={index}
|
<div
|
||||||
className="p-2 bg-[var(--muted)]/10 rounded border border-[var(--border)]"
|
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-start justify-between gap-3">
|
||||||
<div className="flex items-baseline gap-2">
|
<div className="flex-1 min-w-0">
|
||||||
<span className="font-mono text-sm font-bold text-[var(--foreground)] shrink-0">
|
<div className="flex items-baseline gap-2">
|
||||||
PR #{action.pullRequestId}
|
<span className="font-mono text-sm font-bold text-[var(--foreground)] shrink-0">
|
||||||
</span>
|
PR #{action.pullRequestId}
|
||||||
<span className="text-sm text-[var(--muted-foreground)] truncate">
|
</span>
|
||||||
{action.prTitle}
|
<span className="text-sm text-[var(--muted-foreground)] truncate">
|
||||||
</span>
|
{action.prTitle}
|
||||||
</div>
|
</span>
|
||||||
</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}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
|
<Badge variant="outline" size="sm" className="shrink-0">
|
||||||
|
{getActionLabel(action.type)}
|
||||||
|
</Badge>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const mockTasks: Task[] = [
|
|||||||
source: 'manual',
|
source: 'manual',
|
||||||
sourceId: '1',
|
sourceId: '1',
|
||||||
tagDetails: [],
|
tagDetails: [],
|
||||||
|
ownerId: 'mock-user-1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
@@ -34,6 +35,7 @@ const mockTasks: Task[] = [
|
|||||||
source: 'manual',
|
source: 'manual',
|
||||||
sourceId: '2',
|
sourceId: '2',
|
||||||
tagDetails: [],
|
tagDetails: [],
|
||||||
|
ownerId: 'mock-user-1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
@@ -48,6 +50,7 @@ const mockTasks: Task[] = [
|
|||||||
source: 'manual',
|
source: 'manual',
|
||||||
sourceId: '3',
|
sourceId: '3',
|
||||||
tagDetails: [],
|
tagDetails: [],
|
||||||
|
ownerId: 'mock-user-1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: '4',
|
||||||
@@ -62,6 +65,7 @@ const mockTasks: Task[] = [
|
|||||||
source: 'manual',
|
source: 'manual',
|
||||||
sourceId: '4',
|
sourceId: '4',
|
||||||
tagDetails: [],
|
tagDetails: [],
|
||||||
|
ownerId: 'mock-user-1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '5',
|
id: '5',
|
||||||
@@ -76,6 +80,7 @@ const mockTasks: Task[] = [
|
|||||||
source: 'manual',
|
source: 'manual',
|
||||||
sourceId: '5',
|
sourceId: '5',
|
||||||
tagDetails: [],
|
tagDetails: [],
|
||||||
|
ownerId: 'mock-user-1',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export interface Task {
|
|||||||
tfsTargetBranch?: string;
|
tfsTargetBranch?: string;
|
||||||
|
|
||||||
assignee?: string;
|
assignee?: string;
|
||||||
|
ownerId: string; // ID du propriétaire de la tâche
|
||||||
todosCount?: number; // Nombre de todos reliés à cette 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
|
* Calcule les métriques de productivité pour une période donnée
|
||||||
*/
|
*/
|
||||||
static async getProductivityMetrics(
|
static async getProductivityMetrics(
|
||||||
|
userId: string,
|
||||||
timeRange?: TimeRange,
|
timeRange?: TimeRange,
|
||||||
sources?: string[]
|
sources?: string[]
|
||||||
): Promise<ProductivityMetrics> {
|
): 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
|
// Récupérer toutes les tâches depuis la base de données avec leurs tags
|
||||||
const dbTasks = await prisma.task.findMany({
|
const dbTasks = await prisma.task.findMany({
|
||||||
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
taskTags: {
|
taskTags: {
|
||||||
include: {
|
include: {
|
||||||
@@ -83,6 +87,7 @@ export class AnalyticsService {
|
|||||||
jiraKey: task.jiraKey || undefined,
|
jiraKey: task.jiraKey || undefined,
|
||||||
jiraType: task.jiraType || undefined,
|
jiraType: task.jiraType || undefined,
|
||||||
assignee: task.assignee || undefined,
|
assignee: task.assignee || undefined,
|
||||||
|
ownerId: task.ownerId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Filtrer par sources si spécifié
|
// Filtrer par sources si spécifié
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export class DeadlineAnalyticsService {
|
|||||||
* Analyse les tâches selon leurs échéances
|
* Analyse les tâches selon leurs échéances
|
||||||
*/
|
*/
|
||||||
static async getDeadlineMetrics(
|
static async getDeadlineMetrics(
|
||||||
|
userId: string,
|
||||||
sources?: string[]
|
sources?: string[]
|
||||||
): Promise<DeadlineMetrics> {
|
): Promise<DeadlineMetrics> {
|
||||||
try {
|
try {
|
||||||
@@ -42,6 +43,7 @@ export class DeadlineAnalyticsService {
|
|||||||
// Récupérer toutes les tâches non terminées avec échéance
|
// Récupérer toutes les tâches non terminées avec échéance
|
||||||
const dbTasks = await prisma.task.findMany({
|
const dbTasks = await prisma.task.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
dueDate: {
|
dueDate: {
|
||||||
not: null,
|
not: null,
|
||||||
},
|
},
|
||||||
@@ -137,9 +139,10 @@ export class DeadlineAnalyticsService {
|
|||||||
* Retourne les tâches les plus critiques (en retard + échéance dans 48h)
|
* Retourne les tâches les plus critiques (en retard + échéance dans 48h)
|
||||||
*/
|
*/
|
||||||
static async getCriticalDeadlines(
|
static async getCriticalDeadlines(
|
||||||
|
userId: string,
|
||||||
sources?: string[]
|
sources?: string[]
|
||||||
): Promise<DeadlineTask[]> {
|
): 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
|
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
|
* Génère un résumé orienté manager pour les 7 derniers jours
|
||||||
*/
|
*/
|
||||||
static async getManagerSummary(
|
static async getManagerSummary(
|
||||||
|
userId: string,
|
||||||
date: Date = getToday()
|
date: Date = getToday()
|
||||||
): Promise<ManagerSummary> {
|
): Promise<ManagerSummary> {
|
||||||
// Fenêtre glissante de 7 jours au lieu de semaine calendaire
|
// 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
|
// Récupérer les données de base
|
||||||
const [tasks, checkboxes] = await Promise.all([
|
const [tasks, checkboxes] = await Promise.all([
|
||||||
this.getCompletedTasks(weekStart, weekEnd),
|
this.getCompletedTasks(userId, weekStart, weekEnd),
|
||||||
this.getCompletedCheckboxes(weekStart, weekEnd),
|
this.getCompletedCheckboxes(userId, weekStart, weekEnd),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Analyser et extraire les accomplissements clés
|
// Analyser et extraire les accomplissements clés
|
||||||
@@ -107,7 +108,7 @@ export class ManagerSummaryService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Identifier les défis à venir
|
// Identifier les défis à venir
|
||||||
const upcomingChallenges = await this.identifyUpcomingChallenges();
|
const upcomingChallenges = await this.identifyUpcomingChallenges(userId);
|
||||||
|
|
||||||
// Calculer les métriques
|
// Calculer les métriques
|
||||||
const metrics = this.calculateMetrics(tasks, checkboxes);
|
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
|
* 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({
|
const tasks = await prisma.task.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
OR: [
|
OR: [
|
||||||
// Tâches avec completedAt dans la période (priorité)
|
// 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
|
* 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({
|
const checkboxes = await prisma.dailyCheckbox.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
userId: userId,
|
||||||
isChecked: true,
|
isChecked: true,
|
||||||
date: {
|
date: {
|
||||||
gte: startDate,
|
gte: startDate,
|
||||||
lte: endDate,
|
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: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -297,12 +313,13 @@ export class ManagerSummaryService {
|
|||||||
/**
|
/**
|
||||||
* Identifie les défis et enjeux à venir
|
* Identifie les défis et enjeux à venir
|
||||||
*/
|
*/
|
||||||
private static async identifyUpcomingChallenges(): Promise<
|
private static async identifyUpcomingChallenges(
|
||||||
UpcomingChallenge[]
|
userId: string
|
||||||
> {
|
): Promise<UpcomingChallenge[]> {
|
||||||
// Récupérer les tâches à venir (priorité high/medium en premier)
|
// Récupérer les tâches à venir (priorité high/medium en premier)
|
||||||
const upcomingTasks = await prisma.task.findMany({
|
const upcomingTasks = await prisma.task.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
completedAt: null,
|
completedAt: null,
|
||||||
},
|
},
|
||||||
orderBy: [
|
orderBy: [
|
||||||
@@ -331,18 +348,30 @@ export class ManagerSummaryService {
|
|||||||
// Récupérer les checkboxes récurrentes non complétées (meetings + tâches prioritaires)
|
// Récupérer les checkboxes récurrentes non complétées (meetings + tâches prioritaires)
|
||||||
const upcomingCheckboxes = await prisma.dailyCheckbox.findMany({
|
const upcomingCheckboxes = await prisma.dailyCheckbox.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
userId: userId, // Filtrer par utilisateur
|
||||||
isChecked: false,
|
isChecked: false,
|
||||||
date: {
|
date: {
|
||||||
gte: getToday(),
|
gte: getToday(),
|
||||||
},
|
},
|
||||||
OR: [
|
// S'assurer que si le todo est lié à une tâche, cette tâche appartient bien à l'utilisateur
|
||||||
{ type: 'meeting' },
|
AND: [
|
||||||
{
|
{
|
||||||
task: {
|
OR: [
|
||||||
priority: {
|
{ task: null }, // Todos standalone (sans tâche associée)
|
||||||
in: ['high', 'medium'],
|
{ 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
|
* Récupère les métriques journalières des 7 derniers jours
|
||||||
*/
|
*/
|
||||||
static async getWeeklyMetrics(
|
static async getWeeklyMetrics(
|
||||||
|
userId: string,
|
||||||
date: Date = getToday()
|
date: Date = getToday()
|
||||||
): Promise<WeeklyMetricsOverview> {
|
): Promise<WeeklyMetricsOverview> {
|
||||||
// Fenêtre glissante de 7 jours au lieu de semaine calendaire
|
// 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
|
// Récupérer les données pour chaque jour
|
||||||
const dailyBreakdown = await Promise.all(
|
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é
|
// Calculer les métriques de résumé
|
||||||
@@ -114,7 +115,10 @@ export class MetricsService {
|
|||||||
/**
|
/**
|
||||||
* Récupère les métriques pour un jour donné
|
* 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 dayStart = startOfDay(date);
|
||||||
const dayEnd = endOfDay(date);
|
const dayEnd = endOfDay(date);
|
||||||
|
|
||||||
@@ -124,6 +128,7 @@ export class MetricsService {
|
|||||||
// Tâches complétées ce jour
|
// Tâches complétées ce jour
|
||||||
prisma.task.count({
|
prisma.task.count({
|
||||||
where: {
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
OR: [
|
OR: [
|
||||||
{
|
{
|
||||||
completedAt: {
|
completedAt: {
|
||||||
@@ -145,6 +150,7 @@ export class MetricsService {
|
|||||||
// Tâches en cours (status = in_progress à ce moment)
|
// Tâches en cours (status = in_progress à ce moment)
|
||||||
prisma.task.count({
|
prisma.task.count({
|
||||||
where: {
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
createdAt: { lte: dayEnd },
|
createdAt: { lte: dayEnd },
|
||||||
},
|
},
|
||||||
@@ -153,6 +159,7 @@ export class MetricsService {
|
|||||||
// Tâches bloquées
|
// Tâches bloquées
|
||||||
prisma.task.count({
|
prisma.task.count({
|
||||||
where: {
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
status: 'blocked',
|
status: 'blocked',
|
||||||
createdAt: { lte: dayEnd },
|
createdAt: { lte: dayEnd },
|
||||||
},
|
},
|
||||||
@@ -161,6 +168,7 @@ export class MetricsService {
|
|||||||
// Tâches en attente
|
// Tâches en attente
|
||||||
prisma.task.count({
|
prisma.task.count({
|
||||||
where: {
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
createdAt: { lte: dayEnd },
|
createdAt: { lte: dayEnd },
|
||||||
},
|
},
|
||||||
@@ -169,6 +177,7 @@ export class MetricsService {
|
|||||||
// Nouvelles tâches créées ce jour
|
// Nouvelles tâches créées ce jour
|
||||||
prisma.task.count({
|
prisma.task.count({
|
||||||
where: {
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
createdAt: {
|
createdAt: {
|
||||||
gte: dayStart,
|
gte: dayStart,
|
||||||
lte: dayEnd,
|
lte: dayEnd,
|
||||||
@@ -179,6 +188,7 @@ export class MetricsService {
|
|||||||
// Total des tâches existantes ce jour
|
// Total des tâches existantes ce jour
|
||||||
prisma.task.count({
|
prisma.task.count({
|
||||||
where: {
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
createdAt: { lte: dayEnd },
|
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)
|
* Récupère les métriques de vélocité d'équipe (pour graphiques de tendance)
|
||||||
*/
|
*/
|
||||||
static async getVelocityTrends(
|
static async getVelocityTrends(
|
||||||
|
userId: string,
|
||||||
weeksBack: number = 4
|
weeksBack: number = 4
|
||||||
): Promise<VelocityTrend[]> {
|
): Promise<VelocityTrend[]> {
|
||||||
const trends = [];
|
const trends = [];
|
||||||
@@ -388,6 +399,7 @@ export class MetricsService {
|
|||||||
const [completed, created] = await Promise.all([
|
const [completed, created] = await Promise.all([
|
||||||
prisma.task.count({
|
prisma.task.count({
|
||||||
where: {
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
completedAt: {
|
completedAt: {
|
||||||
gte: weekStart,
|
gte: weekStart,
|
||||||
lte: weekEnd,
|
lte: weekEnd,
|
||||||
@@ -396,6 +408,7 @@ export class MetricsService {
|
|||||||
}),
|
}),
|
||||||
prisma.task.count({
|
prisma.task.count({
|
||||||
where: {
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
createdAt: {
|
createdAt: {
|
||||||
gte: weekStart,
|
gte: weekStart,
|
||||||
lte: weekEnd,
|
lte: weekEnd,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export class TagAnalyticsService {
|
|||||||
* Calcule les métriques de distribution par tags
|
* Calcule les métriques de distribution par tags
|
||||||
*/
|
*/
|
||||||
static async getTagDistributionMetrics(
|
static async getTagDistributionMetrics(
|
||||||
|
userId: string,
|
||||||
timeRange?: TimeRange,
|
timeRange?: TimeRange,
|
||||||
sources?: string[]
|
sources?: string[]
|
||||||
): Promise<TagDistributionMetrics> {
|
): Promise<TagDistributionMetrics> {
|
||||||
@@ -60,6 +61,7 @@ export class TagAnalyticsService {
|
|||||||
// Récupérer toutes les tâches avec leurs tags
|
// Récupérer toutes les tâches avec leurs tags
|
||||||
const dbTasks = await prisma.task.findMany({
|
const dbTasks = await prisma.task.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
ownerId: userId,
|
||||||
createdAt: {
|
createdAt: {
|
||||||
gte: start,
|
gte: start,
|
||||||
lte: end,
|
lte: end,
|
||||||
@@ -102,6 +104,7 @@ export class TagAnalyticsService {
|
|||||||
jiraKey: task.jiraKey || undefined,
|
jiraKey: task.jiraKey || undefined,
|
||||||
jiraType: task.jiraType || undefined,
|
jiraType: task.jiraType || undefined,
|
||||||
assignee: task.assignee || undefined,
|
assignee: task.assignee || undefined,
|
||||||
|
ownerId: task.ownerId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Filtrer par sources si spécifié
|
// Filtrer par sources si spécifié
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ export class JiraService {
|
|||||||
/**
|
/**
|
||||||
* Synchronise les tickets Jira avec la base locale
|
* Synchronise les tickets Jira avec la base locale
|
||||||
*/
|
*/
|
||||||
async syncTasks(): Promise<SyncResult> {
|
async syncTasks(userId: string): Promise<SyncResult> {
|
||||||
const result: SyncResult = {
|
const result: SyncResult = {
|
||||||
success: false,
|
success: false,
|
||||||
totalItems: 0,
|
totalItems: 0,
|
||||||
@@ -395,7 +395,7 @@ export class JiraService {
|
|||||||
// Synchroniser chaque ticket
|
// Synchroniser chaque ticket
|
||||||
for (const jiraTask of filteredTasks) {
|
for (const jiraTask of filteredTasks) {
|
||||||
try {
|
try {
|
||||||
const syncAction = await this.syncSingleTask(jiraTask);
|
const syncAction = await this.syncSingleTask(jiraTask, userId);
|
||||||
|
|
||||||
// Convertir JiraSyncAction vers SyncAction
|
// Convertir JiraSyncAction vers SyncAction
|
||||||
const standardAction: SyncAction = {
|
const standardAction: SyncAction = {
|
||||||
@@ -467,7 +467,10 @@ export class JiraService {
|
|||||||
/**
|
/**
|
||||||
* Synchronise un ticket Jira unique
|
* 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
|
// Chercher la tâche existante
|
||||||
const existingTask = await prisma.task.findUnique({
|
const existingTask = await prisma.task.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -496,6 +499,7 @@ export class JiraService {
|
|||||||
jiraType: this.mapJiraTypeToDisplay(jiraTask.issuetype.name),
|
jiraType: this.mapJiraTypeToDisplay(jiraTask.issuetype.name),
|
||||||
assignee: jiraTask.assignee?.displayName || null,
|
assignee: jiraTask.assignee?.displayName || null,
|
||||||
updatedAt: parseDate(jiraTask.updated),
|
updatedAt: parseDate(jiraTask.updated),
|
||||||
|
ownerId: userId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!existingTask) {
|
if (!existingTask) {
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export class JiraScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Effectuer la synchronisation
|
// Effectuer la synchronisation
|
||||||
const result = await jiraService.syncTasks();
|
const result = await jiraService.syncTasks(userId);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
import { userPreferencesService } from '@/services/core/user-preferences';
|
import { userPreferencesService } from '@/services/core/user-preferences';
|
||||||
import { TfsService } from './tfs';
|
import { TfsService } from './tfs';
|
||||||
import { addMinutes, getToday } from '@/lib/date-utils';
|
import { addMinutes, getToday } from '@/lib/date-utils';
|
||||||
|
import { prisma } from '@/services/core/database';
|
||||||
|
|
||||||
export interface TfsSchedulerConfig {
|
export interface TfsSchedulerConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -124,8 +125,18 @@ export class TfsScheduler {
|
|||||||
return;
|
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
|
// Effectuer la synchronisation
|
||||||
const result = await tfsService.syncTasks();
|
const result = await tfsService.syncTasks(firstUser.id);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -558,7 +558,7 @@ export class TfsService {
|
|||||||
/**
|
/**
|
||||||
* Synchronise les Pull Requests avec les tâches locales
|
* Synchronise les Pull Requests avec les tâches locales
|
||||||
*/
|
*/
|
||||||
async syncTasks(): Promise<TfsSyncResult> {
|
async syncTasks(userId: string): Promise<TfsSyncResult> {
|
||||||
const result: TfsSyncResult = {
|
const result: TfsSyncResult = {
|
||||||
success: true,
|
success: true,
|
||||||
totalPullRequests: 0,
|
totalPullRequests: 0,
|
||||||
@@ -592,7 +592,7 @@ export class TfsService {
|
|||||||
// Synchroniser chaque PR
|
// Synchroniser chaque PR
|
||||||
for (const pr of allPullRequests) {
|
for (const pr of allPullRequests) {
|
||||||
try {
|
try {
|
||||||
const syncAction = await this.syncSinglePullRequest(pr);
|
const syncAction = await this.syncSinglePullRequest(pr, userId);
|
||||||
result.actions.push(syncAction);
|
result.actions.push(syncAction);
|
||||||
|
|
||||||
// Compter les actions
|
// Compter les actions
|
||||||
@@ -639,7 +639,8 @@ export class TfsService {
|
|||||||
* Synchronise une Pull Request unique
|
* Synchronise une Pull Request unique
|
||||||
*/
|
*/
|
||||||
private async syncSinglePullRequest(
|
private async syncSinglePullRequest(
|
||||||
pr: TfsPullRequest
|
pr: TfsPullRequest,
|
||||||
|
userId: string
|
||||||
): Promise<TfsSyncAction> {
|
): Promise<TfsSyncAction> {
|
||||||
const pullRequestId = pr.pullRequestId;
|
const pullRequestId = pr.pullRequestId;
|
||||||
const sourceId = `tfs-pr-${pullRequestId}`;
|
const sourceId = `tfs-pr-${pullRequestId}`;
|
||||||
@@ -659,6 +660,7 @@ export class TfsService {
|
|||||||
sourceId,
|
sourceId,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
ownerId: userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1109,10 +1111,10 @@ class TfsServiceInstance extends TfsService {
|
|||||||
return service.validateConfig();
|
return service.validateConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
async syncTasks(userId?: string): Promise<TfsSyncResult> {
|
async syncTasks(userId: string): Promise<TfsSyncResult> {
|
||||||
const config = await this.getConfig(userId);
|
const config = await this.getConfig(userId);
|
||||||
const service = new TfsService(config);
|
const service = new TfsService(config);
|
||||||
return service.syncTasks();
|
return service.syncTasks(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllTasks(): Promise<{
|
async deleteAllTasks(): Promise<{
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export class NotesService {
|
|||||||
tfsRepository: prismaTask.tfsRepository || undefined,
|
tfsRepository: prismaTask.tfsRepository || undefined,
|
||||||
tfsSourceBranch: prismaTask.tfsSourceBranch || undefined,
|
tfsSourceBranch: prismaTask.tfsSourceBranch || undefined,
|
||||||
tfsTargetBranch: prismaTask.tfsTargetBranch || 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,
|
jiraProject: checkbox.task.jiraProject || undefined,
|
||||||
jiraKey: checkbox.task.jiraKey || undefined,
|
jiraKey: checkbox.task.jiraKey || undefined,
|
||||||
assignee: checkbox.task.assignee || 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,
|
: undefined,
|
||||||
isArchived: checkbox.text.includes('[ARCHIVÉ]'),
|
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)
|
* 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({
|
const checkboxes = await prisma.dailyCheckbox.findMany({
|
||||||
|
where: whereConditions,
|
||||||
select: {
|
select: {
|
||||||
date: true,
|
date: true,
|
||||||
},
|
},
|
||||||
@@ -317,6 +326,7 @@ export class DailyService {
|
|||||||
excludeToday?: boolean;
|
excludeToday?: boolean;
|
||||||
type?: DailyCheckboxType;
|
type?: DailyCheckboxType;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
userId?: string; // Filtrer par utilisateur
|
||||||
}): Promise<DailyCheckbox[]> {
|
}): Promise<DailyCheckbox[]> {
|
||||||
const today = normalizeDate(getToday());
|
const today = normalizeDate(getToday());
|
||||||
const maxDays = options?.maxDays ?? 30;
|
const maxDays = options?.maxDays ?? 30;
|
||||||
@@ -327,15 +337,7 @@ export class DailyService {
|
|||||||
limitDate.setDate(limitDate.getDate() - maxDays);
|
limitDate.setDate(limitDate.getDate() - maxDays);
|
||||||
|
|
||||||
// Construire les conditions de filtrage
|
// Construire les conditions de filtrage
|
||||||
const whereConditions: {
|
const whereConditions: Prisma.DailyCheckboxWhereInput = {
|
||||||
isChecked: boolean;
|
|
||||||
date: {
|
|
||||||
gte: Date;
|
|
||||||
lt?: Date;
|
|
||||||
lte?: Date;
|
|
||||||
};
|
|
||||||
type?: DailyCheckboxType;
|
|
||||||
} = {
|
|
||||||
isChecked: false,
|
isChecked: false,
|
||||||
date: {
|
date: {
|
||||||
gte: limitDate,
|
gte: limitDate,
|
||||||
@@ -348,6 +350,17 @@ export class DailyService {
|
|||||||
whereConditions.type = options.type;
|
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({
|
const checkboxes = await prisma.dailyCheckbox.findMany({
|
||||||
where: whereConditions,
|
where: whereConditions,
|
||||||
include: { task: true, user: true },
|
include: { task: true, user: true },
|
||||||
|
|||||||
@@ -20,13 +20,18 @@ export class TasksService {
|
|||||||
/**
|
/**
|
||||||
* Récupère toutes les tâches avec filtres optionnels
|
* Récupère toutes les tâches avec filtres optionnels
|
||||||
*/
|
*/
|
||||||
async getTasks(filters?: {
|
async getTasks(
|
||||||
status?: TaskStatus[];
|
userId: string,
|
||||||
search?: string;
|
filters?: {
|
||||||
limit?: number;
|
status?: TaskStatus[];
|
||||||
offset?: number;
|
search?: string;
|
||||||
}): Promise<Task[]> {
|
limit?: number;
|
||||||
const where: Prisma.TaskWhereInput = {};
|
offset?: number;
|
||||||
|
}
|
||||||
|
): Promise<Task[]> {
|
||||||
|
const where: Prisma.TaskWhereInput = {
|
||||||
|
ownerId: userId, // Toujours filtrer par propriétaire
|
||||||
|
};
|
||||||
|
|
||||||
if (filters?.status) {
|
if (filters?.status) {
|
||||||
where.status = { in: filters.status };
|
where.status = { in: filters.status };
|
||||||
@@ -77,6 +82,7 @@ export class TasksService {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
primaryTagId?: string;
|
primaryTagId?: string;
|
||||||
dueDate?: Date;
|
dueDate?: Date;
|
||||||
|
ownerId: string; // Requis - chaque tâche doit avoir un propriétaire
|
||||||
}): Promise<Task> {
|
}): Promise<Task> {
|
||||||
const status = taskData.status || 'todo';
|
const status = taskData.status || 'todo';
|
||||||
const task = await prisma.task.create({
|
const task = await prisma.task.create({
|
||||||
@@ -87,6 +93,7 @@ export class TasksService {
|
|||||||
priority: taskData.priority || 'medium',
|
priority: taskData.priority || 'medium',
|
||||||
dueDate: taskData.dueDate,
|
dueDate: taskData.dueDate,
|
||||||
primaryTagId: taskData.primaryTagId,
|
primaryTagId: taskData.primaryTagId,
|
||||||
|
ownerId: taskData.ownerId, // Assigner le propriétaire
|
||||||
source: 'manual', // Source manuelle
|
source: 'manual', // Source manuelle
|
||||||
sourceId: `manual-${Date.now()}`, // ID unique
|
sourceId: `manual-${Date.now()}`, // ID unique
|
||||||
// Si créée directement en done/archived, définir completedAt
|
// Si créée directement en done/archived, définir completedAt
|
||||||
@@ -128,6 +135,7 @@ export class TasksService {
|
|||||||
* Met à jour une tâche
|
* Met à jour une tâche
|
||||||
*/
|
*/
|
||||||
async updateTask(
|
async updateTask(
|
||||||
|
userId: string,
|
||||||
taskId: string,
|
taskId: string,
|
||||||
updates: {
|
updates: {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -139,12 +147,14 @@ export class TasksService {
|
|||||||
dueDate?: Date;
|
dueDate?: Date;
|
||||||
}
|
}
|
||||||
): Promise<Task> {
|
): Promise<Task> {
|
||||||
const task = await prisma.task.findUnique({
|
const task = await prisma.task.findFirst({
|
||||||
where: { id: taskId },
|
where: { id: taskId, ownerId: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!task) {
|
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
|
// Logique métier : si on marque comme terminé, on ajoute la date
|
||||||
@@ -198,13 +208,15 @@ export class TasksService {
|
|||||||
/**
|
/**
|
||||||
* Supprime une tâche
|
* Supprime une tâche
|
||||||
*/
|
*/
|
||||||
async deleteTask(taskId: string): Promise<void> {
|
async deleteTask(userId: string, taskId: string): Promise<void> {
|
||||||
const task = await prisma.task.findUnique({
|
const task = await prisma.task.findFirst({
|
||||||
where: { id: taskId },
|
where: { id: taskId, ownerId: userId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!task) {
|
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({
|
await prisma.task.delete({
|
||||||
@@ -215,14 +227,30 @@ export class TasksService {
|
|||||||
/**
|
/**
|
||||||
* Met à jour le statut d'une tâche
|
* Met à jour le statut d'une tâche
|
||||||
*/
|
*/
|
||||||
async updateTaskStatus(taskId: string, newStatus: TaskStatus): Promise<Task> {
|
async updateTaskStatus(
|
||||||
return this.updateTask(taskId, { status: newStatus });
|
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
|
* 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({
|
const checkboxes = await prisma.dailyCheckbox.findMany({
|
||||||
where: { taskId: taskId },
|
where: { taskId: taskId },
|
||||||
include: { task: true },
|
include: { task: true },
|
||||||
@@ -256,6 +284,7 @@ export class TasksService {
|
|||||||
jiraKey: checkbox.task.jiraKey ?? undefined,
|
jiraKey: checkbox.task.jiraKey ?? undefined,
|
||||||
jiraType: checkbox.task.jiraType ?? undefined,
|
jiraType: checkbox.task.jiraType ?? undefined,
|
||||||
assignee: checkbox.task.assignee ?? undefined,
|
assignee: checkbox.task.assignee ?? undefined,
|
||||||
|
ownerId: (checkbox.task as unknown as { ownerId: string }).ownerId, // Cast temporaire
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
isArchived: checkbox.text.includes('[ARCHIVÉ]'),
|
isArchived: checkbox.text.includes('[ARCHIVÉ]'),
|
||||||
@@ -267,7 +296,7 @@ export class TasksService {
|
|||||||
/**
|
/**
|
||||||
* Récupère les statistiques des tâches
|
* Récupère les statistiques des tâches
|
||||||
*/
|
*/
|
||||||
async getTaskStats() {
|
async getTaskStats(userId: string) {
|
||||||
const [
|
const [
|
||||||
total,
|
total,
|
||||||
done,
|
done,
|
||||||
@@ -278,14 +307,14 @@ export class TasksService {
|
|||||||
cancelled,
|
cancelled,
|
||||||
freeze,
|
freeze,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
prisma.task.count(),
|
prisma.task.count({ where: { ownerId: userId } }),
|
||||||
prisma.task.count({ where: { status: 'done' } }),
|
prisma.task.count({ where: { ownerId: userId, status: 'done' } }),
|
||||||
prisma.task.count({ where: { status: 'archived' } }),
|
prisma.task.count({ where: { ownerId: userId, status: 'archived' } }),
|
||||||
prisma.task.count({ where: { status: 'in_progress' } }),
|
prisma.task.count({ where: { ownerId: userId, status: 'in_progress' } }),
|
||||||
prisma.task.count({ where: { status: 'todo' } }),
|
prisma.task.count({ where: { ownerId: userId, status: 'todo' } }),
|
||||||
prisma.task.count({ where: { status: 'backlog' } }),
|
prisma.task.count({ where: { ownerId: userId, status: 'backlog' } }),
|
||||||
prisma.task.count({ where: { status: 'cancelled' } }),
|
prisma.task.count({ where: { ownerId: userId, status: 'cancelled' } }),
|
||||||
prisma.task.count({ where: { status: 'freeze' } }),
|
prisma.task.count({ where: { ownerId: userId, status: 'freeze' } }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const completed = done + archived; // Terminées = Done + Archived
|
const completed = done + archived; // Terminées = Done + Archived
|
||||||
@@ -451,6 +480,7 @@ export class TasksService {
|
|||||||
tfsSourceBranch: prismaTask.tfsSourceBranch ?? undefined,
|
tfsSourceBranch: prismaTask.tfsSourceBranch ?? undefined,
|
||||||
tfsTargetBranch: prismaTask.tfsTargetBranch ?? undefined,
|
tfsTargetBranch: prismaTask.tfsTargetBranch ?? undefined,
|
||||||
assignee: prismaTask.assignee ?? undefined,
|
assignee: prismaTask.assignee ?? undefined,
|
||||||
|
ownerId: prismaTask.ownerId,
|
||||||
todosCount: todosCount,
|
todosCount: todosCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user