Initial commit

This commit is contained in:
Olivier CROGUENNEC committed 2026-06-13 14:57:15 +02:00
commit 48ed7fe65e
209 files changed
+49979

No files matched your search

@@ -0,0 +1,542 @@
import { useState, useRef, useEffect } from 'react';
import { api } from '../api.js';
import { useInteretsChart } from '../context/InteretsChartContext.jsx';
const ICONS_BASE = '/api/icons-files/';
const GRID = 'rgba(255,255,255,0.055)';
const LABEL = '#4a5568';
const MOIS = ['Jan','Fév','Mar','Avr','Mai','Jun','Jul','Aoû','Sep','Oct','Nov','Déc'];
function fmtShort(v) {
if (!v || v === 0) return '';
const abs = Math.abs(v);
if (abs >= 1000) return (v/1000).toLocaleString('fr-FR',{maximumFractionDigits:1})+'k €';
return v.toLocaleString('fr-FR',{minimumFractionDigits:1,maximumFractionDigits:1})+' €';
}
function fmtTotal(v) {
return v.toLocaleString('fr-FR',{minimumFractionDigits:2,maximumFractionDigits:2})+' €';
}
function hexToRgba(hex, a) {
if (!hex||hex.length<7) return `rgba(79,168,232,${a})`;
const r=parseInt(hex.slice(1,3),16), g=parseInt(hex.slice(3,5),16), b=parseInt(hex.slice(5,7),16);
return `rgba(${r},${g},${b},${a})`;
}
export default function InteretsMensuelsChart() {
const {
annee, setAnnee, availableYears,
inclureInterets, setInclureInterets,
inclureCapital, setInclureCapital,
inclureCashback, setInclureCashback,
selectedMonth, setSelectedMonth,
showActual, toggleActual,
showProjected, toggleProjected,
months, annualTotal,
// Mode global (TOUT)
modeGlobal, toggleModeGlobal,
selectedYear, setSelectedYear,
years, globalTotal,
netMode,
chartInterets, chartCapital, chartCashback,
} = useInteretsChart();
// État local uniquement
const [hovered, setHovered] = useState(null);
const [windowStart, setWindowStart] = useState(0);
const [libIcons, setLibIcons] = useState({});
const [isMobile, setIsMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < 768);
const [sheetOpen, setSheetOpen] = useState(false);
const initializedRef = useRef(false);
useEffect(() => {
api.get('/icons').then(rows => {
const m = {};
rows.forEach(r => { m[r.name] = r.filename; });
setLibIcons(m);
}).catch(() => {});
}, []);
useEffect(() => {
const onResize = () => setIsMobile(window.innerWidth < 768);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
// Réinitialiser le hovered au changement de mode
useEffect(() => { setHovered(null); }, [modeGlobal]);
const visibleYears = availableYears.length ? availableYears.slice(windowStart, windowStart+3) : [annee];
const canPrev = windowStart > 0;
const canNext = windowStart + 3 < availableYears.length;
useEffect(() => {
if (!availableYears.length || initializedRef.current) return;
initializedRef.current = true;
const idx = availableYears.indexOf(annee);
const safe = idx >= 0 ? idx : availableYears.length - 1;
setWindowStart(Math.max(0, Math.min(availableYears.length - 3, safe - 1)));
}, [availableYears]);
/* ── Données affichées selon le mode ── */
const items = modeGlobal ? years : months;
const barCount = items.length;
/* ── SVG layout ── */
const W = 900, H = 280;
const PAD = { top: 52, right: 20, bottom: 34, left: 70 };
const plotW = W - PAD.left - PAD.right;
const plotH = H - PAD.top - PAD.bottom;
const gap = modeGlobal ? 10 : 8;
const barW = barCount > 0 ? Math.floor((plotW - gap * (barCount - 1)) / barCount) : 60;
const barBotY = PAD.top + plotH;
const filteredTotal = item =>
(showActual ? item.capitalAmt + item.cashbackAmt + item.interetsAmt : 0) +
(showProjected ? item.capitalProjAmt + item.interetsProjAmt : 0);
const filteredSum = items.reduce((s, item) => s + filteredTotal(item), 0);
const rawMax = Math.max(...items.map(item => filteredTotal(item)), 0.01);
const niceStep = (() => {
const raw = rawMax/4, mag = Math.pow(10,Math.floor(Math.log10(raw))), n = raw/mag;
return (n<1.5?1:n<3.5?2:n<7.5?5:10)*mag;
})();
const niceMax = Math.ceil(rawMax/niceStep)*niceStep || niceStep;
const yScale = v => PAD.top + plotH - (v/niceMax)*plotH;
const barX = i => PAD.left + i*(barW+gap);
const yTicks = Array.from({length: Math.round(niceMax/niceStep)+1}, (_,i) => ({v:i*niceStep, y:yScale(i*niceStep)}));
/* ── Tooltip ── */
const tooltipItem = hovered !== null ? items[hovered] : null;
const tooltipBCX = hovered !== null ? barX(hovered) + barW/2 : 0;
const tooltipBTY = hovered !== null ? yScale(filteredTotal(items[hovered])) : 0;
const anchorRight = tooltipBCX / W > 0.65;
const buildSegments = (item) => {
const actual = [];
if (showActual) {
if (inclureCapital && item.capitalAmt > 0) actual.push({color:chartCapital, v:item.capitalAmt});
if (inclureCashback && item.cashbackAmt > 0) actual.push({color:chartCashback, v:item.cashbackAmt});
if (inclureInterets && item.interetsAmt > 0) actual.push({color:chartInterets, v:item.interetsAmt});
}
const projected = [];
if (showProjected) {
if (inclureCapital && item.capitalProjAmt > 0) projected.push({color:chartCapital, v:item.capitalProjAmt});
if (inclureInterets && item.interetsProjAmt > 0) projected.push({color:chartInterets, v:item.interetsProjAmt});
}
return { actual, projected };
};
const activeTypes = [
inclureCapital && {key:'capital', color:chartCapital, label:'Capital'},
inclureCashback && {key:'cashback', color:chartCashback, label:'Cashback'},
inclureInterets && {key:'interets', color:chartInterets, label:netMode?'Intérêts nets':'Intérêts bruts'},
].filter(Boolean);
const activeTypeCount = [inclureInterets, inclureCapital, inclureCashback].filter(Boolean).length;
const AppIcon = ({ name, size = 28, active = false }) => {
const filename = libIcons[name];
if (filename) return (
<img src={ICONS_BASE + filename} className="app-lib-icon" width={size} height={size}
aria-hidden="true"
style={{ opacity: active ? 1 : 0.35, display: 'block', transition: 'opacity .15s' }} />
);
return <span style={{ width: size, height: size, display: 'block',
borderRadius: 4, background: 'var(--text-muted)',
opacity: active ? 0.55 : 0.2, transition: 'opacity .15s' }} />;
};
/* ── Label en-tête ── */
return (
<div className="solde-chart-wrap" style={{padding:'24px 24px 16px', height:'100%', boxSizing:'border-box'}}>
{/* ── En-tête ── */}
<div className="solde-chart-header">
<div className="solde-chart-info">
<div style={{display:'flex',alignItems:'center',gap:5,flexWrap:'wrap',marginBottom:2}}>
{inclureInterets && (
<span style={{display:'inline-flex',alignItems:'center',gap:4,background:hexToRgba(chartInterets,0.12),borderRadius:5,padding:'3px 8px'}}>
<span style={{width:7,height:7,borderRadius:2,background:chartInterets,flexShrink:0}}/>
<span style={{fontSize:13,color:chartInterets,fontWeight:600}}>{netMode?'Intérêts nets':'Intérêts bruts'}</span>
</span>
)}
{inclureCapital && (
<span style={{display:'inline-flex',alignItems:'center',gap:4,background:hexToRgba(chartCapital,0.12),borderRadius:5,padding:'3px 8px'}}>
<span style={{width:7,height:7,borderRadius:2,background:chartCapital,flexShrink:0}}/>
<span style={{fontSize:13,color:chartCapital,fontWeight:600}}>Capital</span>
</span>
)}
{inclureCashback && (
<span style={{display:'inline-flex',alignItems:'center',gap:4,background:hexToRgba(chartCashback,0.12),borderRadius:5,padding:'3px 8px'}}>
<span style={{width:7,height:7,borderRadius:2,background:chartCashback,flexShrink:0}}/>
<span style={{fontSize:13,color:chartCashback,fontWeight:600}}>Cashback</span>
</span>
)}
{!inclureInterets && !inclureCapital && !inclureCashback && (
<span style={{fontSize:13,color:'var(--text-muted)'}}>—</span>
)}
<span style={{fontSize:13,color:'var(--text-muted)'}}>· {modeGlobal?'Toutes les années':annee}</span>
</div>
<div className="solde-chart-value">{fmtTotal(filteredSum)}</div>
</div>
{isMobile ? (
/* ── Mobile : bouton Filtres ── */
<button
onClick={()=>setSheetOpen(true)}
style={{
display:'flex', alignItems:'center', gap:7,
background:'var(--surface-2)', border:'1px solid var(--border)',
borderRadius:20, padding:'6px 14px', cursor:'pointer',
fontSize:'var(--fs-sm)', fontWeight:600, color:'var(--text)',
}}>
Filtres
{activeTypeCount > 0 && (
<span style={{
background:'var(--primary)', color:'#fff',
borderRadius:10, padding:'1px 7px',
fontSize:11, fontWeight:700,
}}>{activeTypeCount}</span>
)}
</button>
) : (
/* ── Desktop : controls existants ── */
<div className="solde-chart-controls">
<button
title={inclureInterets?'Intérêts inclus — cliquer pour exclure':'Cliquer pour inclure les intérêts'}
onClick={()=>setInclureInterets(v=>!v)}
style={{background:inclureInterets?hexToRgba(chartInterets,0.13):'none', border:'1px solid '+(inclureInterets?chartInterets:'transparent'), borderRadius:8, padding:'4px 6px', cursor:'pointer', display:'flex', alignItems:'center', transition:'background .15s,border-color .15s', marginRight:2}}>
<AppIcon name="interets" active={inclureInterets} />
</button>
<button
title={inclureCapital?'Capital inclus — cliquer pour exclure':'Cliquer pour inclure le capital'}
onClick={()=>setInclureCapital(v=>!v)}
style={{background:inclureCapital?hexToRgba(chartCapital,0.13):'none', border:'1px solid '+(inclureCapital?chartCapital:'transparent'), borderRadius:8, padding:'4px 6px', cursor:'pointer', display:'flex', alignItems:'center', transition:'background .15s,border-color .15s', marginRight:2}}>
<AppIcon name="capital" active={inclureCapital} />
</button>
<button
title={inclureCashback?'Cashback inclus — cliquer pour exclure':'Cliquer pour inclure le cashback'}
onClick={()=>setInclureCashback(v=>!v)}
style={{background:inclureCashback?hexToRgba(chartCashback,0.13):'none', border:'1px solid '+(inclureCashback?chartCashback:'transparent'), borderRadius:8, padding:'4px 6px', cursor:'pointer', display:'flex', alignItems:'center', transition:'background .15s,border-color .15s', marginRight:2}}>
<AppIcon name="cashback" active={inclureCashback} />
</button>
<div className="solde-chart-ranges">
{!modeGlobal && <>
<button className="solde-range-btn" onClick={()=>setWindowStart(w=>Math.max(0,w-1))} disabled={!canPrev} style={{opacity:canPrev?1:0.3}}>‹</button>
{visibleYears.map(y=>(
<button key={y} className={`solde-range-btn${annee===y?' active':''}`}
onClick={()=>{ setAnnee(y); }}>
{y}
</button>
))}
<button className="solde-range-btn" onClick={()=>setWindowStart(w=>Math.min(Math.max(0,availableYears.length-3),w+1))} disabled={!canNext} style={{opacity:canNext?1:0.3}}>›</button>
</>}
<button className={`solde-range-btn${modeGlobal?' active':''}`} onClick={()=>toggleModeGlobal()}>
TOUT
</button>
</div>
</div>
)}
</div>
{/* ── SVG bar chart ── */}
<div style={{position:'relative', userSelect:'none'}}>
<svg
viewBox={`0 0 ${W} ${H}`}
style={{width:'100%', height:'auto', display:'block'}}
onMouseLeave={()=>setHovered(null)}
>
{yTicks.map(({v,y},i)=>(
<g key={i}>
<line x1={PAD.left} y1={y} x2={W-PAD.right} y2={y} stroke={GRID} strokeWidth="1" strokeDasharray={i===0?'':'3 5'}/>
<text x={PAD.left-10} y={y+4} textAnchor="end" fill={LABEL} fontSize="11" fontFamily="system-ui,sans-serif">{v===0?'':fmtShort(v)}</text>
</g>
))}
{items.map((item,i)=>{
const x=barX(i), isHov=hovered===i;
// Sélection : mois en mode mensuel, année en mode global
const isSel = modeGlobal ? (selectedYear===item.y) : (selectedMonth===i);
const dimmed = modeGlobal
? (selectedYear!==null && !isSel)
: (selectedMonth!==null && !isSel);
const {actual:aSegs,projected:pSegs}=buildSegments(item);
let cumH=0;
const aRects=aSegs.map(s=>{const h=(s.v/niceMax)*plotH; const r={color:s.color,x,y:barBotY-cumH-h,w:barW,h,isP:false}; cumH+=h; return r;});
const pRects=pSegs.map(s=>{const h=(s.v/niceMax)*plotH; const r={color:s.color,x,y:barBotY-cumH-h,w:barW,h,isP:true}; cumH+=h; return r;});
const all=[...aRects,...pRects];
const totalH=(filteredTotal(item)/niceMax)*plotH;
const topColor=all.length>0?all[all.length-1].color:LABEL;
const isCurrentMark = modeGlobal ? item.isCurrent : item.isCurrentMonth;
return (
<g key={i}
onMouseEnter={()=>setHovered(i)}
onClick={()=>{
if (modeGlobal) {
setSelectedYear(prev => prev===item.y ? null : item.y);
} else {
setSelectedMonth(prev => prev===i ? null : i);
}
}}
style={{cursor:'pointer', opacity:dimmed?0.35:1, transition:'opacity .15s'}}
>
{all.map((r,ri)=>(
<rect key={ri} x={r.x} y={r.y} width={r.w} height={r.h}
fill={r.color}
fillOpacity={r.isP?(isHov||isSel?0.48:0.28):(isHov||isSel?1.0:0.82)}
rx={ri===all.length-1?2:0} ry={ri===all.length-1?2:0}
/>
))}
{filteredTotal(item)>0&&(
<text x={x+barW/2} y={barBotY-totalH-5} textAnchor="middle"
fill={isHov||isSel?topColor:LABEL} fontSize="9" fontFamily="system-ui,sans-serif"
fontWeight={isHov||isSel?'700':undefined}>
{fmtShort(filteredTotal(item))}
</text>
)}
<text x={x+barW/2} y={H-10} textAnchor="middle"
fill={isCurrentMark||isSel?topColor:LABEL}
fontSize="10" fontFamily="system-ui,sans-serif"
fontWeight={isCurrentMark||isSel?'700':undefined}>
{item.label}
</text>
{isSel&&<rect x={x+barW/2-7} y={H-3} width={14} height={3} rx={1.5} fill={topColor} fillOpacity={0.9}/>}
</g>
);
})}
</svg>
{/* Tooltip */}
{tooltipItem&&tooltipItem.total>0&&(
<div style={{position:'absolute', ...(anchorRight?{right:`calc(${(1-tooltipBCX/W)*100}% + 8px)`}:{left:`calc(${(tooltipBCX/W)*100}% + 8px)`}), top:`calc(${(tooltipBTY/H)*100}% - 8px)`, transform:'translateY(-100%)', pointerEvents:'none', zIndex:20}}>
<div className="sg-tooltip">
<span className="sg-tooltip-date">
{modeGlobal
? `${tooltipItem.label}${tooltipItem.isCurrent?' · année en cours':''}`
: `${MOIS[tooltipItem.m-1]} ${annee}${tooltipItem.isCurrentMonth?' · mois en cours':''}`
}
</span>
{(()=>{
const ROW=({label,value,color=null,indent=false,muted=false})=>(
<span style={{fontSize:'var(--fs-xs)',color:'var(--text-muted)',display:'flex',justifyContent:'space-between',gap:16,alignItems:'center'}}>
{color&&<span style={{display:'inline-block',width:8,height:8,borderRadius:2,background:color,flexShrink:0}}/>}
<span style={{paddingLeft:(!color&&indent)?8:0,flex:1}}>{label}</span>
<span style={{color:muted?undefined:'var(--text)',fontWeight:muted?undefined:600}}>{value}</span>
</span>
);
const hasMix=tooltipItem.actual>0&&tooltipItem.projected>0;
const multi=[inclureCapital,inclureCashback,inclureInterets].filter(Boolean).length>1;
return(<>
{tooltipItem.actual>0&&<>
<ROW label="Reçu" value={fmtTotal(tooltipItem.actual)}/>
{multi&&inclureCapital &&tooltipItem.capitalAmt >0&&<ROW label="Capital" value={fmtTotal(tooltipItem.capitalAmt)} color={chartCapital} indent muted/>}
{multi&&inclureCashback&&tooltipItem.cashbackAmt>0&&<ROW label="Cashback" value={fmtTotal(tooltipItem.cashbackAmt)} color={chartCashback} indent muted/>}
{multi&&inclureInterets&&tooltipItem.interetsAmt>0&&<ROW label={`Intérêts (${netMode?'nets':'bruts'})`} value={fmtTotal(tooltipItem.interetsAmt)} color={chartInterets} indent muted/>}
{!multi&&inclureInterets&&<ROW label={`dont intérêts (${netMode?'nets':'bruts'})`} value={fmtTotal(tooltipItem.interetsAmt)} indent muted/>}
{!multi&&inclureCapital &&<ROW label="dont capital" value={fmtTotal(tooltipItem.capitalAmt)} indent muted/>}
{!multi&&inclureCashback&&<ROW label="dont cashback" value={fmtTotal(tooltipItem.cashbackAmt)} indent muted/>}
</>}
{tooltipItem.projected>0&&<>
<ROW label="Projeté" value={fmtTotal(tooltipItem.projected)}/>
{multi&&inclureCapital &&tooltipItem.capitalProjAmt >0&&<ROW label="Capital" value={fmtTotal(tooltipItem.capitalProjAmt)} color={chartCapital} indent muted/>}
{multi&&inclureInterets&&tooltipItem.interetsProjAmt>0&&<ROW label={`Intérêts (${netMode?'nets est.':'bruts'})`} value={fmtTotal(tooltipItem.interetsProjAmt)} color={chartInterets} indent muted/>}
{!multi&&inclureInterets&&<ROW label={`dont intérêts (${netMode?'nets est.':'bruts'})`} value={fmtTotal(tooltipItem.interetsProjAmt)} indent muted/>}
{!multi&&inclureCapital&&tooltipItem.capitalProjAmt>0&&<ROW label="dont capital" value={fmtTotal(tooltipItem.capitalProjAmt)} indent muted/>}
</>}
{hasMix&&<span className="sg-tooltip-value" style={{borderTop:'1px solid var(--border)',paddingTop:4,marginTop:2,display:'flex',justifyContent:'space-between'}}><span>Total</span><span>{fmtTotal(tooltipItem.total)}</span></span>}
{!hasMix&&<span className="sg-tooltip-value" style={{display:'flex',justifyContent:'space-between'}}><span>Total</span><span>{fmtTotal(tooltipItem.total)}</span></span>}
</>);
})()}
</div>
</div>
)}
</div>
{/* ── Légende + sélecteur Reçu/Projeté ── */}
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginTop:8,gap:12,flexWrap:'wrap'}}>
{/* Sélecteur Reçu/Projeté — desktop uniquement (mobile → bottom sheet) */}
{!isMobile && (
<div style={{
display:'inline-flex',
background:'#f0f0f0',
borderRadius:8,
padding:3,
gap:2,
flexShrink:0,
}}>
{[
{key:'actual', label:'Reçu', active:showActual, toggle:toggleActual},
{key:'projected', label:'Projeté', active:showProjected, toggle:toggleProjected},
].map(btn=>(
<button key={btn.key} onClick={()=>btn.toggle()} style={{
border:'none', cursor:'pointer', padding:'5px 14px', borderRadius:6,
fontSize:'var(--fs-sm)',
fontWeight: btn.active ? 600 : 400,
background: btn.active ? '#ffffff' : 'transparent',
color: btn.active ? '#1a1a2e' : '#9ca3af',
boxShadow: btn.active ? '0 1px 3px rgba(0,0,0,0.13)' : 'none',
transition: 'all .15s', lineHeight: 1.4,
}}>{btn.label}</button>
))}
</div>
)}
{/* Légende couleurs */}
<div style={{display:'flex',gap:12,alignItems:'center',flexWrap:'wrap',flex:1,justifyContent:'flex-end'}}>
{activeTypes.map(t=>(
<div key={t.key} style={{display:'flex',gap:8,alignItems:'center'}}>
{showActual&&(
<span style={{display:'flex',gap:4,alignItems:'center',fontSize:'var(--fs-xs)',color:'var(--text-muted)'}}>
<span style={{width:10,height:10,borderRadius:2,background:t.color,opacity:0.82,flexShrink:0}}/>
{t.label}{t.key==='interets'?' reçus':' reçu'}
</span>
)}
{showProjected&&(
<span style={{display:'flex',gap:4,alignItems:'center',fontSize:'var(--fs-xs)',color:'var(--text-muted)'}}>
<span style={{width:10,height:10,borderRadius:2,background:t.color,opacity:0.28,flexShrink:0}}/>
{t.label}{t.key==='interets'?' projetés':' projeté'}
</span>
)}
</div>
))}
</div>
</div>
{/* ── Bottom sheet mobile ── */}
{isMobile && (
<>
{/* Overlay */}
<div
onClick={()=>setSheetOpen(false)}
style={{
position:'fixed', inset:0, zIndex:200,
background:'rgba(0,0,0,0.45)',
opacity: sheetOpen ? 1 : 0,
pointerEvents: sheetOpen ? 'auto' : 'none',
transition:'opacity .25s',
}}
/>
{/* Sheet */}
<div style={{
position:'fixed', bottom:0, left:0, right:0, zIndex:201,
background:'var(--surface)',
borderRadius:'20px 20px 0 0',
borderTop:'1px solid var(--border)',
padding:'12px 20px 32px',
transform: sheetOpen ? 'translateY(0)' : 'translateY(100%)',
transition:'transform .3s cubic-bezier(.32,.72,0,1)',
maxHeight:'85vh', overflowY:'auto',
}}>
{/* Handle */}
<div style={{width:36,height:4,background:'var(--border)',borderRadius:2,margin:'0 auto 20px'}}/>
{/* Types */}
<div style={{fontSize:'var(--fs-xs)',color:'var(--text-muted)',fontWeight:600,textTransform:'uppercase',letterSpacing:'0.06em',marginBottom:12}}>
Types
</div>
{[
{key:'interets', label:netMode?'Intérêts nets':'Intérêts bruts', color:chartInterets, active:inclureInterets, set:setInclureInterets},
{key:'capital', label:'Capital', color:chartCapital, active:inclureCapital, set:setInclureCapital},
{key:'cashback', label:'Cashback', color:chartCashback, active:inclureCashback, set:setInclureCashback},
].map((t,i,arr)=>(
<div key={t.key} onClick={()=>t.set(v=>!v)} style={{
display:'flex', alignItems:'center', justifyContent:'space-between',
padding:'12px 0',
borderBottom: i<arr.length-1 ? '1px solid var(--border)' : 'none',
cursor:'pointer',
}}>
<div style={{display:'flex',alignItems:'center',gap:10}}>
<span style={{width:10,height:10,borderRadius:2,background:t.color,flexShrink:0}}/>
<span style={{fontSize:'var(--fs-sm)',color:'var(--text)',fontWeight:500}}>{t.label}</span>
</div>
{/* Toggle */}
<div style={{
width:44, height:24, borderRadius:12, flexShrink:0,
background: t.active ? t.color : 'var(--border)',
position:'relative', transition:'background .2s',
}}>
<div style={{
width:20, height:20, borderRadius:10, background:'#fff',
position:'absolute', top:2,
left: t.active ? 22 : 2,
transition:'left .2s',
boxShadow:'0 1px 3px rgba(0,0,0,0.2)',
}}/>
</div>
</div>
))}
{/* Période */}
<div style={{fontSize:'var(--fs-xs)',color:'var(--text-muted)',fontWeight:600,textTransform:'uppercase',letterSpacing:'0.06em',margin:'20px 0 12px'}}>
Période
</div>
<div style={{display:'flex',gap:8,flexWrap:'wrap',alignItems:'center'}}>
{availableYears.map(y=>(
<button key={y}
onClick={()=>{ setAnnee(y); if(modeGlobal) toggleModeGlobal(); }}
style={{
padding:'7px 16px', borderRadius:8, fontSize:'var(--fs-sm)',
fontWeight: !modeGlobal && annee===y ? 700 : 500,
background: !modeGlobal && annee===y ? 'var(--primary)' : 'var(--surface-2)',
color: !modeGlobal && annee===y ? '#fff' : 'var(--text)',
border: !modeGlobal && annee===y ? '1px solid var(--primary)' : '1px solid var(--border)',
cursor:'pointer',
}}>
{y}
</button>
))}
<button
onClick={()=>{ if(!modeGlobal) toggleModeGlobal(); }}
style={{
padding:'7px 16px', borderRadius:8, fontSize:'var(--fs-sm)',
fontWeight: modeGlobal ? 700 : 500,
background: modeGlobal ? 'var(--primary)' : 'var(--surface-2)',
color: modeGlobal ? '#fff' : 'var(--text)',
border: modeGlobal ? '1px solid var(--primary)' : '1px solid var(--border)',
cursor:'pointer',
}}>
Tout
</button>
</div>
{/* Affichage */}
<div style={{fontSize:'var(--fs-xs)',color:'var(--text-muted)',fontWeight:600,textTransform:'uppercase',letterSpacing:'0.06em',margin:'20px 0 12px'}}>
Affichage
</div>
{[
{key:'actual', label:'Reçu', active:showActual, toggle:toggleActual},
{key:'projected', label:'Projeté', active:showProjected, toggle:toggleProjected},
].map((btn,i,arr)=>(
<div key={btn.key} onClick={()=>btn.toggle()} style={{
display:'flex', alignItems:'center', justifyContent:'space-between',
padding:'12px 0',
borderBottom: i<arr.length-1 ? '1px solid var(--border)' : 'none',
cursor:'pointer',
}}>
<span style={{fontSize:'var(--fs-sm)',color:'var(--text)',fontWeight:500}}>{btn.label}</span>
<div style={{
width:44, height:24, borderRadius:12, flexShrink:0,
background: btn.active ? 'var(--primary)' : 'var(--border)',
position:'relative', transition:'background .2s',
}}>
<div style={{
width:20, height:20, borderRadius:10, background:'#fff',
position:'absolute', top:2,
left: btn.active ? 22 : 2,
transition:'left .2s',
boxShadow:'0 1px 3px rgba(0,0,0,0.2)',
}}/>
</div>
</div>
))}
</div>
</>
)}
</div>
);
}