NeahStable/hooks/use-task-notifications.ts
2026-01-16 10:11:35 +01:00

274 lines
9.9 KiB
TypeScript

import { useState, useEffect, useRef } from 'react';
import { CheckSquare } from 'lucide-react';
import { OutlookNotificationData } from '@/components/outlook-notification';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
interface Task {
id: string;
headline: string;
dateToFinish: string | null;
source: 'leantime' | 'twenty-crm';
projectName: string;
url?: string;
}
/**
* Hook to manage task notifications and show Outlook-style notifications
* - Leantime tasks: notification at due date time
* - Twenty CRM tasks: notification at start date time (or due date if no start date)
*/
export function useTaskNotifications() {
const [taskNotification, setTaskNotification] = useState<OutlookNotificationData | null>(null);
const notifiedTaskIdsRef = useRef<Set<string>>(new Set());
const tasksRef = useRef<Task[]>([]);
const checkIntervalRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
console.log('[useTaskNotifications] 🎧 Hook initialized, listening for tasks-updated');
// Listen for tasks updates
const handleTasksUpdate = (event: CustomEvent) => {
const tasks = event.detail?.tasks || [];
console.log('[useTaskNotifications] 📋 Received tasks update', {
tasksCount: tasks.length,
tasks: tasks.map((t: any) => ({
id: t.id,
title: t.headline,
dateToFinish: t.dateToFinish,
source: t.source,
})),
});
if (!tasks || tasks.length === 0) {
tasksRef.current = [];
return;
}
// Convert tasks to Task format
const formattedTasks: Task[] = tasks.map((task: any) => ({
id: task.id?.toString() || '',
headline: task.headline || task.title || '',
dateToFinish: task.dateToFinish || null,
source: task.source || 'leantime',
projectName: task.projectName || '',
url: task.url || null,
}));
tasksRef.current = formattedTasks;
console.log('[useTaskNotifications] Tasks stored', {
count: formattedTasks.length,
tasks: formattedTasks.map(t => ({
id: t.id,
title: t.headline,
dateToFinish: t.dateToFinish,
source: t.source,
})),
});
};
window.addEventListener('tasks-updated', handleTasksUpdate as EventListener);
// Check for tasks that are due/starting now (every 10 seconds)
const checkForDueTasks = () => {
const now = new Date();
const currentTime = now.getTime();
console.log('[useTaskNotifications] 🔍 Checking for due tasks', {
now: now.toISOString(),
tasksCount: tasksRef.current.length,
notifiedCount: notifiedTaskIdsRef.current.size,
});
const dueTasks = tasksRef.current.filter((task) => {
// Skip if already notified
if (notifiedTaskIdsRef.current.has(task.id)) {
console.log('[useTaskNotifications] ⏭️ Task already notified', {
id: task.id,
title: task.headline,
});
return false;
}
// 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;
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;
}
const notificationTime = notificationDate.getTime();
const timeUntilNotification = notificationTime - currentTime;
const timeUntilNotificationMinutes = Math.round(timeUntilNotification / 1000 / 60);
const timeUntilNotificationSeconds = Math.round(timeUntilNotification / 1000);
// Notification window: within 2 minutes of the notification time (1 minute before to 1 minute after)
// This allows catching tasks that are due now or just became due
const notificationWindow = 60 * 1000; // 1 minute
const inNotificationWindow =
timeUntilNotification >= -notificationWindow &&
timeUntilNotification <= notificationWindow;
console.log('[useTaskNotifications] ⏰ Checking task', {
id: task.id,
title: task.headline,
source: task.source,
rawDate: task.dateToFinish,
notificationDateLocal: notificationDate.toLocaleString('fr-FR'),
notificationDateISO: notificationDate.toISOString(),
nowLocal: now.toLocaleString('fr-FR'),
nowISO: now.toISOString(),
timeUntilNotificationMinutes,
timeUntilNotificationSeconds,
inWindow: inNotificationWindow,
});
return inNotificationWindow;
});
if (dueTasks.length > 0) {
// Sort by notification time (earliest first)
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;
});
// Show notification for the first task
const task = dueTasks[0];
const notificationDate = task.dateToFinish
? new Date(task.dateToFinish)
: new Date();
const timeStr = format(notificationDate, 'HH:mm', { locale: fr });
const sourceLabel = task.source === 'twenty-crm' ? 'Médiation' : 'Agilité';
console.log('[useTaskNotifications] ✅ Task due detected!', {
id: task.id,
title: task.headline,
source: task.source,
notificationDate: notificationDate.toISOString(),
now: now.toISOString(),
});
const notification: OutlookNotificationData = {
id: `task-${task.id}-${Date.now()}`,
source: 'leantime',
title: 'Devoirs',
subtitle: task.source === 'twenty-crm' ? 'Tâche Médiation' : 'Tâche Agilité',
message: `${task.headline} - Échéance à ${timeStr} (${sourceLabel})`,
icon: CheckSquare,
iconColor: 'text-blue-600',
iconBgColor: 'bg-blue-100',
borderColor: 'border-blue-500',
link: task.url || '/agilite',
timestamp: notificationDate,
autoDismiss: 30000, // 30 seconds
actions: [
{
label: 'Ouvrir',
onClick: () => {
if (task.url) {
window.open(task.url, '_blank');
} else {
window.location.href = '/agilite';
}
},
variant: 'default',
className: 'bg-blue-600 hover:bg-blue-700 text-white',
},
],
};
setTaskNotification(notification);
notifiedTaskIdsRef.current.add(task.id);
// Clean up old notified tasks (older than 1 hour) to allow re-notification if needed
setTimeout(() => {
notifiedTaskIdsRef.current.delete(task.id);
}, 60 * 60 * 1000); // 1 hour
}
};
// Check immediately and then every 10 seconds for more responsive notifications
checkForDueTasks();
checkIntervalRef.current = setInterval(checkForDueTasks, 10000); // Every 10 seconds
return () => {
window.removeEventListener('tasks-updated', handleTasksUpdate as EventListener);
if (checkIntervalRef.current) {
clearInterval(checkIntervalRef.current);
}
};
}, []);
const handleDismiss = () => {
setTaskNotification(null);
};
return {
taskNotification,
setTaskNotification: handleDismiss,
};
}