Files
towercontrol/src/services/task-management/daily.ts
Julien Froidefond 8340008839 feat(DailyPage, DailyService, Calendar): enhance task deadline management and UI integration
- Implemented user authentication in the daily dates API route to ensure secure access.
- Added functionality to retrieve task deadlines and associated tasks, improving task management capabilities.
- Updated DailyPageClient to display tasks with deadlines in the calendar view, enhancing user experience.
- Enhanced Calendar component to visually indicate deadline dates, providing clearer task management context.
2025-11-11 08:46:19 +01:00

757 lines
20 KiB
TypeScript

import { prisma } from '@/services/core/database';
import { Prisma } from '@prisma/client';
import {
DailyCheckbox,
DailyView,
CreateDailyCheckboxData,
UpdateDailyCheckboxData,
BusinessError,
DailyCheckboxType,
TaskStatus,
TaskPriority,
TaskSource,
Task,
} from '@/lib/types';
import {
getPreviousWorkday,
normalizeDate,
formatDateForAPI,
getToday,
getYesterday,
} from '@/lib/date-utils';
/**
* Service pour la gestion des checkboxes daily
*/
export class DailyService {
/**
* Récupère la vue daily pour une date donnée (checkboxes d'hier et d'aujourd'hui)
*/
async getDailyView(date: Date, userId: string): Promise<DailyView> {
// Normaliser la date (début de journée)
const today = normalizeDate(date);
// Utiliser la logique de jour de travail précédent au lieu de jour-1
const yesterday = getPreviousWorkday(today);
// Récupérer les checkboxes des deux jours
const [yesterdayCheckboxes, todayCheckboxes] = await Promise.all([
this.getCheckboxesByDate(yesterday, userId),
this.getCheckboxesByDate(today, userId),
]);
return {
date: today,
yesterday: yesterdayCheckboxes,
today: todayCheckboxes,
};
}
/**
* Récupère toutes les checkboxes d'une date donnée pour un utilisateur
*/
async getCheckboxesByDate(
date: Date,
userId: string
): Promise<DailyCheckbox[]> {
// Normaliser la date (début de journée)
const normalizedDate = normalizeDate(date);
const checkboxes = await prisma.dailyCheckbox.findMany({
where: {
date: normalizedDate,
userId: userId,
},
include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
orderBy: { order: 'asc' },
});
return checkboxes.map(this.mapPrismaCheckbox);
}
/**
* Ajoute une checkbox à une date donnée
*/
async addCheckbox(data: CreateDailyCheckboxData): Promise<DailyCheckbox> {
// Normaliser la date
const normalizedDate = normalizeDate(data.date);
// Calculer l'ordre suivant pour cette date
const maxOrder = await prisma.dailyCheckbox.aggregate({
where: { date: normalizedDate },
_max: { order: true },
});
const order = data.order ?? (maxOrder._max.order ?? -1) + 1;
const checkbox = await prisma.dailyCheckbox.create({
data: {
date: normalizedDate,
text: data.text.trim(),
type: data.type ?? 'task',
taskId: data.taskId,
userId: data.userId,
order,
isChecked: data.isChecked ?? false,
},
include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
});
return this.mapPrismaCheckbox(checkbox);
}
/**
* Met à jour une checkbox
*/
async updateCheckbox(
checkboxId: string,
data: UpdateDailyCheckboxData
): Promise<DailyCheckbox> {
const updateData: Prisma.DailyCheckboxUpdateInput = {};
if (data.text !== undefined) updateData.text = data.text.trim();
if (data.isChecked !== undefined) updateData.isChecked = data.isChecked;
if (data.type !== undefined) updateData.type = data.type;
if (data.taskId !== undefined) {
if (data.taskId === null) {
updateData.task = { disconnect: true };
} else {
updateData.task = { connect: { id: data.taskId } };
}
}
if (data.order !== undefined) updateData.order = data.order;
if (data.date !== undefined) updateData.date = normalizeDate(data.date);
const checkbox = await prisma.dailyCheckbox.update({
where: { id: checkboxId },
data: updateData,
include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
});
return this.mapPrismaCheckbox(checkbox);
}
/**
* Toggle l'état d'une checkbox par son ID (indépendant de la date)
*/
async toggleCheckbox(checkboxId: string): Promise<DailyCheckbox> {
const existing = await prisma.dailyCheckbox.findUnique({
where: { id: checkboxId },
});
if (!existing) {
throw new BusinessError('Checkbox non trouvée');
}
const updated = await prisma.dailyCheckbox.update({
where: { id: checkboxId },
data: { isChecked: !existing.isChecked, updatedAt: new Date() },
include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
});
return this.mapPrismaCheckbox(updated);
}
/**
* Supprime une checkbox
*/
async deleteCheckbox(checkboxId: string): Promise<void> {
const checkbox = await prisma.dailyCheckbox.findUnique({
where: { id: checkboxId },
});
if (!checkbox) {
throw new BusinessError('Checkbox non trouvée');
}
await prisma.dailyCheckbox.delete({
where: { id: checkboxId },
});
}
/**
* Réordonne les checkboxes d'une date donnée
*/
async reorderCheckboxes(date: Date, checkboxIds: string[]): Promise<void> {
await prisma.$transaction(async (prisma) => {
for (let i = 0; i < checkboxIds.length; i++) {
await prisma.dailyCheckbox.update({
where: { id: checkboxIds[i] },
data: { order: i },
});
}
});
}
/**
* Recherche dans les checkboxes
*/
async searchCheckboxes(
query: string,
limit: number = 20
): Promise<DailyCheckbox[]> {
const checkboxes = await prisma.dailyCheckbox.findMany({
where: {
text: {
contains: query,
},
},
include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
orderBy: { date: 'desc' },
take: limit,
});
return checkboxes.map(this.mapPrismaCheckbox);
}
/**
* Récupère l'historique des checkboxes (groupées par date)
*/
async getCheckboxHistory(
userId: string,
limit: number = 30
): Promise<{ date: Date; checkboxes: DailyCheckbox[] }[]> {
// Récupérer les dates distinctes des dernières checkboxes pour cet utilisateur
const distinctDates = await prisma.dailyCheckbox.findMany({
where: { userId },
select: { date: true },
distinct: ['date'],
orderBy: { date: 'desc' },
take: limit,
});
const history = [];
for (const { date } of distinctDates) {
const checkboxes = await this.getCheckboxesByDate(date, userId);
if (checkboxes.length > 0) {
history.push({ date, checkboxes });
}
}
return history;
}
/**
* Récupère la vue daily d'aujourd'hui
*/
async getTodaysDailyView(userId: string): Promise<DailyView> {
return this.getDailyView(getToday(), userId);
}
/**
* Ajoute une checkbox pour aujourd'hui
*/
async addTodayCheckbox(
userId: string,
text: string,
taskId?: string
): Promise<DailyCheckbox> {
return this.addCheckbox({
date: getToday(),
userId,
text,
taskId,
});
}
/**
* Ajoute une checkbox pour hier
*/
async addYesterdayCheckbox(
userId: string,
text: string,
taskId?: string
): Promise<DailyCheckbox> {
return this.addCheckbox({
date: getYesterday(),
userId,
text,
taskId,
});
}
/**
* Mappe une checkbox Prisma vers notre interface
* Accepte les checkboxes avec ou sans les relations taskTags et primaryTag
*/
private mapPrismaCheckbox(
checkbox: Prisma.DailyCheckboxGetPayload<{
include: {
task:
| true
| {
include: {
taskTags: {
include: {
tag: true;
};
};
primaryTag: true;
};
};
user: true;
};
}>
): DailyCheckbox {
// Extraire les tags de la tâche si elle existe
let taskTags: string[] = [];
let taskTagDetails:
| Array<{ id: string; name: string; color: string; isPinned?: boolean }>
| undefined = undefined;
let taskPrimaryTag:
| { id: string; name: string; color: string; isPinned?: boolean }
| undefined = undefined;
if (checkbox.task) {
// Vérifier si taskTags est disponible (peut être true ou un objet avec include)
const taskWithTags = checkbox.task as unknown as {
taskTags?: Array<{
tag: { id: string; name: string; color: string; isPinned: boolean };
}>;
primaryTag?: {
id: string;
name: string;
color: string;
isPinned: boolean;
} | null;
};
if (
'taskTags' in taskWithTags &&
taskWithTags.taskTags &&
Array.isArray(taskWithTags.taskTags)
) {
// Utiliser les relations Prisma pour récupérer les noms et détails des tags
taskTags = taskWithTags.taskTags.map((tt) => tt.tag.name);
taskTagDetails = taskWithTags.taskTags.map((tt) => ({
id: tt.tag.id,
name: tt.tag.name,
color: tt.tag.color,
isPinned: tt.tag.isPinned,
}));
}
// Extraire le primaryTag si disponible
if ('primaryTag' in taskWithTags && taskWithTags.primaryTag) {
taskPrimaryTag = {
id: taskWithTags.primaryTag.id,
name: taskWithTags.primaryTag.name,
color: taskWithTags.primaryTag.color,
isPinned: taskWithTags.primaryTag.isPinned,
};
}
}
return {
id: checkbox.id,
date: checkbox.date,
text: checkbox.text,
isChecked: checkbox.isChecked,
type: checkbox.type as DailyCheckboxType,
order: checkbox.order,
taskId: checkbox.taskId || undefined,
userId: checkbox.userId,
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: taskTags,
tagDetails: taskTagDetails,
primaryTagId: checkbox.task.primaryTagId || undefined,
primaryTag: taskPrimaryTag,
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,
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É]'),
createdAt: checkbox.createdAt,
updatedAt: checkbox.updatedAt,
};
}
/**
* Récupère toutes les dates qui ont des checkboxes (pour le calendrier)
*/
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,
},
distinct: ['date'],
orderBy: {
date: 'desc',
},
});
return checkboxes.map((checkbox) => {
return formatDateForAPI(checkbox.date);
});
}
/**
* Récupère toutes les dates de fin (dueDate) des tâches non terminées (pour le calendrier)
* Retourne un objet avec les dates comme clés et les tâches associées
*/
async getTaskDeadlineDates(
userId: string
): Promise<Record<string, string[]>> {
const tasks = await prisma.task.findMany({
where: {
ownerId: userId,
dueDate: {
not: null,
},
status: {
notIn: ['done', 'cancelled', 'archived'],
},
},
select: {
id: true,
title: true,
dueDate: true,
},
orderBy: {
dueDate: 'asc',
},
});
const datesMap: Record<string, string[]> = {};
tasks.forEach((task) => {
if (!task.dueDate) return;
// Normaliser la date pour éviter les problèmes de timezone
const normalizedDate = normalizeDate(task.dueDate);
const dateKey = formatDateForAPI(normalizedDate);
if (!datesMap[dateKey]) {
datesMap[dateKey] = [];
}
datesMap[dateKey].push(task.title);
});
return datesMap;
}
/**
* Récupère les tâches avec deadline pour une date donnée
* Retourne un tableau de tâches complètes
*/
async getTasksByDeadlineDate(userId: string, date: Date): Promise<Task[]> {
const normalizedDate = normalizeDate(date);
const dateKey = formatDateForAPI(normalizedDate);
const tasks = await prisma.task.findMany({
where: {
ownerId: userId,
dueDate: {
not: null,
},
status: {
notIn: ['done', 'cancelled', 'archived'],
},
},
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
_count: {
select: {
dailyCheckboxes: true,
},
},
},
orderBy: {
dueDate: 'asc',
},
});
// Filtrer les tâches dont la date de fin correspond à la date demandée
const filteredTasks = tasks
.filter((task) => {
if (!task.dueDate) return false;
const taskDateKey = formatDateForAPI(normalizeDate(task.dueDate));
return taskDateKey === dateKey;
})
.map((task) => ({
id: task.id,
title: task.title,
description: task.description ?? undefined,
status: task.status as TaskStatus,
priority: task.priority as TaskPriority,
source: task.source as TaskSource,
sourceId: task.sourceId ?? undefined,
tags: task.taskTags.map((tt) => tt.tag.name),
tagDetails: task.taskTags.map((tt) => ({
id: tt.tag.id,
name: tt.tag.name,
color: tt.tag.color,
isPinned: tt.tag.isPinned,
})),
primaryTagId: task.primaryTagId ?? undefined,
primaryTag: task.primaryTag
? {
id: task.primaryTag.id,
name: task.primaryTag.name,
color: task.primaryTag.color,
isPinned: task.primaryTag.isPinned,
}
: undefined,
dueDate: task.dueDate ?? undefined,
completedAt: task.completedAt ?? undefined,
createdAt: task.createdAt,
updatedAt: task.updatedAt,
jiraProject: task.jiraProject ?? undefined,
jiraKey: task.jiraKey ?? undefined,
jiraType: task.jiraType ?? undefined,
tfsProject: task.tfsProject ?? undefined,
tfsPullRequestId: task.tfsPullRequestId ?? undefined,
tfsRepository: task.tfsRepository ?? undefined,
tfsSourceBranch: task.tfsSourceBranch ?? undefined,
tfsTargetBranch: task.tfsTargetBranch ?? undefined,
assignee: task.assignee ?? undefined,
ownerId: (task as unknown as { ownerId: string }).ownerId,
todosCount: task._count.dailyCheckboxes,
}));
return filteredTasks;
}
/**
* Récupère toutes les checkboxes non cochées (tâches en attente)
*/
async getPendingCheckboxes(options?: {
maxDays?: number;
excludeToday?: boolean;
type?: DailyCheckboxType;
limit?: number;
userId?: string; // Filtrer par utilisateur
}): Promise<DailyCheckbox[]> {
const today = normalizeDate(getToday());
const maxDays = options?.maxDays ?? 30;
const excludeToday = options?.excludeToday ?? true;
// Calculer la date limite (maxDays jours en arrière)
const limitDate = new Date(today);
limitDate.setDate(limitDate.getDate() - maxDays);
// Construire les conditions de filtrage
const whereConditions: Prisma.DailyCheckboxWhereInput = {
isChecked: false,
date: {
gte: limitDate,
...(excludeToday ? { lt: today } : { lte: today }),
},
};
// Filtrer par type si spécifié
if (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({
where: whereConditions,
include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
orderBy: [{ date: 'desc' }, { order: 'asc' }],
...(options?.limit ? { take: options.limit } : {}),
});
return checkboxes.map(this.mapPrismaCheckbox);
}
/**
* Archive une checkbox (marque comme archivée sans la cocher)
*/
async archiveCheckbox(checkboxId: string): Promise<DailyCheckbox> {
// Pour l'instant, on utilise un champ text pour marquer comme archivé
// Plus tard on pourra ajouter un champ dédié dans la DB
const checkbox = await prisma.dailyCheckbox.update({
where: { id: checkboxId },
data: {
text:
(await prisma.dailyCheckbox.findUnique({ where: { id: checkboxId } }))
?.text + ' [ARCHIVÉ]',
updatedAt: new Date(),
},
include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
});
return this.mapPrismaCheckbox(checkbox);
}
/**
* Déplace une checkbox non cochée à aujourd'hui
*/
async moveCheckboxToToday(checkboxId: string): Promise<DailyCheckbox> {
const checkbox = await prisma.dailyCheckbox.findUnique({
where: { id: checkboxId },
});
if (!checkbox) {
throw new BusinessError('Checkbox non trouvée');
}
if (checkbox.isChecked) {
throw new BusinessError('Impossible de déplacer une tâche déjà cochée');
}
const today = normalizeDate(getToday());
// Vérifier si la checkbox est déjà pour aujourd'hui
if (normalizeDate(checkbox.date).getTime() === today.getTime()) {
throw new BusinessError("La tâche est déjà programmée pour aujourd'hui");
}
// Calculer l'ordre suivant pour aujourd'hui
const maxOrder = await prisma.dailyCheckbox.aggregate({
where: { date: today },
_max: { order: true },
});
const newOrder = (maxOrder._max.order ?? -1) + 1;
const updatedCheckbox = await prisma.dailyCheckbox.update({
where: { id: checkboxId },
data: {
date: today,
order: newOrder,
updatedAt: new Date(),
},
include: {
task: {
include: {
taskTags: {
include: {
tag: true,
},
},
primaryTag: true,
},
},
user: true,
},
});
return this.mapPrismaCheckbox(updatedCheckbox);
}
}
// Instance singleton du service
export const dailyService = new DailyService();