import { useState, useMemo, useEffect } 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 { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { LayoutGrid, List, ExternalLink, Calendar, MapPin, Tag, Layers, FileSearch, Loader2, ChevronLeft, ChevronRight, Eye, Globe, BookOpen, 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 } from "date-fns"; import { fr } from "date-fns/locale"; type TypeVeille = "reglementaire" | "concurrentielle" | "technologique" | "informationnelle"; interface VeilleItem { id: number; titre: string; typeVeille: string; categorie: string | null; niveau: string | null; territoire: string | null; resume: string | null; source: string | null; passage: string | null; lien: string | null; datePublication: Date | null; importedAt: Date; iaRelevant: boolean | null; iaClassifiedBy: "ia" | "rules" | null; iaReason: string | null; iaResume: string | null; } const TYPE_LABELS: Record = { reglementaire: "Réglementaire", concurrentielle: "Concurrentielle", technologique: "Technologique", informationnelle: "Informationnelle", }; const TYPE_COLORS: Record = { reglementaire: "bg-blue-100 text-blue-800 border-blue-200", concurrentielle: "bg-purple-100 text-purple-800 border-purple-200", technologique: "bg-emerald-100 text-emerald-800 border-emerald-200", informationnelle: "bg-amber-100 text-amber-800 border-amber-200", }; const TYPE_ACCENT: Record = { reglementaire: "border-l-blue-500", concurrentielle: "border-l-purple-500", technologique: "border-l-emerald-500", informationnelle: "border-l-amber-500", }; const TYPE_HEADER_BG: Record = { reglementaire: "from-blue-50 to-blue-100/30 border-blue-200", concurrentielle: "from-purple-50 to-purple-100/30 border-purple-200", technologique: "from-emerald-50 to-emerald-100/30 border-emerald-200", informationnelle: "from-amber-50 to-amber-100/30 border-amber-200", }; const PAGE_SIZE = 24; function formatDate(d: Date | null | undefined): string | null { if (!d) return null; try { return format(new Date(d), "d MMMM yyyy", { locale: fr }); } catch { return null; } } // ─── Boîte de dialogue Détail ───────────────────────────────────────────────── function VeilleDetailDialog({ item, open, onClose, }: { item: VeilleItem | null; open: boolean; onClose: () => void; }) { if (!item) return null; const typeKey = item.typeVeille as TypeVeille; const headerBg = TYPE_HEADER_BG[typeKey] || "from-muted to-muted/30 border-border"; return ( !v && onClose()}> {/* En-tête coloré */}
{TYPE_LABELS[typeKey] || item.typeVeille} {item.datePublication && ( {formatDate(item.datePublication)} )}
{item.titre}
{/* Corps */}
{/* Métadonnées en grille */}
{item.niveau && (

Niveau

{item.niveau}

)} {item.territoire && (

Territoire

{item.territoire}

)} {item.source && (

Source

{item.source}

)} {item.passage && (

Passage en vigueur

{item.passage}

)}
{/* Résumé IA ou résumé brut */} {(item.iaResume || item.resume) && (

Résumé

{item.iaResume && ( Généré par l'IA )}

{item.iaResume || item.resume}

)} {/* Lien externe */} {item.lien && ( )}
); } // ─── Composant principal ────────────────────────────────────────────────────── export default function VeilleDashboard() { 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.veille.markAsRead.useMutation({ onSuccess: (_, vars) => { setReadIds((prev) => { const next = new Set(prev); next.add(vars.articleId); return next; }); }, }); const markAllAsReadMutation = trpc.veille.markAllAsRead.useMutation({ onSuccess: () => { utils.veille.unreadCount.invalidate(); }, }); const unreadCountQuery = trpc.veille.unreadCount.useQuery(); const unreadCount = unreadCountQuery.data?.count ?? 0; const purgeMutation = trpc.veille.purge.useMutation({ onSuccess: (data) => { toast.success(`Purge effectuée — ${data.deleted} entrée(s) supprimée(s)`); utils.veille.list.invalidate(); utils.veille.filters.invalidate(); }, onError: (err) => { toast.error(`Erreur lors de la purge : ${err.message}`); }, }); const [viewMode, setViewMode] = useState<"list" | "grid">("list"); const [activeTab, setActiveTab] = useState("all"); const [page, setPage] = useState(1); const [filterValues, setFilterValues] = useState>({}); const [selectedItem, setSelectedItem] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); const filtersQuery = trpc.veille.filters.useQuery(); const queryInput = useMemo(() => ({ typeVeille: activeTab !== "all" ? activeTab : undefined, categorie: filterValues.categorie || undefined, niveau: filterValues.niveau || undefined, territoire: filterValues.territoire || undefined, search: filterValues.search || undefined, dateFrom: filterValues.dateFrom ? new Date(filterValues.dateFrom) : undefined, dateTo: filterValues.dateTo ? new Date(filterValues.dateTo) : undefined, page, pageSize: PAGE_SIZE, }), [activeTab, filterValues, page]); const itemsQuery = trpc.veille.list.useQuery(queryInput); const handleFilterChange = (key: string, value: string) => { setFilterValues((prev) => ({ ...prev, [key]: value })); setPage(1); }; const handleReset = () => { setFilterValues({}); setPage(1); }; const openDetail = (item: VeilleItem) => { setSelectedItem(item); setDialogOpen(true); // Marquer comme lu automatiquement à l'ouverture du détail if (!readIds.has(item.id)) { markAsReadMutation.mutate({ articleId: item.id }); } }; const items = (itemsQuery.data?.items ?? []) as VeilleItem[]; const total = itemsQuery.data?.total ?? 0; const totalPages = Math.ceil(total / PAGE_SIZE); const filterOptions = [ { key: "niveau", label: "Niveau", options: filtersQuery.data?.niveaux ?? [] }, { key: "territoire", label: "Territoire", options: filtersQuery.data?.territoires ?? [] }, { key: "dateFrom", label: "Date depuis", type: "date" as const }, { key: "dateTo", label: "Date jusqu'à", type: "date" as const }, ]; return (
{/* En-tête */}

Veille Stratégique

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

Suivi réglementaire, concurrentiel, technologique et informationnel

{unreadCount > 0 && ( )} {isAdmin && ( <> Purger toutes les données de veille

Cette action va supprimer définitivement toutes les entrées de la veille stratégique (réglementaire, concurrentielle, technologique et générale).

Cette opération est irréversible. Les données ne pourront pas être récupérées.

Annuler purgeMutation.mutate()} disabled={purgeMutation.isPending} > {purgeMutation.isPending ? "Purge en cours..." : "Oui, purger toutes les données"}
)}
{/* Onglets */} { setActiveTab(v as TypeVeille | "all"); setPage(1); }}> Tous {(Object.keys(TYPE_LABELS) as TypeVeille[]).map((t) => ( {TYPE_LABELS[t]} ))} {/* Filtres */} {/* Contenu */} {itemsQuery.isLoading ? (
) : items.length === 0 ? (

Aucun résultat trouvé

Modifiez vos filtres ou importez des données

) : viewMode === "list" ? ( ) : ( )} {/* Pagination */} {totalPages > 1 && (
Page {page} / {totalPages}
)} {/* Boîte de dialogue détail */} setDialogOpen(false)} />
); } // ─── Vue Liste ──────────────────────────────────────────────────────────────── function VeilleListView({ items, onDetail, readIds }: { items: VeilleItem[]; onDetail: (item: VeilleItem) => void; readIds: Set }) { return (
{items.map((item, idx) => ( ))}
# Titre Type Niveau Territoire Date Actions
{idx + 1}
{!readIds.has(item.id) && ( )}

{item.titre}

{(item.iaResume || item.resume) &&

{item.iaResume || item.resume}

}
{TYPE_LABELS[item.typeVeille as TypeVeille] || item.typeVeille} {item.niveau || "—"} {item.territoire || "—"} {formatDate(item.datePublication) || "—"}
{/* Bouton Détail — bleu */} {/* Bouton Lien externe — vert */} {item.lien && ( )}
); } // ─── Vue Vignettes ──────────────────────────────────────────────────────────── function VeilleGridView({ items, onDetail, readIds }: { items: VeilleItem[]; onDetail: (item: VeilleItem) => void; readIds: Set }) { return (
{items.map((item) => (
{TYPE_LABELS[item.typeVeille as TypeVeille] || item.typeVeille}
{/* Bouton Détail */} {item.lien && ( )}
{!readIds.has(item.id) && }

{item.titre}

{(item.iaResume || item.resume) &&

{item.iaResume || item.resume}

}
{item.territoire && ( {item.territoire} )} {item.niveau && ( {item.niveau} )}
{item.datePublication && (
{formatDate(item.datePublication)}
)}
))}
); }