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:
@@ -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
847
client/src/data_bp2027.json
Normal 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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
596
client/src/pages/Budget2027.tsx
Normal file
596
client/src/pages/Budget2027.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
114
client/src/types/bp2027.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user