refactor Notifications agenda
This commit is contained in:
parent
ab6846b3f7
commit
407fb659f2
@ -11,6 +11,7 @@ interface TwentyTask {
|
|||||||
markdown?: string;
|
markdown?: string;
|
||||||
};
|
};
|
||||||
dueAt?: string;
|
dueAt?: string;
|
||||||
|
startAt?: string; // Start date/time for Twenty CRM tasks
|
||||||
status?: string; // e.g., "Done", "Todo", etc.
|
status?: string; // e.g., "Done", "Todo", etc.
|
||||||
type?: string;
|
type?: string;
|
||||||
assigneeId?: string;
|
assigneeId?: string;
|
||||||
@ -172,6 +173,7 @@ async function fetchTwentyTasks(userId?: string): Promise<TwentyTask[]> {
|
|||||||
markdown
|
markdown
|
||||||
}
|
}
|
||||||
dueAt
|
dueAt
|
||||||
|
startAt
|
||||||
status
|
status
|
||||||
assigneeId
|
assigneeId
|
||||||
assignee {
|
assignee {
|
||||||
@ -303,6 +305,7 @@ async function fetchTwentyTasks(userId?: string): Promise<TwentyTask[]> {
|
|||||||
title: node.title || 'Untitled Task',
|
title: node.title || 'Untitled Task',
|
||||||
bodyV2: node.bodyV2 || null,
|
bodyV2: node.bodyV2 || null,
|
||||||
dueAt: node.dueAt || null,
|
dueAt: node.dueAt || null,
|
||||||
|
startAt: node.startAt || null, // Start date for Twenty CRM tasks
|
||||||
status: node.status || null,
|
status: node.status || null,
|
||||||
type: node.type || 'Task',
|
type: node.type || 'Task',
|
||||||
assigneeId: node.assigneeId || null,
|
assigneeId: node.assigneeId || null,
|
||||||
@ -475,6 +478,7 @@ export async function GET(request: NextRequest) {
|
|||||||
headline: task.title,
|
headline: task.title,
|
||||||
description: (task as any)._bodyText || null, // Use extracted body text
|
description: (task as any)._bodyText || null, // Use extracted body text
|
||||||
dateToFinish: task.dueAt || null,
|
dateToFinish: task.dueAt || null,
|
||||||
|
startDate: task.startAt || null, // Start date for Twenty CRM tasks (for notifications)
|
||||||
projectName: 'Médiation',
|
projectName: 'Médiation',
|
||||||
projectId: 0,
|
projectId: 0,
|
||||||
status: task.status === 'Done' ? 5 : 1, // 5 = Done, 1 = New (or other status)
|
status: task.status === 'Done' ? 5 : 1, // 5 = Done, 1 = New (or other status)
|
||||||
|
|||||||
@ -251,6 +251,39 @@ export function Duties() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTasks(sortedTasks);
|
setTasks(sortedTasks);
|
||||||
|
|
||||||
|
// Dispatch event for Outlook-style notifications (when tasks are due/starting)
|
||||||
|
const tasksForNotification = sortedTasks.map(task => ({
|
||||||
|
id: task.id.toString(),
|
||||||
|
headline: task.headline,
|
||||||
|
dateToFinish: task.dateToFinish,
|
||||||
|
source: (task as any).source || 'leantime',
|
||||||
|
projectName: task.projectName,
|
||||||
|
url: (task as any).url || null,
|
||||||
|
startDate: (task as any).startDate || null, // For Twenty CRM tasks
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('[Devoirs Widget] 📋 Dispatching tasks update', {
|
||||||
|
tasksCount: tasksForNotification.length,
|
||||||
|
tasks: tasksForNotification.map(t => ({
|
||||||
|
id: t.id,
|
||||||
|
title: t.headline,
|
||||||
|
dateToFinish: t.dateToFinish,
|
||||||
|
source: t.source,
|
||||||
|
startDate: t.startDate,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(new CustomEvent('tasks-updated', {
|
||||||
|
detail: {
|
||||||
|
tasks: tasksForNotification,
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
console.log('[Devoirs Widget] ✅ Event dispatched successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Devoirs Widget] ❌ Error dispatching event', error);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching tasks:', error);
|
console.error('Error fetching tasks:', error);
|
||||||
setError(error instanceof Error ? error.message : 'Failed to fetch tasks');
|
setError(error instanceof Error ? error.message : 'Failed to fetch tasks');
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { IncomingCallNotification } from "@/components/incoming-call-notificatio
|
|||||||
import { useEmailNotifications } from "@/hooks/use-email-notifications";
|
import { useEmailNotifications } from "@/hooks/use-email-notifications";
|
||||||
import { useRocketChatMessageNotifications } from "@/hooks/use-rocketchat-message-notifications";
|
import { useRocketChatMessageNotifications } from "@/hooks/use-rocketchat-message-notifications";
|
||||||
import { useCalendarEventNotifications } from "@/hooks/use-calendar-event-notifications";
|
import { useCalendarEventNotifications } from "@/hooks/use-calendar-event-notifications";
|
||||||
|
import { useTaskNotifications } from "@/hooks/use-task-notifications";
|
||||||
import { OutlookNotification } from "@/components/outlook-notification";
|
import { OutlookNotification } from "@/components/outlook-notification";
|
||||||
import { NotificationStack } from "@/components/notification-stack";
|
import { NotificationStack } from "@/components/notification-stack";
|
||||||
|
|
||||||
@ -38,6 +39,9 @@ export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: Layou
|
|||||||
// Listen for calendar event notifications
|
// Listen for calendar event notifications
|
||||||
const { eventNotification, setEventNotification } = useCalendarEventNotifications();
|
const { eventNotification, setEventNotification } = useCalendarEventNotifications();
|
||||||
|
|
||||||
|
// Listen for task notifications
|
||||||
|
const { taskNotification, setTaskNotification } = useTaskNotifications();
|
||||||
|
|
||||||
// Also listen for RocketChat iframe events (fallback)
|
// Also listen for RocketChat iframe events (fallback)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSignInPage) return;
|
if (isSignInPage) return;
|
||||||
@ -270,6 +274,15 @@ export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: Layou
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{taskNotification && (
|
||||||
|
<OutlookNotification
|
||||||
|
notification={taskNotification}
|
||||||
|
onDismiss={() => {
|
||||||
|
console.log('[LayoutWrapper] Task notification dismissed');
|
||||||
|
setTaskNotification(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</NotificationStack>
|
</NotificationStack>
|
||||||
)}
|
)}
|
||||||
</AuthCheck>
|
</AuthCheck>
|
||||||
|
|||||||
229
hooks/use-task-notifications.ts
Normal file
229
hooks/use-task-notifications.ts
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
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;
|
||||||
|
startDate?: string | null; // For Twenty CRM tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
startDate: t.startDate,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
startDate: task.startDate || 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 Leantime tasks: notification at due date time (dateToFinish)
|
||||||
|
// For Twenty CRM tasks: notification at start date time (startDate) if available, otherwise due date
|
||||||
|
let notificationDate: Date | null = null;
|
||||||
|
|
||||||
|
if (task.source === 'twenty-crm' && task.startDate) {
|
||||||
|
// Twenty CRM: use start date for notification
|
||||||
|
notificationDate = new Date(task.startDate);
|
||||||
|
} else if (task.dateToFinish) {
|
||||||
|
// Leantime: use due date for notification
|
||||||
|
notificationDate = new Date(task.dateToFinish);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 1 minute of the notification time (30 seconds before to 30 seconds after)
|
||||||
|
const notificationWindow = 30 * 1000; // 30 seconds
|
||||||
|
const inNotificationWindow =
|
||||||
|
timeUntilNotification >= -notificationWindow &&
|
||||||
|
timeUntilNotification <= notificationWindow;
|
||||||
|
|
||||||
|
console.log('[useTaskNotifications] ⏰ Checking task', {
|
||||||
|
id: task.id,
|
||||||
|
title: task.headline,
|
||||||
|
source: task.source,
|
||||||
|
notificationDate: notificationDate.toISOString(),
|
||||||
|
now: 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.source === 'twenty-crm' && a.startDate
|
||||||
|
? new Date(a.startDate).getTime()
|
||||||
|
: a.dateToFinish ? new Date(a.dateToFinish).getTime() : Infinity;
|
||||||
|
const dateB = b.source === 'twenty-crm' && b.startDate
|
||||||
|
? new Date(b.startDate).getTime()
|
||||||
|
: b.dateToFinish ? new Date(b.dateToFinish).getTime() : Infinity;
|
||||||
|
return dateA - dateB;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show notification for the first task
|
||||||
|
const task = dueTasks[0];
|
||||||
|
|
||||||
|
const notificationDate = task.source === 'twenty-crm' && task.startDate
|
||||||
|
? new Date(task.startDate)
|
||||||
|
: 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/starting 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} - ${task.source === 'twenty-crm' ? 'Début' : 'É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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user