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 (
+
+
+
+
+ | 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
+
+
+ |
+ |
+
+
+
+ {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 */}
+ |
+
+ {etab.code}
+
+ |
+
+ {/* Nom */}
+
+
+ {etab.nom}
+
+ |
+
+ {/* PC Fixes */}
+
+
+ |
+
+ {/* PC Portables */}
+
+
+ |
+
+ {/* Total à renouveler */}
+
+ {totalR > 0 ? (
+
+ {totalR}
+
+ ) : (
+
+ 0
+
+ )}
+ |
+
+ {/* Budget */}
+
+
+ {formatEuros(budget)}
+
+ |
+
+ {/* Chevron */}
+
+
+ |
+
+ );
+ })}
+
+
+
+ {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 :
-
-
- {/* 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}
+ />
)}