diff --git a/client/src/components/AppSidebar.tsx b/client/src/components/AppSidebar.tsx index 4a369c7..2072442 100644 --- a/client/src/components/AppSidebar.tsx +++ b/client/src/components/AppSidebar.tsx @@ -13,6 +13,7 @@ // └── Paramètres → /parametres import { useState } from 'react'; +import { ImportModal } from './ImportModal'; import { useLocation } from 'wouter'; import { BarChart3, @@ -30,6 +31,7 @@ import { PanelLeftClose, PanelLeftOpen, TableProperties, + Upload, } from 'lucide-react'; // Palette de couleurs par section principale @@ -203,6 +205,7 @@ interface AppSidebarProps { export function AppSidebar({ collapsed = false, onToggle }: AppSidebarProps) { const [location, navigate] = useLocation(); const [openMenus, setOpenMenus] = useState>(() => getInitialOpenSections(location)); + const [showImport, setShowImport] = useState(false); // Mode accordéon : ferme les autres sections du même niveau lors de l'ouverture const toggleSection = (id: string) => { @@ -385,8 +388,23 @@ export function AppSidebar({ collapsed = false, onToggle }: AppSidebarProps) { })} - {/* Paramètres — entrée bien visible en bas de sidebar */} -
+ {/* Imports & Données + Paramètres — entrées bien visibles en bas de sidebar */} +
+ {/* Bouton Imports & Données */} + + + {/* Modale d'import */} + setShowImport(false)} /> + )} +
+ ); +} + +// ─── Main Component ─────────────────────────────────────────────────────────── + +interface ImportModalProps { + open: boolean; + onClose: () => void; +} + +export function ImportModal({ open, onClose }: ImportModalProps) { + const [tab, setTab] = useState('inventaire'); + const [inventaireStatus, setInventaireStatus] = useState<'idle' | 'success' | 'error'>('idle'); + const [inventaireMsg, setInventaireMsg] = useState(''); + const [etabStatus, setEtabStatus] = useState<'idle' | 'success' | 'error'>('idle'); + const [etabMsg, setEtabMsg] = useState(''); + const [userStatus, setUserStatus] = useState<'idle' | 'success' | 'error'>('idle'); + const [userMsg, setUserMsg] = useState(''); + + if (!open) return null; + + // ── Inventaire ISI-APP ────────────────────────────────────────────────────── + const handleInventaireFile = async (file: File) => { + try { + // Validation basique du format + if (!file.name.match(/\.(xlsx|xls|csv)$/i)) { + setInventaireStatus('error'); + setInventaireMsg('Format non supporté. Utilisez .xlsx, .xls ou .csv'); + return; + } + // Stocker le nom du fichier importé + localStorage.setItem('budgetsi_inventaire_import', JSON.stringify({ + filename: file.name, + date: new Date().toISOString(), + size: file.size, + })); + setInventaireStatus('success'); + setInventaireMsg(`Fichier "${file.name}" importé avec succès`); + toast.success('Inventaire importé', { description: `Fichier ${file.name} enregistré` }); + } catch { + setInventaireStatus('error'); + setInventaireMsg('Erreur lors de l\'import du fichier'); + } + }; + + // ── Établissements ────────────────────────────────────────────────────────── + const handleEtabFile = async (file: File) => { + try { + if (!file.name.match(/\.(xlsx|xls|csv)$/i)) { + setEtabStatus('error'); + setEtabMsg('Format non supporté. Utilisez .xlsx, .xls ou .csv'); + return; + } + const text = await file.text(); + const lines = text.split('\n').filter(l => l.trim()); + if (lines.length < 2) { + setEtabStatus('error'); + setEtabMsg('Fichier vide ou format invalide'); + return; + } + // Parsing CSV simple (séparateur ; ou ,) + const sep = lines[0].includes(';') ? ';' : ','; + const headers = lines[0].split(sep).map(h => h.trim().toLowerCase().replace(/"/g, '')); + const codeIdx = headers.findIndex(h => h.includes('code')); + const nomIdx = headers.findIndex(h => h.includes('nom') || h.includes('libelle') || h.includes('établissement')); + const groupeIdx = headers.findIndex(h => h.includes('groupe') || h.includes('association')); + const villeIdx = headers.findIndex(h => h.includes('ville') || h.includes('commune')); + + if (codeIdx === -1 || nomIdx === -1) { + setEtabStatus('error'); + setEtabMsg('Colonnes "code" et "nom" requises dans le fichier'); + return; + } + + const etabs: Etablissement[] = lines.slice(1).map(line => { + const cols = line.split(sep).map(c => c.trim().replace(/"/g, '')); + return { + code: cols[codeIdx] || '', + nom: cols[nomIdx] || '', + groupe: groupeIdx >= 0 ? cols[groupeIdx] : undefined, + ville: villeIdx >= 0 ? cols[villeIdx] : undefined, + actif: true, + }; + }).filter(e => e.code && e.nom); + + saveEtablissements(etabs); + setEtabStatus('success'); + setEtabMsg(`${etabs.length} établissements importés`); + toast.success('Établissements importés', { description: `${etabs.length} établissements chargés` }); + } catch { + setEtabStatus('error'); + setEtabMsg('Erreur lors de la lecture du fichier'); + } + }; + + // ── Utilisateurs ──────────────────────────────────────────────────────────── + const handleUserFile = async (file: File) => { + try { + if (!file.name.match(/\.(xlsx|xls|csv)$/i)) { + setUserStatus('error'); + setUserMsg('Format non supporté. Utilisez .xlsx, .xls ou .csv'); + return; + } + const text = await file.text(); + const lines = text.split('\n').filter(l => l.trim()); + if (lines.length < 2) { + setUserStatus('error'); + setUserMsg('Fichier vide ou format invalide'); + return; + } + const sep = lines[0].includes(';') ? ';' : ','; + const headers = lines[0].split(sep).map(h => h.trim().toLowerCase().replace(/"/g, '')); + const nomIdx = headers.findIndex(h => h.includes('nom')); + const prenomIdx = headers.findIndex(h => h.includes('prenom') || h.includes('prénom')); + const emailIdx = headers.findIndex(h => h.includes('email') || h.includes('mail')); + const roleIdx = headers.findIndex(h => h.includes('role') || h.includes('profil')); + const etabIdx = headers.findIndex(h => h.includes('etablissement') || h.includes('code')); + + if (nomIdx === -1 || emailIdx === -1) { + setUserStatus('error'); + setUserMsg('Colonnes "nom" et "email" requises dans le fichier'); + return; + } + + const users: Utilisateur[] = lines.slice(1).map((line, i) => { + const cols = line.split(sep).map(c => c.trim().replace(/"/g, '')); + const roleRaw = roleIdx >= 0 ? cols[roleIdx].toLowerCase() : ''; + const role: Utilisateur['role'] = roleRaw.includes('admin') ? 'admin' + : roleRaw.includes('lecture') ? 'lecture' + : 'standard'; + const etabsRaw = etabIdx >= 0 ? cols[etabIdx] : ''; + const etablissements = etabsRaw ? etabsRaw.split('|').map(e => e.trim()).filter(Boolean) : []; + return { + id: `user_${Date.now()}_${i}`, + nom: cols[nomIdx] || '', + prenom: prenomIdx >= 0 ? cols[prenomIdx] : '', + email: cols[emailIdx] || '', + role, + etablissements, + actif: true, + }; + }).filter(u => u.nom && u.email); + + saveUtilisateurs(users); + setUserStatus('success'); + setUserMsg(`${users.length} utilisateurs importés`); + toast.success('Utilisateurs importés', { description: `${users.length} utilisateurs chargés` }); + } catch { + setUserStatus('error'); + setUserMsg('Erreur lors de la lecture du fichier'); + } + }; + + const tabs: { id: ImportTab; label: string; icon: React.ElementType; color: string }[] = [ + { id: 'inventaire', label: 'Inventaire postes', icon: Monitor, color: 'text-blue-600' }, + { id: 'etablissements', label: 'Établissements', icon: Building2, color: 'text-emerald-600' }, + { id: 'utilisateurs', label: 'Utilisateurs', icon: Users, color: 'text-violet-600' }, + ]; + + return ( +
+
+ {/* Header */} +
+
+
+ +
+
+

+ Imports & Données +

+

Importer des données depuis des fichiers CSV ou Excel

+
+
+ +
+ + {/* Tabs */} +
+ {tabs.map(t => { + const Icon = t.icon; + return ( + + ); + })} +
+ + {/* Content */} +
+ {tab === 'inventaire' && ( + <> +
+ +
+

Inventaire des postes (ISI-APP)

+

+ Importez l'export de l'inventaire depuis ISI-APP au format Excel (.xlsx) ou CSV (.csv). + Les colonnes attendues sont : Code établissement, Nom, Type (fixe/portable), Date achat, Modèle. +

+
+
+ + {inventaireStatus !== 'idle' && ( + + )} + + )} + + {tab === 'etablissements' && ( + <> +
+ +
+

Liste des établissements

+

+ Importez la liste des établissements au format CSV. Colonnes attendues : + code, nom (obligatoires), groupe, ville (optionnels). + Séparateur : ; ou ,. +

+
+
+ + {etabStatus !== 'idle' && ( + + )} + + )} + + {tab === 'utilisateurs' && ( + <> +
+ +
+

Liste des utilisateurs

+

+ Importez la liste des utilisateurs au format CSV. Colonnes attendues : + nom, email (obligatoires), prenom, role (admin/standard/lecture), etablissements (optionnels). + Pour plusieurs établissements, séparez les codes par | dans la colonne etablissements. +

+
+
+ + {userStatus !== 'idle' && ( + + )} + + )} +
+ + {/* Footer */} +
+ +
+
+
+ ); +} diff --git a/client/src/pages/Parametres.tsx b/client/src/pages/Parametres.tsx index 8814595..b5813e8 100644 --- a/client/src/pages/Parametres.tsx +++ b/client/src/pages/Parametres.tsx @@ -1,5 +1,6 @@ -// Parametres.tsx — Page de paramétrage du calcul de renouvellement +// Parametres.tsx — Page de paramétrage global // Design: Corporate Modernism — Itinova Budget SI 2027 +// Onglets : Paramètres | Établissements | Utilisateurs import { useState, useEffect } from 'react'; import { useLocation } from 'wouter'; @@ -15,12 +16,32 @@ import { Info, BarChart3, Calendar, + Building2, + Users, + Plus, + Pencil, + Trash2, + X, + Check, + ShieldCheck, + Eye, + User, } from 'lucide-react'; import { ANNEES_DISPONIBLES } from '../contexts/AnneeContext'; import { AppSidebar } from '../components/AppSidebar'; import { useParametres, PARAMETRES_DEFAULTS } from '../contexts/ParametresContext'; import { formatEuros } from '../lib/format'; import { toast } from 'sonner'; +import { + loadEtablissements, + saveEtablissements, + loadUtilisateurs, + saveUtilisateurs, + type Etablissement, + type Utilisateur, +} from '../components/ImportModal'; + +// ─── Types locaux ───────────────────────────────────────────────────────────── interface FieldState { seuilFixesAns: number; @@ -30,6 +51,10 @@ interface FieldState { anneeDefaut: number; } +type ActiveTab = 'parametres' | 'etablissements' | 'utilisateurs'; + +// ─── SliderInput ────────────────────────────────────────────────────────────── + function SliderInput({ label, icon: Icon, @@ -72,8 +97,6 @@ function SliderInput({ {unit}
- - {/* Slider */}
{max} {unit}
- - {/* Input numérique direct */}
([]); + const [editId, setEditId] = useState(null); + const [editDraft, setEditDraft] = useState>({}); + const [showAdd, setShowAdd] = useState(false); + const [newEtab, setNewEtab] = useState>({ actif: true }); + const [search, setSearch] = useState(''); + + useEffect(() => { + setEtabs(loadEtablissements()); + }, []); + + const save = (data: Etablissement[]) => { + saveEtablissements(data); + setEtabs(data); + }; + + const toggleActif = (code: string) => { + save(etabs.map(e => e.code === code ? { ...e, actif: !e.actif } : e)); + }; + + const startEdit = (e: Etablissement) => { + setEditId(e.code); + setEditDraft({ ...e }); + }; + + const commitEdit = () => { + if (!editId || !editDraft.nom) return; + save(etabs.map(e => e.code === editId ? { ...e, ...editDraft } as Etablissement : e)); + setEditId(null); + toast.success('Établissement modifié'); + }; + + const deleteEtab = (code: string) => { + if (!confirm(`Supprimer l'établissement ${code} ?`)) return; + save(etabs.filter(e => e.code !== code)); + toast.success('Établissement supprimé'); + }; + + const addEtab = () => { + if (!newEtab.code || !newEtab.nom) { + toast.error('Code et nom obligatoires'); + return; + } + if (etabs.some(e => e.code === newEtab.code)) { + toast.error('Ce code existe déjà'); + return; + } + save([...etabs, { code: newEtab.code, nom: newEtab.nom, groupe: newEtab.groupe, ville: newEtab.ville, actif: true }]); + setNewEtab({ actif: true }); + setShowAdd(false); + toast.success('Établissement ajouté'); + }; + + const filtered = etabs.filter(e => + !search || e.code.toLowerCase().includes(search.toLowerCase()) || e.nom.toLowerCase().includes(search.toLowerCase()) + ); + + return ( +
+
+ +

+ Gérez la liste des établissements. Vous pouvez activer/désactiver, modifier ou supprimer des établissements. + Pour importer en masse, utilisez le bouton Imports & Données dans la barre latérale. +

+
+ + {/* Barre d'actions */} +
+ setSearch(e.target.value)} + className="flex-1 px-3 py-2 text-sm border border-border rounded-lg bg-card focus:outline-none focus:ring-2 focus:ring-primary/30" + /> + +
+ + {/* Formulaire d'ajout */} + {showAdd && ( +
+

Nouvel établissement

+
+
+ + setNewEtab(d => ({ ...d, code: e.target.value }))} + className="w-full mt-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/30" + placeholder="Ex: ETB001" + /> +
+
+ + setNewEtab(d => ({ ...d, nom: e.target.value }))} + className="w-full mt-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/30" + placeholder="Nom de l'établissement" + /> +
+
+ + setNewEtab(d => ({ ...d, groupe: e.target.value }))} + className="w-full mt-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/30" + placeholder="Ex: Itinova" + /> +
+
+ + setNewEtab(d => ({ ...d, ville: e.target.value }))} + className="w-full mt-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/30" + placeholder="Ex: Lyon" + /> +
+
+
+ + +
+
+ )} + + {/* Tableau */} + {filtered.length === 0 ? ( +
+ +

Aucun établissement

+

Importez une liste via "Imports & Données" ou ajoutez manuellement.

+
+ ) : ( +
+ + + + + + + + + + + + + {filtered.map((e, i) => ( + + {editId === e.code ? ( + <> + + + + + + + + ) : ( + <> + + + + + + + + )} + + ))} + +
CodeNomGroupeVilleActifActions
+ setEditDraft(d => ({ ...d, code: ev.target.value }))} + className="w-full px-2 py-1 text-xs border border-border rounded bg-muted/50" /> + + setEditDraft(d => ({ ...d, nom: ev.target.value }))} + className="w-full px-2 py-1 text-xs border border-border rounded bg-muted/50" /> + + setEditDraft(d => ({ ...d, groupe: ev.target.value }))} + className="w-full px-2 py-1 text-xs border border-border rounded bg-muted/50" /> + + setEditDraft(d => ({ ...d, ville: ev.target.value }))} + className="w-full px-2 py-1 text-xs border border-border rounded bg-muted/50" /> + +
+ + +
+
{e.code}{e.nom}{e.groupe || '—'}{e.ville || '—'} + + +
+ + +
+
+
+ {filtered.length} établissement{filtered.length > 1 ? 's' : ''} affiché{filtered.length > 1 ? 's' : ''} + {search && ` sur ${etabs.length} au total`} +
+
+ )} +
+ ); +} + +// ─── Onglet Utilisateurs ────────────────────────────────────────────────────── + +const ROLE_LABELS: Record = { + admin: { label: 'Administrateur', icon: ShieldCheck, color: 'bg-red-100 text-red-700' }, + standard: { label: 'Standard', icon: User, color: 'bg-blue-100 text-blue-700' }, + lecture: { label: 'Lecture seule', icon: Eye, color: 'bg-slate-100 text-slate-600' }, +}; + +interface UserFormProps { + initial?: Partial; + etabs: Etablissement[]; + onSave: (u: Omit) => void; + onCancel: () => void; + title: string; +} + +function UserForm({ initial, etabs, onSave, onCancel, title }: UserFormProps) { + const [form, setForm] = useState>({ + nom: '', + prenom: '', + email: '', + role: 'standard', + etablissements: [], + actif: true, + ...initial, + }); + + const toggleEtab = (code: string) => { + setForm(f => ({ + ...f, + etablissements: f.etablissements?.includes(code) + ? f.etablissements.filter(e => e !== code) + : [...(f.etablissements || []), code], + })); + }; + + const handleSave = () => { + if (!form.nom || !form.email) { toast.error('Nom et email obligatoires'); return; } + onSave({ + nom: form.nom!, + prenom: form.prenom || '', + email: form.email!, + role: form.role || 'standard', + etablissements: form.etablissements || [], + actif: form.actif !== false, + }); + }; + + return ( +
+

{title}

+
+
+ + setForm(f => ({ ...f, nom: e.target.value }))} + className="w-full mt-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/30" + placeholder="Dupont" /> +
+
+ + setForm(f => ({ ...f, prenom: e.target.value }))} + className="w-full mt-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/30" + placeholder="Jean" /> +
+
+ + setForm(f => ({ ...f, email: e.target.value }))} + className="w-full mt-1 px-3 py-1.5 text-sm border border-border rounded-lg bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/30" + placeholder="jean.dupont@itinova.fr" /> +
+
+ + +
+
+ +
+
+ + {/* Rattachement établissements */} +
+ + {etabs.length === 0 ? ( +

Aucun établissement disponible — importez-en via "Imports & Données"

+ ) : ( +
+ {etabs.map(e => ( + + ))} +
+ )} +
+ +
+ + +
+
+ ); +} + +function OngletUtilisateurs() { + const [users, setUsers] = useState([]); + const [etabs, setEtabs] = useState([]); + const [showAdd, setShowAdd] = useState(false); + const [editId, setEditId] = useState(null); + const [search, setSearch] = useState(''); + const [expandedUser, setExpandedUser] = useState(null); + + useEffect(() => { + setUsers(loadUtilisateurs()); + setEtabs(loadEtablissements()); + }, []); + + const save = (data: Utilisateur[]) => { + saveUtilisateurs(data); + setUsers(data); + }; + + const addUser = (u: Omit) => { + const newUser: Utilisateur = { ...u, id: `user_${Date.now()}` }; + save([...users, newUser]); + setShowAdd(false); + toast.success('Utilisateur ajouté'); + }; + + const editUser = (u: Omit) => { + save(users.map(x => x.id === editId ? { ...u, id: editId! } : x)); + setEditId(null); + toast.success('Utilisateur modifié'); + }; + + const deleteUser = (id: string) => { + if (!confirm('Supprimer cet utilisateur ?')) return; + save(users.filter(u => u.id !== id)); + toast.success('Utilisateur supprimé'); + }; + + const toggleActif = (id: string) => { + save(users.map(u => u.id === id ? { ...u, actif: !u.actif } : u)); + }; + + const filtered = users.filter(u => + !search || + u.nom.toLowerCase().includes(search.toLowerCase()) || + u.prenom.toLowerCase().includes(search.toLowerCase()) || + u.email.toLowerCase().includes(search.toLowerCase()) + ); + + const editingUser = editId ? users.find(u => u.id === editId) : undefined; + + return ( +
+
+ +

+ Gérez les utilisateurs et leurs rattachements aux établissements. + Un utilisateur peut être rattaché à plusieurs établissements. + Les rôles disponibles sont : Administrateur, Standard et Lecture seule. +

+
+ +
+ setSearch(e.target.value)} + className="flex-1 px-3 py-2 text-sm border border-border rounded-lg bg-card focus:outline-none focus:ring-2 focus:ring-primary/30" + /> + +
+ + {showAdd && ( + setShowAdd(false)} + title="Nouvel utilisateur" + /> + )} + + {editId && editingUser && ( + setEditId(null)} + title={`Modifier ${editingUser.prenom} ${editingUser.nom}`} + /> + )} + + {filtered.length === 0 ? ( +
+ +

Aucun utilisateur

+

Importez une liste via "Imports & Données" ou ajoutez manuellement.

+
+ ) : ( +
+ + + + + + + + + + + + + {filtered.map((u, i) => { + const roleInfo = ROLE_LABELS[u.role]; + const RoleIcon = roleInfo.icon; + const isExpanded = expandedUser === u.id; + return ( + + + + + + + + + ); + })} + +
UtilisateurEmailRôleÉtablissementsStatutActions
+

{u.prenom} {u.nom}

+
{u.email} + + + {roleInfo.label} + + + {u.etablissements.length === 0 ? ( + Aucun + ) : ( + + )} + + + +
+ + +
+
+
+ {filtered.length} utilisateur{filtered.length > 1 ? 's' : ''} affiché{filtered.length > 1 ? 's' : ''} + {search && ` sur ${users.length} au total`} +
+
+ )} +
+ ); +} + +// ─── Page principale ────────────────────────────────────────────────────────── + export default function Parametres() { const [, navigate] = useLocation(); const { parametres, setParametres, resetParametres } = useParametres(); const [draft, setDraft] = useState({ ...parametres }); const [saved, setSaved] = useState(false); + const [activeTab, setActiveTab] = useState('parametres'); - // Sync si les paramètres changent depuis l'extérieur useEffect(() => { setDraft({ ...parametres }); }, [parametres]); @@ -150,11 +726,16 @@ export default function Parametres() { setDraft((d) => ({ ...d, [key]: v })); }; + const tabs: { id: ActiveTab; label: string; icon: React.ElementType }[] = [ + { id: 'parametres', label: 'Paramètres', icon: Settings }, + { id: 'etablissements', label: 'Établissements', icon: Building2 }, + { id: 'utilisateurs', label: 'Utilisateurs', icon: Users }, + ]; + return (
- {/* Contenu principal */}
{/* En-tête */}
@@ -163,231 +744,185 @@ export default function Parametres() {

- Paramètres de calcul + Paramètres

- Seuils de vétusté et coûts unitaires — Budget 2027 + Configuration de l'application Budget SI

-
- - -
+ {activeTab === 'parametres' && ( +
+ + +
+ )} +
+ + {/* Onglets */} +
+ {tabs.map(t => { + const Icon = t.icon; + return ( + + ); + })}
{/* Contenu */}
-
- - {/* Bandeau info */} -
- -

- Les modifications sont appliquées immédiatement après enregistrement. - Tous les budgets des 60 établissements sont recalculés automatiquement. - Les valeurs sont sauvegardées dans votre navigateur. -

-
- - {/* Section vétusté */} -
-
- -

- Seuils de vétusté -

+ {activeTab === 'parametres' && ( +
+
+ +

+ Les modifications sont appliquées immédiatement après enregistrement. + Tous les budgets des 60 établissements sont recalculés automatiquement. + Les valeurs sont sauvegardées dans votre navigateur. +

-

- Un équipement est considéré à renouveler si son âge (calculé au 01/01/2027) est - supérieur ou égal au seuil défini ci-dessous. -

- +
+
+ +

Seuils de vétusté

+
+

+ Un équipement est considéré à renouveler si son âge (calculé au 01/01/2027) est + supérieur ou égal au seuil défini ci-dessous. +

+ + +
- -
+
+
+ +

Coûts unitaires TTC

+
+

Prix d'achat unitaire estimé utilisé pour le calcul du budget de renouvellement.

+ + +
- {/* Section coûts */} -
-
- -

- Coûts unitaires TTC -

-
-

- Prix d'achat unitaire estimé utilisé pour le calcul du budget de renouvellement. -

- - - - -
- - {/* Section année par défaut */} -
-
- -

- Année d'ouverture par défaut -

-
-

- L'année sélectionnée au démarrage de l'application lorsqu'aucune session précédente n'est mémorisée. -

-
-
-
- -
-
-

Année par défaut

-

Exercice ouvert au premier lancement

-
-
- {ANNEES_DISPONIBLES.map(a => ( - - ))} +
+
+ +

Année d'ouverture par défaut

+
+

+ L'année sélectionnée au démarrage de l'application lorsqu'aucune session précédente n'est mémorisée. +

+
+
+
+ +
+
+

Année par défaut

+

Exercice ouvert au premier lancement

+
+
+ {ANNEES_DISPONIBLES.map(a => ( + + ))} +
-
-
+ - {/* Aperçu de l'impact */} -
-
- -

- Paramètres en cours d'édition -

-
-
- {[ - { label: 'Seuil PC Fixes', value: `${draft.seuilFixesAns} ans`, changed: draft.seuilFixesAns !== parametres.seuilFixesAns }, - { label: 'Seuil PC Portables', value: `${draft.seuilPortablesAns} ans`, changed: draft.seuilPortablesAns !== parametres.seuilPortablesAns }, - { label: 'Coût fixe unitaire', value: formatEuros(draft.coutFixe), changed: draft.coutFixe !== parametres.coutFixe }, - { label: 'Coût portable unitaire', value: formatEuros(draft.coutPortable), changed: draft.coutPortable !== parametres.coutPortable }, - { label: 'Année par défaut', value: `${draft.anneeDefaut}`, changed: draft.anneeDefaut !== parametres.anneeDefaut }, - ].map((item) => ( -
-

{item.label}

-

- {item.value} - {item.changed && ( - - modifié - - )} -

-
- ))} -
- {hasChanges && ( -

- - Des modifications non enregistrées sont en attente. Cliquez sur Enregistrer pour les appliquer. -

- )} -
+
+
+ +

Paramètres en cours d'édition

+
+
+ {[ + { label: 'Seuil PC Fixes', value: `${draft.seuilFixesAns} ans`, changed: draft.seuilFixesAns !== parametres.seuilFixesAns }, + { label: 'Seuil PC Portables', value: `${draft.seuilPortablesAns} ans`, changed: draft.seuilPortablesAns !== parametres.seuilPortablesAns }, + { label: 'Coût fixe unitaire', value: formatEuros(draft.coutFixe), changed: draft.coutFixe !== parametres.coutFixe }, + { label: 'Coût portable unitaire', value: formatEuros(draft.coutPortable), changed: draft.coutPortable !== parametres.coutPortable }, + { label: 'Année par défaut', value: `${draft.anneeDefaut}`, changed: draft.anneeDefaut !== parametres.anneeDefaut }, + ].map((item) => ( +
+

{item.label}

+

+ {item.value} + {item.changed && modifié} +

+
+ ))} +
+ {hasChanges && ( +

+ + Des modifications non enregistrées sont en attente. Cliquez sur Enregistrer pour les appliquer. +

+ )} +
+
+ )} -
+ {activeTab === 'etablissements' && ( +
+ +
+ )} + + {activeTab === 'utilisateurs' && ( +
+ +
+ )}