Files
crowdlending-app/frontend/src/components/DrillCellPanel.jsx
T
2026-06-13 17:47:29 +02:00

592 lines
28 KiB
React

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 (
<div style={{
margin: alwaysOpen ? '0 24px 28px' : '0 24px 28px',
padding: 0,
border: '2px solid var(--primary)',
borderRadius: 10,
overflow: 'hidden',
overflowX: 'auto',
background: 'var(--surface)',
}}>
{/* ── Header ── */}
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '12px 16px',
background: 'var(--primary)',
color: '#fff',
}}>
<strong style={{ fontSize: '1rem' }}>{headerTitle}</strong>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
{[
{ 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 => (
<button key={btn.label} onClick={() => btn.toggle()} style={{
border: `1px solid ${btn.active ? 'rgba(255,255,255,0.8)' : 'rgba(255,255,255,0.3)'}`,
background: btn.active ? 'rgba(255,255,255,0.2)' : 'transparent',
borderRadius: 6, padding: '3px 10px',
fontSize: '0.8rem', fontWeight: btn.active ? 700 : 400,
color: '#fff', cursor: 'pointer', transition: 'all .15s',
}}>{btn.label}</button>
))}
{!alwaysOpen && (
<button onClick={onClose} title="Fermer" style={{
background: 'rgba(255,255,255,0.2)', border: '1px solid rgba(255,255,255,0.4)',
borderRadius: 6, padding: '3px 10px', color: '#fff', cursor: 'pointer',
fontSize: '0.85rem', fontWeight: 700,
}}></button>
)}
</div>
</div>
{/* ── Barre filtres ── */}
<div style={{
display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap',
padding: '10px 16px', borderBottom: '1px solid var(--border)',
background: 'var(--surface-2)',
}}>
{/* Sélecteur plateforme */}
{plateformes && plateformes.length > 0 && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<label style={{ fontSize: '0.8rem', color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>Plateforme</label>
<select
value={filterPlatId}
onChange={e => setFilterPlatId(e.target.value)}
style={{ fontSize: 'var(--fs-sm)', padding: '3px 8px', borderRadius: 6,
border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)' }}>
<option value="">Toutes</option>
{plateformes.map(p => <option key={p.id} value={String(p.id)}>{p.nom}{multiDetenteur && p.investisseur_nom ? `${p.investisseur_nom}` : ''}</option>)}
</select>
</div>
)}
{/* Toggle Reçus / Projetés */}
<div style={{ display: 'inline-flex', background: 'var(--surface)', borderRadius: 8, padding: 3, gap: 2, marginLeft: 'auto' }}>
{[
{ key: 'recus', label: 'Reçus', active: showRecus, toggle: () => setShowRecus(v => !v) },
{ key: 'projetes', label: 'Projetés', active: showProjetes, toggle: () => setShowProjetes(v => !v) },
].map(btn => (
<button key={btn.key} onClick={() => btn.toggle()} style={{
border: 'none', cursor: 'pointer', padding: '4px 14px', borderRadius: 6,
fontSize: 'var(--fs-sm)',
fontWeight: btn.active ? 600 : 400,
background: btn.active ? 'var(--primary)' : 'transparent',
color: btn.active ? '#fff' : 'var(--text-muted)',
boxShadow: btn.active ? '0 1px 3px rgba(0,0,0,0.13)' : 'none',
transition: 'all .15s',
}}>{btn.label}</button>
))}
</div>
</div>
{/* ── Corps ── */}
{loading && (
<div style={{ padding: 32, textAlign: 'center', color: 'var(--text-muted)' }}>Chargement</div>
)}
{!loading && data && (
<div>
<table className="drill-table" style={{ width: '100%', minWidth: '100%', fontSize: 'var(--fs-sm)' }}>
<thead>
<tr style={{ background: 'var(--surface-2)', borderBottom: '1px solid var(--border)' }}>
<th style={thStyle}>Date</th>
{showPlatCol && <th style={thStyle}>Plateforme</th>}
<th style={{ ...thStyle, width: "100%" }}>Projet</th>
<th style={thStyle}>Détenteur</th>
{cols.map(c => (
<th key={c.key} style={{ ...thStyle, ...numStyle, color: c.color }}>{c.label}</th>
))}
<th style={{ ...thStyle, ...numStyle }}>Total ligne</th>
</tr>
</thead>
{/* ── Section Reçus ── */}
{showRecus && (
<tbody>
<tr>
<td colSpan={colCount} style={{
padding: '6px 12px', fontWeight: 700, fontSize: '0.78rem',
background: hexToRgba(chartInterets, 0.07),
color: 'var(--text-muted)', letterSpacing: '.04em', textTransform: 'uppercase',
}}>
Reçus ({recus.length})
</td>
</tr>
{recus.length === 0 ? (
<tr><td colSpan={colCount} style={{ padding: '10px 16px', color: 'var(--text-muted)', fontStyle: 'italic' }}>
Aucun remboursement reçu ce mois
</td></tr>
) : recus.map(r => (
<tr key={r.id}
style={{ borderBottom: '1px solid var(--border)', cursor: onEditRecu ? 'pointer' : 'default' }}
title={onEditRecu ? 'Modifier ce remboursement' : undefined}
onClick={() => onEditRecu && onEditRecu(r)}
>
<td style={tdStyle}>{fmtDate(r.date_remb)}</td>
{showPlatCol && <td style={{ ...tdStyle, fontWeight: 500 }}>{r.plateforme_nom || '—'}</td>}
<td style={tdStyle}>{r.nom_projet}</td>
<td style={{ ...tdStyle, color: 'var(--text-muted)', fontSize: '0.8em' }}>{r.detenteur_nom || '—'}</td>
{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 <td key={c.key} style={{ ...tdStyle, ...numStyle }}>{fmtEUR(v)}</td>;
})}
<td style={{ ...tdStyle, ...numStyle, fontWeight: 600 }}>{fmtEUR(recuRowValue(r))}</td>
</tr>
))}
{recus.length > 0 && (
<tr className="drill-row-fixed" style={{ background: 'var(--surface-2)', fontWeight: 700 }}>
<td colSpan={3 + (showPlatCol ? 1 : 0)} style={{ ...tdStyle, color: 'var(--text-muted)', fontSize: '0.8em' }}>
Sous-total reçus
</td>
{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 <td key={c.key} style={{ ...tdStyle, ...numStyle }}>{fmtEUR(sum)}</td>;
})}
<td style={{ ...tdStyle, ...numStyle }}>{fmtEUR(totalRecus)}</td>
</tr>
)}
</tbody>
)}
{/* ── Section Projetés ── */}
{showProjetes && (
<tbody>
<tr>
<td colSpan={colCount} style={{
padding: '6px 12px', fontWeight: 700, fontSize: '0.78rem',
background: 'rgba(148,163,184,0.08)',
color: 'var(--text-muted)', letterSpacing: '.04em', textTransform: 'uppercase',
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span>Projetés ({projetes.length})</span>
{projetes.length > 0 && (
<button
onClick={() => openBulkModal()}
style={{
display: 'inline-flex', alignItems: 'center', gap: 5,
padding: '3px 10px', borderRadius: 5, cursor: 'pointer',
fontSize: '0.75rem', fontWeight: 600, letterSpacing: 'normal',
textTransform: 'none',
border: '1px solid var(--primary)',
background: 'var(--primary)', color: '#fff',
transition: 'opacity .15s',
}}
onMouseEnter={e => e.currentTarget.style.opacity = '0.85'}
onMouseLeave={e => e.currentTarget.style.opacity = '1'}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
Valider en masse
</button>
)}
</div>
</td>
</tr>
{projetes.length === 0 ? (
<tr><td colSpan={colCount} style={{ padding: '10px 16px', color: 'var(--text-muted)', fontStyle: 'italic' }}>
Aucune projection ce mois
</td></tr>
) : projetes.map(p => (
<tr key={p.id}
style={{ borderBottom: '1px solid var(--border)', opacity: 0.85, cursor: onEditProjet ? 'pointer' : 'default' }}
title={onEditProjet ? 'Saisir ce remboursement' : undefined}
onClick={() => onEditProjet && onEditProjet(p)}
>
<td style={{ ...tdStyle, fontStyle: 'italic', color: 'var(--text-muted)' }}>{fmtDate(p.date_prevue)}</td>
{showPlatCol && <td style={{ ...tdStyle, fontStyle: 'italic', fontWeight: 500 }}>{p.plateforme_nom || '—'}</td>}
<td style={{ ...tdStyle, fontStyle: 'italic' }}>{p.nom_projet}</td>
<td style={{ ...tdStyle, color: 'var(--text-muted)', fontSize: '0.8em' }}>{p.detenteur_nom || '—'}</td>
{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 <td key={c.key} style={{ ...tdStyle, ...numStyle, fontStyle: 'italic', color: 'var(--text-muted)' }}>{fmtEUR(v)}</td>;
})}
<td style={{ ...tdStyle, ...numStyle, fontStyle: 'italic', color: 'var(--text-muted)' }}>{fmtEUR(projRowValue(p))}</td>
</tr>
))}
{projetes.length > 0 && (
<tr className="drill-row-fixed" style={{ background: 'var(--surface-2)', fontWeight: 700 }}>
<td colSpan={3 + (showPlatCol ? 1 : 0)} style={{ ...tdStyle, color: 'var(--text-muted)', fontSize: '0.8em' }}>
Sous-total projetés
</td>
{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 <td key={c.key} style={{ ...tdStyle, ...numStyle }}>{fmtEUR(sum)}</td>;
})}
<td style={{ ...tdStyle, ...numStyle }}>{fmtEUR(totalProjetes)}</td>
</tr>
)}
</tbody>
)}
{/* ── Grand total ── */}
<tfoot>
<tr className="drill-row-fixed" style={{
background: 'var(--primary)', color: '#fff',
fontWeight: 700, fontSize: '0.9rem', pointerEvents: 'none',
}}>
<td colSpan={3 + (showPlatCol ? 1 : 0)} style={{ ...tdStyle, color: '#fff' }}>Total</td>
{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 <td key={c.key} style={{ ...tdStyle, ...numStyle, color: '#fff' }}>{fmtEUR(sumR + sumP)}</td>;
})}
<td style={{ ...tdStyle, ...numStyle, color: '#fff', fontSize: '1rem' }}>
{fmtEUR(grandTotal)}
</td>
</tr>
</tfoot>
</table>
</div>
)}
{/* ── Modale validation en masse ── */}
{bulkModal && (
<Modal
open={bulkModal}
title={bulkDone ? 'Validation terminée' : `Valider ${bulkItems.length} remboursement${bulkItems.length > 1 ? 's' : ''}`}
onClose={bulkProcessing ? undefined : closeBulkModal}
width={620}
footer={
bulkDone ? (
<button className="btn-primary" onClick={() => closeBulkModal()}>Fermer</button>
) : bulkProcessing ? (
<span style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>
Traitement en cours {bulkProgress}/{bulkItems.length}
</span>
) : (
<>
<button className="btn-secondary" onClick={() => closeBulkModal()}>Annuler</button>
<button className="btn-primary" onClick={() => runBulk()}>
Enregistrer {bulkItems.length} remboursement{bulkItems.length > 1 ? 's' : ''}
</button>
</>
)
}
>
{bulkProcessing && (
<div style={{ marginBottom: 16 }}>
<div style={{ height: 6, borderRadius: 3, background: 'var(--border)', overflow: 'hidden' }}>
<div style={{
height: '100%', borderRadius: 3, background: 'var(--primary)',
width: `${bulkItems.length > 0 ? (bulkProgress / bulkItems.length) * 100 : 0}%`,
transition: 'width .3s ease',
}} />
</div>
</div>
)}
{bulkDone && (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10, padding: '12px 0' }}>
<div style={{ width: 48, height: 48, borderRadius: '50%', background: 'var(--success)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
</div>
<p style={{ margin: 0, fontWeight: 600, fontSize: '1rem' }}>
{bulkItems.length} remboursement{bulkItems.length > 1 ? 's' : ''} enregistré{bulkItems.length > 1 ? 's' : ''}
</p>
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.88rem' }}>
{cell.moisLabel} {cell.annee}
</p>
</div>
)}
{!bulkDone && (
<>
<p style={{ margin: '0 0 12px', color: 'var(--text-muted)', fontSize: '0.88rem' }}>
Les remboursements suivants vont être créés d'après les projections de <strong>{cell.moisLabel} {cell.annee}</strong>.
Les prélèvements fiscaux sont estimés à partir des taux PFU de l'année.
</p>
<div>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.88rem' }}>
<thead>
<tr style={{ background: 'var(--surface-2)', borderBottom: '1px solid var(--border)' }}>
{showPlatCol && <th style={thStyle}>Plateforme</th>}
<th style={{ ...thStyle, width: "100%" }}>Projet</th>
<th style={{ ...thStyle, ...numStyle }}>Date</th>
<th style={{ ...thStyle, ...numStyle }}>Capital</th>
<th style={{ ...thStyle, ...numStyle }}>Intérêts bruts</th>
<th style={{ ...thStyle, ...numStyle }}>Total brut</th>
</tr>
</thead>
<tbody>
{bulkItems.map((item, i) => (
<tr key={i} style={{ borderBottom: '1px solid var(--border)' }}>
{showPlatCol && <td style={tdStyle}>{item._plat || '—'}</td>}
<td style={tdStyle}>{item._label}</td>
<td style={{ ...tdStyle, ...numStyle }}>{fmtDate(item._date)}</td>
<td style={{ ...tdStyle, ...numStyle }}>{fmtEUR(item._capital)}</td>
<td style={{ ...tdStyle, ...numStyle }}>{fmtEUR(item._interets)}</td>
<td style={{ ...tdStyle, ...numStyle, fontWeight: 600 }}>{fmtEUR(item._total)}</td>
</tr>
))}
</tbody>
<tfoot>
<tr style={{ background: 'var(--surface-2)', fontWeight: 700 }}>
<td colSpan={showPlatCol ? 5 : 4} style={{ ...tdStyle, color: 'var(--text-muted)', fontSize: '0.8em' }}>Total</td>
<td style={{ ...tdStyle, ...numStyle }}>{fmtEUR(bulkItems.reduce((s, i) => s + i._total, 0))}</td>
</tr>
</tfoot>
</table>
</div>
</>
)}
</Modal>
)}
</div>
);
}
/* ── 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' };