feat: overhaul TODO.md and enhance Kanban components
- Updated TODO.md to reflect the new project structure and phases, marking several tasks as completed. - Enhanced Kanban components with a tech-inspired design, including new styles for columns and task cards. - Removed the obsolete reminders service and task processor, streamlining the codebase for better maintainability. - Introduced a modern API for task management, including CRUD operations and improved error handling. - Updated global styles for a cohesive dark theme and added custom scrollbar styles.
This commit is contained in:
@@ -39,7 +39,7 @@ export async function closeDatabaseConnection(): Promise<void> {
|
||||
|
||||
// Fonction utilitaire pour les transactions
|
||||
export async function withTransaction<T>(
|
||||
callback: (tx: PrismaClient) => Promise<T>
|
||||
callback: (tx: Omit<PrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$extends'>) => Promise<T>
|
||||
): Promise<T> {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
return await callback(tx);
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { MacOSReminder } from '@/lib/types';
|
||||
import { getTargetRemindersList, getEnabledRemindersLists, isListEnabled, DEBUG_CONFIG } from '@/lib/config';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
/**
|
||||
* Service pour récupérer les rappels macOS via AppleScript
|
||||
* Approche sécurisée qui utilise l'API officielle d'Apple
|
||||
*/
|
||||
export class RemindersService {
|
||||
|
||||
/**
|
||||
* Récupère tous les rappels depuis l'app Rappels macOS
|
||||
* Utilise la configuration pour filtrer les listes autorisées
|
||||
*/
|
||||
async getAllReminders(): Promise<MacOSReminder[]> {
|
||||
try {
|
||||
if (DEBUG_CONFIG.mockData) {
|
||||
console.log('🔧 Mode mock activé - utilisation des données de test');
|
||||
return this.getMockReminders();
|
||||
}
|
||||
|
||||
// Récupérer uniquement les listes autorisées
|
||||
return await this.getRemindersFromEnabledLists();
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération des rappels:', error);
|
||||
return this.getMockReminders();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les rappels uniquement des listes autorisées en configuration
|
||||
*/
|
||||
async getRemindersFromEnabledLists(): Promise<MacOSReminder[]> {
|
||||
try {
|
||||
const reminders: MacOSReminder[] = [];
|
||||
const enabledLists = getEnabledRemindersLists();
|
||||
|
||||
console.log(`📋 Synchronisation des listes autorisées: ${enabledLists.join(', ')}`);
|
||||
|
||||
for (const listName of enabledLists) {
|
||||
try {
|
||||
console.log(`🔄 Traitement de la liste: ${listName}`);
|
||||
const listReminders = await this.getRemindersFromListSimple(listName);
|
||||
|
||||
if (listReminders.length > 0) {
|
||||
console.log(`✅ ${listReminders.length} rappels trouvés dans "${listName}"`);
|
||||
reminders.push(...listReminders);
|
||||
} else {
|
||||
console.log(`ℹ️ Aucun rappel dans "${listName}"`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Erreur pour la liste ${listName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📊 Total: ${reminders.length} rappels récupérés`);
|
||||
return reminders;
|
||||
} catch (error) {
|
||||
console.error('Erreur getRemindersFromEnabledLists:', error);
|
||||
return this.getMockReminders();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les rappels de la liste cible principale uniquement
|
||||
*/
|
||||
async getTargetListReminders(): Promise<MacOSReminder[]> {
|
||||
try {
|
||||
const targetList = getTargetRemindersList();
|
||||
console.log(`🎯 Récupération de la liste cible: ${targetList}`);
|
||||
|
||||
return await this.getRemindersFromListSimple(targetList);
|
||||
} catch (error) {
|
||||
console.error('Erreur getTargetListReminders:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les rappels d'une liste spécifique
|
||||
*/
|
||||
async getRemindersByList(listName: string): Promise<MacOSReminder[]> {
|
||||
try {
|
||||
const script = `
|
||||
tell application "Reminders"
|
||||
set remindersList to {}
|
||||
set targetList to list "${listName}"
|
||||
|
||||
repeat with reminder in reminders of targetList
|
||||
set reminderRecord to {id:(id of reminder as string), title:(name of reminder), notes:(body of reminder), completed:(completed of reminder), dueDate:missing value, completionDate:missing value, priority:(priority of reminder), list:"${listName}"}
|
||||
|
||||
-- Gérer la date d'échéance
|
||||
try
|
||||
set dueDate of reminderRecord to (due date of reminder as string)
|
||||
end try
|
||||
|
||||
-- Gérer la date de completion
|
||||
try
|
||||
if completed of reminder then
|
||||
set completionDate of reminderRecord to (completion date of reminder as string)
|
||||
end if
|
||||
end try
|
||||
|
||||
set end of remindersList to reminderRecord
|
||||
end repeat
|
||||
|
||||
return remindersList
|
||||
end tell
|
||||
`;
|
||||
|
||||
const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}' 2>/dev/null || echo "[]"`);
|
||||
|
||||
return this.parseAppleScriptOutput(stdout);
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors de la récupération des rappels de la liste ${listName}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la liste des listes de rappels
|
||||
*/
|
||||
async getReminderLists(): Promise<string[]> {
|
||||
try {
|
||||
const script = `
|
||||
tell application "Reminders"
|
||||
set listNames to {}
|
||||
repeat with reminderList in lists
|
||||
set end of listNames to name of reminderList
|
||||
end repeat
|
||||
return listNames
|
||||
end tell
|
||||
`;
|
||||
|
||||
const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}' 2>/dev/null || echo ""`);
|
||||
|
||||
// Parse la sortie AppleScript pour extraire les noms de listes
|
||||
const lists = stdout.trim().split(', ').filter(list => list.length > 0);
|
||||
return lists;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération des listes:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test si l'app Rappels est accessible
|
||||
*/
|
||||
async testRemindersAccess(): Promise<boolean> {
|
||||
try {
|
||||
const script = `
|
||||
tell application "Reminders"
|
||||
return count of lists
|
||||
end tell
|
||||
`;
|
||||
|
||||
await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}' 2>/dev/null`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Impossible d\'accéder à l\'app Rappels:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse la sortie AppleScript en objets MacOSReminder
|
||||
*/
|
||||
private parseAppleScriptOutput(output: string): MacOSReminder[] {
|
||||
try {
|
||||
console.log('Sortie AppleScript brute:', output);
|
||||
|
||||
// Si pas de sortie ou sortie vide, retourner tableau vide
|
||||
if (!output || output.trim() === '' || output.trim() === '{}') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Pour l'instant, on utilise une approche simple avec des données réelles
|
||||
// TODO: Implémenter le parsing complet de la sortie AppleScript
|
||||
return this.getRemindersFromEnabledLists();
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du parsing AppleScript:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Récupère les rappels d'une liste avec une approche simple
|
||||
*/
|
||||
private async getRemindersFromListSimple(listName: string): Promise<MacOSReminder[]> {
|
||||
try {
|
||||
if (DEBUG_CONFIG.verboseLogging) {
|
||||
console.log(`🔍 Récupération des rappels de la liste: ${listName}`);
|
||||
}
|
||||
|
||||
// Script simple pour récupérer les infos de base
|
||||
const script = `
|
||||
tell application "Reminders"
|
||||
set remindersList to {}
|
||||
try
|
||||
set targetList to (first list whose name is "${listName}")
|
||||
|
||||
repeat with r in reminders of targetList
|
||||
try
|
||||
set reminderInfo to (name of r) & "|" & (completed of r) & "|" & (priority of r) & "|" & "${listName}"
|
||||
set end of remindersList to reminderInfo
|
||||
end try
|
||||
end repeat
|
||||
on error errMsg
|
||||
return "ERROR: " & errMsg
|
||||
end try
|
||||
|
||||
return remindersList
|
||||
end tell
|
||||
`;
|
||||
|
||||
const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}' 2>/dev/null || echo ""`);
|
||||
|
||||
if (DEBUG_CONFIG.logAppleScript) {
|
||||
console.log(`📝 Sortie AppleScript pour ${listName}:`, stdout.substring(0, 200));
|
||||
}
|
||||
|
||||
// Vérifier si il y a une erreur dans la sortie
|
||||
if (stdout.includes('ERROR:')) {
|
||||
console.error(`❌ Erreur AppleScript pour ${listName}:`, stdout);
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.parseSimpleReminderOutput(stdout, listName);
|
||||
} catch (error) {
|
||||
console.error(`❌ Erreur getRemindersFromListSimple pour ${listName}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse la sortie simple des rappels
|
||||
*/
|
||||
private parseSimpleReminderOutput(output: string, listName: string): MacOSReminder[] {
|
||||
try {
|
||||
if (!output || output.trim() === '') return [];
|
||||
|
||||
// Nettoyer la sortie AppleScript
|
||||
const cleanOutput = output.trim().replace(/^{|}$/g, '');
|
||||
if (!cleanOutput) return [];
|
||||
|
||||
const reminderStrings = cleanOutput.split(', ');
|
||||
const reminders: MacOSReminder[] = [];
|
||||
|
||||
for (let i = 0; i < reminderStrings.length; i++) {
|
||||
const reminderStr = reminderStrings[i].replace(/"/g, '');
|
||||
const parts = reminderStr.split('|');
|
||||
|
||||
if (parts.length >= 4) {
|
||||
const [title, completed, priority, list] = parts;
|
||||
|
||||
reminders.push({
|
||||
id: `${listName}-${i}`,
|
||||
title: title.trim(),
|
||||
completed: completed.trim() === 'true',
|
||||
priority: parseInt(priority.trim()) || 0,
|
||||
list: list.trim(),
|
||||
tags: this.extractTagsFromTitle(title.trim())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return reminders;
|
||||
} catch (error) {
|
||||
console.error('Erreur parseSimpleReminderOutput:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait les tags du titre (format #tag)
|
||||
*/
|
||||
private extractTagsFromTitle(title: string): string[] {
|
||||
const tagRegex = /#(\w+)/g;
|
||||
const tags: string[] = [];
|
||||
let match;
|
||||
|
||||
while ((match = tagRegex.exec(title)) !== null) {
|
||||
tags.push(match[1]);
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Données de test pour le développement
|
||||
*/
|
||||
private getMockReminders(): MacOSReminder[] {
|
||||
return [
|
||||
{
|
||||
id: 'mock-1',
|
||||
title: 'Finir le service reminders',
|
||||
notes: 'Implémenter la récupération des rappels macOS',
|
||||
completed: false,
|
||||
dueDate: new Date('2025-01-16'),
|
||||
priority: 5,
|
||||
list: 'Travail',
|
||||
tags: ['dev', 'backend']
|
||||
},
|
||||
{
|
||||
id: 'mock-2',
|
||||
title: 'Tester l\'intégration Jira',
|
||||
notes: 'Configurer l\'API Jira pour récupérer les tâches',
|
||||
completed: false,
|
||||
dueDate: new Date('2025-01-18'),
|
||||
priority: 9,
|
||||
list: 'Projets',
|
||||
tags: ['jira', 'api']
|
||||
},
|
||||
{
|
||||
id: 'mock-3',
|
||||
title: 'Créer le Kanban board',
|
||||
completed: true,
|
||||
completionDate: new Date('2025-01-10'),
|
||||
priority: 5,
|
||||
list: 'Travail',
|
||||
tags: ['ui', 'frontend']
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Instance singleton
|
||||
export const remindersService = new RemindersService();
|
||||
@@ -1,310 +0,0 @@
|
||||
import { prisma } from './database';
|
||||
import { remindersService } from './reminders';
|
||||
import { Task, TaskStatus, TaskPriority, MacOSReminder, SyncLog, BusinessError } from '@/lib/types';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* Service pour traiter et synchroniser les tâches
|
||||
* Contient toute la logique métier pour les tâches
|
||||
*/
|
||||
export class TaskProcessorService {
|
||||
|
||||
/**
|
||||
* Synchronise les rappels macOS avec la base de données
|
||||
*/
|
||||
async syncRemindersToDatabase(): Promise<SyncLog> {
|
||||
const startTime = Date.now();
|
||||
let tasksSync = 0;
|
||||
|
||||
try {
|
||||
// Récupérer les rappels depuis macOS
|
||||
const reminders = await remindersService.getAllReminders();
|
||||
|
||||
// Traiter chaque rappel
|
||||
for (const reminder of reminders) {
|
||||
await this.processReminder(reminder);
|
||||
tasksSync++;
|
||||
}
|
||||
|
||||
// Créer le log de synchronisation
|
||||
const syncLog = await prisma.syncLog.create({
|
||||
data: {
|
||||
source: 'reminders',
|
||||
status: 'success',
|
||||
message: `Synchronisé ${tasksSync} rappels en ${Date.now() - startTime}ms`,
|
||||
tasksSync
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`✅ Sync reminders terminée: ${tasksSync} tâches`);
|
||||
return syncLog;
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue';
|
||||
|
||||
const syncLog = await prisma.syncLog.create({
|
||||
data: {
|
||||
source: 'reminders',
|
||||
status: 'error',
|
||||
message: `Erreur de sync: ${errorMessage}`,
|
||||
tasksSync
|
||||
}
|
||||
});
|
||||
|
||||
console.error('❌ Erreur sync reminders:', error);
|
||||
return syncLog;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traite un rappel macOS et le sauvegarde/met à jour en base
|
||||
*/
|
||||
private async processReminder(reminder: MacOSReminder): Promise<void> {
|
||||
const taskData = this.mapReminderToTask(reminder);
|
||||
|
||||
try {
|
||||
// Upsert (insert ou update) de la tâche
|
||||
await prisma.task.upsert({
|
||||
where: {
|
||||
source_sourceId: {
|
||||
source: 'reminders',
|
||||
sourceId: reminder.id
|
||||
}
|
||||
},
|
||||
update: {
|
||||
title: taskData.title,
|
||||
description: taskData.description,
|
||||
status: taskData.status,
|
||||
priority: taskData.priority,
|
||||
tagsJson: JSON.stringify(taskData.tags || []),
|
||||
dueDate: taskData.dueDate,
|
||||
completedAt: taskData.completedAt,
|
||||
updatedAt: new Date()
|
||||
},
|
||||
create: {
|
||||
title: taskData.title,
|
||||
description: taskData.description,
|
||||
status: taskData.status,
|
||||
priority: taskData.priority,
|
||||
source: 'reminders',
|
||||
sourceId: reminder.id,
|
||||
tagsJson: JSON.stringify(taskData.tags || []),
|
||||
dueDate: taskData.dueDate,
|
||||
completedAt: taskData.completedAt
|
||||
}
|
||||
});
|
||||
|
||||
// Gérer les tags
|
||||
if (taskData.tags && taskData.tags.length > 0) {
|
||||
await this.processTags(taskData.tags);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors du traitement du rappel ${reminder.id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un rappel macOS en objet Task
|
||||
*/
|
||||
private mapReminderToTask(reminder: MacOSReminder): Partial<Task> {
|
||||
return {
|
||||
title: reminder.title,
|
||||
description: reminder.notes || undefined,
|
||||
status: this.mapReminderStatus(reminder),
|
||||
priority: this.mapReminderPriority(reminder.priority),
|
||||
tags: reminder.tags || [],
|
||||
dueDate: reminder.dueDate || undefined,
|
||||
completedAt: reminder.completionDate || undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit le statut d'un rappel macOS en TaskStatus
|
||||
*/
|
||||
private mapReminderStatus(reminder: MacOSReminder): TaskStatus {
|
||||
if (reminder.completed) {
|
||||
return 'done';
|
||||
}
|
||||
|
||||
// Si la tâche a une date d'échéance passée, elle est en retard
|
||||
if (reminder.dueDate && reminder.dueDate < new Date()) {
|
||||
return 'todo'; // On garde 'todo' mais on pourrait ajouter un statut 'overdue'
|
||||
}
|
||||
|
||||
return 'todo';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit la priorité macOS (0-9) en TaskPriority
|
||||
*/
|
||||
private mapReminderPriority(macosPriority: number): TaskPriority {
|
||||
switch (macosPriority) {
|
||||
case 0: return 'low';
|
||||
case 1: return 'low';
|
||||
case 5: return 'medium';
|
||||
case 9: return 'high';
|
||||
default: return 'medium';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traite et crée les tags s'ils n'existent pas
|
||||
*/
|
||||
private async processTags(tagNames: string[]): Promise<void> {
|
||||
for (const tagName of tagNames) {
|
||||
try {
|
||||
await prisma.tag.upsert({
|
||||
where: { name: tagName },
|
||||
update: {}, // Pas de mise à jour nécessaire
|
||||
create: {
|
||||
name: tagName,
|
||||
color: this.generateTagColor(tagName)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors de la création du tag ${tagName}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les tâches avec filtres optionnels
|
||||
*/
|
||||
async getTasks(filters?: {
|
||||
status?: TaskStatus[];
|
||||
source?: string[];
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<Task[]> {
|
||||
const where: Prisma.TaskWhereInput = {};
|
||||
|
||||
if (filters?.status) {
|
||||
where.status = { in: filters.status };
|
||||
}
|
||||
|
||||
if (filters?.source) {
|
||||
where.source = { in: filters.source };
|
||||
}
|
||||
|
||||
if (filters?.search) {
|
||||
where.OR = [
|
||||
{ title: { contains: filters.search, mode: 'insensitive' } },
|
||||
{ description: { contains: filters.search, mode: 'insensitive' } }
|
||||
];
|
||||
}
|
||||
|
||||
const tasks = await prisma.task.findMany({
|
||||
where,
|
||||
take: filters?.limit || 100,
|
||||
skip: filters?.offset || 0,
|
||||
orderBy: [
|
||||
{ completedAt: 'desc' },
|
||||
{ dueDate: 'asc' },
|
||||
{ createdAt: 'desc' }
|
||||
]
|
||||
});
|
||||
|
||||
return tasks.map(this.mapPrismaTaskToTask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le statut d'une tâche
|
||||
*/
|
||||
async updateTaskStatus(taskId: string, newStatus: TaskStatus): 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 = {
|
||||
status: newStatus,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
if (newStatus === 'done' && !task.completedAt) {
|
||||
updateData.completedAt = new Date();
|
||||
} else if (newStatus !== 'done' && task.completedAt) {
|
||||
updateData.completedAt = null;
|
||||
}
|
||||
|
||||
const updatedTask = await prisma.task.update({
|
||||
where: { id: taskId },
|
||||
data: updateData
|
||||
});
|
||||
|
||||
return this.mapPrismaTaskToTask(updatedTask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une tâche Prisma en objet Task
|
||||
*/
|
||||
private mapPrismaTaskToTask(prismaTask: any): Task {
|
||||
return {
|
||||
id: prismaTask.id,
|
||||
title: prismaTask.title,
|
||||
description: prismaTask.description,
|
||||
status: prismaTask.status as TaskStatus,
|
||||
priority: prismaTask.priority as TaskPriority,
|
||||
source: prismaTask.source,
|
||||
sourceId: prismaTask.sourceId,
|
||||
tags: JSON.parse(prismaTask.tagsJson || '[]'),
|
||||
dueDate: prismaTask.dueDate,
|
||||
completedAt: prismaTask.completedAt,
|
||||
createdAt: prismaTask.createdAt,
|
||||
updatedAt: prismaTask.updatedAt,
|
||||
jiraProject: prismaTask.jiraProject,
|
||||
jiraKey: prismaTask.jiraKey,
|
||||
assignee: prismaTask.assignee
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les statistiques des tâches
|
||||
*/
|
||||
async getTaskStats() {
|
||||
const [total, completed, inProgress, todo] = 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' } })
|
||||
]);
|
||||
|
||||
return {
|
||||
total,
|
||||
completed,
|
||||
inProgress,
|
||||
todo,
|
||||
completionRate: total > 0 ? Math.round((completed / total) * 100) : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Instance singleton
|
||||
export const taskProcessorService = new TaskProcessorService();
|
||||
237
services/tasks.ts
Normal file
237
services/tasks.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { prisma } from './database';
|
||||
import { Task, TaskStatus, TaskPriority, TaskSource, BusinessError } from '@/lib/types';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
/**
|
||||
* 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,
|
||||
take: filters?.limit || 100,
|
||||
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',
|
||||
tagsJson: JSON.stringify(taskData.tags || []),
|
||||
dueDate: taskData.dueDate,
|
||||
source: 'manual', // Source manuelle
|
||||
sourceId: `manual-${Date.now()}` // ID unique
|
||||
}
|
||||
});
|
||||
|
||||
// Gérer les tags
|
||||
if (taskData.tags && taskData.tags.length > 0) {
|
||||
await this.processTags(taskData.tags);
|
||||
}
|
||||
|
||||
return this.mapPrismaTaskToTask(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = {
|
||||
...updates,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
if (updates.tags) {
|
||||
updateData.tagsJson = JSON.stringify(updates.tags);
|
||||
}
|
||||
|
||||
if (updates.status === 'done' && !task.completedAt) {
|
||||
updateData.completedAt = new Date();
|
||||
} else if (updates.status && updates.status !== 'done' && task.completedAt) {
|
||||
updateData.completedAt = null;
|
||||
}
|
||||
|
||||
const updatedTask = await prisma.task.update({
|
||||
where: { id: taskId },
|
||||
data: updateData
|
||||
});
|
||||
|
||||
// Gérer les tags
|
||||
if (updates.tags && updates.tags.length > 0) {
|
||||
await this.processTags(updates.tags);
|
||||
}
|
||||
|
||||
return this.mapPrismaTaskToTask(updatedTask);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 statistiques des tâches
|
||||
*/
|
||||
async getTaskStats() {
|
||||
const [total, completed, inProgress, todo, cancelled] = 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: 'cancelled' } })
|
||||
]);
|
||||
|
||||
return {
|
||||
total,
|
||||
completed,
|
||||
inProgress,
|
||||
todo,
|
||||
cancelled,
|
||||
completionRate: total > 0 ? Math.round((completed / total) * 100) : 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Traite et crée les tags s'ils n'existent pas
|
||||
*/
|
||||
private async processTags(tagNames: string[]): Promise<void> {
|
||||
for (const tagName of tagNames) {
|
||||
try {
|
||||
await prisma.tag.upsert({
|
||||
where: { name: tagName },
|
||||
update: {}, // Pas de mise à jour nécessaire
|
||||
create: {
|
||||
name: tagName,
|
||||
color: this.generateTagColor(tagName)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors de la création du tag ${tagName}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<object>): Task {
|
||||
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: JSON.parse(prismaTask.tagsJson || '[]'),
|
||||
dueDate: prismaTask.dueDate ?? undefined,
|
||||
completedAt: prismaTask.completedAt ?? undefined,
|
||||
createdAt: prismaTask.createdAt,
|
||||
updatedAt: prismaTask.updatedAt,
|
||||
jiraProject: prismaTask.jiraProject ?? undefined,
|
||||
jiraKey: prismaTask.jiraKey ?? undefined,
|
||||
assignee: prismaTask.assignee ?? undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Instance singleton
|
||||
export const tasksService = new TasksService();
|
||||
Reference in New Issue
Block a user