Initial commit
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { api } from '../api.js';
|
||||
|
||||
const AuthCtx = createContext(null);
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const [token, setToken] = useState(() => localStorage.getItem('cl_token'));
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) { setLoading(false); return; }
|
||||
api.get('/auth/me')
|
||||
.then(({ user }) => setUser(user))
|
||||
.catch(() => logout())
|
||||
.finally(() => setLoading(false));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
const login = async (email, password) => {
|
||||
const r = await api.post('/auth/login', { email, password });
|
||||
localStorage.setItem('cl_token', r.token);
|
||||
setToken(r.token);
|
||||
setUser(r.user);
|
||||
return r.user;
|
||||
};
|
||||
|
||||
const register = async (email, password, displayName) => {
|
||||
const r = await api.post('/auth/register', { email, password, displayName });
|
||||
localStorage.setItem('cl_token', r.token);
|
||||
setToken(r.token);
|
||||
setUser(r.user);
|
||||
return r.user;
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('cl_token');
|
||||
localStorage.removeItem('cl_investisseur_id');
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
const updateUser = async (payload) => {
|
||||
const r = await api.put('/auth/me', payload);
|
||||
setUser(r.user);
|
||||
// If backend issued a fresh token (email changed), persist it
|
||||
if (r.token) {
|
||||
localStorage.setItem('cl_token', r.token);
|
||||
setToken(r.token);
|
||||
}
|
||||
return r.user;
|
||||
};
|
||||
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
return (
|
||||
<AuthCtx.Provider value={{ token, user, loading, login, register, logout, updateUser, isAdmin }}>
|
||||
{children}
|
||||
</AuthCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useAuth = () => useContext(AuthCtx);
|
||||
@@ -0,0 +1,202 @@
|
||||
import { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { api } from '../api.js';
|
||||
import { useUi } from './UiContext.jsx';
|
||||
|
||||
const Ctx = createContext(null);
|
||||
|
||||
const MOIS = ['Jan','Fév','Mar','Avr','Mai','Jun','Jul','Aoû','Sep','Oct','Nov','Déc'];
|
||||
const MOIS_LONG = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'];
|
||||
|
||||
export function InteretsChartProvider({ children, netMode, pfuRates, activeView, activeId }) {
|
||||
const { chartInterets, chartCapital, chartCashback } = useUi();
|
||||
|
||||
const today = new Date();
|
||||
const currentYear = today.getFullYear();
|
||||
const currentMonth = today.getMonth() + 1;
|
||||
|
||||
// ── État partagé ────────────────────────────────────────────────
|
||||
const [annee, setAnnee] = useState(currentYear);
|
||||
const [inclureInterets, setInclureInterets] = useState(true);
|
||||
const [inclureCapital, setInclureCapital] = useState(false);
|
||||
const [inclureCashback, setInclureCashback] = useState(false);
|
||||
const [selectedMonth, setSelectedMonth] = useState(null); // 0-11 | null
|
||||
const [rawData, setRawData] = useState({ rembourses: [], projections: [], annees: [] });
|
||||
|
||||
// ── Mode global (TOUT) ──────────────────────────────────────────
|
||||
const [modeGlobal, setModeGlobal] = useState(false);
|
||||
const [selectedYear, setSelectedYear] = useState(null); // year number | null
|
||||
const [rawDataGlobal, setRawDataGlobal] = useState({ rembourses: [], projections: [], annees: [] });
|
||||
|
||||
const toggleModeGlobal = useCallback(() => {
|
||||
setModeGlobal(prev => {
|
||||
if (!prev) setSelectedYear(null); // reset sélection en entrant dans le mode
|
||||
return !prev;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Filtres reçu / projeté (force reçu si les deux seraient désactivés)
|
||||
const [showActual, setShowActualRaw] = useState(true);
|
||||
const [showProjected, setShowProjected] = useState(true);
|
||||
const setShowActual = useCallback((val) => {
|
||||
setShowActualRaw(prev => (val === false || val === prev ? (!prev && !showProjected ? true : val === false ? false : !prev) : val));
|
||||
}, [showProjected]);
|
||||
const toggleActual = useCallback(() => setShowActualRaw(prev => (!prev && !showProjected) ? true : !prev), [showProjected]);
|
||||
const toggleProjected = useCallback(() => setShowProjected(prev => !prev), []);
|
||||
const selectActualOnly = useCallback(() => { setShowActualRaw(true); setShowProjected(false); }, []);
|
||||
const selectProjectedOnly = useCallback(() => { setShowActualRaw(false); setShowProjected(true); }, []);
|
||||
const setActualProjected = useCallback((actual, projected) => { setShowActualRaw(actual); setShowProjected(projected); }, []);
|
||||
|
||||
// ── Fetch mensuel ────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
const params = { annee, ...(activeView === 'all' ? { scope: 'all' } : {}) };
|
||||
api.get('/dashboard/interets-mensuels', params)
|
||||
.then(res => setRawData(res))
|
||||
.catch(() => {});
|
||||
}, [annee, activeView, activeId]);
|
||||
|
||||
// ── Fetch annuel (global) ────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
const params = activeView === 'all' ? { scope: 'all' } : {};
|
||||
api.get('/dashboard/interets-annuels', params)
|
||||
.then(res => setRawDataGlobal(res))
|
||||
.catch(() => {});
|
||||
}, [activeView, activeId]);
|
||||
|
||||
// Réinitialiser la sélection au changement d'année
|
||||
useEffect(() => { setSelectedMonth(null); }, [annee]);
|
||||
|
||||
// ── PFU helper ───────────────────────────────────────────────────
|
||||
const getPfuReduction = useCallback((year) => {
|
||||
if (!pfuRates?.length) return 0;
|
||||
const r = pfuRates.find(r => r.annee === year)
|
||||
?? pfuRates.reduce((best, r) => r.annee > best.annee ? r : best, pfuRates[0]);
|
||||
return (r.prelev_sociaux + r.impot_revenu) / 100;
|
||||
}, [pfuRates]);
|
||||
|
||||
// ── Calcul mensuel ───────────────────────────────────────────────
|
||||
const months = useMemo(() => {
|
||||
const reduction = getPfuReduction(annee);
|
||||
return Array.from({ length: 12 }, (_, i) => {
|
||||
const m = i + 1;
|
||||
const moisStr = `${annee}-${String(m).padStart(2,'0')}`;
|
||||
const isCurrent = annee === currentYear && m === currentMonth;
|
||||
const isFuture = annee > currentYear || (annee === currentYear && m > currentMonth);
|
||||
|
||||
const rembData = rawData.rembourses?.find(r => r.mois === moisStr);
|
||||
const projData = rawData.projections?.find(p => p.mois === moisStr);
|
||||
|
||||
const rawInterets = rembData ? (netMode ? (rembData.interets_nets||0) : (rembData.interets_bruts||0)) : 0;
|
||||
const rawInteretsProj = projData && (isCurrent||isFuture)
|
||||
? (netMode ? (projData.interets_prevus||0)*(1-reduction) : (projData.interets_prevus||0)) : 0;
|
||||
|
||||
const cashbackAmt = inclureCashback ? (rembData?.cashback||0) : 0;
|
||||
const capitalAmt = inclureCapital ? (rembData?.capital||0) : 0;
|
||||
const interetsAmt = inclureInterets ? rawInterets : 0;
|
||||
const capitalProjAmt = inclureCapital && projData && (isCurrent||isFuture) ? (projData.capital_prevu||0) : 0;
|
||||
const interetsProjAmt= inclureInterets ? rawInteretsProj : 0;
|
||||
|
||||
return {
|
||||
m, idx: i,
|
||||
label: MOIS[i], labelLong: MOIS_LONG[i],
|
||||
actual: capitalAmt + cashbackAmt + interetsAmt,
|
||||
projected: capitalProjAmt + interetsProjAmt,
|
||||
total: capitalAmt + cashbackAmt + interetsAmt + capitalProjAmt + interetsProjAmt,
|
||||
isCurrentMonth: isCurrent, isFuture,
|
||||
cashbackAmt, capitalAmt, interetsAmt, capitalProjAmt, interetsProjAmt,
|
||||
};
|
||||
});
|
||||
}, [rawData, netMode, getPfuReduction, annee, currentYear, currentMonth,
|
||||
inclureInterets, inclureCapital, inclureCashback]);
|
||||
|
||||
const annualTotal = useMemo(() => months.reduce((s, m) => s + m.total, 0), [months]);
|
||||
const availableYears = rawData.annees ?? [];
|
||||
|
||||
// ── Calcul annuel (mode global) ──────────────────────────────────
|
||||
const years = useMemo(() => {
|
||||
// Collecter toutes les années connues (rembourses + projections + annees)
|
||||
const allKnown = new Set([
|
||||
...(rawDataGlobal.annees ?? []),
|
||||
...(rawDataGlobal.rembourses ?? []).map(r => r.annee),
|
||||
...(rawDataGlobal.projections ?? []).map(p => p.annee),
|
||||
]);
|
||||
// Toujours inclure l'année courante
|
||||
allKnown.add(currentYear);
|
||||
|
||||
const allSorted = [...allKnown].map(Number).sort((a, b) => a - b);
|
||||
|
||||
// Algorithme de fenêtrage
|
||||
const pastYears = allSorted.filter(y => y < currentYear);
|
||||
const futureYears = allSorted.filter(y => y > currentYear);
|
||||
const pastCount = Math.min(pastYears.length, 8); // currentYear en position max 9
|
||||
const pastSlice = pastYears.slice(-pastCount);
|
||||
const usedSlots = pastCount + 1; // +1 for currentYear
|
||||
const projCount = Math.max(3, 12 - usedSlots);
|
||||
const futureSlice = futureYears.slice(0, projCount);
|
||||
const visibleYears = [...pastSlice, currentYear, ...futureSlice];
|
||||
|
||||
return visibleYears.map(y => {
|
||||
const isCurrent = y === currentYear;
|
||||
const isFuture = y > currentYear;
|
||||
const reduction = getPfuReduction(y);
|
||||
|
||||
const rembData = rawDataGlobal.rembourses?.find(r => Number(r.annee) === y);
|
||||
const projData = rawDataGlobal.projections?.find(p => Number(p.annee) === y);
|
||||
|
||||
const rawInterets = rembData ? (netMode ? (rembData.interets_nets||0) : (rembData.interets_bruts||0)) : 0;
|
||||
const rawInteretsProj = projData
|
||||
? (netMode ? (projData.interets_prevus||0)*(1-reduction) : (projData.interets_prevus||0)) : 0;
|
||||
|
||||
const cashbackAmt = inclureCashback ? (rembData?.cashback||0) : 0;
|
||||
const capitalAmt = inclureCapital ? (rembData?.capital||0) : 0;
|
||||
const interetsAmt = inclureInterets ? rawInterets : 0;
|
||||
const capitalProjAmt = inclureCapital ? (projData?.capital_prevu||0) : 0;
|
||||
const interetsProjAmt = inclureInterets ? rawInteretsProj : 0;
|
||||
|
||||
return {
|
||||
y,
|
||||
label: String(y),
|
||||
isCurrent, isFuture,
|
||||
actual: capitalAmt + cashbackAmt + interetsAmt,
|
||||
projected: capitalProjAmt + interetsProjAmt,
|
||||
total: capitalAmt + cashbackAmt + interetsAmt + capitalProjAmt + interetsProjAmt,
|
||||
cashbackAmt, capitalAmt, interetsAmt, capitalProjAmt, interetsProjAmt,
|
||||
};
|
||||
});
|
||||
}, [rawDataGlobal, netMode, getPfuReduction, currentYear,
|
||||
inclureInterets, inclureCapital, inclureCashback]);
|
||||
|
||||
const globalTotal = useMemo(() => years.reduce((s, y) => s + y.total, 0), [years]);
|
||||
|
||||
return (
|
||||
<Ctx.Provider value={{
|
||||
// Année
|
||||
annee, setAnnee, availableYears,
|
||||
// Toggles types
|
||||
inclureInterets, setInclureInterets,
|
||||
inclureCapital, setInclureCapital,
|
||||
inclureCashback, setInclureCashback,
|
||||
// Mois sélectionné (liaison bar ↔ donut)
|
||||
selectedMonth, setSelectedMonth,
|
||||
// Filtres reçu / projeté
|
||||
showActual, toggleActual,
|
||||
showProjected, toggleProjected,
|
||||
selectActualOnly, selectProjectedOnly, setActualProjected,
|
||||
// Mode global (TOUT)
|
||||
modeGlobal, toggleModeGlobal,
|
||||
selectedYear, setSelectedYear,
|
||||
years, globalTotal,
|
||||
// Données
|
||||
months, annualTotal,
|
||||
rawData, rawDataGlobal,
|
||||
currentYear, currentMonth,
|
||||
// Affichage
|
||||
netMode,
|
||||
// Couleurs palette
|
||||
chartInterets, chartCapital, chartCashback,
|
||||
}}>
|
||||
{children}
|
||||
</Ctx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useInteretsChart = () => useContext(Ctx);
|
||||
@@ -0,0 +1,72 @@
|
||||
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
||||
import { api } from '../api.js';
|
||||
import { useAuth } from './AuthContext.jsx';
|
||||
|
||||
const InvCtx = createContext(null);
|
||||
|
||||
const KEY_ID = 'cl_investisseur_id';
|
||||
const KEY_VIEW = 'cl_active_view'; // 'all' | '<id>'
|
||||
|
||||
export function InvestisseurProvider({ children }) {
|
||||
const { token } = useAuth();
|
||||
const [investisseurs, setInvestisseurs] = useState([]);
|
||||
const [activeId, setActiveIdState] = useState(() => Number(localStorage.getItem(KEY_ID)) || null);
|
||||
const [activeView, setActiveViewState] = useState(() => localStorage.getItem(KEY_VIEW) || 'all');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
if (!token) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const list = await api.get('/investisseurs');
|
||||
setInvestisseurs(list);
|
||||
// S'assurer que activeId pointe sur un membre valide
|
||||
if (list.length && (!activeId || !list.find(i => i.id === activeId))) {
|
||||
const first = list[0].id;
|
||||
setActiveIdState(first);
|
||||
localStorage.setItem(KEY_ID, String(first));
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token, activeId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) reload();
|
||||
else setInvestisseurs([]);
|
||||
}, [token, reload]);
|
||||
|
||||
/** Changer le membre actif pour les requêtes (topbar) */
|
||||
const setActive = (id) => {
|
||||
setActiveIdState(id);
|
||||
localStorage.setItem(KEY_ID, String(id));
|
||||
};
|
||||
|
||||
/** Changer la vue du profil : 'all' ou id numérique d'un investisseur */
|
||||
const setActiveView = (v) => {
|
||||
setActiveViewState(v);
|
||||
localStorage.setItem(KEY_VIEW, String(v));
|
||||
// Si on sélectionne un membre précis → l'activer aussi pour les requêtes
|
||||
if (v !== 'all') {
|
||||
setActiveIdState(Number(v));
|
||||
localStorage.setItem(KEY_ID, String(v));
|
||||
}
|
||||
};
|
||||
|
||||
const active = investisseurs.find(i => i.id === activeId) || null;
|
||||
const activeViewMember = activeView !== 'all'
|
||||
? investisseurs.find(i => i.id === Number(activeView)) || null
|
||||
: null;
|
||||
|
||||
return (
|
||||
<InvCtx.Provider value={{
|
||||
investisseurs, active, activeId, setActive,
|
||||
activeView, activeViewMember, setActiveView,
|
||||
reload, loading,
|
||||
}}>
|
||||
{children}
|
||||
</InvCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useInvestisseur = () => useContext(InvCtx);
|
||||
@@ -0,0 +1,58 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState, useCallback } from 'react';
|
||||
|
||||
/**
|
||||
* ThemeContext
|
||||
* - mode : 'light' | 'dark' | 'system' (préférence utilisateur, persistée)
|
||||
* - resolved : 'light' | 'dark' (thème effectivement appliqué)
|
||||
*
|
||||
* Le thème effectif est appliqué via l'attribut data-theme sur <html>.
|
||||
* En mode 'system', on écoute les changements du media query prefers-color-scheme.
|
||||
*/
|
||||
|
||||
const ThemeCtx = createContext(null);
|
||||
|
||||
const STORAGE_KEY = 'cl_theme';
|
||||
|
||||
function getSystemTheme() {
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
const root = document.documentElement;
|
||||
root.setAttribute('data-theme', theme);
|
||||
// Inform the browser (form controls, scrollbars) of the active scheme
|
||||
root.style.colorScheme = theme;
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }) {
|
||||
const [mode, setMode] = useState(() => localStorage.getItem(STORAGE_KEY) || 'system');
|
||||
const [systemTheme, setSystemTheme] = useState(getSystemTheme);
|
||||
|
||||
// Listen to OS preference changes when in 'system' mode
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const onChange = (e) => setSystemTheme(e.matches ? 'dark' : 'light');
|
||||
mq.addEventListener('change', onChange);
|
||||
return () => mq.removeEventListener('change', onChange);
|
||||
}, []);
|
||||
|
||||
const resolved = mode === 'system' ? systemTheme : mode;
|
||||
|
||||
// Apply the resolved theme to <html>
|
||||
useEffect(() => { applyTheme(resolved); }, [resolved]);
|
||||
|
||||
const setThemeMode = useCallback((m) => {
|
||||
setMode(m);
|
||||
localStorage.setItem(STORAGE_KEY, m);
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ mode, resolved, setMode: setThemeMode }),
|
||||
[mode, resolved, setThemeMode],
|
||||
);
|
||||
|
||||
return <ThemeCtx.Provider value={value}>{children}</ThemeCtx.Provider>;
|
||||
}
|
||||
|
||||
export const useTheme = () => useContext(ThemeCtx);
|
||||
@@ -0,0 +1,165 @@
|
||||
import { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { api } from '../api.js';
|
||||
|
||||
/**
|
||||
* UI state that persists across sessions:
|
||||
* - sidebarCollapsed : sidebar visibility
|
||||
* - fontScale : 'large' (max/accessibilité) | 'medium' | 'compact'
|
||||
*/
|
||||
|
||||
const UiCtx = createContext(null);
|
||||
|
||||
const KEY_SIDEBAR = 'cl_ui_sidebar_collapsed';
|
||||
const KEY_FONT = 'cl_fontscale';
|
||||
const KEY_LANGUE = 'cl_langue';
|
||||
const KEY_DEVISE = 'cl_devise';
|
||||
const KEY_DISPLAY = 'cl_display_mode';
|
||||
const KEY_CHART_INTERETS = 'cl_chart_interets';
|
||||
const KEY_CHART_CAPITAL = 'cl_chart_capital';
|
||||
const KEY_CHART_CASHBACK = 'cl_chart_cashback';
|
||||
const KEY_PFO_ASSUJETTI = 'cl_pfo_assujetti';
|
||||
|
||||
const FONT_SIZES = ['large', 'medium', 'compact'];
|
||||
const LANGUES = ['fr', 'en'];
|
||||
const DEVISES = ['EUR', 'USD', 'GBP', 'CHF', 'CAD', 'SGD'];
|
||||
|
||||
const DEFAULT_CHART_INTERETS = '#2196f3'; // Blue 500
|
||||
const DEFAULT_CHART_CAPITAL = '#4caf50'; // Green 500
|
||||
const DEFAULT_CHART_CASHBACK = '#ffc107'; // Amber 500
|
||||
|
||||
function applyFontScale(scale) {
|
||||
document.documentElement.setAttribute('data-fontsize', scale);
|
||||
}
|
||||
|
||||
export function UiProvider({ children }) {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(
|
||||
() => localStorage.getItem(KEY_SIDEBAR) === '1'
|
||||
);
|
||||
|
||||
const [fontScale, setFontScaleState] = useState(() => {
|
||||
const saved = localStorage.getItem(KEY_FONT);
|
||||
return FONT_SIZES.includes(saved) ? saved : 'large';
|
||||
});
|
||||
|
||||
const [langue, setLangueState] = useState(() => {
|
||||
const saved = localStorage.getItem(KEY_LANGUE);
|
||||
return LANGUES.includes(saved) ? saved : 'fr';
|
||||
});
|
||||
|
||||
const [devise, setDeviseState] = useState(() => {
|
||||
const saved = localStorage.getItem(KEY_DEVISE);
|
||||
return DEVISES.includes(saved) ? saved : 'EUR';
|
||||
});
|
||||
|
||||
const [displayMode, setDisplayModeState] = useState(() => {
|
||||
const saved = localStorage.getItem(KEY_DISPLAY);
|
||||
return saved === 'brut' ? 'brut' : 'net';
|
||||
});
|
||||
|
||||
const [chartInterets, setChartInteretsState] = useState(() =>
|
||||
localStorage.getItem(KEY_CHART_INTERETS) || DEFAULT_CHART_INTERETS
|
||||
);
|
||||
const [chartCapital, setChartCapitalState] = useState(() =>
|
||||
localStorage.getItem(KEY_CHART_CAPITAL) || DEFAULT_CHART_CAPITAL
|
||||
);
|
||||
const [chartCashback, setChartCashbackState] = useState(() =>
|
||||
localStorage.getItem(KEY_CHART_CASHBACK) || DEFAULT_CHART_CASHBACK
|
||||
);
|
||||
|
||||
const [pfoAssujetti, setPfoAssujettiState] = useState(() =>
|
||||
localStorage.getItem(KEY_PFO_ASSUJETTI) === '1'
|
||||
);
|
||||
|
||||
// ── Sync DB → localStorage au montage ──────────────────────────
|
||||
// La DB fait foi : ses valeurs écrasent le localStorage si présentes.
|
||||
useEffect(() => {
|
||||
api.get('/preferences').then(prefs => {
|
||||
if (prefs.chart_interets) {
|
||||
localStorage.setItem(KEY_CHART_INTERETS, prefs.chart_interets);
|
||||
setChartInteretsState(prefs.chart_interets);
|
||||
}
|
||||
if (prefs.chart_capital) {
|
||||
localStorage.setItem(KEY_CHART_CAPITAL, prefs.chart_capital);
|
||||
setChartCapitalState(prefs.chart_capital);
|
||||
}
|
||||
if (prefs.chart_cashback) {
|
||||
localStorage.setItem(KEY_CHART_CASHBACK, prefs.chart_cashback);
|
||||
setChartCashbackState(prefs.chart_cashback);
|
||||
}
|
||||
if (prefs.pfo_assujetti !== undefined) {
|
||||
const val = prefs.pfo_assujetti === '1';
|
||||
localStorage.setItem(KEY_PFO_ASSUJETTI, val ? '1' : '0');
|
||||
setPfoAssujettiState(val);
|
||||
}
|
||||
}).catch(() => {
|
||||
// Silencieux : token absent au premier rendu, on garde le localStorage
|
||||
});
|
||||
}, []);
|
||||
|
||||
// ── Helper : persist une pref en DB (silencieux en cas d'erreur) ─
|
||||
const persistPref = useCallback((key, value) => {
|
||||
api.patch('/preferences', { [key]: value }).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(KEY_SIDEBAR, sidebarCollapsed ? '1' : '0');
|
||||
}, [sidebarCollapsed]);
|
||||
|
||||
useEffect(() => {
|
||||
applyFontScale(fontScale);
|
||||
localStorage.setItem(KEY_FONT, fontScale);
|
||||
}, [fontScale]);
|
||||
|
||||
// Apply on mount
|
||||
useEffect(() => { applyFontScale(fontScale); }, []);
|
||||
|
||||
const toggleSidebar = useCallback(() => setSidebarCollapsed((v) => !v), []);
|
||||
const setFontScale = useCallback((s) => {
|
||||
if (FONT_SIZES.includes(s)) setFontScaleState(s);
|
||||
}, []);
|
||||
const setLangue = useCallback((l) => {
|
||||
if (LANGUES.includes(l)) { setLangueState(l); localStorage.setItem(KEY_LANGUE, l); }
|
||||
}, []);
|
||||
const setDevise = useCallback((d) => {
|
||||
if (DEVISES.includes(d)) { setDeviseState(d); localStorage.setItem(KEY_DEVISE, d); }
|
||||
}, []);
|
||||
|
||||
const setDisplayMode = useCallback((m) => {
|
||||
if (m === 'net' || m === 'brut') {
|
||||
setDisplayModeState(m);
|
||||
localStorage.setItem(KEY_DISPLAY, m);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setChartInterets = useCallback((hex) => {
|
||||
setChartInteretsState(hex);
|
||||
localStorage.setItem(KEY_CHART_INTERETS, hex);
|
||||
persistPref('chart_interets', hex);
|
||||
}, [persistPref]);
|
||||
|
||||
const setChartCapital = useCallback((hex) => {
|
||||
setChartCapitalState(hex);
|
||||
localStorage.setItem(KEY_CHART_CAPITAL, hex);
|
||||
persistPref('chart_capital', hex);
|
||||
}, [persistPref]);
|
||||
|
||||
const setChartCashback = useCallback((hex) => {
|
||||
setChartCashbackState(hex);
|
||||
localStorage.setItem(KEY_CHART_CASHBACK, hex);
|
||||
persistPref('chart_cashback', hex);
|
||||
}, [persistPref]);
|
||||
|
||||
const setPfoAssujetti = useCallback((val) => {
|
||||
setPfoAssujettiState(val);
|
||||
localStorage.setItem(KEY_PFO_ASSUJETTI, val ? '1' : '0');
|
||||
persistPref('pfo_assujetti', val ? '1' : '0');
|
||||
}, [persistPref]);
|
||||
|
||||
return (
|
||||
<UiCtx.Provider value={{ sidebarCollapsed, setSidebarCollapsed, toggleSidebar, fontScale, setFontScale, langue, setLangue, devise, setDevise, displayMode, setDisplayMode, chartInterets, setChartInterets, chartCapital, setChartCapital, chartCashback, setChartCashback, pfoAssujetti, setPfoAssujetti }}>
|
||||
{children}
|
||||
</UiCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useUi = () => useContext(UiCtx);
|
||||
Reference in New Issue
Block a user