import { useEffect, useMemo, useState } from 'react'; import { api } from '../api.js'; import { useInteretsChart } from '../context/InteretsChartContext.jsx'; import { fmtEUR, fmtDate } from '../utils/format.js'; import Modal from './Modal.jsx'; const MOIS_LONG = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre']; 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})`; } const round2 = v => Math.round(v * 100) / 100; /** * DrillCellPanel * * Props : * cell { platId, platNom, annee, mois, moisLabel } | null * platId peut être null → toutes les plateformes * onClose () => void (masqué si alwaysOpen=true) * alwaysOpen bool — cache le bouton Fermer, ajuste le style * pfuRates [] * activeView 'single'|'all' * activeId number|null * plateformes [] — pour le sélecteur (optionnel) * investissements [] — pour le calcul bulk (optionnel) * onBulkDone () => void — callback après validation en masse */ export default function DrillCellPanel({ cell, onClose, alwaysOpen = false, pfuRates, activeView, activeId, onEditRecu, onEditProjet, refreshKey, investissements, plateformes, onBulkDone, }) { const { inclureInterets, setInclureInterets, inclureCapital, setInclureCapital, inclureCashback, setInclureCashback, netMode, showActual, showProjected, chartInterets, chartCapital, chartCashback, } = useInteretsChart(); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); /* filterPlatId : '' = toutes, sinon l'id (string) de la plateforme sélectionnée dans le dropdown */ const [filterPlatId, setFilterPlatId] = useState(''); const [showRecus, setShowRecus] = useState(showActual); const [showProjetes, setShowProjetes] = useState(showProjected); /* ── Bulk validation ── */ const [bulkModal, setBulkModal] = useState(false); const [bulkItems, setBulkItems] = useState([]); const [bulkProcessing, setBulkProcessing] = useState(false); const [bulkProgress, setBulkProgress] = useState(0); const [bulkDone, setBulkDone] = useState(false); /* ── Reset filtres quand la cellule change ── */ useEffect(() => { if (!cell) { setData(null); return; } setShowRecus(showActual); setShowProjetes(showProjected); /* Quand une cellule spécifique est cliquée, pré-sélectionner sa plateforme */ setFilterPlatId(cell.platId ? String(cell.platId) : ''); }, [cell?.platId, cell?.annee, cell?.mois]); /* ── Fetch quand cellule OU filterPlatId change ── */ useEffect(() => { if (!cell) { setData(null); return; } setLoading(true); setData(null); const params = { annee: cell.annee, mois: cell.mois, ...(filterPlatId ? { plateforme_id: filterPlatId } : {}), ...(activeView === 'all' ? { scope: 'all' } : {}), }; api.get('/dashboard/detail-cellule', params) .then(d => setData(d)) .catch(() => setData({ recus: [], projetes: [] })) .finally(() => setLoading(false)); }, [cell?.annee, cell?.mois, filterPlatId, activeView, activeId, refreshKey]); /* ── Taux PFU pour l'année de la cellule ── */ const pfuReduction = useMemo(() => { if (!pfuRates?.length || !cell) return 0; const r = pfuRates.find(r => r.annee === cell.annee) ?? pfuRates.reduce((best, r) => r.annee > best.annee ? r : best, pfuRates[0]); return (r.prelev_sociaux + r.impot_revenu) / 100; }, [pfuRates, cell]); /* ── Données filtrées (la plateforme est gérée côté fetch) ── */ const recus = useMemo(() => { if (!data) return []; return [...data.recus].sort((a, b) => (b.date_remb || '').localeCompare(a.date_remb || '')); }, [data]); const projetes = useMemo(() => { if (!data) return []; return [...data.projetes].sort((a, b) => (a.date_prevue || '').localeCompare(b.date_prevue || '')); }, [data]); if (!cell) return null; /* ── Afficher la colonne Plateforme quand on est en mode "toutes" ── */ const showPlatCol = !filterPlatId; const multiDetenteur = plateformes && new Set(plateformes.map(p => p.investisseur_id)).size > 1; /* ── Calcul valeur ligne reçue ── */ const recuRowValue = (r) => { let v = 0; if (inclureInterets) v += netMode ? r.interets_nets : r.interets_bruts; if (inclureCashback) v += r.cashback ?? 0; if (inclureCapital) v += r.capital ?? 0; return v; }; /* ── Calcul valeur ligne projetée ── */ const projRowValue = (p) => { let v = 0; if (inclureInterets) v += netMode ? p.interets_prevus * (1 - pfuReduction) : p.interets_prevus; if (inclureCapital) v += p.capital_prevu ?? 0; return v; }; /* ── Totaux ── */ const totalRecus = recus.reduce((s, r) => s + recuRowValue(r), 0); const totalProjetes = projetes.reduce((s, p) => s + projRowValue(p), 0); const grandTotal = (showRecus ? totalRecus : 0) + (showProjetes ? totalProjetes : 0); /* ── Colonnes dynamiques ── */ const cols = [ inclureInterets && { key: 'interets', label: netMode ? 'Intérêts nets' : 'Intérêts bruts', color: chartInterets }, inclureCapital && { key: 'capital', label: 'Capital', color: chartCapital }, inclureCashback && { key: 'cashback', label: 'Cashback', color: chartCashback }, ].filter(Boolean); /* Date + Plateforme(opt) + Projet + Détenteur + cols + Total */ const colCount = 3 + (showPlatCol ? 1 : 0) + cols.length + 1; /* Titre du header */ const headerTitle = filterPlatId ? `${plateformes?.find(p => String(p.id) === filterPlatId)?.nom ?? 'Plateforme'} — ${cell.moisLabel} ${cell.annee}` : `Toutes les plateformes — ${cell.moisLabel} ${cell.annee}`; /* ── Bulk : construction des payloads ── */ const openBulkModal = () => { if (!projetes.length) return; const items = projetes.map(p => { const inv = investissements?.find(i => i.id === p.investissement_id); const plat = inv ? plateformes?.find(pl => pl.id === inv.plateforme_id) : null; const bruts = p.interets_prevus || 0; const year = p.date_prevue ? Number(p.date_prevue.slice(0, 4)) : cell.annee; const rates = pfuRates?.find(r => r.annee === year) ?? (pfuRates?.length ? pfuRates.reduce((best, r) => r.annee > best.annee ? r : best, pfuRates[0]) : null); const methode = plat && plat.methode_remboursement !== 'choix_investisseur' ? plat.methode_remboursement : (inv?.methode_remboursement || 'portefeuille'); const hasLocalTax = plat?.fiscalite === 'avec_fiscalite_locale' && plat?.taux_fiscalite_locale; const taxe_locale = hasLocalTax ? round2(bruts * plat.taux_fiscalite_locale / 100) : 0; const brutsApresLocal = hasLocalTax ? round2(bruts - taxe_locale) : bruts; return { _label: p.nom_projet, _plat: p.plateforme_nom, _date: p.date_prevue, _capital: p.capital_prevu || 0, _interets: bruts, _total: (p.capital_prevu || 0) + bruts, investissement_id: p.investissement_id, date_remb: p.date_prevue, capital: p.capital_prevu || 0, cashback: 0, interets_bruts_avant_local: hasLocalTax ? bruts : 0, taxe_locale, interets_bruts: brutsApresLocal, prelev_sociaux: rates ? round2(brutsApresLocal * rates.prelev_sociaux / 100) : 0, prelev_forfaitaire: rates ? round2(brutsApresLocal * rates.impot_revenu / 100) : 0, statut: 'paye', notes: '', methode_remboursement: methode, compte_id: methode === 'compte_courant' ? (inv?.compte_id || '') : '', }; }); setBulkItems(items); setBulkProgress(0); setBulkDone(false); setBulkProcessing(false); setBulkModal(true); }; const runBulk = async () => { setBulkProcessing(true); let done = 0; for (const item of bulkItems) { const { _label, _plat, _date, _capital, _interets, _total, ...payload } = item; try { await api.post('/remboursements', payload); } catch (e) { /* continuer */ } done++; setBulkProgress(done); } setBulkProcessing(false); setBulkDone(true); if (onBulkDone) onBulkDone(); }; const closeBulkModal = () => { setBulkModal(false); setBulkItems([]); setBulkProgress(0); setBulkDone(false); setBulkProcessing(false); }; /* ── Rendu ── */ return (
{/* ── Header ── */}
{headerTitle}
{[ { active: inclureInterets, toggle: () => setInclureInterets(v => !v), label: netMode ? 'Intérêts nets' : 'Intérêts bruts' }, { active: inclureCapital, toggle: () => setInclureCapital(v => !v), label: 'Capital' }, { active: inclureCashback, toggle: () => setInclureCashback(v => !v), label: 'Cashback' }, ].map(btn => ( ))} {!alwaysOpen && ( )}
{/* ── Barre filtres ── */}
{/* Sélecteur plateforme */} {plateformes && plateformes.length > 0 && (
)} {/* Toggle Reçus / Projetés */}
{[ { key: 'recus', label: 'Reçus', active: showRecus, toggle: () => setShowRecus(v => !v) }, { key: 'projetes', label: 'Projetés', active: showProjetes, toggle: () => setShowProjetes(v => !v) }, ].map(btn => ( ))}
{/* ── Corps ── */} {loading && (
Chargement…
)} {!loading && data && (
{showPlatCol && } {cols.map(c => ( ))} {/* ── Section Reçus ── */} {showRecus && ( {recus.length === 0 ? ( ) : recus.map(r => ( onEditRecu && onEditRecu(r)} > {showPlatCol && } {cols.map(c => { let v; if (c.key === 'interets') v = netMode ? r.interets_nets : r.interets_bruts; else if (c.key === 'capital') v = r.capital ?? 0; else v = r.cashback ?? 0; return ; })} ))} {recus.length > 0 && ( {cols.map(c => { const sum = recus.reduce((s, r) => { if (c.key === 'interets') return s + (netMode ? r.interets_nets : r.interets_bruts); if (c.key === 'capital') return s + (r.capital ?? 0); return s + (r.cashback ?? 0); }, 0); return ; })} )} )} {/* ── Section Projetés ── */} {showProjetes && ( {projetes.length === 0 ? ( ) : projetes.map(p => ( onEditProjet && onEditProjet(p)} > {showPlatCol && } {cols.map(c => { let v; if (c.key === 'interets') v = netMode ? p.interets_prevus * (1 - pfuReduction) : p.interets_prevus; else if (c.key === 'capital') v = p.capital_prevu ?? 0; else v = 0; return ; })} ))} {projetes.length > 0 && ( {cols.map(c => { const sum = projetes.reduce((s, p) => { if (c.key === 'interets') return s + (netMode ? p.interets_prevus * (1 - pfuReduction) : p.interets_prevus); if (c.key === 'capital') return s + (p.capital_prevu ?? 0); return s; }, 0); return ; })} )} )} {/* ── Grand total ── */} {cols.map(c => { const sumR = showRecus ? recus.reduce((s, r) => { if (c.key === 'interets') return s + (netMode ? r.interets_nets : r.interets_bruts); if (c.key === 'capital') return s + (r.capital ?? 0); return s + (r.cashback ?? 0); }, 0) : 0; const sumP = showProjetes ? projetes.reduce((s, p) => { if (c.key === 'interets') return s + (netMode ? p.interets_prevus * (1 - pfuReduction) : p.interets_prevus); if (c.key === 'capital') return s + (p.capital_prevu ?? 0); return s; }, 0) : 0; return ; })}
DatePlateformeProjet Détenteur{c.label}Total ligne
Reçus ({recus.length})
Aucun remboursement reçu ce mois
{fmtDate(r.date_remb)}{r.plateforme_nom || '—'}{r.nom_projet} {r.detenteur_nom || '—'}{fmtEUR(v)}{fmtEUR(recuRowValue(r))}
Sous-total reçus {fmtEUR(sum)}{fmtEUR(totalRecus)}
Projetés ({projetes.length}) {projetes.length > 0 && ( )}
Aucune projection ce mois
{fmtDate(p.date_prevue)}{p.plateforme_nom || '—'}{p.nom_projet} {p.detenteur_nom || '—'}{fmtEUR(v)}{fmtEUR(projRowValue(p))}
Sous-total projetés {fmtEUR(sum)}{fmtEUR(totalProjetes)}
Total{fmtEUR(sumR + sumP)} {fmtEUR(grandTotal)}
)} {/* ── Modale validation en masse ── */} {bulkModal && ( 1 ? 's' : ''}`} onClose={bulkProcessing ? undefined : closeBulkModal} width={620} footer={ bulkDone ? ( ) : bulkProcessing ? ( Traitement en cours… {bulkProgress}/{bulkItems.length} ) : ( <> ) } > {bulkProcessing && (
0 ? (bulkProgress / bulkItems.length) * 100 : 0}%`, transition: 'width .3s ease', }} />
)} {bulkDone && (

{bulkItems.length} remboursement{bulkItems.length > 1 ? 's' : ''} enregistré{bulkItems.length > 1 ? 's' : ''}

{cell.moisLabel} {cell.annee}

)} {!bulkDone && ( <>

Les remboursements suivants vont être créés d'après les projections de {cell.moisLabel} {cell.annee}. Les prélèvements fiscaux sont estimés à partir des taux PFU de l'année.

{showPlatCol && } {bulkItems.map((item, i) => ( {showPlatCol && } ))}
PlateformeProjet Date Capital Intérêts bruts Total brut
{item._plat || '—'}{item._label} {fmtDate(item._date)} {fmtEUR(item._capital)} {fmtEUR(item._interets)} {fmtEUR(item._total)}
Total {fmtEUR(bulkItems.reduce((s, i) => s + i._total, 0))}
)} )}
); } /* ── Styles partagés ── */ const thStyle = { padding: '7px 12px', textAlign: 'left', fontWeight: 600, fontSize: '0.78rem', letterSpacing: '.03em', color: 'var(--text-muted)', whiteSpace: 'nowrap', borderBottom: '1px solid var(--border)', }; const tdStyle = { padding: '7px 12px', verticalAlign: 'middle', whiteSpace: 'nowrap' }; const numStyle = { textAlign: 'right' };