diff --git a/hooks/use-task-notifications.ts b/hooks/use-task-notifications.ts index 69e9a18..63f33d3 100644 --- a/hooks/use-task-notifications.ts +++ b/hooks/use-task-notifications.ts @@ -81,79 +81,66 @@ export function useTaskNotifications() { notifiedCount: notifiedTaskIdsRef.current.size, }); - const dueTasks = tasksRef.current.filter((task) => { + // Helper function to parse date correctly (handles Leantime dates with Z) + const parseTaskDate = (dateStr: string | null): Date | null => { + if (!dateStr) return null; + + // Leantime dates with 'Z' are actually local time stored as UTC + // We need to extract the time components and create a local date + if (dateStr.endsWith('Z')) { + // Parse as UTC first to get the components, then create local date + // Example: "2026-01-16T01:13:00.000Z" -> treat 01:13 as local time, not UTC + const utcDate = new Date(dateStr); + // Extract components from UTC date and create local date + // This assumes the UTC date actually represents local time + return new Date( + utcDate.getUTCFullYear(), + utcDate.getUTCMonth(), + utcDate.getUTCDate(), + utcDate.getUTCHours(), + utcDate.getUTCMinutes(), + utcDate.getUTCSeconds(), + utcDate.getUTCMilliseconds() + ); + } else if (dateStr.includes('T')) { + // ISO format without Z - treat as local time + return new Date(dateStr); + } else if (dateStr.includes(' ')) { + // MySQL format: "YYYY-MM-DD HH:MM:SS" - parse as local time + const isoLike = dateStr.replace(' ', 'T'); + return new Date(isoLike); + } else { + // Try direct parsing + return new Date(dateStr); + } + }; + + interface TaskWithParsedDate extends Task { + parsedNotificationDate: Date; + } + + const dueTasksWithDates: TaskWithParsedDate[] = []; + + for (const task of tasksRef.current) { // Skip if already notified if (notifiedTaskIdsRef.current.has(task.id)) { console.log('[useTaskNotifications] ⏭️ Task already notified', { id: task.id, title: task.headline, }); - return false; + continue; } - // For both Leantime and Twenty CRM tasks: notification at due date time (dateToFinish) - // Note: Leantime dates might be in MySQL format (YYYY-MM-DD HH:MM:SS) without timezone - // We need to parse them correctly to avoid timezone issues - let notificationDate: Date | null = null; + // Parse the notification date + const notificationDate = parseTaskDate(task.dateToFinish); - if (task.dateToFinish) { - const dateStr = task.dateToFinish; - - // Leantime dates with 'Z' are actually local time stored as UTC - // We need to extract the time components and create a local date - if (dateStr.endsWith('Z')) { - // Parse as UTC first to get the components, then create local date - // Example: "2026-01-16T01:13:00.000Z" -> treat 01:13 as local time, not UTC - const utcDate = new Date(dateStr); - // Extract components from UTC date and create local date - // This assumes the UTC date actually represents local time - notificationDate = new Date( - utcDate.getUTCFullYear(), - utcDate.getUTCMonth(), - utcDate.getUTCDate(), - utcDate.getUTCHours(), - utcDate.getUTCMinutes(), - utcDate.getUTCSeconds(), - utcDate.getUTCMilliseconds() - ); - - console.log('[useTaskNotifications] 📅 Parsing Leantime date (Z -> local)', { - id: task.id, - title: task.headline, - rawDate: dateStr, - utcComponents: { - year: utcDate.getUTCFullYear(), - month: utcDate.getUTCMonth(), - day: utcDate.getUTCDate(), - hour: utcDate.getUTCHours(), - minute: utcDate.getUTCMinutes(), - }, - localDate: notificationDate.toLocaleString('fr-FR'), - localISO: notificationDate.toISOString(), - }); - } else if (dateStr.includes('T')) { - // ISO format without Z - treat as local time - notificationDate = new Date(dateStr); - } else if (dateStr.includes(' ')) { - // MySQL format: "YYYY-MM-DD HH:MM:SS" - parse as local time - const isoLike = dateStr.replace(' ', 'T'); - notificationDate = new Date(isoLike); - } else { - // Try direct parsing - notificationDate = new Date(dateStr); - } - - if (!notificationDate || isNaN(notificationDate.getTime())) { - console.error('[useTaskNotifications] ❌ Invalid date', { - id: task.id, - rawDate: dateStr, - }); - return false; - } - } - if (!notificationDate || isNaN(notificationDate.getTime())) { - return false; + console.log('[useTaskNotifications] ⏭️ Task has no valid date', { + id: task.id, + title: task.headline, + dateToFinish: task.dateToFinish, + }); + continue; } const notificationTime = notificationDate.getTime(); @@ -182,23 +169,25 @@ export function useTaskNotifications() { inWindow: inNotificationWindow, }); - return inNotificationWindow; - }); + if (inNotificationWindow) { + dueTasksWithDates.push({ + ...task, + parsedNotificationDate: notificationDate, + }); + } + } + + const dueTasks = dueTasksWithDates; if (dueTasks.length > 0) { - // Sort by notification time (earliest first) + // Sort by notification time (earliest first) using parsed dates dueTasks.sort((a, b) => { - const dateA = a.dateToFinish ? new Date(a.dateToFinish).getTime() : Infinity; - const dateB = b.dateToFinish ? new Date(b.dateToFinish).getTime() : Infinity; - return dateA - dateB; + return a.parsedNotificationDate.getTime() - b.parsedNotificationDate.getTime(); }); // Show notification for the first task const task = dueTasks[0]; - - const notificationDate = task.dateToFinish - ? new Date(task.dateToFinish) - : new Date(); + const notificationDate = task.parsedNotificationDate; const timeStr = format(notificationDate, 'HH:mm', { locale: fr }); const sourceLabel = task.source === 'twenty-crm' ? 'Médiation' : 'Agilité'; @@ -207,8 +196,11 @@ export function useTaskNotifications() { id: task.id, title: task.headline, source: task.source, - notificationDate: notificationDate.toISOString(), - now: now.toISOString(), + rawDate: task.dateToFinish, + parsedNotificationDateLocal: notificationDate.toLocaleString('fr-FR'), + parsedNotificationDateISO: notificationDate.toISOString(), + nowLocal: now.toLocaleString('fr-FR'), + nowISO: now.toISOString(), }); const notification: OutlookNotificationData = {