(() => {
+ const defaults = getDefaultValues(selectedCode, renouvellement2027Calcule);
+ const saved = saisies[selectedCode] ?? {};
+ return {
+ ...defaults,
+ ...saved,
+ // Si pas encore saisi, on prend le calculé
+ renouvellement_2027: saved.renouvellement_2027 !== undefined ? saved.renouvellement_2027 : renouvellement2027Calcule,
+ };
+ }, [selectedCode, saisies, renouvellement2027Calcule]);
+
+ // Total BP 2027
+ const totalBP2027 = useMemo(() => {
+ const v = formValues;
+ return (
+ v.renouvellement_2027 +
+ v.machines_supplementaires +
+ v.appel_malade +
+ v.telephonie +
+ v.wifi +
+ v.video_surveillance +
+ v.copieurs +
+ v.visio +
+ v.autres
+ );
+ }, [formValues]);
+
+ const updateField = (key: keyof BudgetFormValues, value: number | string) => {
+ setSaisies(prev => {
+ const next = {
+ ...prev,
+ [selectedCode]: {
+ ...(prev[selectedCode] ?? {}),
+ [key]: value,
+ },
+ };
+ saveSaisies(next);
+ return next;
+ });
+ setSaved(false);
+ };
+
+ const handleSave = () => {
+ saveSaisies(saisies);
+ setSaved(true);
+ setTimeout(() => setSaved(false), 2500);
+ };
+
+ const handleReset = () => {
+ setSaisies(prev => {
+ const next = { ...prev };
+ delete next[selectedCode];
+ saveSaisies(next);
+ return next;
+ });
+ };
+
+ // Établissements filtrés pour le dropdown
+ const etabsFiltres = useMemo(() => {
+ const q = searchEtab.toLowerCase();
+ return bp2027Data.etablissements.filter(
+ e => e.nom.toLowerCase().includes(q) || e.code.toLowerCase().includes(q)
+ );
+ }, [searchEtab]);
+
+ // Nb d'établissements avec saisie
+ const nbSaisies = Object.keys(saisies).length;
+
+ return (
+
+ {/* Sidebar */}
+
+
+ {/* Contenu principal */}
+
+ {/* En-tête */}
+
+
+
+
+ {/* Sélection établissement */}
+
+
+
+
+ Sélection de l'établissement
+
+
+
+
+
+
+ {dropdownOpen && (
+
+
+ setSearchEtab(e.target.value)}
+ className="w-full px-3 py-2 text-sm border border-border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary/30"
+ autoFocus
+ />
+
+
+ {etabsFiltres.length === 0 ? (
+
Aucun résultat
+ ) : (
+ etabsFiltres.map(e => (
+
+ ))
+ )}
+
+
+ )}
+
+
+
+ {/* Formulaire (visible uniquement si établissement sélectionné) */}
+ {!selectedCode && (
+
+
+
+
+
+ Sélectionnez un établissement
+
+
+ Choisissez votre établissement dans la liste ci-dessus pour accéder au formulaire de construction budgétaire.
+
+
+ )}
+
+ {selectedCode && selectedBP && (
+ <>
+ {/* Données informatives 2025/2026 */}
+
+
+
+
+
+ Données historiques — à titre informatif
+
+
+ Enveloppes de renouvellement allouées par le Siège Itinova (non modifiables)
+
+
+
+
+
+
Renouvellement 2025
+
+ {selectedBP.renouvellement_2025 > 0 ? formatEuros(selectedBP.renouvellement_2025) : '—'}
+
+
Enveloppe Siège
+
+
+
Renouvellement 2026
+
+ {selectedBP.renouvellement_2026 > 0 ? formatEuros(selectedBP.renouvellement_2026) : '—'}
+
+
Enveloppe Siège
+
+
+
+
+ {/* Formulaire de saisie */}
+
+
+
+
+ Budget prévisionnel 2027 — Saisie directeur
+
+ (tous les champs sont optionnels)
+
+
+ {CHAMPS.map(champ => {
+ const Icon = ICON_MAP[champ.key] ?? Package;
+ const color = COLOR_MAP[champ.key] ?? 'slate';
+ const cls = COLOR_CLASSES[color];
+ const isRenouvellement = champ.key === 'renouvellement_2027';
+ const val = formValues[champ.key];
+ const numVal = typeof val === 'number' ? val : 0;
+
+ return (
+ 0 ? `${cls.border} ${cls.light}` : 'border-border'
+ }`}
+ >
+
+
+
+
+
+
+
{champ.label}
+ {isRenouvellement && (
+
+ Pré-rempli
+
+ )}
+
+
{champ.description}
+ {isRenouvellement && renouvellement2027Calcule > 0 && (
+
+
+
+ Proposé par vétusté : {formatEuros(renouvellement2027Calcule)}
+ {formValues.renouvellement_2027 !== renouvellement2027Calcule && (
+ (modifié)
+ )}
+
+
+ )}
+
updateField(champ.key, v)}
+ />
+
+ {numVal > 0 && (
+
+
+ {formatEuros(numVal)}
+
+
+ )}
+
+
+ );
+ })}
+
+ {/* Commentaires */}
+
+
+
+
Commentaires
+
(optionnel)
+
+
+
+
+ {/* Récapitulatif total */}
+
+
+
+
+ Total BP 2027
+
+
{selectedBP.nom}
+
+
+
+ {formatEuros(totalBP2027)}
+
+
TTC — exercice 2027
+
+
+
+ {/* Détail des lignes non nulles */}
+
+ {CHAMPS.map(champ => {
+ const val = formValues[champ.key];
+ const numVal = typeof val === 'number' ? val : 0;
+ if (numVal === 0) return null;
+ const color = COLOR_MAP[champ.key] ?? 'slate';
+ const cls = COLOR_CLASSES[color];
+ return (
+
+ {champ.label}
+ {formatEuros(numVal)}
+
+ );
+ })}
+ {totalBP2027 === 0 && (
+
+ Aucun montant saisi — remplissez les champs ci-dessus
+
+ )}
+
+
+ {/* Comparaison avec 2026 */}
+ {selectedBP.renouvellement_2026 > 0 && totalBP2027 > 0 && (
+
+
vs Enveloppe 2026
+
+ {formatEuros(selectedBP.renouvellement_2026)}
+ selectedBP.renouvellement_2026 ? 'text-red-400' : 'text-emerald-400'}`}>
+ {totalBP2027 > selectedBP.renouvellement_2026 ? '+' : ''}
+ {formatEuros(totalBP2027 - selectedBP.renouvellement_2026)}
+
+
+
+ )}
+
+ >
+ )}
+
+
+
+ );
+}
diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx
index 1fb6d85..8637cc9 100644
--- a/client/src/pages/Home.tsx
+++ b/client/src/pages/Home.tsx
@@ -19,6 +19,7 @@ import {
LayoutGrid,
List,
Settings,
+ FileText,
} from 'lucide-react';
import { useBudgetData } from '../hooks/useBudgetData';
import { formatEuros, formatNumber } from '../lib/format';
@@ -93,6 +94,13 @@ export default function Home() {
{sidebarOpen && Établissements}
+
+
Paramètres
diff --git a/client/src/types/bp2027.ts b/client/src/types/bp2027.ts
new file mode 100644
index 0000000..53b9d50
--- /dev/null
+++ b/client/src/types/bp2027.ts
@@ -0,0 +1,114 @@
+// Types pour la page Construction Budget 2027 - CAPEX DSI
+
+export interface EtablissementBP {
+ code: string;
+ nom: string;
+ // Champs informatifs (source ITINOVA Siège)
+ renouvellement_2025: number;
+ renouvellement_2026: number;
+ // Champs de saisie optionnels
+ machines_supplementaires: number;
+ appel_malade: number;
+ telephonie: number;
+ wifi: number;
+ video_surveillance: number;
+ copieurs: number;
+ visio: number;
+ autres: number;
+ commentaires: string;
+}
+
+export interface BP2027Data {
+ meta: {
+ titre: string;
+ nb_etablissements: number;
+ colonnes: Record;
+ };
+ etablissements: EtablissementBP[];
+}
+
+// Champs saisissables par le directeur (hors renouvellement qui est pré-rempli)
+export interface BudgetFormValues {
+ renouvellement_2027: number; // pré-rempli depuis vétusté, modifiable
+ machines_supplementaires: number;
+ appel_malade: number;
+ telephonie: number;
+ wifi: number;
+ video_surveillance: number;
+ copieurs: number;
+ visio: number;
+ autres: number;
+ commentaires: string;
+}
+
+export const CHAMPS_SAISIE: {
+ key: keyof BudgetFormValues;
+ label: string;
+ description: string;
+ icon: string;
+ color: string;
+}[] = [
+ {
+ key: 'renouvellement_2027',
+ label: 'Renouvellement parc informatique',
+ description: 'Pré-rempli depuis le calcul de vétusté (modifiable)',
+ icon: 'monitor',
+ color: 'blue',
+ },
+ {
+ key: 'machines_supplementaires',
+ label: 'Acquisition machines supplémentaires',
+ description: 'Nouveaux postes hors renouvellement',
+ icon: 'plus-circle',
+ color: 'indigo',
+ },
+ {
+ key: 'appel_malade',
+ label: 'Système Appel Malade / Anti Fugue',
+ description: 'Équipements d\'appel et de sécurité',
+ icon: 'bell',
+ color: 'red',
+ },
+ {
+ key: 'telephonie',
+ label: 'Installation téléphonique',
+ description: 'Infrastructure téléphonique fixe et mobile',
+ icon: 'phone',
+ color: 'green',
+ },
+ {
+ key: 'wifi',
+ label: 'Réseau WiFi',
+ description: 'Infrastructure réseau sans fil',
+ icon: 'wifi',
+ color: 'cyan',
+ },
+ {
+ key: 'video_surveillance',
+ label: 'Vidéo Surveillance',
+ description: 'Caméras et système de surveillance',
+ icon: 'camera',
+ color: 'orange',
+ },
+ {
+ key: 'copieurs',
+ label: 'Copieurs',
+ description: 'Imprimantes et copieurs multifonctions',
+ icon: 'printer',
+ color: 'purple',
+ },
+ {
+ key: 'visio',
+ label: 'Équipement Visio Conférence',
+ description: 'Matériel de visioconférence',
+ icon: 'video',
+ color: 'teal',
+ },
+ {
+ key: 'autres',
+ label: 'Autres investissements SI',
+ description: 'Tout autre investissement informatique',
+ icon: 'package',
+ color: 'slate',
+ },
+];