Checkpoint: Ajout du champ anneeDefaut dans ParametresContext (avec persistance localStorage). Nouvelle section "Année d'ouverture par défaut" dans la page Paramètres avec sélecteur de boutons (2025-2030). AnneeContext mis à jour pour utiliser cette valeur au premier lancement (quand aucune session n'est mémorisée). La page Paramètres reste accessible via l'icône engrenage en bas de la sidebar.

This commit is contained in:
Manus
2026-06-03 14:48:16 +00:00
parent 5cad041d95
commit 938a567cce
3 changed files with 69 additions and 3 deletions

View File

@@ -6,6 +6,21 @@ import { createContext, useContext, useState, ReactNode } from 'react';
export const ANNEES_DISPONIBLES = [2025, 2026, 2027, 2028, 2029, 2030]; export const ANNEES_DISPONIBLES = [2025, 2026, 2027, 2028, 2029, 2030];
export const ANNEE_DEFAUT = 2027; export const ANNEE_DEFAUT = 2027;
const PARAMETRES_STORAGE_KEY = 'itinova_budget_parametres';
function getAnneeDefautFromParametres(): number {
try {
const raw = localStorage.getItem(PARAMETRES_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (typeof parsed.anneeDefaut === 'number' && ANNEES_DISPONIBLES.includes(parsed.anneeDefaut)) {
return parsed.anneeDefaut;
}
}
} catch { /* ignore */ }
return ANNEE_DEFAUT;
}
interface AnneeContextType { interface AnneeContextType {
annee: number; annee: number;
setAnnee: (a: number) => void; setAnnee: (a: number) => void;
@@ -20,8 +35,12 @@ export function AnneeProvider({ children }: { children: ReactNode }) {
const [annee, setAnneeState] = useState<number>(() => { const [annee, setAnneeState] = useState<number>(() => {
try { try {
const stored = localStorage.getItem('itinova_annee_selectionnee'); const stored = localStorage.getItem('itinova_annee_selectionnee');
const parsed = stored ? parseInt(stored, 10) : ANNEE_DEFAUT; if (stored) {
return ANNEES_DISPONIBLES.includes(parsed) ? parsed : ANNEE_DEFAUT; const parsed = parseInt(stored, 10);
if (ANNEES_DISPONIBLES.includes(parsed)) return parsed;
}
// Pas de session mémorisée → utiliser l'année par défaut des paramètres
return getAnneeDefautFromParametres();
} catch { return ANNEE_DEFAUT; } } catch { return ANNEE_DEFAUT; }
}); });

View File

@@ -8,6 +8,7 @@ export interface Parametres {
seuilPortablesAns: number; seuilPortablesAns: number;
coutFixe: number; coutFixe: number;
coutPortable: number; coutPortable: number;
anneeDefaut: number;
} }
const DEFAULTS: Parametres = { const DEFAULTS: Parametres = {
@@ -15,6 +16,7 @@ const DEFAULTS: Parametres = {
seuilPortablesAns: 4, seuilPortablesAns: 4,
coutFixe: 450, coutFixe: 450,
coutPortable: 1000, coutPortable: 1000,
anneeDefaut: 2027,
}; };
const STORAGE_KEY = 'itinova_budget_parametres'; const STORAGE_KEY = 'itinova_budget_parametres';
@@ -29,6 +31,7 @@ function loadFromStorage(): Parametres {
seuilPortablesAns: typeof parsed.seuilPortablesAns === 'number' ? parsed.seuilPortablesAns : DEFAULTS.seuilPortablesAns, seuilPortablesAns: typeof parsed.seuilPortablesAns === 'number' ? parsed.seuilPortablesAns : DEFAULTS.seuilPortablesAns,
coutFixe: typeof parsed.coutFixe === 'number' ? parsed.coutFixe : DEFAULTS.coutFixe, coutFixe: typeof parsed.coutFixe === 'number' ? parsed.coutFixe : DEFAULTS.coutFixe,
coutPortable: typeof parsed.coutPortable === 'number' ? parsed.coutPortable : DEFAULTS.coutPortable, coutPortable: typeof parsed.coutPortable === 'number' ? parsed.coutPortable : DEFAULTS.coutPortable,
anneeDefaut: typeof parsed.anneeDefaut === 'number' ? parsed.anneeDefaut : DEFAULTS.anneeDefaut,
}; };
} catch { } catch {
return DEFAULTS; return DEFAULTS;

View File

@@ -14,7 +14,9 @@ import {
ChevronLeft, ChevronLeft,
Info, Info,
BarChart3, BarChart3,
Calendar,
} from 'lucide-react'; } from 'lucide-react';
import { ANNEES_DISPONIBLES } from '../contexts/AnneeContext';
import { AppSidebar } from '../components/AppSidebar'; import { AppSidebar } from '../components/AppSidebar';
import { useParametres, PARAMETRES_DEFAULTS } from '../contexts/ParametresContext'; import { useParametres, PARAMETRES_DEFAULTS } from '../contexts/ParametresContext';
import { formatEuros } from '../lib/format'; import { formatEuros } from '../lib/format';
@@ -25,6 +27,7 @@ interface FieldState {
seuilPortablesAns: number; seuilPortablesAns: number;
coutFixe: number; coutFixe: number;
coutPortable: number; coutPortable: number;
anneeDefaut: number;
} }
function SliderInput({ function SliderInput({
@@ -123,7 +126,8 @@ export default function Parametres() {
draft.seuilFixesAns !== parametres.seuilFixesAns || draft.seuilFixesAns !== parametres.seuilFixesAns ||
draft.seuilPortablesAns !== parametres.seuilPortablesAns || draft.seuilPortablesAns !== parametres.seuilPortablesAns ||
draft.coutFixe !== parametres.coutFixe || draft.coutFixe !== parametres.coutFixe ||
draft.coutPortable !== parametres.coutPortable; draft.coutPortable !== parametres.coutPortable ||
draft.anneeDefaut !== parametres.anneeDefaut;
const handleSave = () => { const handleSave = () => {
setParametres(draft); setParametres(draft);
@@ -300,6 +304,45 @@ export default function Parametres() {
/> />
</section> </section>
{/* Section année par défaut */}
<section className="space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-border">
<Calendar className="w-4 h-4 text-muted-foreground" />
<h2 className="font-semibold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>
Année d'ouverture par défaut
</h2>
</div>
<p className="text-sm text-muted-foreground">
L'année sélectionnée au démarrage de l'application lorsqu'aucune session précédente n'est mémorisée.
</p>
<div className="bg-card border border-border rounded-xl p-5">
<div className="flex items-center gap-4 flex-wrap">
<div className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 bg-indigo-100 text-indigo-700">
<Calendar className="w-5 h-5" />
</div>
<div className="flex-1">
<p className="font-semibold text-foreground" style={{ fontFamily: 'Sora, sans-serif' }}>Année par défaut</p>
<p className="text-sm text-muted-foreground mt-0.5">Exercice ouvert au premier lancement</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
{ANNEES_DISPONIBLES.map(a => (
<button
key={a}
onClick={() => setDraft(d => ({ ...d, anneeDefaut: a }))}
className={`px-4 py-2 rounded-lg text-sm font-semibold transition-all ${
draft.anneeDefaut === a
? 'bg-indigo-600 text-white shadow-sm'
: 'bg-muted text-muted-foreground hover:bg-muted/70'
}`}
>
{a}
</button>
))}
</div>
</div>
</div>
</section>
{/* Aperçu de l'impact */} {/* Aperçu de l'impact */}
<section className="space-y-4"> <section className="space-y-4">
<div className="flex items-center gap-2 pb-2 border-b border-border"> <div className="flex items-center gap-2 pb-2 border-b border-border">
@@ -314,6 +357,7 @@ export default function Parametres() {
{ label: 'Seuil PC Portables', value: `${draft.seuilPortablesAns} ans`, changed: draft.seuilPortablesAns !== parametres.seuilPortablesAns }, { 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 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: '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) => ( ].map((item) => (
<div <div
key={item.label} key={item.label}