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:
@@ -25,6 +25,8 @@ import {
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||
import { useAnnee, getCapexStorageKey } from '../contexts/AnneeContext';
|
||||
import bp2027Raw from '../data_bp2027.json';
|
||||
import budgetRaw from '../data_budget.json';
|
||||
import { useParametres } from '../contexts/ParametresContext';
|
||||
@@ -35,7 +37,7 @@ import type { BudgetData } from '../types/budget';
|
||||
const bp2027Data = bp2027Raw as BP2027Data;
|
||||
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
|
||||
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' },
|
||||
];
|
||||
|
||||
function loadSaisies(): Record<string, Partial<BudgetFormValues>> {
|
||||
function loadSaisiesForAnnee(annee: number): Record<string, Partial<BudgetFormValues>> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
const raw = localStorage.getItem(getCapexStorageKey(annee));
|
||||
return raw ? JSON.parse(raw) : {};
|
||||
} catch { return {}; }
|
||||
}
|
||||
|
||||
function saveSaisies(data: Record<string, Partial<BudgetFormValues>>) {
|
||||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch {}
|
||||
function saveSaisiesForAnnee(annee: number, data: Record<string, Partial<BudgetFormValues>>) {
|
||||
try { localStorage.setItem(getCapexStorageKey(annee), JSON.stringify(data)); } catch {}
|
||||
}
|
||||
|
||||
function getDefaultValues(etabCode: string, renouvellement2027: number): BudgetFormValues {
|
||||
@@ -167,8 +169,9 @@ function CurrencyInput({
|
||||
export default function Budget2027() {
|
||||
const [, navigate] = useLocation();
|
||||
const { parametres } = useParametres();
|
||||
const { annee } = useAnnee();
|
||||
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 [searchEtab, setSearchEtab] = useState('');
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
@@ -213,6 +216,12 @@ export default function Budget2027() {
|
||||
);
|
||||
}, [formValues]);
|
||||
|
||||
// Recharger les saisies quand l'année change
|
||||
useEffect(() => {
|
||||
setSaisies(loadSaisiesForAnnee(annee));
|
||||
setSelectedCode('');
|
||||
}, [annee]);
|
||||
|
||||
const updateField = (key: keyof BudgetFormValues, value: number | string) => {
|
||||
setSaisies(prev => {
|
||||
const next = {
|
||||
@@ -222,14 +231,14 @@ export default function Budget2027() {
|
||||
[key]: value,
|
||||
},
|
||||
};
|
||||
saveSaisies(next);
|
||||
saveSaisiesForAnnee(annee, next);
|
||||
return next;
|
||||
});
|
||||
setSaved(false);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
saveSaisies(saisies);
|
||||
saveSaisiesForAnnee(annee, saisies);
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2500);
|
||||
};
|
||||
@@ -238,7 +247,7 @@ export default function Budget2027() {
|
||||
setSaisies(prev => {
|
||||
const next = { ...prev };
|
||||
delete next[selectedCode];
|
||||
saveSaisies(next);
|
||||
saveSaisiesForAnnee(annee, next);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
@@ -265,12 +274,15 @@ export default function Budget2027() {
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
Construction Budget 2027
|
||||
CAPEX - Construction {annee}
|
||||
</h1>
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<AnneeSelectorBar />
|
||||
</div>
|
||||
{selectedCode && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
Info,
|
||||
} from 'lucide-react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||
import { useAnnee, getCapexStorageKey } from '../contexts/AnneeContext';
|
||||
import bp2027Raw from '../data_bp2027.json';
|
||||
import budgetRaw from '../data_budget.json';
|
||||
import { useParametres } from '../contexts/ParametresContext';
|
||||
@@ -27,7 +29,7 @@ import type { BudgetData } from '../types/budget';
|
||||
|
||||
const bp2027Data = bp2027Raw as BP2027Data;
|
||||
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)
|
||||
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' },
|
||||
};
|
||||
|
||||
function loadSaisies(): Record<string, Partial<BudgetFormValues>> {
|
||||
function loadSaisiesForAnnee(annee: number): Record<string, Partial<BudgetFormValues>> {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
const raw = localStorage.getItem(getCapexStorageKey(annee));
|
||||
return raw ? JSON.parse(raw) : {};
|
||||
} catch { return {}; }
|
||||
}
|
||||
@@ -83,6 +85,7 @@ type SortDir = 'asc' | 'desc';
|
||||
|
||||
export default function DsiCapex() {
|
||||
const { parametres } = useParametres();
|
||||
const { annee } = useAnnee();
|
||||
const [saisies, setSaisies] = useState<Record<string, Partial<BudgetFormValues>>>({});
|
||||
const [search, setSearch] = useState('');
|
||||
const [sortCol, setSortCol] = useState<string>('nom');
|
||||
@@ -91,8 +94,8 @@ export default function DsiCapex() {
|
||||
const [lastRefresh, setLastRefresh] = useState(Date.now());
|
||||
|
||||
useEffect(() => {
|
||||
setSaisies(loadSaisies());
|
||||
}, [lastRefresh]);
|
||||
setSaisies(loadSaisiesForAnnee(annee));
|
||||
}, [lastRefresh, annee]);
|
||||
|
||||
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>
|
||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
DSI CAPEX — Prévisionnel Budget 2027
|
||||
CAPEX - Synthèse {annee}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Tableau consolidé des investissements SI par établissement
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
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"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Actualiser
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
<AnneeSelectorBar />
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
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"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Actualiser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -213,7 +219,7 @@ export default function DsiCapex() {
|
||||
<div className="bg-card border border-border rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<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>
|
||||
<p className="text-2xl font-bold text-orange-600 tabular-nums" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
{formatEuros(totalGeneral)}
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
ArrowUpDown,
|
||||
} from 'lucide-react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||
import { useAnnee } from '../contexts/AnneeContext';
|
||||
import opexRaw from '../data_opex.json';
|
||||
import { formatEuros } from '../lib/format';
|
||||
|
||||
@@ -82,7 +84,7 @@ type ViewMode = 'etablissements' | 'postes';
|
||||
type SortDir = 'asc' | 'desc';
|
||||
|
||||
export default function DsiOpex() {
|
||||
const [selectedYear, setSelectedYear] = useState<2026 | 2027>(2026);
|
||||
const { annee } = useAnnee();
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('postes');
|
||||
const [search, setSearch] = useState('');
|
||||
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
|
||||
</p>
|
||||
</div>
|
||||
{/* Sélecteur d'année */}
|
||||
<div className="flex items-center gap-1 bg-muted rounded-lg p-1">
|
||||
{([2026, 2027] as const).map(year => (
|
||||
<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>
|
||||
))}
|
||||
{/* Sélecteur d'année — remplacé par AnneeSelectorBar global */}
|
||||
<div className="flex items-center gap-3">
|
||||
<AnneeSelectorBar />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -215,7 +206,7 @@ export default function DsiOpex() {
|
||||
<div className="bg-card border border-border rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<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>
|
||||
<p className="text-2xl font-bold text-orange-600 tabular-nums" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
{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>}
|
||||
<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">
|
||||
<SortBtn col="montant" label={`Prév. ${selectedYear}`} />
|
||||
<SortBtn col="montant" label={`Prév. ${annee}`} />
|
||||
</th>
|
||||
<th className="text-right px-4 py-3 font-medium text-muted-foreground w-24">Répartition</th>
|
||||
</tr>
|
||||
@@ -441,7 +432,7 @@ export default function DsiOpex() {
|
||||
<tfoot>
|
||||
<tr className="bg-muted/40 border-t-2 border-border">
|
||||
<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 className="px-4 py-3 text-right font-bold text-orange-600 text-base tabular-nums">
|
||||
{formatEuros(totalFiltre)}
|
||||
@@ -460,7 +451,7 @@ export default function DsiOpex() {
|
||||
<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' }}>
|
||||
<BarChart3 className="w-4 h-4 text-muted-foreground" />
|
||||
Répartition par catégorie — {selectedYear}
|
||||
Répartition par catégorie — {annee}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(totalParCategorie)
|
||||
@@ -518,7 +509,7 @@ export default function DsiOpex() {
|
||||
</th>
|
||||
))}
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
List,
|
||||
} from 'lucide-react';
|
||||
import { AppSidebar } from '../components/AppSidebar';
|
||||
import { AnneeSelectorBar } from '../components/AnneeSelectorBar';
|
||||
import { useAnnee } from '../contexts/AnneeContext';
|
||||
import { useBudgetData } from '../hooks/useBudgetData';
|
||||
import { formatEuros, formatNumber } from '../lib/format';
|
||||
import { EtablissementCard } from '../components/EtablissementCard';
|
||||
@@ -42,6 +44,7 @@ export default function Home() {
|
||||
setFilterType,
|
||||
} = useBudgetData();
|
||||
|
||||
const { annee } = useAnnee();
|
||||
const [selectedEtab, setSelectedEtab] = useState<Etablissement | null>(null);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||
@@ -71,13 +74,14 @@ export default function Home() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
|
||||
Renouvellement PC 2027
|
||||
Renouvellement PC {annee}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
{totalEtablissements} établissements Itinova — Vétusté du parc PC
|
||||
</p>
|
||||
</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">
|
||||
{etablissements.length} affiché{etablissements.length > 1 ? 's' : ''}
|
||||
</span>
|
||||
@@ -92,7 +96,7 @@ export default function Home() {
|
||||
icon={Euro}
|
||||
label="Budget global"
|
||||
value={formatEuros(meta.budget_global)}
|
||||
subtitle="TTC — exercice 2027"
|
||||
subtitle={`TTC — exercice ${annee}`}
|
||||
accentColor="text-orange-600"
|
||||
delay={0}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user