import { useState, useMemo } from "react"; import { useLocalAuth } from "@/contexts/LocalAuthContext"; import { toast } from "sonner"; import { trpc } from "@/lib/trpc"; import { FilterBar } from "@/components/FilterBar"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { LayoutGrid, List, Eye, EyeOff, ExternalLink, Calendar, MapPin, Target, Loader2, ChevronLeft, ChevronRight, AlertCircle, Clock, Trash2, AlertTriangle, } from "lucide-react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { cn } from "@/lib/utils"; import { format, isPast, differenceInDays } from "date-fns"; import { fr } from "date-fns/locale"; type AAPCategorie = "Handicap" | "PA" | "Enfance" | "Précarité" | "Sanitaire" | "Autre"; type ReadFilter = "unread" | "all" | "read"; interface AAPItem { id: number; titre: string; categorie: string; region: string | null; departement: string | null; dateCloture: Date | null; datePublication: Date | null; lien: string | null; importedAt: Date; iaRelevant: boolean | null; iaCategorie: string | null; iaClassifiedBy: "ia" | "rules" | null; iaReason: string | null; iaResume: string | null; } const CAT_COLORS: Record = { Handicap: "bg-violet-100 text-violet-800 border-violet-200", PA: "bg-sky-100 text-sky-800 border-sky-200", Enfance: "bg-pink-100 text-pink-800 border-pink-200", "Précarité": "bg-orange-100 text-orange-800 border-orange-200", Sanitaire: "bg-teal-100 text-teal-800 border-teal-200", Autre: "bg-gray-100 text-gray-700 border-gray-200", }; const CAT_ACCENT: Record = { Handicap: "border-l-violet-500", PA: "border-l-sky-500", Enfance: "border-l-pink-500", "Précarité": "border-l-orange-500", Sanitaire: "border-l-teal-500", Autre: "border-l-gray-400", }; const PAGE_SIZE = 24; function formatDate(d: Date | null | undefined): string | null { if (!d) return null; try { return format(new Date(d), "d MMM yyyy", { locale: fr }); } catch { return null; } } function ClotureStatus({ date }: { date: Date | null | undefined }) { if (!date) return ; const d = new Date(date); const past = isPast(d); const daysLeft = differenceInDays(d, new Date()); if (past) { return ( Clôturé ); } if (daysLeft <= 7) { return ( {daysLeft}j restants ); } return {formatDate(d)}; } export default function AAPDashboard() { const { user } = useLocalAuth(); const isAdmin = user?.role === "admin"; const utils = trpc.useUtils(); // Marquage lu/non lu const [readIds, setReadIds] = useState>(new Set()); const markAsReadMutation = trpc.aap.markAsRead.useMutation({ onSuccess: (_, vars) => { setReadIds((prev) => { const next = new Set(prev); next.add(vars.articleId); return next; }); }, }); const markAllAsReadMutation = trpc.aap.markAllAsRead.useMutation({ onSuccess: () => { utils.aap.unreadCount.invalidate(); }, }); const unreadCountQuery = trpc.aap.unreadCount.useQuery(); const unreadCount = unreadCountQuery.data?.count ?? 0; const purgeMutation = trpc.aap.purge.useMutation({ onSuccess: (data) => { toast.success(`Purge effectuée — ${data.deleted} entrée(s) supprimée(s)`); utils.aap.list.invalidate(); utils.aap.filters.invalidate(); }, onError: (err) => { toast.error(`Erreur lors de la purge : ${err.message}`); }, }); // ── État UI ───────────────────────────────────────────────────────────────── // Mode vignette par défaut const [viewMode, setViewMode] = useState<"list" | "grid">("grid"); const [activeTab, setActiveTab] = useState("all"); const [page, setPage] = useState(1); const [filterValues, setFilterValues] = useState>({}); // Filtre Lu/Non lu — Non lu par défaut const [readFilter, setReadFilter] = useState("unread"); // Ligne sélectionnée en mode liste const [selectedRowId, setSelectedRowId] = useState(null); const filtersQuery = trpc.aap.filters.useQuery(); const queryInput = useMemo(() => ({ categorie: activeTab !== "all" ? activeTab : undefined, region: filterValues.region || undefined, departement: filterValues.departement || undefined, search: filterValues.search || undefined, dateFrom: filterValues.dateFrom ? new Date(filterValues.dateFrom) : undefined, dateTo: filterValues.dateTo ? new Date(filterValues.dateTo) : undefined, clotureFrom: filterValues.clotureFrom ? new Date(filterValues.clotureFrom) : undefined, clotureTo: filterValues.clotureTo ? new Date(filterValues.clotureTo) : undefined, page, pageSize: PAGE_SIZE, }), [activeTab, filterValues, page]); const itemsQuery = trpc.aap.list.useQuery(queryInput); const handleFilterChange = (key: string, value: string) => { setFilterValues((prev) => ({ ...prev, [key]: value })); setPage(1); }; const handleReset = () => { setFilterValues({}); setPage(1); }; const allItems = (itemsQuery.data?.items ?? []) as AAPItem[]; const total = itemsQuery.data?.total ?? 0; const totalPages = Math.ceil(total / PAGE_SIZE); // Filtrage Lu/Non lu côté client const items = useMemo(() => { if (readFilter === "unread") return allItems.filter((i) => !readIds.has(i.id)); if (readFilter === "read") return allItems.filter((i) => readIds.has(i.id)); return allItems; }, [allItems, readIds, readFilter]); const filterOptions = [ { key: "region", label: "Région", options: filtersQuery.data?.regions ?? [] }, { key: "departement", label: "Département", options: filtersQuery.data?.departements ?? [] }, { key: "dateFrom", label: "Publié depuis", type: "date" as const }, { key: "dateTo", label: "Publié jusqu'à", type: "date" as const }, { key: "clotureFrom", label: "Clôture depuis", type: "date" as const }, { key: "clotureTo", label: "Clôture jusqu'à", type: "date" as const }, ]; const categories: AAPCategorie[] = ["Handicap", "PA", "Enfance", "Précarité", "Sanitaire", "Autre"]; const READ_FILTER_LABELS: Record = { unread: "Non lus", all: "Tous", read: "Lus", }; const READ_FILTER_ORDER: ReadFilter[] = ["unread", "all", "read"]; return (
{/* En-tête */}

Appels à Projets

{unreadCount > 0 && ( {unreadCount} )}

Handicap, Personnes Âgées, Enfance, Précarité, Sanitaire et Autre

{unreadCount > 0 && ( )} {/* Boutons mode d'affichage */} {isAdmin && ( Purger tous les appels à projets

Cette action va supprimer définitivement tous les appels à projets.

Cette opération est irréversible.

Annuler purgeMutation.mutate()} disabled={purgeMutation.isPending} > {purgeMutation.isPending ? "Purge en cours..." : "Oui, purger tous les appels à projets"}
)}
{/* Onglets catégories + bouton Lu/Non lu */}
{ setActiveTab(v as AAPCategorie | "all"); setPage(1); }}> Tous {categories.map((c) => ( {c} ))} {/* Bouton segmenté Lu / Non lu */}
{READ_FILTER_ORDER.map((f) => ( ))}
{/* Filtres */} {/* Contenu */} {itemsQuery.isLoading ? (
) : items.length === 0 ? (

{readFilter === "unread" ? "Aucun appel à projets non lu" : "Aucun appel à projets trouvé"}

{readFilter === "unread" ? "Tous les appels à projets ont été lus, ou modifiez le filtre Lu/Non lu" : "Modifiez vos filtres ou importez des données"}

) : viewMode === "list" ? ( markAsReadMutation.mutate({ articleId: id })} selectedRowId={selectedRowId} onSelectRow={setSelectedRowId} /> ) : ( markAsReadMutation.mutate({ articleId: id })} /> )} {/* Pagination */} {totalPages > 1 && (
Page {page} / {totalPages}
)}
); } // ─── Vue Liste ──────────────────────────────────────────────────────────────── function AAPListView({ items, readIds, onMarkRead, selectedRowId, onSelectRow, }: { items: AAPItem[]; readIds: Set; onMarkRead: (id: number) => void; selectedRowId: number | null; onSelectRow: (id: number | null) => void; }) { return (
{items.map((item, idx) => { const isSelected = selectedRowId === item.id; const isRead = readIds.has(item.id); const isEven = idx % 2 === 0; const catKey = item.iaCategorie || item.categorie; return ( onSelectRow(isSelected ? null : item.id)} className={cn( "border-b border-border/50 border-l-4 cursor-pointer transition-colors", CAT_ACCENT[catKey] || "border-l-transparent", isSelected ? "bg-primary/10 hover:bg-primary/15" : isEven ? "bg-white hover:bg-primary/5" : "bg-slate-50/80 hover:bg-primary/5" )} > ); })}
# Titre Catégorie Région Département Publication Clôture Lien
{idx + 1}
{!isRead && ( )}

{item.titre}

{item.iaResume && (

{item.iaResume}

)}
{catKey} {item.region || "—"} {item.departement || "—"} {formatDate(item.datePublication) || "—"} e.stopPropagation()}>
{!isRead && ( )} {item.lien && ( )}
); } // ─── Vue Vignettes ──────────────────────────────────────────────────────────── function AAPGridView({ items, readIds, onMarkRead, }: { items: AAPItem[]; readIds: Set; onMarkRead: (id: number) => void; }) { return (
{items.map((item) => { const catKey = item.iaCategorie || item.categorie; return (
{catKey}
{!readIds.has(item.id) && ( )} {item.lien && ( )}
{!readIds.has(item.id) && ( )}

{item.titre}

{item.iaResume && (

{item.iaResume}

)}
{item.region && ( {item.region} )} {item.departement && ( {item.departement} )}
{item.datePublication && ( {formatDate(item.datePublication)} )}
); })}
); }