import { useCallback, useEffect, useRef, useState } from 'react'; import { api } from '../api.js'; import { useAuth } from '../context/AuthContext.jsx'; import NotifTypeAvatar, { TYPE_META } from '../components/NotifTypeAvatar.jsx'; const PAGE_SIZE = 10; // Dropdown générique (Status / Type) function FilterDropdown({ label, value, options, onChange }) { const [open, setOpen] = useState(false); const ref = useRef(null); const current = options.find(o => o.value === value); useEffect(() => { if (!open) return; const h = (e) => { if (!ref.current?.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, [open]); return (
{open && (
{options.map(o => ( ))}
)}
); } function timeAgo(dateStr) { const diff = (Date.now() - new Date(dateStr + 'Z').getTime()) / 1000; if (diff < 60) return 'À l\'instant'; if (diff < 3600) return `Il y a ${Math.floor(diff / 60)} min`; if (diff < 86400) return `Il y a ${Math.floor(diff / 3600)} h`; if (diff < 86400 * 7) return `Il y a ${Math.floor(diff / 86400)} j`; return new Date(dateStr + 'Z').toLocaleDateString('fr-FR', { day: '2-digit', month: 'short', year: 'numeric' }); } const STATUS_OPTIONS = [ { value: 'all', label: 'Tous' }, { value: 'unread', label: 'Non lus' }, { value: 'read', label: 'Lus' }, ]; const TYPE_OPTIONS = [ { value: 'all', label: 'Tous les types' }, ...Object.entries(TYPE_META).map(([v, m]) => ({ value: v, label: m.label })), ]; export default function Notifications() { const { isAdmin } = useAuth(); const [notifs, setNotifs] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true); const [page, setPage] = useState(0); const [search, setSearch] = useState(''); const [filterStatus, setFilterStatus] = useState('all'); const [filterType, setFilterType] = useState('all'); const [seeding, setSeeding] = useState(false); const [seedCount, setSeedCount] = useState(3); const fetchNotifs = useCallback(async (p = 0, status = filterStatus, type = filterType) => { setLoading(true); try { const params = { limit: PAGE_SIZE, offset: p * PAGE_SIZE }; if (status === 'unread') params.unread_only = 'true'; if (type !== 'all') params.type = type; const data = await api.get('/notifications', params); setNotifs(data.notifications ?? []); setTotal(data.total ?? 0); } catch { /* silencieux */ } setLoading(false); }, [filterStatus, filterType]); useEffect(() => { fetchNotifs(page, filterStatus, filterType); }, [page, filterStatus, filterType]); // eslint-disable-line const handleStatus = (v) => { setFilterStatus(v); setPage(0); }; const handleType = (v) => { setFilterType(v); setPage(0); }; const markRead = async (id) => { try { await api.patch(`/notifications/${id}/read`); setNotifs(prev => prev.map(n => n.id === id ? { ...n, read: 1 } : n)); } catch { /* silencieux */ } }; const markAll = async () => { try { await api.patch('/notifications/read-all'); setNotifs(prev => prev.map(n => ({ ...n, read: 1 }))); } catch { /* silencieux */ } }; const deleteNotif = async (id) => { try { await api.del(`/notifications/${id}`); setNotifs(prev => prev.filter(n => n.id !== id)); setTotal(prev => Math.max(0, prev - 1)); } catch { /* silencieux */ } }; const seedNotifs = async () => { setSeeding(true); try { await api.post('/notifications/seed', { count: seedCount }); await fetchNotifs(0, filterStatus, filterType); setPage(0); // Forcer le rafraîchissement du compteur dans la cloche window.dispatchEvent(new CustomEvent('notif:refresh')); } catch { /* silencieux */ } setSeeding(false); }; // Filtrage local par recherche (sur titre + body) const visible = search.trim() ? notifs.filter(n => n.title.toLowerCase().includes(search.toLowerCase()) || (n.body ?? '').toLowerCase().includes(search.toLowerCase()) ) : notifs; const unreadTotal = notifs.filter(n => !n.read).length; const totalPages = Math.ceil(total / PAGE_SIZE); const from = page * PAGE_SIZE + 1; const to = Math.min((page + 1) * PAGE_SIZE, total); return ( <> {/* topbar vide pour respecter le layout (pas de contenu ici) */}
{/* ── Wrapper centré (.main ajoute padding: 0 24px) ── */}
{/* ── Bloc principal notifications ── */}
{/* En-tête du bloc */}

Notifications

{/* Barre recherche + filtres */}
setSearch(e.target.value)} />
{/* Séparateur */}
{/* Liste */} {loading && (
Chargement…
)} {!loading && visible.length === 0 && (
🔔
Aucune notification
Vous êtes à jour !
)} {!loading && visible.map((n, i) => (
{n.title}
{n.body &&
{n.body}
}
{(TYPE_META[n.type] ?? TYPE_META.info).label} {timeAgo(n.created_at)}
{!n.read && ( )} {!n.read && }
))} {/* Pagination */}
{total === 0 ? 'Aucune notification' : `Affichage de ${from} à ${to} sur ${total} notification${total > 1 ? 's' : ''}`}
{/* fin .notif-block */} {/* ── Bloc Simulation (admin uniquement) ── */} {isAdmin && (

Simulation

Générer des notifications de test :
)}
{/* fin .notif-center-wrap */} ); }