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 (
-
+
| # |
Titre |
Type |
@@ -456,55 +525,86 @@ function VeilleListView({ items, onDetail, readIds }: { items: VeilleItem[]; onD
Actions |
-
- {items.map((item, idx) => (
-
- | {idx + 1} |
-
-
- {!readIds.has(item.id) && (
-
- )}
-
- {item.titre}
- {(item.iaResume || item.resume) && {item.iaResume || item.resume} }
+
+ {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"
+ )}
+ >
+ | {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()}>
+
+
- |
- |
- ))}
+
+
+ {item.lien && (
+
+
+
+ )}
+
+
+
+ );
+ })}
|
@@ -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