Files
crowdlending-app/frontend/src/components/InteretsMensuelsChart.jsx
T
2026-06-13 14:57:15 +02:00

543 lines
28 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}