Checkpoint: Inventaire PC distinct par année : clé budgetsi_inventaire_data_YYYY dans localStorage, ImportModal utilise l'année active (useAnnee), indicateur visuel des imports par année dans la modale, useBudgetData + Budget2027 + DsiCapex rechargent l'inventaire à chaque changement d'année
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
// Design: Corporate Modernism — Itinova Budget SI
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useAnnee, getInventaireStorageKey } from '../contexts/AnneeContext';
|
||||
import * as XLSX from 'xlsx';
|
||||
import {
|
||||
Upload,
|
||||
@@ -251,6 +252,7 @@ interface ImportModalProps {
|
||||
}
|
||||
|
||||
export function ImportModal({ open, onClose }: ImportModalProps) {
|
||||
const { annee } = useAnnee();
|
||||
const [tab, setTab] = useState<ImportTab>('inventaire');
|
||||
const [inventaireStatus, setInventaireStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
const [inventaireMsg, setInventaireMsg] = useState('');
|
||||
@@ -535,7 +537,7 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
|
||||
const inventaireData = {
|
||||
meta: {
|
||||
date_calcul: new Date().toISOString(),
|
||||
date_reference: '2027-01-01',
|
||||
date_reference: `${annee}-01-01`,
|
||||
nb_etablissements: etabData.size,
|
||||
total_fixes: nbFixes,
|
||||
total_portables: nbPortables,
|
||||
@@ -543,7 +545,7 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
|
||||
},
|
||||
etablissements: etablissementsArray,
|
||||
};
|
||||
localStorage.setItem('budgetsi_inventaire_data', JSON.stringify(inventaireData));
|
||||
localStorage.setItem(getInventaireStorageKey(annee), JSON.stringify(inventaireData));
|
||||
|
||||
// ── Mettre à jour aussi la liste des établissements (code + nom) ────────────
|
||||
const existing = loadEtablissements();
|
||||
@@ -567,7 +569,7 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
|
||||
|
||||
// ── Métadonnées d'import ────────────────────────────────────────────────────────
|
||||
const nbLignes = rows.length - 1;
|
||||
localStorage.setItem('budgetsi_inventaire_import', JSON.stringify({
|
||||
localStorage.setItem(`budgetsi_inventaire_import_${annee}`, JSON.stringify({
|
||||
filename: file.name,
|
||||
date: new Date().toISOString(),
|
||||
size: file.size,
|
||||
@@ -587,7 +589,7 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
|
||||
].filter(Boolean).join(' — ');
|
||||
|
||||
// Notifier les autres composants (useBudgetData, DsiCapex, Budget2027)
|
||||
window.dispatchEvent(new Event('budgetsi_inventaire_updated'));
|
||||
window.dispatchEvent(new CustomEvent('budgetsi_inventaire_updated', { detail: { annee } }));
|
||||
|
||||
setInventaireStatus('success');
|
||||
setInventaireMsg(`"${file.name}" importé — ${details}`);
|
||||
@@ -850,6 +852,38 @@ export function ImportModal({ open, onClose }: ImportModalProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Indicateur imports par année ── */}
|
||||
<div className="border border-border rounded-xl overflow-hidden">
|
||||
<div className="px-4 py-2.5 bg-muted/30 border-b border-border">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">État des inventaires par année</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 p-3">
|
||||
{[2025, 2026, 2027, 2028, 2029, 2030].map(yr => {
|
||||
const hasData = !!localStorage.getItem(`budgetsi_inventaire_data_${yr}`);
|
||||
const meta = (() => { try { const d = localStorage.getItem(`budgetsi_inventaire_import_${yr}`); return d ? JSON.parse(d) : null; } catch { return null; } })();
|
||||
return (
|
||||
<div
|
||||
key={yr}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg border text-xs font-medium ${
|
||||
yr === annee
|
||||
? hasData
|
||||
? 'bg-emerald-100 border-emerald-400 text-emerald-800'
|
||||
: 'bg-blue-100 border-blue-400 text-blue-800'
|
||||
: hasData
|
||||
? 'bg-emerald-50 border-emerald-200 text-emerald-700'
|
||||
: 'bg-muted/40 border-border text-muted-foreground'
|
||||
}`}
|
||||
title={meta ? `${meta.filename} — ${meta.nbLignes} lignes — ${new Date(meta.date).toLocaleDateString('fr-FR')}` : 'Aucun inventaire importé'}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${hasData ? 'bg-emerald-500' : 'bg-muted-foreground/40'}`} />
|
||||
<span>{yr}</span>
|
||||
{yr === annee && <span className="text-[10px] opacity-70">(actif)</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Info résumée ── */}
|
||||
<div className="flex items-start gap-3 p-3 bg-blue-50 border border-blue-200 rounded-xl">
|
||||
<Info className="w-4 h-4 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||
|
||||
@@ -69,3 +69,8 @@ export function getCapexStorageKey(annee: number): string {
|
||||
export function getOpexStorageKey(annee: number): string {
|
||||
return `itinova_budget_opex_${annee}`;
|
||||
}
|
||||
|
||||
// Génère la clé localStorage pour l'inventaire PC d'une année donnée
|
||||
export function getInventaireStorageKey(annee: number): string {
|
||||
return `budgetsi_inventaire_data_${annee}`;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
// Hook pour charger et gérer les données budget depuis localStorage (import inventaire)
|
||||
// Hook pour charger et gérer les données budget depuis localStorage (import inventaire par année)
|
||||
// Design: Corporate Modernism — Itinova Budget SI 2027
|
||||
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import type { BudgetData, Etablissement, EtablissementStats, SortField, SortOrder, FilterType } from '../types/budget';
|
||||
import { useParametres } from '../contexts/ParametresContext';
|
||||
|
||||
const INVENTAIRE_KEY = 'budgetsi_inventaire_data';
|
||||
import { useAnnee, getInventaireStorageKey } from '../contexts/AnneeContext';
|
||||
|
||||
// ─── Lecture localStorage ─────────────────────────────────────────────────────
|
||||
|
||||
function loadInventaireData(): BudgetData | null {
|
||||
function loadInventaireData(annee: number): BudgetData | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(INVENTAIRE_KEY);
|
||||
const raw = localStorage.getItem(getInventaireStorageKey(annee));
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw) as BudgetData;
|
||||
} catch {
|
||||
@@ -65,29 +64,41 @@ function recalculerStats(
|
||||
|
||||
export function useBudgetData() {
|
||||
const { parametres } = useParametres();
|
||||
const { annee } = useAnnee();
|
||||
const [search, setSearch] = useState('');
|
||||
const [sortField, setSortField] = useState<SortField>('budget_total');
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>('desc');
|
||||
const [filterType, setFilterType] = useState<FilterType>('all');
|
||||
|
||||
// Données brutes depuis localStorage — rechargées à chaque montage + écoute storage
|
||||
const [rawData, setRawData] = useState<BudgetData | null>(() => loadInventaireData());
|
||||
// Données brutes depuis localStorage — rechargées à chaque changement d'année + écoute storage
|
||||
const [rawData, setRawData] = useState<BudgetData | null>(() => loadInventaireData(annee));
|
||||
|
||||
// Recharger quand l'année change
|
||||
useEffect(() => {
|
||||
setRawData(loadInventaireData(annee));
|
||||
}, [annee]);
|
||||
|
||||
useEffect(() => {
|
||||
const onStorage = (e: StorageEvent) => {
|
||||
if (e.key === INVENTAIRE_KEY) {
|
||||
setRawData(loadInventaireData());
|
||||
if (e.key === getInventaireStorageKey(annee)) {
|
||||
setRawData(loadInventaireData(annee));
|
||||
}
|
||||
};
|
||||
window.addEventListener('storage', onStorage);
|
||||
// Écouter aussi les changements dans le même onglet via custom event
|
||||
const onImport = () => setRawData(loadInventaireData());
|
||||
const onImport = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
// Recharger seulement si l'import concerne l'année active
|
||||
if (!detail || detail.annee === annee) {
|
||||
setRawData(loadInventaireData(annee));
|
||||
}
|
||||
};
|
||||
window.addEventListener('budgetsi_inventaire_updated', onImport);
|
||||
return () => {
|
||||
window.removeEventListener('storage', onStorage);
|
||||
window.removeEventListener('budgetsi_inventaire_updated', onImport);
|
||||
};
|
||||
}, []);
|
||||
}, [annee]);
|
||||
|
||||
// Recalcul de tous les établissements selon les paramètres courants
|
||||
const etablissementsRecalcules = useMemo<Etablissement[]>(() => {
|
||||
@@ -113,7 +124,7 @@ export function useBudgetData() {
|
||||
const budgetGlobal = etablissementsRecalcules.reduce((s, e) => s + e.stats.budget_total, 0);
|
||||
return {
|
||||
date_calcul: rawData?.meta?.date_calcul ?? new Date().toISOString(),
|
||||
date_reference: '2027-01-01',
|
||||
date_reference: `${annee}-01-01`,
|
||||
seuil_fixes_ans: parametres.seuilFixesAns,
|
||||
seuil_portables_ans: parametres.seuilPortablesAns,
|
||||
cout_fixe: parametres.coutFixe,
|
||||
@@ -125,7 +136,7 @@ export function useBudgetData() {
|
||||
total_fixes_renouveler: totalFixesR,
|
||||
total_portables_renouveler: totalPortablesR,
|
||||
};
|
||||
}, [etablissementsRecalcules, rawData, parametres]);
|
||||
}, [etablissementsRecalcules, rawData, parametres, annee]);
|
||||
|
||||
const filteredAndSorted = useMemo(() => {
|
||||
let list: Etablissement[] = [...etablissementsRecalcules];
|
||||
|
||||
@@ -35,9 +35,9 @@ import type { BudgetData } from '../types/budget';
|
||||
|
||||
const bp2027Data = bp2027Raw as BP2027Data;
|
||||
|
||||
function loadInventaireData(): BudgetData | null {
|
||||
function loadInventaireData(annee: number): BudgetData | null {
|
||||
try {
|
||||
const raw = localStorage.getItem('budgetsi_inventaire_data');
|
||||
const raw = localStorage.getItem(`budgetsi_inventaire_data_${annee}`);
|
||||
return raw ? JSON.parse(raw) as BudgetData : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
@@ -185,16 +185,28 @@ export default function Budget2027() {
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [searchEtab, setSearchEtab] = useState('');
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [inventaire, setInventaire] = useState<BudgetData | null>(() => loadInventaireData());
|
||||
const [inventaire, setInventaire] = useState<BudgetData | null>(() => loadInventaireData(annee));
|
||||
|
||||
// Recharger quand l'année change
|
||||
useEffect(() => {
|
||||
setInventaire(loadInventaireData(annee));
|
||||
}, [annee]);
|
||||
|
||||
useEffect(() => {
|
||||
const onImport = () => setInventaire(loadInventaireData());
|
||||
const onImport = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if (!detail || detail.annee === annee) setInventaire(loadInventaireData(annee));
|
||||
};
|
||||
const onStorage = (e: StorageEvent) => {
|
||||
if (e.key === `budgetsi_inventaire_data_${annee}`) setInventaire(loadInventaireData(annee));
|
||||
};
|
||||
window.addEventListener('budgetsi_inventaire_updated', onImport);
|
||||
window.addEventListener('storage', (e: StorageEvent) => {
|
||||
if (e.key === 'budgetsi_inventaire_data') onImport();
|
||||
});
|
||||
return () => window.removeEventListener('budgetsi_inventaire_updated', onImport);
|
||||
}, []);
|
||||
window.addEventListener('storage', onStorage);
|
||||
return () => {
|
||||
window.removeEventListener('budgetsi_inventaire_updated', onImport);
|
||||
window.removeEventListener('storage', onStorage);
|
||||
};
|
||||
}, [annee]);
|
||||
|
||||
// Établissement sélectionné (depuis BP2027)
|
||||
const selectedBP = useMemo(
|
||||
|
||||
@@ -28,9 +28,9 @@ import type { BudgetData } from '../types/budget';
|
||||
|
||||
const bp2027Data = bp2027Raw as BP2027Data;
|
||||
|
||||
function loadInventaireData(): BudgetData | null {
|
||||
function loadInventaireData(annee: number): BudgetData | null {
|
||||
try {
|
||||
const raw = localStorage.getItem('budgetsi_inventaire_data');
|
||||
const raw = localStorage.getItem(`budgetsi_inventaire_data_${annee}`);
|
||||
return raw ? JSON.parse(raw) as BudgetData : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
@@ -99,20 +99,32 @@ export default function DsiCapex() {
|
||||
const [sortDir, setSortDir] = useState<SortDir>('asc');
|
||||
const [showOnlyFilled, setShowOnlyFilled] = useState(false);
|
||||
const [lastRefresh, setLastRefresh] = useState(Date.now());
|
||||
const [inventaire, setInventaire] = useState<BudgetData | null>(() => loadInventaireData());
|
||||
const [inventaire, setInventaire] = useState<BudgetData | null>(() => loadInventaireData(annee));
|
||||
|
||||
useEffect(() => {
|
||||
setSaisies(loadSaisiesForAnnee(annee));
|
||||
}, [lastRefresh, annee]);
|
||||
|
||||
// Recharger l'inventaire quand l'année change
|
||||
useEffect(() => {
|
||||
const onImport = () => setInventaire(loadInventaireData());
|
||||
setInventaire(loadInventaireData(annee));
|
||||
}, [annee]);
|
||||
|
||||
useEffect(() => {
|
||||
const onImport = (e: Event) => {
|
||||
const detail = (e as CustomEvent).detail;
|
||||
if (!detail || detail.annee === annee) setInventaire(loadInventaireData(annee));
|
||||
};
|
||||
const onStorage = (e: StorageEvent) => {
|
||||
if (e.key === `budgetsi_inventaire_data_${annee}`) setInventaire(loadInventaireData(annee));
|
||||
};
|
||||
window.addEventListener('budgetsi_inventaire_updated', onImport);
|
||||
window.addEventListener('storage', (e: StorageEvent) => {
|
||||
if (e.key === 'budgetsi_inventaire_data') onImport();
|
||||
});
|
||||
return () => window.removeEventListener('budgetsi_inventaire_updated', onImport);
|
||||
}, []);
|
||||
window.addEventListener('storage', onStorage);
|
||||
return () => {
|
||||
window.removeEventListener('budgetsi_inventaire_updated', onImport);
|
||||
window.removeEventListener('storage', onStorage);
|
||||
};
|
||||
}, [annee]);
|
||||
|
||||
const handleRefresh = () => setLastRefresh(Date.now());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user