Checkpoint: Renouvellement PC et CAPEX-Synthèse : KPIs, barre de filtres et tableau sont masqués pour les années sans données importées/saisies. Seul l'écran vide explicite s'affiche. Les années 2026-2027 restent pleinement fonctionnelles sur Renouvellement PC. CAPEX-Synthèse se vide dès qu'aucun budget n'a été saisi pour l'année sélectionnée.
This commit is contained in:
@@ -172,6 +172,9 @@ export default function DsiCapex() {
|
|||||||
const totalGeneral = useMemo(() => sortedLignes.reduce((s, l) => s + l.total, 0), [sortedLignes]);
|
const totalGeneral = useMemo(() => sortedLignes.reduce((s, l) => s + l.total, 0), [sortedLignes]);
|
||||||
const nbFilled = useMemo(() => lignes.filter(l => l.hasSaisie).length, [lignes]);
|
const nbFilled = useMemo(() => lignes.filter(l => l.hasSaisie).length, [lignes]);
|
||||||
|
|
||||||
|
// La synthèse n'affiche des données que si au moins un établissement a été saisi pour cette année
|
||||||
|
const hasDonnees = nbFilled > 0;
|
||||||
|
|
||||||
const handleSort = (col: string) => {
|
const handleSort = (col: string) => {
|
||||||
if (sortCol === col) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
|
if (sortCol === col) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
|
||||||
else { setSortCol(col); setSortDir('asc'); }
|
else { setSortCol(col); setSortDir('asc'); }
|
||||||
@@ -213,7 +216,8 @@ export default function DsiCapex() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* KPIs */}
|
{/* KPIs — uniquement si données saisies pour cette année */}
|
||||||
|
{hasDonnees && (
|
||||||
<div className="px-6 py-4 border-b border-border bg-muted/20 flex-shrink-0">
|
<div className="px-6 py-4 border-b border-border bg-muted/20 flex-shrink-0">
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
<div className="bg-card border border-border rounded-xl p-4">
|
<div className="bg-card border border-border rounded-xl p-4">
|
||||||
@@ -260,8 +264,10 @@ export default function DsiCapex() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Barre d'outils */}
|
{/* Barre d'outils — uniquement si données saisies */}
|
||||||
|
{hasDonnees && (
|
||||||
<div className="px-6 py-3 border-b border-border bg-background flex-shrink-0 flex flex-wrap items-center gap-3">
|
<div className="px-6 py-3 border-b border-border bg-background flex-shrink-0 flex flex-wrap items-center gap-3">
|
||||||
<div className="relative flex-1 min-w-48 max-w-72">
|
<div className="relative flex-1 min-w-48 max-w-72">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
@@ -287,9 +293,24 @@ export default function DsiCapex() {
|
|||||||
<span>{sortedLignes.length} établissement{sortedLignes.length > 1 ? 's' : ''} affichés</span>
|
<span>{sortedLignes.length} établissement{sortedLignes.length > 1 ? 's' : ''} affichés</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Tableau principal — scroll horizontal */}
|
{/* Tableau principal — scroll horizontal ou état vide */}
|
||||||
<div className="flex-1 overflow-auto px-6 py-4">
|
<div className="flex-1 overflow-auto px-6 py-4">
|
||||||
|
{!hasDonnees ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||||
|
<div className="w-16 h-16 rounded-2xl bg-muted/40 flex items-center justify-center mb-4">
|
||||||
|
<FileSpreadsheet className="w-8 h-8 text-muted-foreground/40" />
|
||||||
|
</div>
|
||||||
|
<p className="text-lg font-semibold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||||
|
Aucune donnée pour {annee}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2 max-w-sm">
|
||||||
|
Aucun budget CAPEX n'a encore été saisi pour l'exercice {annee}.
|
||||||
|
Utilisez la page <strong>CAPEX - Construction</strong> pour saisir les budgets des établissements.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="min-w-max">
|
<div className="min-w-max">
|
||||||
<table className="w-full text-xs border-collapse shadow-sm">
|
<table className="w-full text-xs border-collapse shadow-sm">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -442,9 +463,10 @@ export default function DsiCapex() {
|
|||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Message si aucun résultat */}
|
{/* Message si aucun résultat (visible uniquement quand hasDonnees=true mais filtre vide) */}
|
||||||
{sortedLignes.length === 0 && (
|
{hasDonnees && sortedLignes.length === 0 && (
|
||||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||||
<FileSpreadsheet className="w-12 h-12 text-muted-foreground/30 mb-3" />
|
<FileSpreadsheet className="w-12 h-12 text-muted-foreground/30 mb-3" />
|
||||||
<p className="text-muted-foreground font-medium">Aucun établissement trouvé</p>
|
<p className="text-muted-foreground font-medium">Aucun établissement trouvé</p>
|
||||||
|
|||||||
@@ -92,53 +92,56 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* KPIs globaux */}
|
{/* KPIs globaux — uniquement si données disponibles */}
|
||||||
<div className="px-6 py-4 border-b border-border bg-muted/20 flex-shrink-0">
|
{hasDonnees && (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
<div className="px-6 py-4 border-b border-border bg-muted/20 flex-shrink-0">
|
||||||
<KpiCard
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||||
icon={Euro}
|
<KpiCard
|
||||||
label="Budget global"
|
icon={Euro}
|
||||||
value={formatEuros(meta.budget_global)}
|
label="Budget global"
|
||||||
subtitle={`TTC — exercice ${annee}`}
|
value={formatEuros(meta.budget_global)}
|
||||||
accentColor="text-orange-600"
|
subtitle={`TTC — exercice ${annee}`}
|
||||||
delay={0}
|
accentColor="text-orange-600"
|
||||||
/>
|
delay={0}
|
||||||
<KpiCard
|
/>
|
||||||
icon={Building2}
|
<KpiCard
|
||||||
label="Établissements"
|
icon={Building2}
|
||||||
value={formatNumber(meta.nb_etablissements)}
|
label="Établissements"
|
||||||
subtitle="dans le périmètre"
|
value={formatNumber(meta.nb_etablissements)}
|
||||||
accentColor="text-primary"
|
subtitle="dans le périmètre"
|
||||||
delay={60}
|
accentColor="text-primary"
|
||||||
/>
|
delay={60}
|
||||||
<KpiCard
|
/>
|
||||||
icon={Monitor}
|
<KpiCard
|
||||||
label="PC Fixes à renouveler"
|
icon={Monitor}
|
||||||
value={formatNumber(meta.total_fixes_renouveler)}
|
label="PC Fixes à renouveler"
|
||||||
subtitle={`sur ${formatNumber(meta.total_fixes)} au total`}
|
value={formatNumber(meta.total_fixes_renouveler)}
|
||||||
accentColor="text-primary"
|
subtitle={`sur ${formatNumber(meta.total_fixes)} au total`}
|
||||||
delay={120}
|
accentColor="text-primary"
|
||||||
/>
|
delay={120}
|
||||||
<KpiCard
|
/>
|
||||||
icon={Laptop}
|
<KpiCard
|
||||||
label="PC Portables à renouveler"
|
icon={Laptop}
|
||||||
value={formatNumber(meta.total_portables_renouveler)}
|
label="PC Portables à renouveler"
|
||||||
subtitle={`sur ${formatNumber(meta.total_portables)} au total`}
|
value={formatNumber(meta.total_portables_renouveler)}
|
||||||
accentColor="text-primary"
|
subtitle={`sur ${formatNumber(meta.total_portables)} au total`}
|
||||||
delay={180}
|
accentColor="text-primary"
|
||||||
/>
|
delay={180}
|
||||||
<KpiCard
|
/>
|
||||||
icon={AlertTriangle}
|
<KpiCard
|
||||||
label="Équipements à renouveler"
|
icon={AlertTriangle}
|
||||||
value={formatNumber(meta.total_fixes_renouveler + meta.total_portables_renouveler)}
|
label="Équipements à renouveler"
|
||||||
subtitle="fixes + portables"
|
value={formatNumber(meta.total_fixes_renouveler + meta.total_portables_renouveler)}
|
||||||
accentColor="text-orange-500"
|
subtitle="fixes + portables"
|
||||||
delay={240}
|
accentColor="text-orange-500"
|
||||||
/>
|
delay={240}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Barre de filtres + toggle vue */}
|
{/* Barre de filtres + toggle vue — uniquement si données disponibles */}
|
||||||
|
{hasDonnees && (
|
||||||
<div className="px-6 py-3 border-b border-border bg-card flex-shrink-0">
|
<div className="px-6 py-3 border-b border-border bg-card flex-shrink-0">
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
{/* Recherche */}
|
{/* Recherche */}
|
||||||
@@ -232,6 +235,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Contenu — vue vignettes ou liste */}
|
{/* Contenu — vue vignettes ou liste */}
|
||||||
<div className="flex-1 overflow-y-auto px-6 py-5">
|
<div className="flex-1 overflow-y-auto px-6 py-5">
|
||||||
|
|||||||
Reference in New Issue
Block a user