diff --git a/client/public/__manus__/version.json b/client/public/__manus__/version.json index 9ef06ca..4a9c4c8 100644 --- a/client/public/__manus__/version.json +++ b/client/public/__manus__/version.json @@ -1,4 +1,4 @@ { - "version": "c7ed64d2", - "timestamp": 1782483733259 + "version": "d5ca5837", + "timestamp": 1782484339948 } \ No newline at end of file diff --git a/client/src/pages/AAPDashboard.tsx b/client/src/pages/AAPDashboard.tsx index 9a4ebae..40e6ffd 100644 --- a/client/src/pages/AAPDashboard.tsx +++ b/client/src/pages/AAPDashboard.tsx @@ -11,6 +11,7 @@ import { LayoutGrid, List, Eye, + EyeOff, ExternalLink, Calendar, MapPin, @@ -39,6 +40,7 @@ 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; @@ -136,10 +138,17 @@ export default function AAPDashboard() { 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>({}); + // 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(); @@ -168,10 +177,17 @@ export default function AAPDashboard() { setPage(1); }; - const items = (itemsQuery.data?.items ?? []) as AAPItem[]; + 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 ?? [] }, @@ -183,6 +199,13 @@ export default function AAPDashboard() { 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 */} @@ -191,20 +214,22 @@ export default function AAPDashboard() {

Appels à Projets

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

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

-
+
{unreadCount > 0 && ( )} + {/* Boutons mode d'affichage */} @@ -227,8 +252,8 @@ export default function AAPDashboard() {
-

Cette action va supprimer définitivement tous les appels à projets (Handicap, PA, Enfance, Précarité, Sanitaire et Autre).

-

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

+

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

+

Cette opération est irréversible.

@@ -248,15 +273,40 @@ export default function AAPDashboard() {
- {/* Onglets */} - { setActiveTab(v as AAPCategorie | "all"); setPage(1); }}> - - Tous - {categories.map((c) => ( - {c} + {/* 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 */} -

Aucun appel à projets trouvé

-

Modifiez vos filtres ou importez des données

+

+ {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 })} /> + markAsReadMutation.mutate({ articleId: id })} + selectedRowId={selectedRowId} + onSelectRow={setSelectedRowId} + /> ) : ( markAsReadMutation.mutate({ articleId: id })} /> )} @@ -303,13 +365,25 @@ export default function AAPDashboard() { // ─── Vue Liste ──────────────────────────────────────────────────────────────── -function AAPListView({ items, readIds, onMarkRead }: { items: AAPItem[]; readIds: Set; onMarkRead: (id: number) => void }) { +function AAPListView({ + items, + readIds, + onMarkRead, + selectedRowId, + onSelectRow, +}: { + items: AAPItem[]; + readIds: Set; + onMarkRead: (id: number) => void; + selectedRowId: number | null; + onSelectRow: (id: number | null) => void; +}) { return (
- + @@ -320,46 +394,85 @@ function AAPListView({ items, readIds, onMarkRead }: { items: AAPItem[]; readIds - - {items.map((item, idx) => ( - - - + {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égorieLien
{idx + 1} -
- {!readIds.has(item.id) && ( - - )} -
-

{item.titre}

- {item.iaResume &&

{item.iaResume}

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

+ {item.titre} +

+ {item.iaResume && ( +

{item.iaResume}

+ )} +
- -
- - {item.iaCategorie || item.categorie} - - {item.region || "—"}{item.departement || "—"}{formatDate(item.datePublication) || "—"} -
- {!readIds.has(item.id) && ( - - )} - {item.lien && ( - - - - )} -
-
+ + {catKey} + + {item.region || "—"}{item.departement || "—"} + {formatDate(item.datePublication) || "—"} + e.stopPropagation()}> +
+ {!isRead && ( + + )} + {item.lien && ( + + + + )} +
+
@@ -369,52 +482,96 @@ function AAPListView({ items, readIds, onMarkRead }: { items: AAPItem[]; readIds // ─── Vue Vignettes ──────────────────────────────────────────────────────────── -function AAPGridView({ items, readIds, onMarkRead }: { items: AAPItem[]; readIds: Set; onMarkRead: (id: number) => void }) { +function AAPGridView({ + items, + readIds, + onMarkRead, +}: { + items: AAPItem[]; + readIds: Set; + onMarkRead: (id: number) => void; +}) { return (
- {items.map((item) => ( - - -
- - {item.iaCategorie || item.categorie} - - {item.lien && ( - - - + {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}

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

{item.titre}

-
-
- -
- {item.region && ( - - {item.region} - - )} - {item.departement && ( - - {item.departement} - - )} -
-
- {item.datePublication && ( - - - {formatDate(item.datePublication)} - - )} - -
-
-
- ))} +
+ {item.region && ( + + {item.region} + + )} + {item.departement && ( + + {item.departement} + + )} +
+
+ {item.datePublication && ( + + + {formatDate(item.datePublication)} + + )} + +
+ + + ); + })}
); } diff --git a/todo.md b/todo.md index 531a461..ede053a 100644 --- a/todo.md +++ b/todo.md @@ -169,7 +169,7 @@ - [x] rssEngine.ts : même filtre avant insertion aap_items - [x] 21 tests passés, 0 erreur TypeScript - [x] Déployer en recette -- [ ] Déployer en production +- [ ] Déployer en production (en attente validation recette) - [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)