Initial commit
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user