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 ( ); return ; }; /* ── Label en-tête ── */ return (
{/* ── En-tête ── */}
{inclureInterets && ( {netMode?'Intérêts nets':'Intérêts bruts'} )} {inclureCapital && ( Capital )} {inclureCashback && ( Cashback )} {!inclureInterets && !inclureCapital && !inclureCashback && ( — )} · {modeGlobal?'Toutes les années':annee}
{fmtTotal(filteredSum)}
{isMobile ? ( /* ── Mobile : bouton Filtres ── */ ) : ( /* ── Desktop : controls existants ── */
{!modeGlobal && <> {visibleYears.map(y=>( ))} }
)}
{/* ── SVG bar chart ── */}
setHovered(null)} > {yTicks.map(({v,y},i)=>( {v===0?'':fmtShort(v)} ))} {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 ( 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)=>( ))} {filteredTotal(item)>0&&( {fmtShort(filteredTotal(item))} )} {item.label} {isSel&&} ); })} {/* Tooltip */} {tooltipItem&&tooltipItem.total>0&&(
{modeGlobal ? `${tooltipItem.label}${tooltipItem.isCurrent?' · année en cours':''}` : `${MOIS[tooltipItem.m-1]} ${annee}${tooltipItem.isCurrentMonth?' · mois en cours':''}` } {(()=>{ const ROW=({label,value,color=null,indent=false,muted=false})=>( {color&&} {label} {value} ); const hasMix=tooltipItem.actual>0&&tooltipItem.projected>0; const multi=[inclureCapital,inclureCashback,inclureInterets].filter(Boolean).length>1; return(<> {tooltipItem.actual>0&&<> {multi&&inclureCapital &&tooltipItem.capitalAmt >0&&} {multi&&inclureCashback&&tooltipItem.cashbackAmt>0&&} {multi&&inclureInterets&&tooltipItem.interetsAmt>0&&} {!multi&&inclureInterets&&} {!multi&&inclureCapital &&} {!multi&&inclureCashback&&} } {tooltipItem.projected>0&&<> {multi&&inclureCapital &&tooltipItem.capitalProjAmt >0&&} {multi&&inclureInterets&&tooltipItem.interetsProjAmt>0&&} {!multi&&inclureInterets&&} {!multi&&inclureCapital&&tooltipItem.capitalProjAmt>0&&} } {hasMix&&Total{fmtTotal(tooltipItem.total)}} {!hasMix&&Total{fmtTotal(tooltipItem.total)}} ); })()}
)}
{/* ── Légende + sélecteur Reçu/Projeté ── */}
{/* Sélecteur Reçu/Projeté — desktop uniquement (mobile → bottom sheet) */} {!isMobile && (
{[ {key:'actual', label:'Reçu', active:showActual, toggle:toggleActual}, {key:'projected', label:'Projeté', active:showProjected, toggle:toggleProjected}, ].map(btn=>( ))}
)} {/* Légende couleurs */}
{activeTypes.map(t=>(
{showActual&&( {t.label}{t.key==='interets'?' reçus':' reçu'} )} {showProjected&&( {t.label}{t.key==='interets'?' projetés':' projeté'} )}
))}
{/* ── Bottom sheet mobile ── */} {isMobile && ( <> {/* Overlay */}
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 */}
{/* Handle */}
{/* Types */}
Types
{[ {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)=>(
t.set(v=>!v)} style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'12px 0', borderBottom: i
{t.label}
{/* Toggle */}
))} {/* Période */}
Période
{availableYears.map(y=>( ))}
{/* Affichage */}
Affichage
{[ {key:'actual', label:'Reçu', active:showActual, toggle:toggleActual}, {key:'projected', label:'Projeté', active:showProjected, toggle:toggleProjected}, ].map((btn,i,arr)=>(
btn.toggle()} style={{ display:'flex', alignItems:'center', justifyContent:'space-between', padding:'12px 0', borderBottom: i {btn.label}
))}
)}
); }