From c4dce77ecccb975ccc238c25172d4694b8c6f30d Mon Sep 17 00:00:00 2001 From: Manus Date: Wed, 1 Apr 2026 05:45:51 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Ajout=20de=20la=20vue=20liste=20(?= =?UTF-8?q?tableau)=20en=20compl=C3=A9ment=20de=20la=20vue=20vignettes=20?= =?UTF-8?q?=E2=80=94=20toggle=20Vignettes/Liste=20dans=20la=20barre=20de?= =?UTF-8?q?=20filtres,=20tableau=20avec=20tri=20par=20colonne,=20barres=20?= =?UTF-8?q?de=20v=C3=A9tust=C3=A9=20color=C3=A9es,=20budget=20en=20orange/?= =?UTF-8?q?vert.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/EtablissementListView.tsx | 181 ++++++++++++++++++ client/src/pages/Home.tsx | 99 +++++++--- 2 files changed, 254 insertions(+), 26 deletions(-) create mode 100644 client/src/components/EtablissementListView.tsx diff --git a/client/src/components/EtablissementListView.tsx b/client/src/components/EtablissementListView.tsx new file mode 100644 index 0000000..e61145c --- /dev/null +++ b/client/src/components/EtablissementListView.tsx @@ -0,0 +1,181 @@ +// EtablissementListView.tsx — Vue liste tableau des établissements +// Design: Corporate Modernism — Tableau dense avec indicateurs visuels de vétusté + +import { Monitor, Laptop, ChevronRight, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; +import type { Etablissement, SortField, SortOrder } from '../types/budget'; +import { formatEuros } from '../lib/format'; + +interface Props { + etablissements: Etablissement[]; + onSelect: (etab: Etablissement) => void; + sortField: SortField; + sortOrder: SortOrder; + onSort: (field: SortField) => void; +} + +function SortIcon({ field, sortField, sortOrder }: { field: SortField; sortField: SortField; sortOrder: SortOrder }) { + if (sortField !== field) return ; + return sortOrder === 'asc' + ? + : ; +} + +function VetusteBar({ value, total, color }: { value: number; total: number; color: string }) { + const pct = total > 0 ? Math.round((value / total) * 100) : 0; + const barColor = + pct >= 80 ? 'bg-red-500' : + pct >= 50 ? 'bg-orange-400' : + pct > 0 ? 'bg-yellow-400' : + 'bg-emerald-400'; + return ( +
+ = 80 ? 'text-red-600' : pct >= 50 ? 'text-orange-500' : pct > 0 ? 'text-yellow-600' : 'text-emerald-600'}`}> + {value} + +
+
+
+ {value}/{total} +
+ ); +} + +export function EtablissementListView({ etablissements, onSelect, sortField, sortOrder, onSort }: Props) { + const thClass = "px-3 py-2.5 text-left text-[11px] font-semibold uppercase tracking-wider text-muted-foreground select-none"; + const thSortClass = `${thClass} cursor-pointer hover:text-foreground hover:bg-muted/60 transition-colors`; + + return ( +
+ + + + + + + + + + + + + + {etablissements.map((etab, index) => { + const nbFixesTotal = etab.fixes?.length ?? 0; + const nbPortablesTotal = etab.portables?.length ?? 0; + const nbFixesR = etab.stats.nb_fixes_renouveler; + const nbPortablesR = etab.stats.nb_portables_renouveler; + const totalR = nbFixesR + nbPortablesR; + const budget = etab.stats.budget_total; + const hasBudget = budget > 0; + + return ( + onSelect(etab)} + className={` + border-b border-border last:border-0 cursor-pointer transition-colors + hover:bg-primary/5 group + ${index % 2 === 0 ? 'bg-card' : 'bg-muted/10'} + `} + > + {/* Code */} + + + {/* Nom */} + + + {/* PC Fixes */} + + + {/* PC Portables */} + + + {/* Total à renouveler */} + + + {/* Budget */} + + + {/* Chevron */} + + + ); + })} + +
Code onSort('nom')} + > +
+ Établissement + +
+
onSort('nb_fixes_renouveler')} + style={{ width: '200px' }} + > +
+ + PC Fixes + +
+
onSort('nb_portables_renouveler')} + style={{ width: '200px' }} + > +
+ + PC Portables + +
+
+ Total à renouveler + onSort('budget_total')} + style={{ width: '130px' }} + > +
+ Budget 2027 + +
+
+ + {etab.code} + + + + {etab.nom} + + + + + + + {totalR > 0 ? ( + + {totalR} + + ) : ( + + 0 + + )} + + + {formatEuros(budget)} + + + +
+ + {etablissements.length === 0 && ( +
+ Aucun établissement correspondant aux critères +
+ )} +
+ ); +} diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index aeeda80..cbef1ec 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -1,5 +1,5 @@ // Page principale — Budget Renouvellement SI 2027 - Itinova -// Design: Corporate Modernism — Sidebar bleu marine + grille de cards +// Design: Corporate Modernism — Sidebar bleu marine + grille de cards + vue liste import { useState } from 'react'; import { @@ -15,14 +15,19 @@ import { AlertTriangle, CheckCircle, ChevronDown, + LayoutGrid, + List, } from 'lucide-react'; import { useBudgetData } from '../hooks/useBudgetData'; import { formatEuros, formatNumber } from '../lib/format'; import { EtablissementCard } from '../components/EtablissementCard'; import { EtablissementDetail } from '../components/EtablissementDetail'; +import { EtablissementListView } from '../components/EtablissementListView'; import { KpiCard } from '../components/KpiCard'; import type { Etablissement, SortField, FilterType } from '../types/budget'; +type ViewMode = 'grid' | 'list'; + export default function Home() { const { meta, @@ -39,6 +44,7 @@ export default function Home() { const [selectedEtab, setSelectedEtab] = useState(null); const [sidebarOpen, setSidebarOpen] = useState(true); + const [viewMode, setViewMode] = useState('grid'); const sortOptions: { value: SortField; label: string }[] = [ { value: 'budget_total', label: 'Budget total' }, @@ -194,7 +200,7 @@ export default function Home() {
- {/* Barre de filtres */} + {/* Barre de filtres + toggle vue */}
{/* Recherche */} @@ -225,38 +231,71 @@ export default function Home() {
- {/* Tri */} -
- Trier par : -
- toggleSort(e.target.value as SortField)} + className="pl-3 pr-8 py-2 text-sm bg-muted/50 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 appearance-none cursor-pointer" + > + {sortOptions.map((opt) => ( + + ))} + +
+
+ )} + + {/* Spacer */} +
+ + {/* Toggle vue vignettes / liste */} +
+
- {/* Grille des établissements */} + {/* Contenu — vue vignettes ou liste */}
{etablissements.length === 0 ? (
@@ -266,7 +305,7 @@ export default function Home() { Modifiez vos critères de recherche ou de filtre.

- ) : ( + ) : viewMode === 'grid' ? (
{etablissements.map((etab, index) => ( ))}
+ ) : ( + setSelectedEtab(etab)} + sortField={sortField} + sortOrder={sortOrder} + onSort={toggleSort} + /> )}