Checkpoint: Ajout de la page Construction Budget 2027 : formulaire CAPEX DSI par établissement, renouvellement 2027 pré-rempli depuis vétusté, données historiques 2025/2026 en lecture seule, 9 champs optionnels, récapitulatif total avec comparaison vs 2026, persistance localStorage, navigation complète dans la sidebar.

This commit is contained in:
Manus
2026-04-06 13:29:00 -04:00
parent b651705e98
commit 34e320cced
6 changed files with 1575 additions and 0 deletions

View File

@@ -10,12 +10,14 @@ import { ThemeProvider } from "./contexts/ThemeContext";
import { ParametresProvider } from "./contexts/ParametresContext";
import Home from "./pages/Home";
import Parametres from "./pages/Parametres";
import Budget2027 from "./pages/Budget2027";
function Router() {
return (
<Switch>
<Route path={"/"} component={Home} />
<Route path={"/parametres"} component={Parametres} />
<Route path={"/budget2027"} component={Budget2027} />
<Route path={"/404"} component={NotFound} />
<Route component={NotFound} />
</Switch>

847
client/src/data_bp2027.json Normal file
View File

@@ -0,0 +1,847 @@
{
"meta": {
"titre": "BP2027 - Prévisionnel 2027 CAPEX DSI",
"nb_etablissements": 55,
"colonnes": {
"renouvellement_2025": "Parc informatique — Enveloppe Renouvellement 2025 (Source : ITINOVA Siège)",
"renouvellement_2026": "Parc informatique — Enveloppe Renouvellement 2026 (Source : ITINOVA Siège)",
"renouvellement_2027": "Parc informatique — Renouvellement 2027 (calculé par vétusté)",
"machines_supplementaires": "Parc informatique — Acquisition machines supplémentaires",
"appel_malade": "Système Appel Malade / Anti Fugue",
"telephonie": "Installation téléphonique",
"wifi": "Réseau WiFi",
"video_surveillance": "Vidéo Surveillance",
"copieurs": "Copieurs",
"visio": "Équipement Visio Conférence",
"autres": "Autres investissements SI",
"commentaires": "Commentaires"
}
},
"etablissements": [
{
"code": "1001BPT",
"nom": "FAM ST JOSEPH",
"renouvellement_2025": 0,
"renouvellement_2026": 7141,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1001GRF",
"nom": "EAM / EANM CORBONOD",
"renouvellement_2025": 0,
"renouvellement_2026": 8966,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1001VAR",
"nom": "ITEP / SESSAD CHATEAU DE VAREY",
"renouvellement_2025": 0,
"renouvellement_2026": 13219,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1015CHA",
"nom": "MECS CHANTECLAIR",
"renouvellement_2025": 0,
"renouvellement_2026": 6027,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1031MER",
"nom": "MECS MERLY",
"renouvellement_2025": 0,
"renouvellement_2026": 14184,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1038MBN",
"nom": "ITEP / SESSAD MONTBERNIER",
"renouvellement_2025": 0,
"renouvellement_2026": 17945,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1038RAC",
"nom": "MECS L'ETOILE DU RACHAIS",
"renouvellement_2025": 0,
"renouvellement_2026": 9157,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1042CLA",
"nom": "MECS LA CLAIRIERE",
"renouvellement_2025": 0,
"renouvellement_2026": 7059,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1063CRD",
"nom": "CRDV",
"renouvellement_2025": 0,
"renouvellement_2026": 22000,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1069BOU",
"nom": "IME JEAN BOURJADE",
"renouvellement_2025": 0,
"renouvellement_2026": 12835,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1069CJO",
"nom": "ITEP SESSAD CLAIR'JOIE",
"renouvellement_2025": 0,
"renouvellement_2026": 16000,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1069GOR",
"nom": "CEM HENRY GORMAND",
"renouvellement_2025": 0,
"renouvellement_2026": 26272,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1069IVP",
"nom": "IMP INSTITUT ST VINCENT DE PAUL",
"renouvellement_2025": 0,
"renouvellement_2026": 12850,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1069POR",
"nom": "MECS LE PORT",
"renouvellement_2025": 0,
"renouvellement_2026": 5693,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1069SEG",
"nom": "IME EDOUARD SEGUIN",
"renouvellement_2025": 0,
"renouvellement_2026": 6758,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1083ADV",
"nom": "CHRS LES ADRETS DU VAR",
"renouvellement_2025": 0,
"renouvellement_2026": 11550,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1083MAS",
"nom": "IME MASYLVA",
"renouvellement_2025": 0,
"renouvellement_2026": 1900,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1083MIC",
"nom": "ESAT LES MIMOSAS BC",
"renouvellement_2025": 0,
"renouvellement_2026": 0,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1083MIS",
"nom": "ESAT LES MIMOSAS BS",
"renouvellement_2025": 0,
"renouvellement_2026": 1900,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1083QVT",
"nom": "SIAO 115 TOULON",
"renouvellement_2025": 0,
"renouvellement_2026": 5700,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1083SAV",
"nom": "SAVS LES MIMOSAS",
"renouvellement_2025": 0,
"renouvellement_2026": 0,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1083SYL",
"nom": "IME SYLVABELLE",
"renouvellement_2025": 0,
"renouvellement_2026": 9250,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1084CAC",
"nom": "ESAT LES ATELIERS CHAUD D'ABRIEU BC",
"renouvellement_2025": 0,
"renouvellement_2026": 0,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1084CAS",
"nom": "ESAT LES ATELIERS CHAUD D'ABRIEU BS",
"renouvellement_2025": 0,
"renouvellement_2026": 3500,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1084RAM",
"nom": "FOYER DE VIE LA RAMADE",
"renouvellement_2025": 0,
"renouvellement_2026": 1500,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1084ROU",
"nom": "FOYER D'HEBERGEMENT LA ROUVILLIERE",
"renouvellement_2025": 0,
"renouvellement_2026": 1000,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "1084SLM",
"nom": "SAVS LA MERCI-SAVA84",
"renouvellement_2025": 0,
"renouvellement_2026": 500,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2001AJB",
"nom": "ACCUEUIL DE JOUR BON REPOS",
"renouvellement_2025": 0,
"renouvellement_2026": 0,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2001BRP",
"nom": "EHPAD BON REPOS",
"renouvellement_2025": 0,
"renouvellement_2026": 3263,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2001GRE",
"nom": "EHPAD LE CLOS DE GREX",
"renouvellement_2025": 0,
"renouvellement_2026": 8967,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2001MUS",
"nom": "EHPAD ST VINCENT DE PAUL BELLEGARDE",
"renouvellement_2025": 0,
"renouvellement_2026": 8303,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2001ROS",
"nom": "EHPAD SOEUR ROSALIE",
"renouvellement_2025": 0,
"renouvellement_2026": 5924,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2003BBN",
"nom": "EHPAD ST JOSEPH",
"renouvellement_2025": 0,
"renouvellement_2026": 4992,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2011MON",
"nom": "EHPAD ST VINCENT DE PAUL MONTOLIEU",
"renouvellement_2025": 0,
"renouvellement_2026": 6006,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2013ANG",
"nom": "SSR L'ANGELUS",
"renouvellement_2025": 0,
"renouvellement_2026": 10570,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2021BEA",
"nom": "EHPAD ST VINCENT DE PAUL VIGNOLES",
"renouvellement_2025": 0,
"renouvellement_2026": 5108,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2021MOU",
"nom": "EHPAD LES LOGIS DU PARC MOUSSIER",
"renouvellement_2025": 0,
"renouvellement_2026": 4728,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2026STA",
"nom": "EHPAD SAINTE ANNE",
"renouvellement_2025": 0,
"renouvellement_2026": 6190,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2026STM",
"nom": "EHPAD SAINTE MARTHE",
"renouvellement_2025": 0,
"renouvellement_2026": 5341,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2032RSR",
"nom": "EHPAD LA ROSERAIE",
"renouvellement_2025": 0,
"renouvellement_2026": 5589,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2038ROC",
"nom": "EHPAD NOTRE DAME DES ROCHES",
"renouvellement_2025": 0,
"renouvellement_2026": 8053,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2042PRO",
"nom": "EHPAD LA PROVIDENCE",
"renouvellement_2025": 0,
"renouvellement_2026": 17079,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2063ABV",
"nom": "SSR AUVERGNE BASSE VISION",
"renouvellement_2025": 0,
"renouvellement_2026": 2031,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2063VSJ",
"nom": "EHPAD LA VILLA SAINT JEAN",
"renouvellement_2025": 0,
"renouvellement_2026": 5324,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2064CON",
"nom": "SSR VILLA CONCHA BERRI",
"renouvellement_2025": 0,
"renouvellement_2026": 14267,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2064CSH",
"nom": "CSI HENDAYE",
"renouvellement_2025": 0,
"renouvellement_2026": 0,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2069MAU",
"nom": "EHPAD RESIDENCE CARDINAL MAURIN",
"renouvellement_2025": 0,
"renouvellement_2026": 7455,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2069PET",
"nom": "EHPAD DOROTHEE PETIT",
"renouvellement_2025": 0,
"renouvellement_2026": 5406,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2069SAL",
"nom": "EHPAD NOTRE DAME DE LA SALETTE",
"renouvellement_2025": 0,
"renouvellement_2026": 9206,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2069THE",
"nom": "EHPAD LOUISE THERESE",
"renouvellement_2025": 0,
"renouvellement_2026": 4111,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2073NDM",
"nom": "EHPAD FOYER NOTRE-DAME",
"renouvellement_2025": 0,
"renouvellement_2026": 7507,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2074MAR",
"nom": "SSR LA MARTERAYE",
"renouvellement_2025": 0,
"renouvellement_2026": 22573,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2081ANC",
"nom": "EHPAD LOUISE ANCEAU",
"renouvellement_2025": 0,
"renouvellement_2026": 5776,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2081BLA",
"nom": "EHPAD SAINT VINCENT DE PAUL BLAN",
"renouvellement_2025": 0,
"renouvellement_2026": 5806,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
},
{
"code": "2083SCL",
"nom": "EHPAD SAINTE CATHERINE LABOURE",
"renouvellement_2025": 0,
"renouvellement_2026": 7368,
"machines_supplementaires": 0,
"appel_malade": 0,
"telephonie": 0,
"wifi": 0,
"video_surveillance": 0,
"copieurs": 0,
"visio": 0,
"autres": 0,
"commentaires": ""
}
]
}

View File

@@ -0,0 +1,596 @@
// Budget2027.tsx — Page de construction du budget CAPEX DSI 2027
// Design: Corporate Modernism — Itinova Budget SI 2027
// Chaque directeur sélectionne son établissement et remplit les champs optionnels.
// Le renouvellement 2027 est pré-rempli depuis les données de vétusté.
import { useState, useMemo, useEffect } from 'react';
import { useLocation } from 'wouter';
import {
Building2,
BarChart3,
Settings,
ChevronDown,
Monitor,
PlusCircle,
Bell,
Phone,
Wifi,
Camera,
Printer,
Video,
Package,
Info,
CheckCircle,
FileText,
RotateCcw,
TrendingUp,
AlertCircle,
} from 'lucide-react';
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;
const STORAGE_KEY = 'itinova_budget2027_saisies';
// Icônes par clé de champ
const ICON_MAP: Record<string, React.ElementType> = {
renouvellement_2027: Monitor,
machines_supplementaires: PlusCircle,
appel_malade: Bell,
telephonie: Phone,
wifi: Wifi,
video_surveillance: Camera,
copieurs: Printer,
visio: Video,
autres: Package,
};
// Couleurs par champ
const COLOR_MAP: Record<string, string> = {
renouvellement_2027: 'blue',
machines_supplementaires: 'indigo',
appel_malade: 'red',
telephonie: 'green',
wifi: 'cyan',
video_surveillance: 'orange',
copieurs: 'purple',
visio: 'teal',
autres: 'slate',
};
const COLOR_CLASSES: Record<string, { bg: string; text: string; border: string; light: string }> = {
blue: { bg: 'bg-blue-600', text: 'text-blue-700', border: 'border-blue-200', light: 'bg-blue-50' },
indigo: { bg: 'bg-indigo-600', text: 'text-indigo-700', border: 'border-indigo-200', light: 'bg-indigo-50' },
red: { bg: 'bg-red-500', text: 'text-red-700', border: 'border-red-200', light: 'bg-red-50' },
green: { bg: 'bg-emerald-600',text: 'text-emerald-700',border: 'border-emerald-200',light: 'bg-emerald-50' },
cyan: { bg: 'bg-cyan-600', text: 'text-cyan-700', border: 'border-cyan-200', light: 'bg-cyan-50' },
orange: { bg: 'bg-orange-500', text: 'text-orange-700', border: 'border-orange-200', light: 'bg-orange-50' },
purple: { bg: 'bg-purple-600', text: 'text-purple-700', border: 'border-purple-200', light: 'bg-purple-50' },
teal: { bg: 'bg-teal-600', text: 'text-teal-700', border: 'border-teal-200', light: 'bg-teal-50' },
slate: { bg: 'bg-slate-500', text: 'text-slate-700', border: 'border-slate-200', light: 'bg-slate-50' },
};
const CHAMPS: { key: keyof BudgetFormValues; label: string; description: string }[] = [
{ key: 'renouvellement_2027', label: 'Renouvellement parc informatique', description: 'Pré-rempli depuis le calcul de vétusté — modifiable' },
{ key: 'machines_supplementaires', label: 'Acquisition machines supplémentaires', description: 'Nouveaux postes hors renouvellement' },
{ key: 'appel_malade', label: 'Système Appel Malade / Anti Fugue', description: 'Équipements d\'appel et de sécurité' },
{ key: 'telephonie', label: 'Installation téléphonique', description: 'Infrastructure téléphonique fixe et mobile' },
{ key: 'wifi', label: 'Réseau WiFi', description: 'Infrastructure réseau sans fil' },
{ key: 'video_surveillance', label: 'Vidéo Surveillance', description: 'Caméras et système de surveillance' },
{ key: 'copieurs', label: 'Copieurs', description: 'Imprimantes et copieurs multifonctions' },
{ key: 'visio', label: 'Équipement Visio Conférence', description: 'Matériel de visioconférence' },
{ key: 'autres', label: 'Autres investissements SI', description: 'Tout autre investissement informatique' },
];
function loadSaisies(): Record<string, Partial<BudgetFormValues>> {
try {
const raw = localStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : {};
} catch { return {}; }
}
function saveSaisies(data: Record<string, Partial<BudgetFormValues>>) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch {}
}
function getDefaultValues(etabCode: string, renouvellement2027: number): BudgetFormValues {
return {
renouvellement_2027: renouvellement2027,
machines_supplementaires: 0,
appel_malade: 0,
telephonie: 0,
wifi: 0,
video_surveillance: 0,
copieurs: 0,
visio: 0,
autres: 0,
commentaires: '',
};
}
// 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);
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;
return nbFixesR * parametres.coutFixe + nbPortablesR * parametres.coutPortable;
}
function CurrencyInput({
value,
onChange,
placeholder = '0',
disabled = false,
}: {
value: number;
onChange: (v: number) => void;
placeholder?: string;
disabled?: boolean;
}) {
const [raw, setRaw] = useState(value === 0 ? '' : String(value));
useEffect(() => {
setRaw(value === 0 ? '' : String(value));
}, [value]);
return (
<div className="relative">
<input
type="number"
min={0}
step={100}
value={raw}
placeholder={placeholder}
disabled={disabled}
onChange={e => {
setRaw(e.target.value);
const v = parseFloat(e.target.value);
onChange(isNaN(v) || v < 0 ? 0 : Math.round(v));
}}
onBlur={() => setRaw(value === 0 ? '' : String(value))}
className={`w-full pl-8 pr-12 py-2.5 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary tabular-nums transition-colors ${
disabled ? 'bg-muted/50 text-muted-foreground cursor-not-allowed' : 'bg-background'
}`}
/>
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground text-sm"></span>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-muted-foreground">TTC</span>
</div>
);
}
export default function Budget2027() {
const [, navigate] = useLocation();
const { parametres } = useParametres();
const [selectedCode, setSelectedCode] = useState<string>('');
const [saisies, setSaisies] = useState<Record<string, Partial<BudgetFormValues>>>(loadSaisies);
const [saved, setSaved] = useState(false);
const [searchEtab, setSearchEtab] = useState('');
const [dropdownOpen, setDropdownOpen] = useState(false);
// Établissement sélectionné (depuis BP2027)
const selectedBP = useMemo(
() => bp2027Data.etablissements.find(e => e.code === selectedCode) ?? null,
[selectedCode]
);
// Budget renouvellement 2027 calculé par vétusté
const renouvellement2027Calcule = useMemo(
() => selectedCode ? getBudgetRenouvellement2027(selectedCode, parametres) : 0,
[selectedCode, parametres]
);
// Valeurs du formulaire pour l'établissement sélectionné
const formValues = useMemo<BudgetFormValues>(() => {
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 (
<div className="min-h-screen flex bg-background">
{/* Sidebar */}
<aside className="w-64 flex-shrink-0 bg-[oklch(0.22_0.06_240)] text-white flex flex-col" style={{ minHeight: '100vh' }}>
<div className="px-4 py-5 border-b border-white/10">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center flex-shrink-0">
<BarChart3 className="w-4 h-4 text-white" />
</div>
<div className="min-w-0">
<p className="font-bold text-sm leading-tight" style={{ fontFamily: 'Sora, sans-serif' }}>Budget SI 2027</p>
<p className="text-[10px] text-white/50 mt-0.5">Itinova Renouvellement</p>
</div>
</div>
</div>
<nav className="flex-1 px-3 py-4 space-y-1">
<button onClick={() => navigate('/')} className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-white/60 hover:bg-white/10 hover:text-white transition-colors">
<Building2 className="w-4 h-4 flex-shrink-0" />
<span className="text-sm font-medium">Établissements</span>
</button>
<div className="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-white/10 text-white">
<FileText className="w-4 h-4 flex-shrink-0" />
<span className="text-sm font-medium">Construction BP 2027</span>
</div>
<button onClick={() => navigate('/parametres')} className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-white/60 hover:bg-white/10 hover:text-white transition-colors">
<Settings className="w-4 h-4 flex-shrink-0" />
<span className="text-sm font-medium">Paramètres</span>
</button>
</nav>
{/* Stats saisies */}
<div className="px-4 py-4 border-t border-white/10">
<p className="text-[10px] text-white/40 uppercase tracking-wider mb-2">Saisies enregistrées</p>
<div className="flex items-center gap-2">
<div className="flex-1 bg-white/10 rounded-full h-1.5">
<div
className="bg-emerald-400 h-1.5 rounded-full transition-all"
style={{ width: `${Math.min(100, (nbSaisies / bp2027Data.etablissements.length) * 100)}%` }}
/>
</div>
<span className="text-xs text-white font-medium">{nbSaisies}/{bp2027Data.etablissements.length}</span>
</div>
<p className="text-[10px] text-white/40 mt-1">établissements renseignés</p>
</div>
</aside>
{/* Contenu principal */}
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
{/* En-tête */}
<header className="bg-card border-b border-border px-6 py-4 flex-shrink-0">
<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
</h1>
<p className="text-sm text-muted-foreground mt-0.5">
Campagne budgétaire CAPEX DSI Prévisionnel 2027
</p>
</div>
{selectedCode && (
<div className="flex items-center gap-2">
<button
onClick={handleReset}
className="flex items-center gap-2 px-3 py-2 text-sm rounded-lg border border-border bg-card hover:bg-muted transition-colors text-muted-foreground"
>
<RotateCcw className="w-3.5 h-3.5" />
Réinitialiser
</button>
<button
onClick={handleSave}
className={`flex items-center gap-2 px-4 py-2 text-sm rounded-lg font-medium transition-all ${
saved ? 'bg-emerald-600 text-white' : 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm'
}`}
>
{saved ? <><CheckCircle className="w-3.5 h-3.5" />Enregistré</> : <><CheckCircle className="w-3.5 h-3.5" />Enregistrer</>}
</button>
</div>
)}
</div>
</header>
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6">
{/* Sélection établissement */}
<section className="bg-card border border-border rounded-xl p-5">
<div className="flex items-center gap-2 mb-4">
<Building2 className="w-4 h-4 text-muted-foreground" />
<h2 className="font-semibold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
Sélection de l'établissement
</h2>
</div>
<div className="relative">
<button
onClick={() => setDropdownOpen(!dropdownOpen)}
className="w-full flex items-center justify-between px-4 py-3 border border-border rounded-lg bg-background hover:bg-muted/50 transition-colors text-left"
>
{selectedBP ? (
<div className="flex items-center gap-3">
<span className="font-mono text-xs bg-primary/10 text-primary px-2 py-0.5 rounded">{selectedBP.code}</span>
<span className="font-medium text-foreground">{selectedBP.nom}</span>
</div>
) : (
<span className="text-muted-foreground">Choisir un établissement...</span>
)}
<ChevronDown className={`w-4 h-4 text-muted-foreground transition-transform ${dropdownOpen ? 'rotate-180' : ''}`} />
</button>
{dropdownOpen && (
<div className="absolute z-50 top-full left-0 right-0 mt-1 bg-card border border-border rounded-xl shadow-lg overflow-hidden">
<div className="p-2 border-b border-border">
<input
type="text"
placeholder="Rechercher..."
value={searchEtab}
onChange={e => 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
/>
</div>
<div className="max-h-64 overflow-y-auto">
{etabsFiltres.length === 0 ? (
<p className="px-4 py-3 text-sm text-muted-foreground">Aucun résultat</p>
) : (
etabsFiltres.map(e => (
<button
key={e.code}
onClick={() => { setSelectedCode(e.code); setDropdownOpen(false); setSearchEtab(''); }}
className={`w-full flex items-center gap-3 px-4 py-2.5 text-left hover:bg-muted/50 transition-colors ${
selectedCode === e.code ? 'bg-primary/5' : ''
}`}
>
<span className="font-mono text-xs bg-muted text-muted-foreground px-1.5 py-0.5 rounded flex-shrink-0">{e.code}</span>
<span className="text-sm text-foreground">{e.nom}</span>
{saisies[e.code] && (
<CheckCircle className="w-3.5 h-3.5 text-emerald-500 ml-auto flex-shrink-0" />
)}
</button>
))
)}
</div>
</div>
)}
</div>
</section>
{/* Formulaire (visible uniquement si établissement sélectionné) */}
{!selectedCode && (
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="w-16 h-16 rounded-2xl bg-muted flex items-center justify-center mb-4">
<FileText className="w-8 h-8 text-muted-foreground" />
</div>
<h3 className="font-semibold text-foreground mb-2" style={{ fontFamily: 'Sora, sans-serif' }}>
Sélectionnez un établissement
</h3>
<p className="text-sm text-muted-foreground max-w-sm">
Choisissez votre établissement dans la liste ci-dessus pour accéder au formulaire de construction budgétaire.
</p>
</div>
)}
{selectedCode && selectedBP && (
<>
{/* Données informatives 2025/2026 */}
<section className="bg-amber-50 border border-amber-200 rounded-xl p-5">
<div className="flex items-start gap-3 mb-4">
<Info className="w-4 h-4 text-amber-600 mt-0.5 flex-shrink-0" />
<div>
<h3 className="font-semibold text-amber-900" style={{ fontFamily: 'Sora, sans-serif' }}>
Données historiques — à titre informatif
</h3>
<p className="text-sm text-amber-700 mt-0.5">
Enveloppes de renouvellement allouées par le Siège Itinova (non modifiables)
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-white/70 rounded-lg px-4 py-3 border border-amber-200">
<p className="text-xs text-amber-600 font-medium uppercase tracking-wide">Renouvellement 2025</p>
<p className="text-xl font-bold text-amber-900 mt-1 tabular-nums">
{selectedBP.renouvellement_2025 > 0 ? formatEuros(selectedBP.renouvellement_2025) : ''}
</p>
<p className="text-xs text-amber-600 mt-0.5">Enveloppe Siège</p>
</div>
<div className="bg-white/70 rounded-lg px-4 py-3 border border-amber-200">
<p className="text-xs text-amber-600 font-medium uppercase tracking-wide">Renouvellement 2026</p>
<p className="text-xl font-bold text-amber-900 mt-1 tabular-nums">
{selectedBP.renouvellement_2026 > 0 ? formatEuros(selectedBP.renouvellement_2026) : ''}
</p>
<p className="text-xs text-amber-600 mt-0.5">Enveloppe Siège</p>
</div>
</div>
</section>
{/* Formulaire de saisie */}
<section className="space-y-3">
<div className="flex items-center gap-2 pb-2 border-b border-border">
<TrendingUp className="w-4 h-4 text-muted-foreground" />
<h2 className="font-semibold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
Budget prévisionnel 2027 — Saisie directeur
</h2>
<span className="text-xs text-muted-foreground ml-1">(tous les champs sont optionnels)</span>
</div>
{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 (
<div
key={champ.key}
className={`bg-card border rounded-xl p-4 transition-colors ${
numVal > 0 ? `${cls.border} ${cls.light}` : 'border-border'
}`}
>
<div className="flex items-start gap-3">
<div className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 ${cls.light} border ${cls.border}`}>
<Icon className={`w-4 h-4 ${cls.text}`} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<p className="font-medium text-sm text-foreground">{champ.label}</p>
{isRenouvellement && (
<span className="text-[10px] bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded font-medium">
Pré-rempli
</span>
)}
</div>
<p className="text-xs text-muted-foreground mb-2">{champ.description}</p>
{isRenouvellement && renouvellement2027Calcule > 0 && (
<div className="flex items-center gap-1.5 mb-2">
<AlertCircle className="w-3 h-3 text-blue-500" />
<p className="text-xs text-blue-600">
Proposé par vétusté : <strong>{formatEuros(renouvellement2027Calcule)}</strong>
{formValues.renouvellement_2027 !== renouvellement2027Calcule && (
<span className="ml-1 text-orange-500">(modifié)</span>
)}
</p>
</div>
)}
<CurrencyInput
value={numVal}
onChange={v => updateField(champ.key, v)}
/>
</div>
{numVal > 0 && (
<div className="flex-shrink-0 text-right">
<span className={`text-sm font-bold ${cls.text} tabular-nums`}>
{formatEuros(numVal)}
</span>
</div>
)}
</div>
</div>
);
})}
{/* Commentaires */}
<div className="bg-card border border-border rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<FileText className="w-4 h-4 text-muted-foreground" />
<p className="font-medium text-sm text-foreground">Commentaires</p>
<span className="text-xs text-muted-foreground">(optionnel)</span>
</div>
<textarea
rows={3}
value={formValues.commentaires}
onChange={e => updateField('commentaires', e.target.value as unknown as number)}
placeholder="Précisions, justifications, contexte particulier..."
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 focus:border-primary resize-none"
/>
</div>
</section>
{/* Récapitulatif total */}
<section className="bg-gradient-to-br from-slate-800 to-slate-900 rounded-xl p-6 text-white">
<div className="flex items-center justify-between mb-5">
<div>
<h3 className="font-bold text-lg" style={{ fontFamily: 'Sora, sans-serif' }}>
Total BP 2027
</h3>
<p className="text-white/60 text-sm mt-0.5">{selectedBP.nom}</p>
</div>
<div className="text-right">
<p className="text-3xl font-bold tabular-nums text-white">
{formatEuros(totalBP2027)}
</p>
<p className="text-white/50 text-xs mt-0.5">TTC — exercice 2027</p>
</div>
</div>
{/* Détail des lignes non nulles */}
<div className="space-y-2 border-t border-white/10 pt-4">
{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 (
<div key={champ.key} className="flex items-center justify-between text-sm">
<span className="text-white/70">{champ.label}</span>
<span className="font-medium tabular-nums text-white">{formatEuros(numVal)}</span>
</div>
);
})}
{totalBP2027 === 0 && (
<p className="text-white/40 text-sm text-center py-2">
Aucun montant saisi — remplissez les champs ci-dessus
</p>
)}
</div>
{/* Comparaison avec 2026 */}
{selectedBP.renouvellement_2026 > 0 && totalBP2027 > 0 && (
<div className="mt-4 pt-4 border-t border-white/10 flex items-center justify-between text-sm">
<span className="text-white/50">vs Enveloppe 2026</span>
<div className="flex items-center gap-2">
<span className="text-white/50">{formatEuros(selectedBP.renouvellement_2026)}</span>
<span className={`font-medium ${totalBP2027 > selectedBP.renouvellement_2026 ? 'text-red-400' : 'text-emerald-400'}`}>
{totalBP2027 > selectedBP.renouvellement_2026 ? '+' : ''}
{formatEuros(totalBP2027 - selectedBP.renouvellement_2026)}
</span>
</div>
</div>
)}
</section>
</>
)}
</div>
</main>
</div>
);
}

View File

@@ -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() {
<Building2 className="w-4 h-4 flex-shrink-0" />
{sidebarOpen && <span className="text-sm font-medium">Établissements</span>}
</div>
<button
onClick={() => navigate('/budget2027')}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-white/60 hover:bg-white/10 hover:text-white transition-colors"
>
<FileText className="w-4 h-4 flex-shrink-0" />
{sidebarOpen && <span className="text-sm font-medium">Construction BP 2027</span>}
</button>
<button
onClick={() => navigate('/parametres')}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-white/60 hover:bg-white/10 hover:text-white transition-colors"

View File

@@ -15,6 +15,7 @@ import {
Info,
BarChart3,
Building2,
FileText,
} from 'lucide-react';
import { useParametres, PARAMETRES_DEFAULTS } from '../contexts/ParametresContext';
import { formatEuros } from '../lib/format';
@@ -175,6 +176,13 @@ export default function Parametres() {
<Building2 className="w-4 h-4 flex-shrink-0" />
<span className="text-sm font-medium">Établissements</span>
</button>
<button
onClick={() => navigate('/budget2027')}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-white/60 hover:bg-white/10 hover:text-white transition-colors"
>
<FileText className="w-4 h-4 flex-shrink-0" />
<span className="text-sm font-medium">Construction BP 2027</span>
</button>
<div className="flex items-center gap-3 px-3 py-2.5 rounded-lg bg-white/10 text-white">
<Settings className="w-4 h-4 flex-shrink-0" />
<span className="text-sm font-medium">Paramètres</span>

114
client/src/types/bp2027.ts Normal file
View File

@@ -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<string, string>;
};
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',
},
];