From 1c425f25ad6826f169813ea6cd895e0d6bd564ce Mon Sep 17 00:00:00 2001 From: Manus Date: Fri, 26 Jun 2026 10:22:13 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=204=20=C3=A9volutions=20UI=20sur=20?= =?UTF-8?q?VeilleDashboard=20:=201.=20Mode=20vignette=20par=20d=C3=A9faut?= =?UTF-8?q?=20(viewMode=20initialis=C3=A9=20=C3=A0=20"grid")=202.=20Altern?= =?UTF-8?q?ance=20de=20couleurs=20en=20mode=20liste=20:=20lignes=20paires?= =?UTF-8?q?=20=3D=20blanc,=20lignes=20impaires=20=3D=20gris=20clair=20(sla?= =?UTF-8?q?te-50)=203.=20Couleur=20de=20s=C3=A9lection=20au=20clic=20sur?= =?UTF-8?q?=20une=20ligne=20:=20bg-primary/10=20avec=20titre=20en=20couleu?= =?UTF-8?q?r=20primaire=204.=20Bouton=20segment=C3=A9=20Lu/Non=20lu/Tous?= =?UTF-8?q?=20au=20niveau=20des=20onglets=20=E2=80=94=20Non=20lu=20par=20d?= =?UTF-8?q?=C3=A9faut=205.=20Message=20d'=C3=A9tat=20vide=20adapt=C3=A9=20?= =?UTF-8?q?selon=20le=20filtre=20actif=20Planification=20RSS=20v=C3=A9rifi?= =?UTF-8?q?=C3=A9e=20:=20scheduleRssFetch()=20lit=20bien=20rss=5Fsettings?= =?UTF-8?q?=20(fetchMode,=20fetchIntervalMinutes,=20scheduledTime)=20depui?= =?UTF-8?q?s=20la=20BDD.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/public/__manus__/version.json | 4 +- client/src/pages/VeilleDashboard.tsx | 296 +++++++++++++++++++-------- todo.md | 7 +- 3 files changed, 221 insertions(+), 86 deletions(-) diff --git a/client/public/__manus__/version.json b/client/public/__manus__/version.json index 31a49ea..9ef06ca 100644 --- a/client/public/__manus__/version.json +++ b/client/public/__manus__/version.json @@ -1,4 +1,4 @@ { - "version": "f01773b1", - "timestamp": 1782481858153 + "version": "c7ed64d2", + "timestamp": 1782483733259 } \ No newline at end of file diff --git a/client/src/pages/VeilleDashboard.tsx b/client/src/pages/VeilleDashboard.tsx index 395bee5..d8d7473 100644 --- a/client/src/pages/VeilleDashboard.tsx +++ b/client/src/pages/VeilleDashboard.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect } from "react"; +import { useState, useMemo } from "react"; import { useLocalAuth } from "@/contexts/LocalAuthContext"; import { toast } from "sonner"; import { trpc } from "@/lib/trpc"; @@ -19,7 +19,6 @@ import { ExternalLink, Calendar, MapPin, - Tag, Layers, FileSearch, Loader2, @@ -30,6 +29,7 @@ import { BookOpen, Trash2, AlertTriangle, + EyeOff, } from "lucide-react"; import { AlertDialog, @@ -47,6 +47,7 @@ import { format } from "date-fns"; import { fr } from "date-fns/locale"; type TypeVeille = "reglementaire" | "concurrentielle" | "technologique" | "informationnelle"; +type ReadFilter = "unread" | "all" | "read"; interface VeilleItem { id: number; @@ -150,7 +151,6 @@ function VeilleDetailDialog({ {/* Métadonnées en grille */}
- {item.niveau && (
@@ -205,7 +205,6 @@ function VeilleDetailDialog({
)} - {/* Lien externe */} {item.lien && (
@@ -258,12 +257,19 @@ export default function VeilleDashboard() { toast.error(`Erreur lors de la purge : ${err.message}`); }, }); - const [viewMode, setViewMode] = useState<"list" | "grid">("list"); + + // ── É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>({}); const [selectedItem, setSelectedItem] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); + // 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.veille.filters.useQuery(); @@ -294,16 +300,22 @@ export default function VeilleDashboard() { 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 allItems = (itemsQuery.data?.items ?? []) as VeilleItem[]; const total = itemsQuery.data?.total ?? 0; const totalPages = Math.ceil(total / PAGE_SIZE); + // Filtrage Lu/Non lu côté client (readIds est local à la session) + 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: "niveau", label: "Niveau", options: filtersQuery.data?.niveaux ?? [] }, { key: "territoire", label: "Territoire", options: filtersQuery.data?.territoires ?? [] }, @@ -311,6 +323,14 @@ export default function VeilleDashboard() { { key: "dateTo", label: "Date jusqu'à", type: "date" as const }, ]; + // Labels et styles du bouton Lu/Non lu + const READ_FILTER_LABELS: Record = { + unread: "Non lus", + all: "Tous", + read: "Lus", + }; + const READ_FILTER_ORDER: ReadFilter[] = ["unread", "all", "read"]; + return (
{/* En-tête */} @@ -319,21 +339,22 @@ export default function VeilleDashboard() {

Veille Stratégique

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

Suivi réglementaire, concurrentiel, technologique et informationnel

-
+
{unreadCount > 0 && ( )} + {/* Boutons mode d'affichage */} @@ -341,7 +362,6 @@ export default function VeilleDashboard() { Vignettes {isAdmin && ( - <>
- {/* Onglets */} - { setActiveTab(v as TypeVeille | "all"); setPage(1); }}> - - Tous - {(Object.keys(TYPE_LABELS) as TypeVeille[]).map((t) => ( - {TYPE_LABELS[t]} + + {/* Onglets type de veille + bouton Lu/Non lu */} +
+ { setActiveTab(v as TypeVeille | "all"); setPage(1); }}> + + Tous + {(Object.keys(TYPE_LABELS) as TypeVeille[]).map((t) => ( + {TYPE_LABELS[t]} + ))} + + + + {/* Bouton segmenté Lu / Non lu */} +
+ {READ_FILTER_ORDER.map((f) => ( + ))} - - +
+
{/* Filtres */} -

Aucun résultat trouvé

-

Modifiez vos filtres ou importez des données

+

+ {readFilter === "unread" ? "Aucun article non lu" : "Aucun résultat trouvé"} +

+

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

) : viewMode === "list" ? ( - + ) : ( )} @@ -440,13 +497,25 @@ export default function VeilleDashboard() { // ─── Vue Liste ──────────────────────────────────────────────────────────────── -function VeilleListView({ items, onDetail, readIds }: { items: VeilleItem[]; onDetail: (item: VeilleItem) => void; readIds: Set }) { +function VeilleListView({ + items, + onDetail, + readIds, + selectedRowId, + onSelectRow, +}: { + items: VeilleItem[]; + onDetail: (item: VeilleItem) => void; + readIds: Set; + selectedRowId: number | null; + onSelectRow: (id: number | null) => void; +}) { return (
- + @@ -456,55 +525,86 @@ function VeilleListView({ items, onDetail, readIds }: { items: VeilleItem[]; onD - - {items.map((item, idx) => ( - - - + {items.map((item, idx) => { + const isSelected = selectedRowId === item.id; + const isRead = readIds.has(item.id); + // Alternance de couleurs : pair = blanc, impair = gris très léger + const isEven = idx % 2 === 0; + + return ( + onSelectRow(isSelected ? null : item.id)} + className={cn( + "border-b border-border/50 border-l-4 cursor-pointer transition-colors", + TYPE_ACCENT[item.typeVeille] || "border-l-transparent", + // Ligne sélectionnée — priorité maximale + isSelected + ? "bg-primary/10 hover:bg-primary/15" + // Non sélectionnée : alternance pair/impair + : isEven + ? "bg-white hover:bg-primary/5" + : "bg-slate-50/80 hover:bg-primary/5" + )} + > + + - - - - - + + + + - - ))} + + + {item.lien && ( + + + + )} + + + + ); + })}
# Titre TypeActions
{idx + 1} -
- {!readIds.has(item.id) && ( - - )} -
-

{item.titre}

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

{item.iaResume || item.resume}

} +
{idx + 1} +
+ {!isRead && ( + + )} +
+

+ {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 && ( - +
+ + {TYPE_LABELS[item.typeVeille as TypeVeille] || item.typeVeille} + + {item.niveau || "—"}{item.territoire || "—"} + {formatDate(item.datePublication) || "—"} + e.stopPropagation()}> +
+
-
@@ -514,18 +614,32 @@ function VeilleListView({ items, onDetail, readIds }: { items: VeilleItem[]; onD // ─── Vue Vignettes ──────────────────────────────────────────────────────────── -function VeilleGridView({ items, onDetail, readIds }: { items: VeilleItem[]; onDetail: (item: VeilleItem) => void; readIds: Set }) { +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}

+ {!readIds.has(item.id) && ( + + )} +

+ {item.titre} +

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

{item.iaResume || item.resume}

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

+ {item.iaResume || item.resume} +

+ )}
- {item.territoire && ( + {item.territoire && ( {item.territoire} diff --git a/todo.md b/todo.md index 2819954..531a461 100644 --- a/todo.md +++ b/todo.md @@ -168,5 +168,10 @@ - [x] rssEngine.ts : ajouter filtre `if (!aiResult.relevant) { skip; continue; }` avant insertion veille_items - [x] rssEngine.ts : même filtre avant insertion aap_items - [x] 21 tests passés, 0 erreur TypeScript -- [ ] Déployer en recette +- [x] Déployer en recette - [ ] Déployer en production +- [x] Mode vignette par défaut +- [x] Alternance de couleurs en mode liste (pair=blanc, impair=gris clair) +- [x] Couleur de sélection au clic sur une ligne (bg-primary/10) +- [x] Filtre Lu/Non lu avec bouton segmenté (Non lu par défaut) +- [x] Message vide adapté selon le filtre actif