Files
towercontrol/services/task-processor.ts
2025-09-13 09:15:31 +02:00

311 lines
8.5 KiB
TypeScript

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();