Checkpoint: Suppression complète de la dépendance à data_budget.json : l'inventaire est désormais exclusivement alimenté par l'import utilisateur (localStorage budgetsi_inventaire_data). useBudgetData lit depuis localStorage, Budget2027 et DsiCapex calculent le renouvellement depuis l'inventaire importé, les pages affichent un état vide avec invitation à importer si aucun inventaire n'est présent. L'import stocke les postes complets (fixes/portables avec âge calculé) par établissement, utilise la colonne Type de matériel en priorité, et émet un événement custom pour notifier toutes les pages en temps réel.
This commit is contained in:
@@ -28,14 +28,19 @@ 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';
|
||||
import { formatEuros } from '../lib/format';
|
||||
import type { BP2027Data, EtablissementBP, BudgetFormValues } from '../types/bp2027';
|
||||
import type { BudgetData } from '../types/budget';
|
||||
|
||||
const bp2027Data = bp2027Raw as BP2027Data;
|
||||
const budgetData = budgetRaw as BudgetData;
|
||||
|
||||
function loadInventaireData(): BudgetData | null {
|
||||
try {
|
||||
const raw = localStorage.getItem('budgetsi_inventaire_data');
|
||||
return raw ? JSON.parse(raw) as BudgetData : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
// La clé de stockage est désormais dynamique par année (voir getCapexStorageKey)
|
||||
|
||||
@@ -115,9 +120,14 @@ function getDefaultValues(etabCode: string, renouvellement2027: number): BudgetF
|
||||
};
|
||||
}
|
||||
|
||||
// Calcule le budget de renouvellement 2027 depuis les données de vétusté
|
||||
function getBudgetRenouvellement2027(code: string, parametres: { coutFixe: number; coutPortable: number; seuilFixesAns: number; seuilPortablesAns: number }): number {
|
||||
const etab = budgetData.etablissements.find(e => e.code === code);
|
||||
// Calcule le budget de renouvellement depuis l'inventaire importé (localStorage)
|
||||
function getBudgetRenouvellement2027(
|
||||
code: string,
|
||||
parametres: { coutFixe: number; coutPortable: number; seuilFixesAns: number; seuilPortablesAns: number },
|
||||
inventaire: BudgetData | null
|
||||
): number {
|
||||
if (!inventaire) return 0;
|
||||
const etab = inventaire.etablissements.find(e => e.code === code);
|
||||
if (!etab) return 0;
|
||||
const nbFixesR = (etab.fixes ?? []).filter(f => f.age_ans !== null && f.age_ans >= parametres.seuilFixesAns).length;
|
||||
const nbPortablesR = (etab.portables ?? []).filter(p => p.age_ans !== null && p.age_ans >= parametres.seuilPortablesAns).length;
|
||||
@@ -175,6 +185,16 @@ 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());
|
||||
|
||||
useEffect(() => {
|
||||
const onImport = () => setInventaire(loadInventaireData());
|
||||
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);
|
||||
}, []);
|
||||
|
||||
// Établissement sélectionné (depuis BP2027)
|
||||
const selectedBP = useMemo(
|
||||
@@ -182,10 +202,10 @@ export default function Budget2027() {
|
||||
[selectedCode]
|
||||
);
|
||||
|
||||
// Budget renouvellement 2027 calculé par vétusté
|
||||
// Budget renouvellement calculé depuis l'inventaire importé
|
||||
const renouvellement2027Calcule = useMemo(
|
||||
() => selectedCode ? getBudgetRenouvellement2027(selectedCode, parametres) : 0,
|
||||
[selectedCode, parametres]
|
||||
() => selectedCode ? getBudgetRenouvellement2027(selectedCode, parametres, inventaire) : 0,
|
||||
[selectedCode, parametres, inventaire]
|
||||
);
|
||||
|
||||
// Valeurs du formulaire pour l'établissement sélectionné
|
||||
|
||||
@@ -21,14 +21,19 @@ 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';
|
||||
import { formatEuros } from '../lib/format';
|
||||
import type { BP2027Data, BudgetFormValues } from '../types/bp2027';
|
||||
import type { BudgetData } from '../types/budget';
|
||||
|
||||
const bp2027Data = bp2027Raw as BP2027Data;
|
||||
const budgetData = budgetRaw as BudgetData;
|
||||
|
||||
function loadInventaireData(): BudgetData | null {
|
||||
try {
|
||||
const raw = localStorage.getItem('budgetsi_inventaire_data');
|
||||
return raw ? JSON.parse(raw) as BudgetData : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
// Clé de stockage dynamique par année (voir getCapexStorageKey)
|
||||
|
||||
// Colonnes du tableau (correspondance exacte avec le fichier Excel BP2027)
|
||||
@@ -67,9 +72,11 @@ function loadSaisiesForAnnee(annee: number): Record<string, Partial<BudgetFormVa
|
||||
|
||||
function getBudgetRenouvellement2027(
|
||||
code: string,
|
||||
parametres: { coutFixe: number; coutPortable: number; seuilFixesAns: number; seuilPortablesAns: number }
|
||||
parametres: { coutFixe: number; coutPortable: number; seuilFixesAns: number; seuilPortablesAns: number },
|
||||
inventaire: BudgetData | null
|
||||
): number {
|
||||
const etab = budgetData.etablissements.find(e => e.code === code);
|
||||
if (!inventaire) return 0;
|
||||
const etab = inventaire.etablissements.find(e => e.code === code);
|
||||
if (!etab) return 0;
|
||||
const nbFixesR = (etab.fixes ?? []).filter(f => f.age_ans !== null && f.age_ans >= parametres.seuilFixesAns).length;
|
||||
const nbPortablesR = (etab.portables ?? []).filter(p => p.age_ans !== null && p.age_ans >= parametres.seuilPortablesAns).length;
|
||||
@@ -92,11 +99,21 @@ 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());
|
||||
|
||||
useEffect(() => {
|
||||
setSaisies(loadSaisiesForAnnee(annee));
|
||||
}, [lastRefresh, annee]);
|
||||
|
||||
useEffect(() => {
|
||||
const onImport = () => setInventaire(loadInventaireData());
|
||||
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);
|
||||
}, []);
|
||||
|
||||
const handleRefresh = () => setLastRefresh(Date.now());
|
||||
|
||||
// Construire les lignes du tableau
|
||||
@@ -105,7 +122,7 @@ export default function DsiCapex() {
|
||||
const saisie = saisies[etab.code] || {};
|
||||
const renouv = saisie.renouvellement_2027 !== undefined
|
||||
? saisie.renouvellement_2027
|
||||
: getBudgetRenouvellement2027(etab.code, parametres);
|
||||
: getBudgetRenouvellement2027(etab.code, parametres, inventaire);
|
||||
|
||||
const valeurs: Record<ColKey, number> = {
|
||||
renouvellement_2027: renouv,
|
||||
@@ -172,11 +189,8 @@ export default function DsiCapex() {
|
||||
const totalGeneral = useMemo(() => sortedLignes.reduce((s, l) => s + l.total, 0), [sortedLignes]);
|
||||
const nbFilled = useMemo(() => lignes.filter(l => l.hasSaisie).length, [lignes]);
|
||||
|
||||
// La synthèse affiche les données si :
|
||||
// - au moins un établissement a été saisi manuellement (saisie CAPEX), OU
|
||||
// - l'année a des données de renouvellement calculées par vétusté (2026 et 2027)
|
||||
const ANNEES_AVEC_INVENTAIRE = [2026, 2027];
|
||||
const hasDonnees = nbFilled > 0 || ANNEES_AVEC_INVENTAIRE.includes(annee);
|
||||
// La synthèse affiche les données si un inventaire a été importé ou si des saisies CAPEX existent
|
||||
const hasDonnees = inventaire !== null || nbFilled > 0;
|
||||
|
||||
const handleSort = (col: string) => {
|
||||
if (sortCol === col) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
|
||||
@@ -306,11 +320,10 @@ export default function DsiCapex() {
|
||||
<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}
|
||||
Aucun inventaire importé
|
||||
</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.
|
||||
Importez l'export ISI-APP via le bouton <strong>Imports & Données</strong> pour pré-remplir les budgets de renouvellement, puis saisissez les autres postes CAPEX via la page <strong>CAPEX - Construction</strong>.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -35,6 +35,7 @@ export default function Home() {
|
||||
meta,
|
||||
etablissements,
|
||||
totalEtablissements,
|
||||
hasInventaire,
|
||||
search,
|
||||
setSearch,
|
||||
sortField,
|
||||
@@ -45,9 +46,8 @@ export default function Home() {
|
||||
} = useBudgetData();
|
||||
|
||||
const { annee } = useAnnee();
|
||||
// Les données de parc ne sont disponibles que pour 2026 et 2027
|
||||
const ANNEES_AVEC_DONNEES = [2026, 2027];
|
||||
const hasDonnees = ANNEES_AVEC_DONNEES.includes(annee);
|
||||
// Les données de parc proviennent uniquement d'un import inventaire
|
||||
const hasDonnees = hasInventaire;
|
||||
const [selectedEtab, setSelectedEtab] = useState<Etablissement | null>(null);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('grid');
|
||||
@@ -245,11 +245,10 @@ export default function Home() {
|
||||
<Monitor 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}
|
||||
Aucun inventaire importé
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-2 max-w-sm">
|
||||
Les données d'inventaire parc PC pour l'exercice {annee} n'ont pas encore été importées.
|
||||
Les années disponibles sont 2026 et 2027.
|
||||
Importez l'export ISI-APP via le bouton <strong>Imports & Données</strong> dans le menu de gauche pour afficher les données de vétusté et les budgets de renouvellement.
|
||||
</p>
|
||||
</div>
|
||||
) : etablissements.length === 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user