"use client"; import { useEffect, useState, useRef } from "react"; import { useSession } from "next-auth/react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { RefreshCw, Share2, Folder } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { useUnifiedRefresh } from "@/hooks/use-unified-refresh"; import { REFRESH_INTERVALS } from "@/lib/constants/refresh-intervals"; import { useWidgetNotification } from "@/hooks/use-widget-notification"; interface Task { id: number; headline: string; description: string; dateToFinish: string | null; projectId: number; projectName: string; status: number; editorId?: string; editorFirstname?: string; editorLastname?: string; authorFirstname: string; authorLastname: string; milestoneHeadline?: string; editTo?: string; editFrom?: string; type?: string; dependingTicketId?: number | null; } interface ProjectSummary { name: string; tasks: { status: number; count: number; }[]; } interface TaskWithDate extends Task { validDate?: Date; } export function Duties() { const { data: session, status } = useSession(); const [tasks, setTasks] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const { triggerNotification } = useWidgetNotification(); const lastTaskCountRef = useRef(-1); const getStatusLabel = (status: number): string => { switch (status) { case 1: return 'New'; case 2: return 'Blocked'; case 3: return 'In Progress'; case 4: return 'Waiting for Approval'; case 5: return 'Done'; default: return 'Unknown'; } }; const getStatusColor = (status: number): string => { switch (status) { case 1: return 'bg-blue-500'; // New - blue case 2: return 'bg-red-500'; // Blocked - red case 3: return 'bg-yellow-500'; // In Progress - yellow case 4: return 'bg-purple-500'; // Waiting for Approval - purple case 5: return 'bg-gray-500'; // Done - gray default: return 'bg-gray-300'; } }; const formatDate = (dateStr: string): string => { if (!dateStr || dateStr === '0000-00-00 00:00:00') return ''; try { const date = new Date(dateStr); if (isNaN(date.getTime())) return ''; return date.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' }); } catch { return ''; } }; const getValidDate = (task: Task): string | null => { if (task.dateToFinish && task.dateToFinish !== '0000-00-00 00:00:00') { return task.dateToFinish; } return null; }; const fetchTasks = async (forceRefresh = false) => { // Only show loading spinner on initial load, not on auto-refresh if (!tasks.length) { setLoading(true); } setRefreshing(true); setError(null); try { // Fetch tasks from both Leantime and Twenty CRM in parallel const leantimeUrl = forceRefresh ? '/api/leantime/tasks?refresh=true' : '/api/leantime/tasks'; const twentyCrmUrl = forceRefresh ? '/api/twenty-crm/tasks?refresh=true' : '/api/twenty-crm/tasks'; const [leantimeResponse, twentyCrmResponse] = await Promise.allSettled([ fetch(leantimeUrl), fetch(twentyCrmUrl), ]); // Process Leantime tasks let leantimeTasks: Task[] = []; if (leantimeResponse.status === 'fulfilled' && leantimeResponse.value.ok) { const leantimeData = await leantimeResponse.value.json(); if (Array.isArray(leantimeData)) { // Log ALL tasks with their statuses to see what we receive console.log('[Devoirs Widget] 📥 RAW Leantime tasks from API:', leantimeData.map((t: any) => ({ id: t.id, headline: t.headline, status: t.status, statusType: typeof t.status, }))); leantimeTasks = leantimeData; // Log tasks with status 5 to debug const doneTasks = leantimeData.filter((t: Task) => { const taskStatus = (t as any).status; // Use any to handle potential string/number mismatch if (taskStatus === null || taskStatus === undefined) return false; const statusNum = typeof taskStatus === 'string' ? parseInt(taskStatus, 10) : taskStatus; const statusStr = typeof taskStatus === 'string' ? taskStatus.toLowerCase() : String(taskStatus).toLowerCase(); return statusNum === 5 || statusStr === '5' || statusStr === 'done'; }); if (doneTasks.length > 0) { console.warn('[Devoirs Widget] ⚠️ Received done tasks from Leantime API:', doneTasks.map((t: Task) => ({ id: t.id, headline: t.headline, status: t.status, statusType: typeof t.status, }))); } } } else { console.warn('Failed to fetch Leantime tasks:', leantimeResponse); } // Process Twenty CRM tasks let twentyCrmTasks: Task[] = []; if (twentyCrmResponse.status === 'fulfilled' && twentyCrmResponse.value.ok) { const twentyCrmData = await twentyCrmResponse.value.json(); if (Array.isArray(twentyCrmData)) { twentyCrmTasks = twentyCrmData; } } else { console.warn('Failed to fetch Twenty CRM tasks:', twentyCrmResponse); } // Combine tasks from both sources const allTasks = [...leantimeTasks, ...twentyCrmTasks]; // Log detailed status information const leantimeStatusDetails = leantimeTasks.map((t: Task) => { const rawStatus = (t as any).status; const statusNum = typeof rawStatus === 'string' ? parseInt(rawStatus, 10) : rawStatus; const statusStr = typeof rawStatus === 'string' ? rawStatus.toLowerCase().trim() : String(rawStatus).toLowerCase().trim(); const isDone = statusNum === 3 || statusNum === 5 || statusStr === '3' || statusStr === '5' || statusStr === 'done'; return { id: t.id, headline: t.headline, status: rawStatus, statusType: typeof rawStatus, statusNum, statusStr, isDone, }; }); // Group by status for better visibility const statusGroups = leantimeStatusDetails.reduce((acc: any, t) => { const key = String(t.status); if (!acc[key]) { acc[key] = { status: t.status, count: 0, tasks: [] }; } acc[key].count++; if (acc[key].tasks.length < 3) { acc[key].tasks.push({ id: t.id, headline: t.headline }); } return acc; }, {}); const doneTasks = leantimeStatusDetails.filter(t => t.isDone); // Always log status breakdown for debugging console.log('[Devoirs Widget] 📊 Status Breakdown:', { totalTasks: leantimeTasks.length, statusGroups: Object.keys(statusGroups).map(key => ({ status: key, count: statusGroups[key].count, sample: statusGroups[key].tasks, })), doneTasksCount: doneTasks.length, }); if (doneTasks.length > 0) { console.error('[Devoirs Widget] ❌❌❌ FOUND DONE TASKS - THEY SHOULD BE FILTERED ❌❌❌', { total: leantimeTasks.length, doneCount: doneTasks.length, doneTasks: doneTasks.map(t => ({ id: t.id, headline: t.headline, status: t.status, statusType: t.statusType, statusNum: t.statusNum, statusStr: t.statusStr, })), }); } console.log('[Devoirs Widget] Combined tasks:', { leantime: leantimeTasks.length, twentyCrm: twentyCrmTasks.length, total: allTasks.length, }); if (allTasks.length === 0) { setTasks([]); return; } // Backend already filters out status=5 (Done) and filters by editorId for Leantime // Backend also filters Twenty CRM tasks to include overdue and due today // Filter to keep only tasks with due date <= today (overdue or due today) const now = new Date(); const todayYear = now.getFullYear(); const todayMonth = now.getMonth(); const todayDay = now.getDate(); const filteredTasks = allTasks.filter((task: Task) => { // Exclude tasks with status Done // In Leantime: status 3 = DONE (see api/leantime/status-labels/route.ts), also check status 5 const rawStatus = (task as any).status; // Use any to handle potential string/number mismatch if (rawStatus === null || rawStatus === undefined) { // If status is null/undefined, keep the task (let other filters handle it) } else { const taskStatus = typeof rawStatus === 'string' ? parseInt(rawStatus, 10) : rawStatus; const statusStr = typeof rawStatus === 'string' ? rawStatus.toLowerCase() : String(rawStatus).toLowerCase(); if (taskStatus === 3 || taskStatus === 5 || statusStr === '3' || statusStr === '5' || statusStr === 'done') { console.log('[Devoirs Widget] Filtering out done task:', { id: task.id, headline: task.headline, status: rawStatus, taskStatus, }); return false; } } const dueDate = getValidDate(task); if (!dueDate) { return false; // Exclude tasks without a due date } // Use local date comparison to avoid timezone issues // Leantime dates with 'Z' are actually local time, not UTC - remove Z before parsing const dateStrForParsing = dueDate.endsWith('Z') ? dueDate.slice(0, -1) : dueDate; const taskDueDate = new Date(dateStrForParsing); const taskYear = taskDueDate.getFullYear(); const taskMonth = taskDueDate.getMonth(); const taskDay = taskDueDate.getDate(); // Keep tasks with due date <= today (overdue or due today, not future) const isOverdueOrDueToday = taskYear < todayYear || (taskYear === todayYear && taskMonth < todayMonth) || (taskYear === todayYear && taskMonth === todayMonth && taskDay <= todayDay); return isOverdueOrDueToday; }); // Log filtered results console.log('[Devoirs Widget] Filtering results:', { before: allTasks.length, after: filteredTasks.length, filteredOut: allTasks.length - filteredTasks.length, filteredTasksStatuses: filteredTasks.map((t: Task) => ({ id: t.id, headline: t.headline, status: (t as any).status, })), }); // Sort by dateToFinish (oldest first) const sortedTasks = filteredTasks .sort((a: Task, b: Task) => { // First sort by dateToFinish (oldest first) const dateA = getValidDate(a); const dateB = getValidDate(b); // Both dates are guaranteed to exist after filtering if (dateA && dateB) { const timeA = new Date(dateA).getTime(); const timeB = new Date(dateB).getTime(); if (timeA !== timeB) { return timeA - timeB; } } // If dates are equal, sort by status (4 before others) if (a.status === 4 && b.status !== 4) return -1; if (b.status === 4 && a.status !== 4) return 1; // If status is also equal, maintain original order return 0; }); console.log('Sorted tasks:', sortedTasks.map(t => ({ id: t.id, date: t.dateToFinish, status: t.status, type: t.type || 'main', source: (t as any).source || 'leantime' }))); // Calculate current task count const currentTaskCount = sortedTasks.length; // Always trigger notification to keep the count fresh in Redis // This prevents the count from expiring if it hasn't changed const shouldUpdate = currentTaskCount !== lastTaskCountRef.current || lastTaskCountRef.current === -1; if (shouldUpdate) { lastTaskCountRef.current = currentTaskCount; } // Prepare notification items (max 10) const notificationItems = sortedTasks .slice(0, 10) .map(task => ({ id: task.id.toString(), title: task.headline, message: task.dateToFinish ? `Due: ${formatDate(task.dateToFinish)}` : 'Tâche en retard', link: (task as any).source === 'twenty-crm' ? (task as any).url : `https://agilite.slm-lab.net/tickets/showTicket/${String(task.id).replace('twenty-', '')}`, timestamp: task.dateToFinish ? new Date(task.dateToFinish) : new Date(), metadata: { source: (task as any).source || 'leantime', projectName: task.projectName, status: task.status, }, })); // Always trigger notification update to keep count fresh in Redis // This ensures the count doesn't expire even if it hasn't changed await triggerNotification({ source: 'leantime', count: currentTaskCount, items: notificationItems, }); setTasks(sortedTasks); // Dispatch event for Outlook-style notifications (when tasks are due) // Filter out done tasks one more time before sending to notifications const tasksForNotification = sortedTasks .filter(task => { const rawStatus = (task as any).status; if (rawStatus === null || rawStatus === undefined) { return true; // Keep tasks without status } const taskStatus = typeof rawStatus === 'string' ? parseInt(rawStatus, 10) : rawStatus; const statusStr = typeof rawStatus === 'string' ? rawStatus.toLowerCase().trim() : String(rawStatus).toLowerCase().trim(); const isDone = taskStatus === 3 || taskStatus === 5 || statusStr === '3' || statusStr === '5' || statusStr === 'done'; if (isDone) { console.warn('[Devoirs Widget] ⚠️ Filtering out done task before notification:', { id: task.id, headline: task.headline, status: rawStatus, }); return false; } return true; }) .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, })); 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, })), }); 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) { console.error('Error fetching tasks:', error); setError(error instanceof Error ? error.message : 'Failed to fetch tasks'); } finally { setLoading(false); setRefreshing(false); } }; // Initial fetch on mount useEffect(() => { if (status === 'authenticated') { fetchTasks(false); // Use cache on initial load } }, [status]); // Integrate unified refresh for automatic polling const { refresh } = useUnifiedRefresh({ resource: 'duties', interval: REFRESH_INTERVALS.DUTIES, // 30 seconds (harmonized) enabled: status === 'authenticated', onRefresh: async () => { await fetchTasks(false); // Use cache for auto-refresh }, priority: 'high', }); // Manual refresh handler (bypasses cache) const handleManualRefresh = async () => { await fetchTasks(true); // Force refresh, bypass cache }; // Update the TaskDate component to handle dates better const TaskDate = ({ task }: { task: TaskWithDate }) => { const dateStr = task.dateToFinish; if (!dateStr || dateStr === '0000-00-00 00:00:00') { return (
NO DATE
); } try { const date = new Date(dateStr); if (isNaN(date.getTime())) { throw new Error('Invalid date'); } const today = new Date(); today.setHours(0, 0, 0, 0); const isPastDue = date < today; const month = date.toLocaleString('fr-FR', { month: 'short' }).toUpperCase(); const day = date.getDate(); const year = date.getFullYear(); return (
{month} {day}
{year}
); } catch (error) { console.error('Error formatting date for task', task.id, error); return (
ERR DATE
); } }; return ( Devoirs {loading ? (
) : error ? (
{error}
) : tasks.length === 0 ? (
Aucune tâche en retard
) : (
{tasks.map((task) => (
{task.headline}
{(task as any).source === 'twenty-crm' ? ( <> SLM IF (Médiation) ) : ( <> {task.projectName} (Agilité) )}
))}
)} ); }