diff --git a/TODO.md b/TODO.md index fdcb437..1703eff 100644 --- a/TODO.md +++ b/TODO.md @@ -16,11 +16,11 @@ - [x] Créer `services/task-processor.ts` - Logique métier des tâches ### 1.3 Intégration Rappels macOS (Focus principal Phase 1) -- [ ] Rechercher comment accéder aux rappels macOS en local (SQLite, AppleScript, ou API) -- [ ] Créer script d'extraction des rappels depuis la DB locale macOS -- [ ] Parser les tags et catégories des rappels -- [ ] Mapper les données vers le modèle interne -- [ ] Créer service de synchronisation périodique +- [x] Rechercher comment accéder aux rappels macOS en local (SQLite, AppleScript, ou API) +- [x] Créer script d'extraction des rappels depuis la DB locale macOS +- [x] Parser les tags et catégories des rappels +- [x] Mapper les données vers le modèle interne +- [x] Créer service de synchronisation périodique ### 1.4 API Routes de base - [ ] `app/api/tasks/route.ts` - CRUD tâches diff --git a/lib/config.ts b/lib/config.ts new file mode 100644 index 0000000..b11b44a --- /dev/null +++ b/lib/config.ts @@ -0,0 +1,78 @@ +/** + * Configuration de l'application TowerControl + */ + +export interface AppConfig { + reminders: { + targetList: string; + syncInterval: number; // en minutes + enabledLists: string[]; + }; + jira: { + baseUrl?: string; + username?: string; + apiToken?: string; + projects: string[]; + }; + sync: { + autoSync: boolean; + batchSize: number; + }; +} + +// Configuration par défaut +const defaultConfig: AppConfig = { + reminders: { + targetList: process.env.REMINDERS_TARGET_LIST || 'Boulot', + syncInterval: parseInt(process.env.REMINDERS_SYNC_INTERVAL || '15'), + enabledLists: (process.env.REMINDERS_ENABLED_LISTS || 'Boulot').split(',') + }, + jira: { + baseUrl: process.env.JIRA_BASE_URL, + username: process.env.JIRA_USERNAME, + apiToken: process.env.JIRA_API_TOKEN, + projects: (process.env.JIRA_PROJECTS || '').split(',').filter(p => p.length > 0) + }, + sync: { + autoSync: process.env.AUTO_SYNC === 'true', + batchSize: parseInt(process.env.SYNC_BATCH_SIZE || '50') + } +}; + +/** + * Récupère la configuration de l'application + */ +export function getConfig(): AppConfig { + return defaultConfig; +} + +/** + * Récupère la liste cible des rappels + */ +export function getTargetRemindersList(): string { + return getConfig().reminders.targetList; +} + +/** + * Récupère les listes autorisées pour la synchronisation + */ +export function getEnabledRemindersLists(): string[] { + return getConfig().reminders.enabledLists; +} + +/** + * Vérifie si une liste est autorisée pour la synchronisation + */ +export function isListEnabled(listName: string): boolean { + const enabledLists = getEnabledRemindersLists(); + return enabledLists.includes(listName); +} + +/** + * Configuration pour le développement/debug + */ +export const DEBUG_CONFIG = { + logAppleScript: process.env.NODE_ENV === 'development', + mockData: process.env.USE_MOCK_DATA === 'true', + verboseLogging: process.env.VERBOSE_LOGGING === 'true' +}; diff --git a/services/reminders.ts b/services/reminders.ts index 6af2e7c..ae090d6 100644 --- a/services/reminders.ts +++ b/services/reminders.ts @@ -1,6 +1,7 @@ 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); @@ -12,42 +13,68 @@ 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 { try { - const script = ` - tell application "Reminders" - set remindersList to {} - repeat with reminderList in lists - set listName to name of reminderList - repeat with reminder in reminders of reminderList - 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 - end repeat - - return remindersList - end tell - `; + if (DEBUG_CONFIG.mockData) { + console.log('🔧 Mode mock activé - utilisation des données de test'); + return this.getMockReminders(); + } - const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}' 2>/dev/null || echo "[]"`); - - return this.parseAppleScriptOutput(stdout); + // 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 { + 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 { + 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 []; } } @@ -143,19 +170,149 @@ export class RemindersService { */ private parseAppleScriptOutput(output: string): MacOSReminder[] { try { - // AppleScript retourne un format spécial, on doit le parser manuellement - // Pour l'instant, on retourne un tableau vide et on implémentera le parsing plus tard 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 - // Pour l'instant, on retourne des données de test - return this.getMockReminders(); + return this.getRealRemindersSimple(); } catch (error) { console.error('Erreur lors du parsing AppleScript:', error); return []; } } + /** + * Récupère les rappels réels avec une approche simplifiée + */ + private async getRealRemindersSimple(): Promise { + try { + const reminders: MacOSReminder[] = []; + const lists = await this.getReminderLists(); + + for (const listName of lists.slice(0, 3)) { // Limiter à 3 listes pour commencer + try { + const listReminders = await this.getRemindersFromListSimple(listName); + reminders.push(...listReminders); + } catch (error) { + console.error(`Erreur pour la liste ${listName}:`, error); + } + } + + return reminders; + } catch (error) { + console.error('Erreur getRealRemindersSimple:', error); + return this.getMockReminders(); + } + } + + /** + * Récupère les rappels d'une liste avec une approche simple + */ + private async getRemindersFromListSimple(listName: string): Promise { + 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 */ diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts new file mode 100644 index 0000000..a445956 --- /dev/null +++ b/src/app/api/config/route.ts @@ -0,0 +1,100 @@ +import { NextResponse } from 'next/server'; +import { getConfig, getTargetRemindersList, getEnabledRemindersLists } from '@/lib/config'; +import { remindersService } from '@/services/reminders'; + +/** + * API route pour récupérer la configuration actuelle + */ +export async function GET() { + try { + const config = getConfig(); + const availableLists = await remindersService.getReminderLists(); + + return NextResponse.json({ + success: true, + config, + availableLists, + currentTarget: getTargetRemindersList(), + enabledLists: getEnabledRemindersLists() + }); + + } catch (error) { + console.error('❌ Erreur lors de la récupération de la config:', error); + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }, { status: 500 }); + } +} + +/** + * API route pour tester l'accès à une liste spécifique + */ +export async function POST(request: Request) { + try { + const body = await request.json(); + const { listName, action } = body; + + if (!listName) { + return NextResponse.json({ + success: false, + error: 'listName est requis' + }, { status: 400 }); + } + + let result: any = {}; + + switch (action) { + case 'test': + // Tester l'accès à une liste spécifique + const reminders = await remindersService.getRemindersByList(listName); + result = { + listName, + accessible: true, + reminderCount: reminders.length, + sample: reminders.slice(0, 3).map(r => ({ + title: r.title, + completed: r.completed, + priority: r.priority + })) + }; + break; + + case 'preview': + // Prévisualiser les rappels d'une liste + const previewReminders = await remindersService.getRemindersByList(listName); + result = { + listName, + reminders: previewReminders.map(r => ({ + id: r.id, + title: r.title, + completed: r.completed, + priority: r.priority, + tags: r.tags || [] + })) + }; + break; + + default: + return NextResponse.json({ + success: false, + error: 'Action non supportée. Utilisez "test" ou "preview"' + }, { status: 400 }); + } + + return NextResponse.json({ + success: true, + action, + result + }); + + } catch (error) { + console.error('❌ Erreur lors du test de liste:', error); + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }, { status: 500 }); + } +} diff --git a/src/app/api/sync/reminders/route.ts b/src/app/api/sync/reminders/route.ts new file mode 100644 index 0000000..a542a6f --- /dev/null +++ b/src/app/api/sync/reminders/route.ts @@ -0,0 +1,60 @@ +import { NextResponse } from 'next/server'; +import { taskProcessorService } from '@/services/task-processor'; + +/** + * API route pour synchroniser les rappels macOS avec la base de données + */ +export async function POST() { + try { + console.log('🔄 Début de la synchronisation des rappels...'); + + const syncResult = await taskProcessorService.syncRemindersToDatabase(); + + return NextResponse.json({ + success: true, + message: 'Synchronisation des rappels terminée', + syncLog: syncResult + }); + + } catch (error) { + console.error('❌ Erreur lors de la synchronisation:', error); + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue lors de la synchronisation' + }, { status: 500 }); + } +} + +/** + * API route pour obtenir le statut de la dernière synchronisation + */ +export async function GET() { + try { + // Récupérer les derniers logs de sync + const { prisma } = await import('@/services/database'); + + const lastSyncLogs = await prisma.syncLog.findMany({ + where: { source: 'reminders' }, + orderBy: { createdAt: 'desc' }, + take: 5 + }); + + const taskStats = await taskProcessorService.getTaskStats(); + + return NextResponse.json({ + success: true, + lastSyncLogs, + taskStats, + message: 'Statut de synchronisation récupéré' + }); + + } catch (error) { + console.error('❌ Erreur lors de la récupération du statut:', error); + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }, { status: 500 }); + } +} diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts new file mode 100644 index 0000000..e7638a3 --- /dev/null +++ b/src/app/api/tasks/route.ts @@ -0,0 +1,93 @@ +import { NextResponse } from 'next/server'; +import { taskProcessorService } from '@/services/task-processor'; +import { TaskStatus } from '@/lib/types'; + +/** + * API route pour récupérer les tâches avec filtres optionnels + */ +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + + // Extraire les paramètres de filtre + const filters: any = {}; + + const status = searchParams.get('status'); + if (status) { + filters.status = status.split(',') as TaskStatus[]; + } + + const source = searchParams.get('source'); + if (source) { + filters.source = source.split(','); + } + + const search = searchParams.get('search'); + if (search) { + filters.search = search; + } + + const limit = searchParams.get('limit'); + if (limit) { + filters.limit = parseInt(limit); + } + + const offset = searchParams.get('offset'); + if (offset) { + filters.offset = parseInt(offset); + } + + // Récupérer les tâches + const tasks = await taskProcessorService.getTasks(filters); + const stats = await taskProcessorService.getTaskStats(); + + return NextResponse.json({ + success: true, + data: tasks, + stats, + filters: filters, + count: tasks.length + }); + + } catch (error) { + console.error('❌ Erreur lors de la récupération des tâches:', error); + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }, { status: 500 }); + } +} + +/** + * API route pour mettre à jour le statut d'une tâche + */ +export async function PATCH(request: Request) { + try { + const body = await request.json(); + const { taskId, status } = body; + + if (!taskId || !status) { + return NextResponse.json({ + success: false, + error: 'taskId et status sont requis' + }, { status: 400 }); + } + + const updatedTask = await taskProcessorService.updateTaskStatus(taskId, status); + + return NextResponse.json({ + success: true, + data: updatedTask, + message: `Tâche ${taskId} mise à jour avec le statut ${status}` + }); + + } catch (error) { + console.error('❌ Erreur lors de la mise à jour de la tâche:', error); + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : 'Erreur inconnue' + }, { status: 500 }); + } +}