feat: complete Phase 4 of service refactoring
- Marked tasks in `TODO.md` as completed for moving task-related files to the `task-management` directory and correcting imports across the codebase. - Updated imports in `seed-data.ts`, `seed-tags.ts`, API routes, and various components to reflect the new structure. - Removed obsolete `daily.ts`, `tags.ts`, and `tasks.ts` files to streamline the codebase. - Added new tasks in `TODO.md` for future cleaning and organization of service imports.
This commit is contained in:
380
src/services/task-management/tasks.ts
Normal file
380
src/services/task-management/tasks.ts
Normal file
@@ -0,0 +1,380 @@
|
||||
import { prisma } from '../core/database';
|
||||
import { Task, TaskStatus, TaskPriority, TaskSource, BusinessError, DailyCheckbox, DailyCheckboxType } from '@/lib/types';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { getToday } from '@/lib/date-utils';
|
||||
|
||||
/**
|
||||
* Service pour la gestion des tâches (version standalone)
|
||||
*/
|
||||
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 = {};
|
||||
|
||||
if (filters?.status) {
|
||||
where.status = { in: filters.status };
|
||||
}
|
||||
|
||||
if (filters?.search) {
|
||||
where.OR = [
|
||||
{ title: { contains: filters.search } },
|
||||
{ description: { contains: filters.search } }
|
||||
];
|
||||
}
|
||||
|
||||
const tasks = await prisma.task.findMany({
|
||||
where,
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true
|
||||
}
|
||||
}
|
||||
},
|
||||
take: filters?.limit, // Pas de limite par défaut - récupère toutes les tâches
|
||||
skip: filters?.offset || 0,
|
||||
orderBy: [
|
||||
{ completedAt: 'desc' },
|
||||
{ dueDate: 'asc' },
|
||||
{ createdAt: 'desc' }
|
||||
]
|
||||
});
|
||||
|
||||
return tasks.map(this.mapPrismaTaskToTask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une nouvelle tâche
|
||||
*/
|
||||
async createTask(taskData: {
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: TaskStatus;
|
||||
priority?: TaskPriority;
|
||||
tags?: string[];
|
||||
dueDate?: Date;
|
||||
}): Promise<Task> {
|
||||
const task = await prisma.task.create({
|
||||
data: {
|
||||
title: taskData.title,
|
||||
description: taskData.description,
|
||||
status: taskData.status || 'todo',
|
||||
priority: taskData.priority || 'medium',
|
||||
dueDate: taskData.dueDate,
|
||||
source: 'manual', // Source manuelle
|
||||
sourceId: `manual-${Date.now()}` // ID unique
|
||||
},
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Créer les relations avec les tags
|
||||
if (taskData.tags && taskData.tags.length > 0) {
|
||||
await this.createTaskTagRelations(task.id, taskData.tags);
|
||||
}
|
||||
|
||||
// Récupérer la tâche avec les tags pour le retour
|
||||
const taskWithTags = await prisma.task.findUnique({
|
||||
where: { id: task.id },
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return this.mapPrismaTaskToTask(taskWithTags!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une tâche
|
||||
*/
|
||||
async updateTask(taskId: string, updates: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: TaskStatus;
|
||||
priority?: TaskPriority;
|
||||
tags?: string[];
|
||||
dueDate?: Date;
|
||||
}): Promise<Task> {
|
||||
const task = await prisma.task.findUnique({
|
||||
where: { id: taskId }
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new BusinessError(`Tâche ${taskId} introuvable`);
|
||||
}
|
||||
|
||||
// Logique métier : si on marque comme terminé, on ajoute la date
|
||||
const updateData: Prisma.TaskUpdateInput = {
|
||||
title: updates.title,
|
||||
description: updates.description,
|
||||
status: updates.status,
|
||||
priority: updates.priority,
|
||||
dueDate: updates.dueDate,
|
||||
updatedAt: getToday()
|
||||
};
|
||||
|
||||
|
||||
if (updates.status === 'done' && !task.completedAt) {
|
||||
updateData.completedAt = getToday();
|
||||
} else if (updates.status && updates.status !== 'done' && task.completedAt) {
|
||||
updateData.completedAt = null;
|
||||
}
|
||||
|
||||
await prisma.task.update({
|
||||
where: { id: taskId },
|
||||
data: updateData
|
||||
});
|
||||
|
||||
// Mettre à jour les relations avec les tags
|
||||
if (updates.tags !== undefined) {
|
||||
await this.updateTaskTagRelations(taskId, updates.tags);
|
||||
}
|
||||
|
||||
// Récupérer la tâche avec les tags pour le retour
|
||||
const taskWithTags = await prisma.task.findUnique({
|
||||
where: { id: taskId },
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return this.mapPrismaTaskToTask(taskWithTags!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime une tâche
|
||||
*/
|
||||
async deleteTask(taskId: string): Promise<void> {
|
||||
const task = await prisma.task.findUnique({
|
||||
where: { id: taskId }
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new BusinessError(`Tâche ${taskId} introuvable`);
|
||||
}
|
||||
|
||||
await prisma.task.delete({
|
||||
where: { id: taskId }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le statut d'une tâche
|
||||
*/
|
||||
async updateTaskStatus(taskId: string, newStatus: TaskStatus): Promise<Task> {
|
||||
return this.updateTask(taskId, { status: newStatus });
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les daily checkboxes liées à une tâche
|
||||
*/
|
||||
async getTaskRelatedCheckboxes(taskId: string): Promise<DailyCheckbox[]> {
|
||||
const checkboxes = await prisma.dailyCheckbox.findMany({
|
||||
where: { taskId: taskId },
|
||||
include: { task: true },
|
||||
orderBy: [
|
||||
{ date: 'desc' },
|
||||
{ order: 'asc' }
|
||||
]
|
||||
});
|
||||
|
||||
return checkboxes.map(checkbox => ({
|
||||
id: checkbox.id,
|
||||
date: checkbox.date,
|
||||
text: checkbox.text,
|
||||
isChecked: checkbox.isChecked,
|
||||
type: checkbox.type as DailyCheckboxType,
|
||||
order: checkbox.order,
|
||||
taskId: checkbox.taskId ?? undefined,
|
||||
task: checkbox.task ? {
|
||||
id: checkbox.task.id,
|
||||
title: checkbox.task.title,
|
||||
description: checkbox.task.description ?? undefined,
|
||||
status: checkbox.task.status as TaskStatus,
|
||||
priority: checkbox.task.priority as TaskPriority,
|
||||
source: checkbox.task.source as TaskSource,
|
||||
sourceId: checkbox.task.sourceId ?? undefined,
|
||||
tags: [], // Les tags ne sont pas nécessaires dans ce contexte
|
||||
dueDate: checkbox.task.dueDate ?? undefined,
|
||||
completedAt: checkbox.task.completedAt ?? undefined,
|
||||
createdAt: checkbox.task.createdAt,
|
||||
updatedAt: checkbox.task.updatedAt,
|
||||
jiraProject: checkbox.task.jiraProject ?? undefined,
|
||||
jiraKey: checkbox.task.jiraKey ?? undefined,
|
||||
jiraType: checkbox.task.jiraType ?? undefined,
|
||||
assignee: checkbox.task.assignee ?? undefined
|
||||
} : undefined,
|
||||
createdAt: checkbox.createdAt,
|
||||
updatedAt: checkbox.updatedAt
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les statistiques des tâches
|
||||
*/
|
||||
async getTaskStats() {
|
||||
const [total, completed, inProgress, todo, backlog, cancelled, freeze, archived] = await Promise.all([
|
||||
prisma.task.count(),
|
||||
prisma.task.count({ where: { status: 'done' } }),
|
||||
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: { status: 'archived' } })
|
||||
]);
|
||||
|
||||
return {
|
||||
total,
|
||||
completed,
|
||||
inProgress,
|
||||
todo,
|
||||
backlog,
|
||||
cancelled,
|
||||
freeze,
|
||||
archived,
|
||||
completionRate: total > 0 ? Math.round((completed / total) * 100) : 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée les relations TaskTag pour une tâche
|
||||
*/
|
||||
private async createTaskTagRelations(taskId: string, tagNames: string[]): Promise<void> {
|
||||
for (const tagName of tagNames) {
|
||||
try {
|
||||
// Créer ou récupérer le tag
|
||||
const tag = await prisma.tag.upsert({
|
||||
where: { name: tagName },
|
||||
update: {}, // Pas de mise à jour nécessaire
|
||||
create: {
|
||||
name: tagName,
|
||||
color: this.generateTagColor(tagName)
|
||||
}
|
||||
});
|
||||
|
||||
// Créer la relation TaskTag si elle n'existe pas
|
||||
await prisma.taskTag.upsert({
|
||||
where: {
|
||||
taskId_tagId: {
|
||||
taskId: taskId,
|
||||
tagId: tag.id
|
||||
}
|
||||
},
|
||||
update: {}, // Pas de mise à jour nécessaire
|
||||
create: {
|
||||
taskId: taskId,
|
||||
tagId: tag.id
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors de la création de la relation tag ${tagName}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les relations TaskTag pour une tâche
|
||||
*/
|
||||
private async updateTaskTagRelations(taskId: string, tagNames: string[]): Promise<void> {
|
||||
// Supprimer toutes les relations existantes
|
||||
await prisma.taskTag.deleteMany({
|
||||
where: { taskId: taskId }
|
||||
});
|
||||
|
||||
// Créer les nouvelles relations
|
||||
if (tagNames.length > 0) {
|
||||
await this.createTaskTagRelations(taskId, tagNames);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère une couleur pour un tag basée sur son nom
|
||||
*/
|
||||
private generateTagColor(tagName: string): string {
|
||||
const colors = [
|
||||
'#ef4444', '#f97316', '#f59e0b', '#eab308',
|
||||
'#84cc16', '#22c55e', '#10b981', '#14b8a6',
|
||||
'#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1',
|
||||
'#8b5cf6', '#a855f7', '#d946ef', '#ec4899'
|
||||
];
|
||||
|
||||
// Hash simple du nom pour choisir une couleur
|
||||
let hash = 0;
|
||||
for (let i = 0; i < tagName.length; i++) {
|
||||
hash = tagName.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
return colors[Math.abs(hash) % colors.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une tâche Prisma en objet Task
|
||||
*/
|
||||
private mapPrismaTaskToTask(prismaTask: Prisma.TaskGetPayload<{
|
||||
include: {
|
||||
taskTags: {
|
||||
include: {
|
||||
tag: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}> | Prisma.TaskGetPayload<object>): Task {
|
||||
// Extraire les tags depuis les relations TaskTag ou fallback sur tagsJson
|
||||
let tags: string[] = [];
|
||||
|
||||
if ('taskTags' in prismaTask && prismaTask.taskTags && Array.isArray(prismaTask.taskTags)) {
|
||||
// Utiliser les relations Prisma
|
||||
tags = prismaTask.taskTags.map((tt) => tt.tag.name);
|
||||
}
|
||||
|
||||
return {
|
||||
id: prismaTask.id,
|
||||
title: prismaTask.title,
|
||||
description: prismaTask.description ?? undefined,
|
||||
status: prismaTask.status as TaskStatus,
|
||||
priority: prismaTask.priority as TaskPriority,
|
||||
source: prismaTask.source as TaskSource,
|
||||
sourceId: prismaTask.sourceId ?? undefined,
|
||||
tags: tags,
|
||||
dueDate: prismaTask.dueDate ?? undefined,
|
||||
completedAt: prismaTask.completedAt ?? undefined,
|
||||
createdAt: prismaTask.createdAt,
|
||||
updatedAt: prismaTask.updatedAt,
|
||||
jiraProject: prismaTask.jiraProject ?? undefined,
|
||||
jiraKey: prismaTask.jiraKey ?? undefined,
|
||||
jiraType: prismaTask.jiraType ?? undefined,
|
||||
// Champs TFS
|
||||
tfsProject: prismaTask.tfsProject ?? undefined,
|
||||
tfsPullRequestId: prismaTask.tfsPullRequestId ?? undefined,
|
||||
tfsRepository: prismaTask.tfsRepository ?? undefined,
|
||||
tfsSourceBranch: prismaTask.tfsSourceBranch ?? undefined,
|
||||
tfsTargetBranch: prismaTask.tfsTargetBranch ?? undefined,
|
||||
assignee: prismaTask.assignee ?? undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Instance singleton
|
||||
export const tasksService = new TasksService();
|
||||
Reference in New Issue
Block a user