"use client"; import { useEffect, useState, useMemo, useRef } from "react"; import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { RefreshCw, MessageSquare, Mail, MailOpen, Loader2 } from "lucide-react"; import Link from 'next/link'; import { useUnifiedRefresh } from "@/hooks/use-unified-refresh"; import { REFRESH_INTERVALS } from "@/lib/constants/refresh-intervals"; import { Badge } from "@/components/ui/badge"; import { useWidgetNotification } from "@/hooks/use-widget-notification"; interface Email { id: string; subject: string; from: string; fromName?: string; date: string; read: boolean; starred: boolean; folder: string; } interface EmailResponse { emails: Email[]; mailUrl: string; error?: string; } export function Email() { const { data: session, status } = useSession(); const router = useRouter(); const [emails, setEmails] = useState([]); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); const [mailUrl, setMailUrl] = useState(null); const [accounts, setAccounts] = useState>([]); const [unreadCount, setUnreadCount] = useState(0); const [accountErrors, setAccountErrors] = useState>({}); const { triggerNotification } = useWidgetNotification(); const lastUnreadCountRef = useRef(-1); const lastEmailIdsRef = useRef>(new Set()); const isInitializedRef = useRef(false); // Create a map for quick account lookup by ID (recalculated when accounts change) const accountMap = useMemo(() => { return new Map(accounts.map(acc => [acc.id, acc])); }, [accounts]); useEffect(() => { if (status === 'authenticated') { loadAccounts(); } }, [status]); useEffect(() => { if (accounts.length > 0 && status === 'authenticated') { fetchEmails(false); fetchUnreadCount(); } }, [accounts, status]); const loadAccounts = async () => { try { const response = await fetch('/api/courrier/accounts'); if (response.ok) { const data = await response.json(); if (data.accounts) { const loadedAccounts = data.accounts.map((acc: any) => { // If no color is set, generate a consistent color based on account ID let accountColor = acc.color; if (!accountColor && acc.id) { // Generate a color hash from accountId let hash = 0; for (let i = 0; i < acc.id.length; i++) { hash = acc.id.charCodeAt(i) + ((hash << 5) - hash); } const hue = Math.abs(hash % 360); accountColor = `hsl(${hue}, 70%, 50%)`; } return { id: acc.id || acc.email, email: acc.email, color: accountColor || '#0082c9' // Fallback default color }; }); console.log('[Email Widget] Loaded accounts with colors:', loadedAccounts.map((acc: { id: string; email: string; color: string }) => ({ id: acc.id, email: acc.email, color: acc.color }))); setAccounts(loadedAccounts); } } } catch (err) { console.error('Error loading accounts:', err); } }; const fetchEmails = async (forceRefresh = false) => { // Only show loading spinner on initial load, not on auto-refresh if (!emails.length) { setLoading(true); } setRefreshing(true); setError(null); setAccountErrors({}); try { // Fetch emails from all accounts in parallel const emailPromises = accounts.map(async (account) => { try { const url = `/api/courrier?folder=INBOX&page=1&perPage=33&accountId=${encodeURIComponent(account.id)}${forceRefresh ? '&refresh=true' : ''}`; const response = await fetch(url); if (!response.ok) { const errorMsg = `Failed to fetch emails for ${account.email}`; console.warn(errorMsg); setAccountErrors(prev => ({ ...prev, [account.id]: errorMsg })); return []; } const data = await response.json(); if (data.error || !data.emails) { const errorMsg = data.error || `No emails returned for ${account.email}`; setAccountErrors(prev => ({ ...prev, [account.id]: errorMsg })); return []; } // Add accountId to each email for proper identification return data.emails.map((email: any) => ({ ...email, accountId: account.id, _accountEmail: account.email // Debug: store account email for verification })); } catch (err) { const errorMsg = `Error fetching emails for ${account.email}: ${err instanceof Error ? err.message : 'Unknown error'}`; console.error(errorMsg, err); setAccountErrors(prev => ({ ...prev, [account.id]: errorMsg })); return []; } }); const allEmailsArrays = await Promise.allSettled(emailPromises); const allEmails = allEmailsArrays .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') .flatMap(result => result.value); // Transform and sort all emails const transformedEmails = allEmails .map((email: any) => ({ id: email.id, subject: email.subject, from: email.from[0]?.address || '', fromName: email.from[0]?.name || '', date: email.date, read: email.flags.seen, starred: email.flags.flagged, folder: email.folder, accountId: email.accountId, _accountEmail: email._accountEmail // Keep for debugging })) // Sort emails by date (most recent first) .sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime()) .slice(0, 33); // Show up to 33 emails console.log('[Email Widget] Transformed emails with accountIds:', transformedEmails.map(e => ({ id: e.id, subject: e.subject, accountId: (e as any).accountId, accountEmail: (e as any)._accountEmail }))); setEmails(transformedEmails); setMailUrl('/courrier'); // Calculate unread count const currentUnreadCount = transformedEmails.filter(e => !e.read).length; const currentEmailIds = new Set(transformedEmails.map(e => e.id)); // Detect new emails by comparing IDs (more reliable than count) const newEmailIds = new Set( Array.from(currentEmailIds).filter(id => !lastEmailIdsRef.current.has(id)) ); const hasNewEmails = newEmailIds.size > 0; // On first load, just store email IDs without triggering notifications if (!isInitializedRef.current) { console.log('[Email Widget] 📧 Initializing - storing existing email IDs without notifications', { emailCount: transformedEmails.length, unreadCount: currentUnreadCount, }); lastEmailIdsRef.current = currentEmailIds; lastUnreadCountRef.current = currentUnreadCount; isInitializedRef.current = true; } else { // Update count if it changed if (currentUnreadCount !== lastUnreadCountRef.current) { lastUnreadCountRef.current = currentUnreadCount; } } // Always prepare notification items (unread emails only, max 10) const notificationItems = transformedEmails .filter(e => !e.read) .slice(0, 10) .map(email => { const account = accountMap.get((email as any).accountId); return { id: email.id, title: email.subject || 'Sans objet', message: `De ${email.fromName || email.from.split('@')[0]}`, link: '/courrier', timestamp: new Date(email.date), metadata: { accountId: (email as any).accountId, accountEmail: account?.email, }, }; }); // 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: 'email', count: currentUnreadCount, items: notificationItems, }); // Dispatch event for Outlook-style notifications (for new emails detected by ID) if (hasNewEmails) { // Get only the newly arrived emails (by ID comparison) const newEmails = transformedEmails .filter(e => newEmailIds.has(e.id) && !e.read) .slice(0, 5); // Limit to 5 most recent new emails if (newEmails.length > 0) { console.log('[Email Widget] 📧 Dispatching new emails event', { newEmailsCount: newEmails.length, newEmailIds: Array.from(newEmailIds), previousCount: lastUnreadCountRef.current, currentCount: currentUnreadCount, previousEmailIds: Array.from(lastEmailIdsRef.current), }); window.dispatchEvent(new CustomEvent('new-emails-detected', { detail: { emails: transformedEmails, accountMap: accountMap, previousCount: lastUnreadCountRef.current, currentCount: currentUnreadCount, } })); } } // Always update lastEmailIdsRef to track current state lastEmailIdsRef.current = currentEmailIds; // Show error only if all accounts failed if (allEmails.length === 0 && accounts.length > 0 && Object.keys(accountErrors).length === accounts.length) { setError('Failed to load emails from all accounts'); } } catch (error) { console.error('Error fetching emails:', error); setError(error instanceof Error ? error.message : 'Failed to load emails'); setEmails([]); } finally { setLoading(false); setRefreshing(false); } }; const fetchUnreadCount = async () => { try { const response = await fetch('/api/courrier/unread-counts'); if (response.ok) { const data = await response.json(); if (data && typeof data === 'object') { // Sum up unread counts from all accounts and folders let totalUnread = 0; Object.values(data).forEach((accountCounts: any) => { if (typeof accountCounts === 'object') { Object.values(accountCounts).forEach((count: any) => { if (typeof count === 'number') { totalUnread += count; } }); } }); setUnreadCount(totalUnread); } } } catch (err) { console.error('Error fetching unread count:', err); // Don't set error state for unread count failures } }; // Integrate unified refresh for automatic polling // Use forceRefresh=true to ensure we get the latest emails immediately const { refresh } = useUnifiedRefresh({ resource: 'email', interval: REFRESH_INTERVALS.EMAIL, // 30 seconds (harmonized) enabled: status === 'authenticated', onRefresh: async () => { // Use forceRefresh to bypass cache and get latest emails immediately await fetchEmails(true); // Force refresh to get new emails immediately await fetchUnreadCount(); }, priority: 'high', }); // Manual refresh handler (bypasses cache) const handleManualRefresh = async () => { await fetchEmails(true); // Force refresh, bypass cache await fetchUnreadCount(); }; const formatDate = (dateString: string) => { try { const date = new Date(dateString); return new Intl.DateTimeFormat('fr-FR', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(date); } catch (e) { return ''; } }; return ( Courrier {unreadCount > 0 && ( {unreadCount} )} {accounts.length === 0 ? (

Aucun compte email configuré

Configurez votre compte email pour voir vos messages

) : error ? (

{error}

{Object.keys(accountErrors).length > 0 && (
{Object.entries(accountErrors).map(([accountId, errMsg]) => (

{errMsg}

))}
)}
) : loading && emails.length === 0 ? (

Chargement des emails...

) : emails.length === 0 ? (

Aucun email

) : (
{emails.map((email, index) => { // Find the account color for this email using the accountId const emailAccountId = (email as any).accountId; const account = emailAccountId ? accountMap.get(emailAccountId) : null; // Use account color, or generate a color based on accountId if not found let accountColor = account?.color || '#0082c9'; // If no color is set, generate a consistent color based on accountId if (!account?.color && emailAccountId) { // Generate a color hash from accountId let hash = 0; for (let i = 0; i < emailAccountId.length; i++) { hash = emailAccountId.charCodeAt(i) + ((hash << 5) - hash); } const hue = Math.abs(hash % 360); accountColor = `hsl(${hue}, 70%, 50%)`; } // Debug log for first few emails if (index < 3) { console.log('[Email Widget] Email account mapping', { emailId: email.id, emailAccountId, accountFound: !!account, accountEmail: account?.email, accountColor: account?.color, finalColor: accountColor, allAccountIds: Array.from(accountMap.keys()), allAccounts: Array.from(accountMap.values()).map(a => ({ id: a.id, email: a.email, color: a.color })) }); } return (

{email.fromName || email.from.split('@')[0]}

{formatDate(email.date)}

{email.subject}

); })} {mailUrl && (
Voir tous les emails →
)}
)} ); }