Checkpoint: Sélecteur d'année global (2025, 2026, 2027+) sur Renouvellement PC, CAPEX-Construction, CAPEX-Synthèse et DSI-OPEX. Données CAPEX isolées par année en localStorage. Titres dynamiques selon l'année sélectionnée.
This commit is contained in:
@@ -5,6 +5,7 @@ import { Route, Switch } from "wouter";
|
|||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||||
import { ParametresProvider } from "./contexts/ParametresContext";
|
import { ParametresProvider } from "./contexts/ParametresContext";
|
||||||
|
import { AnneeProvider } from "./contexts/AnneeContext";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
import Parametres from "./pages/Parametres";
|
import Parametres from "./pages/Parametres";
|
||||||
import Budget2027 from "./pages/Budget2027";
|
import Budget2027 from "./pages/Budget2027";
|
||||||
@@ -35,12 +36,14 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ParametresProvider>
|
<ParametresProvider>
|
||||||
<ThemeProvider defaultTheme="light">
|
<AnneeProvider>
|
||||||
<TooltipProvider>
|
<ThemeProvider defaultTheme="light">
|
||||||
<Toaster />
|
<TooltipProvider>
|
||||||
<Router />
|
<Toaster />
|
||||||
</TooltipProvider>
|
<Router />
|
||||||
</ThemeProvider>
|
</TooltipProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</AnneeProvider>
|
||||||
</ParametresProvider>
|
</ParametresProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|||||||
66
client/src/components/AnneeSelectorBar.tsx
Normal file
66
client/src/components/AnneeSelectorBar.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// AnneeSelectorBar.tsx — Sélecteur d'année réutilisable
|
||||||
|
// Utilisé dans : Renouvellement PC, CAPEX-Construction, CAPEX-Synthèse, DSI-OPEX
|
||||||
|
|
||||||
|
import { Calendar, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import { useAnnee, ANNEES_DISPONIBLES } from '../contexts/AnneeContext';
|
||||||
|
|
||||||
|
interface AnneeSelectorBarProps {
|
||||||
|
label?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnneeSelectorBar({ label, className = '' }: AnneeSelectorBarProps) {
|
||||||
|
const { annee, setAnnee } = useAnnee();
|
||||||
|
const idx = ANNEES_DISPONIBLES.indexOf(annee);
|
||||||
|
|
||||||
|
const prev = () => { if (idx > 0) setAnnee(ANNEES_DISPONIBLES[idx - 1]); };
|
||||||
|
const next = () => { if (idx < ANNEES_DISPONIBLES.length - 1) setAnnee(ANNEES_DISPONIBLES[idx + 1]); };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center gap-2 ${className}`}>
|
||||||
|
{label && (
|
||||||
|
<span className="text-xs text-muted-foreground font-medium mr-1">{label}</span>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-1 bg-card border border-border rounded-lg px-1 py-0.5 shadow-sm">
|
||||||
|
<button
|
||||||
|
onClick={prev}
|
||||||
|
disabled={idx === 0}
|
||||||
|
className="p-1 rounded hover:bg-muted/60 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
|
title="Année précédente"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Boutons d'année */}
|
||||||
|
<div className="flex items-center gap-0.5">
|
||||||
|
{ANNEES_DISPONIBLES.map(a => (
|
||||||
|
<button
|
||||||
|
key={a}
|
||||||
|
onClick={() => setAnnee(a)}
|
||||||
|
className={`px-2.5 py-1 rounded text-xs font-semibold transition-all duration-150 ${
|
||||||
|
a === annee
|
||||||
|
? 'bg-primary text-white shadow-sm'
|
||||||
|
: 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{a}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={next}
|
||||||
|
disabled={idx === ANNEES_DISPONIBLES.length - 1}
|
||||||
|
className="p-1 rounded hover:bg-muted/60 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
|
title="Année suivante"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
|
<Calendar className="w-3.5 h-3.5" />
|
||||||
|
<span>Exercice {annee}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
client/src/contexts/AnneeContext.tsx
Normal file
52
client/src/contexts/AnneeContext.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// AnneeContext.tsx — Contexte global pour l'année sélectionnée
|
||||||
|
// Partagé entre toutes les pages : Renouvellement PC, CAPEX-Construction, CAPEX-Synthèse, DSI-OPEX
|
||||||
|
|
||||||
|
import { createContext, useContext, useState, ReactNode } from 'react';
|
||||||
|
|
||||||
|
export const ANNEES_DISPONIBLES = [2025, 2026, 2027, 2028, 2029, 2030];
|
||||||
|
export const ANNEE_DEFAUT = 2027;
|
||||||
|
|
||||||
|
interface AnneeContextType {
|
||||||
|
annee: number;
|
||||||
|
setAnnee: (a: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnneeContext = createContext<AnneeContextType>({
|
||||||
|
annee: ANNEE_DEFAUT,
|
||||||
|
setAnnee: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function AnneeProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [annee, setAnneeState] = useState<number>(() => {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('itinova_annee_selectionnee');
|
||||||
|
const parsed = stored ? parseInt(stored, 10) : ANNEE_DEFAUT;
|
||||||
|
return ANNEES_DISPONIBLES.includes(parsed) ? parsed : ANNEE_DEFAUT;
|
||||||
|
} catch { return ANNEE_DEFAUT; }
|
||||||
|
});
|
||||||
|
|
||||||
|
const setAnnee = (a: number) => {
|
||||||
|
setAnneeState(a);
|
||||||
|
try { localStorage.setItem('itinova_annee_selectionnee', String(a)); } catch {}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnneeContext.Provider value={{ annee, setAnnee }}>
|
||||||
|
{children}
|
||||||
|
</AnneeContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAnnee() {
|
||||||
|
return useContext(AnneeContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Génère la clé localStorage pour les saisies CAPEX d'une année donnée
|
||||||
|
export function getCapexStorageKey(annee: number): string {
|
||||||
|
return `itinova_budget_capex_${annee}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Génère la clé localStorage pour les saisies OPEX d'une année donnée
|
||||||
|
export function getOpexStorageKey(annee: number): string {
|
||||||
|
return `itinova_budget_opex_${annee}`;
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@ import {
|
|||||||
AlertCircle,
|
AlertCircle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { AppSidebar } from '../components/AppSidebar';
|
import { AppSidebar } from '../components/AppSidebar';
|
||||||
|
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||||
|
import { useAnnee, getCapexStorageKey } from '../contexts/AnneeContext';
|
||||||
import bp2027Raw from '../data_bp2027.json';
|
import bp2027Raw from '../data_bp2027.json';
|
||||||
import budgetRaw from '../data_budget.json';
|
import budgetRaw from '../data_budget.json';
|
||||||
import { useParametres } from '../contexts/ParametresContext';
|
import { useParametres } from '../contexts/ParametresContext';
|
||||||
@@ -35,7 +37,7 @@ import type { BudgetData } from '../types/budget';
|
|||||||
const bp2027Data = bp2027Raw as BP2027Data;
|
const bp2027Data = bp2027Raw as BP2027Data;
|
||||||
const budgetData = budgetRaw as BudgetData;
|
const budgetData = budgetRaw as BudgetData;
|
||||||
|
|
||||||
const STORAGE_KEY = 'itinova_budget2027_saisies';
|
// La clé de stockage est désormais dynamique par année (voir getCapexStorageKey)
|
||||||
|
|
||||||
// Icônes par clé de champ
|
// Icônes par clé de champ
|
||||||
const ICON_MAP: Record<string, React.ElementType> = {
|
const ICON_MAP: Record<string, React.ElementType> = {
|
||||||
@@ -87,15 +89,15 @@ const CHAMPS: { key: keyof BudgetFormValues; label: string; description: string
|
|||||||
{ key: 'autres', label: 'Autres investissements SI', description: 'Tout autre investissement informatique' },
|
{ key: 'autres', label: 'Autres investissements SI', description: 'Tout autre investissement informatique' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function loadSaisies(): Record<string, Partial<BudgetFormValues>> {
|
function loadSaisiesForAnnee(annee: number): Record<string, Partial<BudgetFormValues>> {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
const raw = localStorage.getItem(getCapexStorageKey(annee));
|
||||||
return raw ? JSON.parse(raw) : {};
|
return raw ? JSON.parse(raw) : {};
|
||||||
} catch { return {}; }
|
} catch { return {}; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSaisies(data: Record<string, Partial<BudgetFormValues>>) {
|
function saveSaisiesForAnnee(annee: number, data: Record<string, Partial<BudgetFormValues>>) {
|
||||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch {}
|
try { localStorage.setItem(getCapexStorageKey(annee), JSON.stringify(data)); } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultValues(etabCode: string, renouvellement2027: number): BudgetFormValues {
|
function getDefaultValues(etabCode: string, renouvellement2027: number): BudgetFormValues {
|
||||||
@@ -167,8 +169,9 @@ function CurrencyInput({
|
|||||||
export default function Budget2027() {
|
export default function Budget2027() {
|
||||||
const [, navigate] = useLocation();
|
const [, navigate] = useLocation();
|
||||||
const { parametres } = useParametres();
|
const { parametres } = useParametres();
|
||||||
|
const { annee } = useAnnee();
|
||||||
const [selectedCode, setSelectedCode] = useState<string>('');
|
const [selectedCode, setSelectedCode] = useState<string>('');
|
||||||
const [saisies, setSaisies] = useState<Record<string, Partial<BudgetFormValues>>>(loadSaisies);
|
const [saisies, setSaisies] = useState<Record<string, Partial<BudgetFormValues>>>(() => loadSaisiesForAnnee(annee));
|
||||||
const [saved, setSaved] = useState(false);
|
const [saved, setSaved] = useState(false);
|
||||||
const [searchEtab, setSearchEtab] = useState('');
|
const [searchEtab, setSearchEtab] = useState('');
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
@@ -213,6 +216,12 @@ export default function Budget2027() {
|
|||||||
);
|
);
|
||||||
}, [formValues]);
|
}, [formValues]);
|
||||||
|
|
||||||
|
// Recharger les saisies quand l'année change
|
||||||
|
useEffect(() => {
|
||||||
|
setSaisies(loadSaisiesForAnnee(annee));
|
||||||
|
setSelectedCode('');
|
||||||
|
}, [annee]);
|
||||||
|
|
||||||
const updateField = (key: keyof BudgetFormValues, value: number | string) => {
|
const updateField = (key: keyof BudgetFormValues, value: number | string) => {
|
||||||
setSaisies(prev => {
|
setSaisies(prev => {
|
||||||
const next = {
|
const next = {
|
||||||
@@ -222,14 +231,14 @@ export default function Budget2027() {
|
|||||||
[key]: value,
|
[key]: value,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
saveSaisies(next);
|
saveSaisiesForAnnee(annee, next);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
setSaved(false);
|
setSaved(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
saveSaisies(saisies);
|
saveSaisiesForAnnee(annee, saisies);
|
||||||
setSaved(true);
|
setSaved(true);
|
||||||
setTimeout(() => setSaved(false), 2500);
|
setTimeout(() => setSaved(false), 2500);
|
||||||
};
|
};
|
||||||
@@ -238,7 +247,7 @@ export default function Budget2027() {
|
|||||||
setSaisies(prev => {
|
setSaisies(prev => {
|
||||||
const next = { ...prev };
|
const next = { ...prev };
|
||||||
delete next[selectedCode];
|
delete next[selectedCode];
|
||||||
saveSaisies(next);
|
saveSaisiesForAnnee(annee, next);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -265,12 +274,15 @@ export default function Budget2027() {
|
|||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||||
Construction Budget 2027
|
CAPEX - Construction {annee}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-0.5">
|
<p className="text-sm text-muted-foreground mt-0.5">
|
||||||
Campagne budgétaire CAPEX DSI — Prévisionnel 2027
|
Campagne budgétaire CAPEX DSI — Prévisionnel {annee}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<AnneeSelectorBar />
|
||||||
|
</div>
|
||||||
{selectedCode && (
|
{selectedCode && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
Info,
|
Info,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { AppSidebar } from '../components/AppSidebar';
|
import { AppSidebar } from '../components/AppSidebar';
|
||||||
|
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||||
|
import { useAnnee, getCapexStorageKey } from '../contexts/AnneeContext';
|
||||||
import bp2027Raw from '../data_bp2027.json';
|
import bp2027Raw from '../data_bp2027.json';
|
||||||
import budgetRaw from '../data_budget.json';
|
import budgetRaw from '../data_budget.json';
|
||||||
import { useParametres } from '../contexts/ParametresContext';
|
import { useParametres } from '../contexts/ParametresContext';
|
||||||
@@ -27,7 +29,7 @@ import type { BudgetData } from '../types/budget';
|
|||||||
|
|
||||||
const bp2027Data = bp2027Raw as BP2027Data;
|
const bp2027Data = bp2027Raw as BP2027Data;
|
||||||
const budgetData = budgetRaw as BudgetData;
|
const budgetData = budgetRaw as BudgetData;
|
||||||
const STORAGE_KEY = 'itinova_budget2027_saisies';
|
// Clé de stockage dynamique par année (voir getCapexStorageKey)
|
||||||
|
|
||||||
// Colonnes du tableau (correspondance exacte avec le fichier Excel BP2027)
|
// Colonnes du tableau (correspondance exacte avec le fichier Excel BP2027)
|
||||||
const COLONNES = [
|
const COLONNES = [
|
||||||
@@ -56,9 +58,9 @@ const COL_COLORS: Record<string, { header: string; cell: string; total: string }
|
|||||||
slate: { header: 'bg-slate-500 text-white', cell: 'text-slate-700', total: 'bg-slate-50 text-slate-800 font-semibold' },
|
slate: { header: 'bg-slate-500 text-white', cell: 'text-slate-700', total: 'bg-slate-50 text-slate-800 font-semibold' },
|
||||||
};
|
};
|
||||||
|
|
||||||
function loadSaisies(): Record<string, Partial<BudgetFormValues>> {
|
function loadSaisiesForAnnee(annee: number): Record<string, Partial<BudgetFormValues>> {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
const raw = localStorage.getItem(getCapexStorageKey(annee));
|
||||||
return raw ? JSON.parse(raw) : {};
|
return raw ? JSON.parse(raw) : {};
|
||||||
} catch { return {}; }
|
} catch { return {}; }
|
||||||
}
|
}
|
||||||
@@ -83,6 +85,7 @@ type SortDir = 'asc' | 'desc';
|
|||||||
|
|
||||||
export default function DsiCapex() {
|
export default function DsiCapex() {
|
||||||
const { parametres } = useParametres();
|
const { parametres } = useParametres();
|
||||||
|
const { annee } = useAnnee();
|
||||||
const [saisies, setSaisies] = useState<Record<string, Partial<BudgetFormValues>>>({});
|
const [saisies, setSaisies] = useState<Record<string, Partial<BudgetFormValues>>>({});
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [sortCol, setSortCol] = useState<string>('nom');
|
const [sortCol, setSortCol] = useState<string>('nom');
|
||||||
@@ -91,8 +94,8 @@ export default function DsiCapex() {
|
|||||||
const [lastRefresh, setLastRefresh] = useState(Date.now());
|
const [lastRefresh, setLastRefresh] = useState(Date.now());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSaisies(loadSaisies());
|
setSaisies(loadSaisiesForAnnee(annee));
|
||||||
}, [lastRefresh]);
|
}, [lastRefresh, annee]);
|
||||||
|
|
||||||
const handleRefresh = () => setLastRefresh(Date.now());
|
const handleRefresh = () => setLastRefresh(Date.now());
|
||||||
|
|
||||||
@@ -191,19 +194,22 @@ export default function DsiCapex() {
|
|||||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||||
DSI CAPEX — Prévisionnel Budget 2027
|
CAPEX - Synthèse {annee}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-0.5">
|
<p className="text-sm text-muted-foreground mt-0.5">
|
||||||
Tableau consolidé des investissements SI par établissement
|
Tableau consolidé des investissements SI par établissement
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="flex items-center gap-3">
|
||||||
onClick={handleRefresh}
|
<AnneeSelectorBar />
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border text-sm text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
|
<button
|
||||||
>
|
onClick={handleRefresh}
|
||||||
<RefreshCw className="w-4 h-4" />
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border text-sm text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
|
||||||
Actualiser
|
>
|
||||||
</button>
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
Actualiser
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -213,7 +219,7 @@ export default function DsiCapex() {
|
|||||||
<div className="bg-card border border-border rounded-xl p-4">
|
<div className="bg-card border border-border rounded-xl p-4">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<Euro className="w-4 h-4 text-orange-500" />
|
<Euro className="w-4 h-4 text-orange-500" />
|
||||||
<span className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Total CAPEX 2027</span>
|
<span className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Total CAPEX {annee}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-bold text-orange-600 tabular-nums" style={{ fontFamily: 'Sora, sans-serif' }}>
|
<p className="text-2xl font-bold text-orange-600 tabular-nums" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||||
{formatEuros(totalGeneral)}
|
{formatEuros(totalGeneral)}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { AppSidebar } from '../components/AppSidebar';
|
import { AppSidebar } from '../components/AppSidebar';
|
||||||
|
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||||
|
import { useAnnee } from '../contexts/AnneeContext';
|
||||||
import opexRaw from '../data_opex.json';
|
import opexRaw from '../data_opex.json';
|
||||||
import { formatEuros } from '../lib/format';
|
import { formatEuros } from '../lib/format';
|
||||||
|
|
||||||
@@ -82,7 +84,7 @@ type ViewMode = 'etablissements' | 'postes';
|
|||||||
type SortDir = 'asc' | 'desc';
|
type SortDir = 'asc' | 'desc';
|
||||||
|
|
||||||
export default function DsiOpex() {
|
export default function DsiOpex() {
|
||||||
const [selectedYear, setSelectedYear] = useState<2026 | 2027>(2026);
|
const { annee } = useAnnee();
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('postes');
|
const [viewMode, setViewMode] = useState<ViewMode>('postes');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [selectedCategorie, setSelectedCategorie] = useState('Toutes');
|
const [selectedCategorie, setSelectedCategorie] = useState('Toutes');
|
||||||
@@ -190,22 +192,11 @@ export default function DsiOpex() {
|
|||||||
{opexData.meta.nb_postes} postes de charges · {opexData.meta.nb_etablissements} établissements
|
{opexData.meta.nb_postes} postes de charges · {opexData.meta.nb_etablissements} établissements
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/* Sélecteur d'année */}
|
{/* Sélecteur d'année — remplacé par AnneeSelectorBar global */}
|
||||||
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
|
<div className="flex items-center gap-3">
|
||||||
{([2026, 2027] as const).map(year => (
|
<AnneeSelectorBar />
|
||||||
<button
|
|
||||||
key={year}
|
|
||||||
onClick={() => setSelectedYear(year)}
|
|
||||||
className={`px-4 py-1.5 rounded-md text-sm font-semibold transition-all ${
|
|
||||||
selectedYear === year
|
|
||||||
? 'bg-primary text-white shadow-sm'
|
|
||||||
: 'text-muted-foreground hover:text-foreground'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{year}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -215,7 +206,7 @@ export default function DsiOpex() {
|
|||||||
<div className="bg-card border border-border rounded-xl p-4">
|
<div className="bg-card border border-border rounded-xl p-4">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<Euro className="w-4 h-4 text-orange-500" />
|
<Euro className="w-4 h-4 text-orange-500" />
|
||||||
<span className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Total OPEX {selectedYear}</span>
|
<span className="text-xs text-muted-foreground uppercase tracking-wide font-medium">Total OPEX {annee}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-bold text-orange-600 tabular-nums" style={{ fontFamily: 'Sora, sans-serif' }}>
|
<p className="text-2xl font-bold text-orange-600 tabular-nums" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||||
{formatEuros(opexData.total_global)}
|
{formatEuros(opexData.total_global)}
|
||||||
@@ -354,7 +345,7 @@ export default function DsiOpex() {
|
|||||||
{showDetails && <th className="text-left px-4 py-3 font-medium text-muted-foreground hidden xl:table-cell">Compte</th>}
|
{showDetails && <th className="text-left px-4 py-3 font-medium text-muted-foreground hidden xl:table-cell">Compte</th>}
|
||||||
<th className="text-right px-4 py-3 font-medium text-muted-foreground">Budget N-1</th>
|
<th className="text-right px-4 py-3 font-medium text-muted-foreground">Budget N-1</th>
|
||||||
<th className="text-right px-4 py-3 font-medium text-muted-foreground">
|
<th className="text-right px-4 py-3 font-medium text-muted-foreground">
|
||||||
<SortBtn col="montant" label={`Prév. ${selectedYear}`} />
|
<SortBtn col="montant" label={`Prév. ${annee}`} />
|
||||||
</th>
|
</th>
|
||||||
<th className="text-right px-4 py-3 font-medium text-muted-foreground w-24">Répartition</th>
|
<th className="text-right px-4 py-3 font-medium text-muted-foreground w-24">Répartition</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -441,7 +432,7 @@ export default function DsiOpex() {
|
|||||||
<tfoot>
|
<tfoot>
|
||||||
<tr className="bg-muted/40 border-t-2 border-border">
|
<tr className="bg-muted/40 border-t-2 border-border">
|
||||||
<td colSpan={showDetails ? 7 : 6} className="px-4 py-3 font-bold text-foreground">
|
<td colSpan={showDetails ? 7 : 6} className="px-4 py-3 font-bold text-foreground">
|
||||||
TOTAL {selectedCategorie !== 'Toutes' ? selectedCategorie : 'OPEX DSI'} {selectedYear}
|
TOTAL {selectedCategorie !== 'Toutes' ? selectedCategorie : 'OPEX DSI'} {annee}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-right font-bold text-orange-600 text-base tabular-nums">
|
<td className="px-4 py-3 text-right font-bold text-orange-600 text-base tabular-nums">
|
||||||
{formatEuros(totalFiltre)}
|
{formatEuros(totalFiltre)}
|
||||||
@@ -460,7 +451,7 @@ export default function DsiOpex() {
|
|||||||
<div className="mt-6 bg-card border border-border rounded-xl p-5">
|
<div className="mt-6 bg-card border border-border rounded-xl p-5">
|
||||||
<h3 className="font-semibold text-foreground mb-4 flex items-center gap-2" style={{ fontFamily: 'Sora, sans-serif' }}>
|
<h3 className="font-semibold text-foreground mb-4 flex items-center gap-2" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||||
<BarChart3 className="w-4 h-4 text-muted-foreground" />
|
<BarChart3 className="w-4 h-4 text-muted-foreground" />
|
||||||
Répartition par catégorie — {selectedYear}
|
Répartition par catégorie — {annee}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{Object.entries(totalParCategorie)
|
{Object.entries(totalParCategorie)
|
||||||
@@ -518,7 +509,7 @@ export default function DsiOpex() {
|
|||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
<th className="text-right px-4 py-3 font-medium text-muted-foreground">
|
<th className="text-right px-4 py-3 font-medium text-muted-foreground">
|
||||||
<SortBtn col="total" label={`Total ${selectedYear}`} />
|
<SortBtn col="total" label={`Total ${annee}`} />
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
List,
|
List,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { AppSidebar } from '../components/AppSidebar';
|
import { AppSidebar } from '../components/AppSidebar';
|
||||||
|
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||||
|
import { useAnnee } from '../contexts/AnneeContext';
|
||||||
import { useBudgetData } from '../hooks/useBudgetData';
|
import { useBudgetData } from '../hooks/useBudgetData';
|
||||||
import { formatEuros, formatNumber } from '../lib/format';
|
import { formatEuros, formatNumber } from '../lib/format';
|
||||||
import { EtablissementCard } from '../components/EtablissementCard';
|
import { EtablissementCard } from '../components/EtablissementCard';
|
||||||
@@ -42,6 +44,7 @@ export default function Home() {
|
|||||||
setFilterType,
|
setFilterType,
|
||||||
} = useBudgetData();
|
} = useBudgetData();
|
||||||
|
|
||||||
|
const { annee } = useAnnee();
|
||||||
const [selectedEtab, setSelectedEtab] = useState<Etablissement | null>(null);
|
const [selectedEtab, setSelectedEtab] = useState<Etablissement | null>(null);
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||||
@@ -71,13 +74,14 @@ export default function Home() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||||
Renouvellement PC 2027
|
Renouvellement PC {annee}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-0.5">
|
<p className="text-sm text-muted-foreground mt-0.5">
|
||||||
{totalEtablissements} établissements Itinova — Vétusté du parc PC
|
{totalEtablissements} établissements Itinova — Vétusté du parc PC
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
|
<AnneeSelectorBar />
|
||||||
<span className="text-xs text-muted-foreground bg-muted px-2.5 py-1 rounded-full">
|
<span className="text-xs text-muted-foreground bg-muted px-2.5 py-1 rounded-full">
|
||||||
{etablissements.length} affiché{etablissements.length > 1 ? 's' : ''}
|
{etablissements.length} affiché{etablissements.length > 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
@@ -92,7 +96,7 @@ export default function Home() {
|
|||||||
icon={Euro}
|
icon={Euro}
|
||||||
label="Budget global"
|
label="Budget global"
|
||||||
value={formatEuros(meta.budget_global)}
|
value={formatEuros(meta.budget_global)}
|
||||||
subtitle="TTC — exercice 2027"
|
subtitle={`TTC — exercice ${annee}`}
|
||||||
accentColor="text-orange-600"
|
accentColor="text-orange-600"
|
||||||
delay={0}
|
delay={0}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user