Ajout du paneau mois en cours + correction bug validation en masse

This commit is contained in:
ocroguennec committed 2026-08-15 11:10:27 +02:00
1 parent b78fbbb101
commit 6f8f7bab01
6 files changed
+779 -406

No files matched your search

+161 -18
View File
@@ -14,6 +14,19 @@ function hexToRgba(hex, a) {
const round2 = v => Math.round(v * 100) / 100;
/* Petite bulle de couleur — indicateur vert/orange/gris à côté du nom du projet */
function Bubble({ color, title }) {
return (
<span
title={title}
style={{
display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
background: color, marginLeft: 7, flex: '0 0 auto',
}}
/>
);
}
/**
* DrillCellPanel
*
@@ -45,6 +58,7 @@ export default function DrillCellPanel({
} = useInteretsChart();
const [data, setData] = useState(null);
const [prevData, setPrevData] = 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('');
@@ -57,6 +71,7 @@ export default function DrillCellPanel({
const [bulkProcessing, setBulkProcessing] = useState(false);
const [bulkProgress, setBulkProgress] = useState(0);
const [bulkDone, setBulkDone] = useState(false);
const [bulkErrors, setBulkErrors] = useState([]);
/* ── Reset filtres quand la cellule change ── */
useEffect(() => {
@@ -94,6 +109,32 @@ export default function DrillCellPanel({
.finally(() => setLoading(false));
}, [cell?.annee, cell?.mois, filterPlatId, activeView, activeId, refreshKey]);
/* ── Fetch du mois précédent (pour l'indicateur vert/orange/gris) ── */
useEffect(() => {
if (!cell) { setPrevData(null); return; }
const isInitialFilter = cell.platId ? filterPlatId === String(cell.platId) : !filterPlatId;
const consolidatedIds = isInitialFilter && cell.platIds?.length > 1
? cell.platIds.join(',')
: null;
let prevMois = cell.mois - 1;
let prevAnnee = cell.annee;
if (prevMois < 1) { prevMois = 12; prevAnnee -= 1; }
const params = {
annee: prevAnnee,
mois: prevMois,
...(consolidatedIds
? { plateforme_ids: consolidatedIds }
: filterPlatId ? { plateforme_id: filterPlatId } : {}),
...(activeView === 'all' ? { scope: 'all' } : {}),
};
api.get('/dashboard/detail-cellule', params)
.then(d => setPrevData(d))
.catch(() => setPrevData({ recus: [], projetes: [] }));
}, [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;
@@ -113,6 +154,22 @@ export default function DrillCellPanel({
return [...data.projetes].sort((a, b) => (a.date_prevue || '').localeCompare(b.date_prevue || ''));
}, [data]);
/* ── Agrégation des reçus du mois précédent, par investissement ──
Sert de référence pour l'indicateur vert/orange/gris à côté du nom du projet. */
const prevRecusByInv = useMemo(() => {
const map = new Map();
if (!prevData) return map;
for (const r of prevData.recus) {
const acc = map.get(r.investissement_id) || { interets_bruts: 0, interets_nets: 0, capital: 0, cashback: 0 };
acc.interets_bruts += r.interets_bruts || 0;
acc.interets_nets += r.interets_nets || 0;
acc.capital += r.capital || 0;
acc.cashback += r.cashback || 0;
map.set(r.investissement_id, acc);
}
return map;
}, [prevData]);
if (!cell) return null;
/* ── Afficher la colonne Plateforme quand on est en mode "toutes" ── */
@@ -136,6 +193,36 @@ export default function DrillCellPanel({
return v;
};
/* ── Valeur du mois précédent pour un investissement (mêmes critères actifs) ──
undefined = aucun remboursement reçu ce mois-là pour cet investissement */
const prevRecuValue = (investissementId) => {
const acc = prevRecusByInv.get(investissementId);
if (!acc) return undefined;
let v = 0;
if (inclureInterets) v += netMode ? acc.interets_nets : acc.interets_bruts;
if (inclureCashback) v += acc.cashback;
if (inclureCapital) v += acc.capital;
return v;
};
/* ── Indicateur visuel (bulle) — reçus : vert si ≥ mois précédent ou si rien n'existait, orange sinon ── */
const recuIndicator = (r) => {
const prev = prevRecuValue(r.investissement_id);
const cur = recuRowValue(r);
if (prev === undefined) return { color: 'var(--success)', title: 'Aucun remboursement le mois précédent' };
if (round2(cur) >= round2(prev)) return { color: 'var(--success)', title: `≥ mois précédent (${fmtEUR(prev)})` };
return { color: 'var(--warning)', title: `< mois précédent (${fmtEUR(prev)})` };
};
/* ── Indicateur visuel (bulle) — projetés : vert si = reçu du mois précédent, gris si rien n'existait, orange sinon ── */
const projIndicator = (p) => {
const prev = prevRecuValue(p.investissement_id);
const cur = projRowValue(p);
if (prev === undefined) return { color: 'var(--text-muted)', title: 'Aucun remboursement le mois précédent' };
if (round2(cur) === round2(prev)) return { color: 'var(--success)', title: `= mois précédent (${fmtEUR(prev)})` };
return { color: 'var(--warning)', title: `≠ mois précédent (${fmtEUR(prev)})` };
};
/* ── Totaux ── */
const totalRecus = recus.reduce((s, r) => s + recuRowValue(r), 0);
const totalProjetes = projetes.reduce((s, p) => s + projRowValue(p), 0);
@@ -195,7 +282,7 @@ export default function DrillCellPanel({
statut: 'paye',
notes: '',
methode_remboursement: methode,
compte_id: methode === 'compte_courant' ? (inv?.compte_id || '') : '',
compte_id: methode === 'compte_courant' && inv?.compte_id ? inv.compte_id : null,
};
});
setBulkItems(items);
@@ -208,12 +295,18 @@ export default function DrillCellPanel({
const runBulk = async () => {
setBulkProcessing(true);
let done = 0;
const errors = [];
for (const item of bulkItems) {
const { _label, _plat, _date, _capital, _interets, _total, ...payload } = item;
try { await api.post('/remboursements', payload); } catch (e) { /* continuer */ }
try {
await api.post('/remboursements', payload);
} catch (e) {
errors.push({ label: _label, plat: _plat, date: _date, message: e.message });
}
done++;
setBulkProgress(done);
}
setBulkErrors(errors);
setBulkProcessing(false);
setBulkDone(true);
if (onBulkDone) onBulkDone();
@@ -225,6 +318,7 @@ export default function DrillCellPanel({
setBulkProgress(0);
setBulkDone(false);
setBulkProcessing(false);
setBulkErrors([]);
};
/* ── Rendu ── */
@@ -356,7 +450,12 @@ export default function DrillCellPanel({
>
<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}>
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
{r.nom_projet}
<Bubble {...recuIndicator(r)} />
</span>
</td>
<td style={{ ...tdStyle, color: 'var(--text-muted)', fontSize: '0.8em' }}>{r.detenteur_nom || '—'}</td>
{cols.map(c => {
let v;
@@ -434,7 +533,12 @@ export default function DrillCellPanel({
>
<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, fontStyle: 'italic' }}>
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
{p.nom_projet}
<Bubble {...projIndicator(p)} />
</span>
</td>
<td style={{ ...tdStyle, color: 'var(--text-muted)', fontSize: '0.8em' }}>{p.detenteur_nom || '—'}</td>
{cols.map(c => {
let v;
@@ -529,21 +633,60 @@ export default function DrillCellPanel({
</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>
{bulkDone && (() => {
const successCount = bulkItems.length - bulkErrors.length;
const allOk = bulkErrors.length === 0;
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10, padding: '12px 0' }}>
<div style={{
width: 48, height: 48, borderRadius: '50%',
background: allOk ? 'var(--success)' : 'var(--warning)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
{allOk ? (
<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>
) : (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="8" x2="12" y2="13"/><line x1="12" y1="16.5" x2="12.01" y2="16.5"/>
<circle cx="12" cy="12" r="10"/>
</svg>
)}
</div>
<p style={{ margin: 0, fontWeight: 600, fontSize: '1rem' }}>
{allOk
? `${bulkItems.length} remboursement${bulkItems.length > 1 ? 's' : ''} enregistré${bulkItems.length > 1 ? 's' : ''}`
: `${successCount}/${bulkItems.length} remboursement${bulkItems.length > 1 ? 's' : ''} enregistré${successCount > 1 ? 's' : ''} — ${bulkErrors.length} échec${bulkErrors.length > 1 ? 's' : ''}`}
</p>
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.88rem' }}>
{cell.moisLabel} {cell.annee}
</p>
{!allOk && (
<div style={{ width: '100%', marginTop: 8, border: '1px solid var(--danger)', borderRadius: 6, overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.82rem' }}>
<thead>
<tr style={{ background: 'var(--surface-2)' }}>
<th style={{ ...thStyle, color: 'var(--danger)' }}>Projet</th>
<th style={{ ...thStyle, color: 'var(--danger)' }}>Date</th>
<th style={{ ...thStyle, color: 'var(--danger)' }}>Erreur</th>
</tr>
</thead>
<tbody>
{bulkErrors.map((e, i) => (
<tr key={i} style={{ borderTop: '1px solid var(--border)' }}>
<td style={tdStyle}>{e.plat ? `${e.plat} — ` : ''}{e.label}</td>
<td style={tdStyle}>{fmtDate(e.date)}</td>
<td style={{ ...tdStyle, color: 'var(--danger)' }}>{e.message}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</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' }}>