Mise en place de la notion de perte définitive, ainsi que la révision avec statut Polongation

This commit is contained in:
ocroguennec committed 2026-08-22 16:27:41 +02:00
1 parent 4ad6ea79e8
commit 8e1b82d161
21 files changed
+1069 -184

No files matched your search

+21 -8
View File
@@ -90,7 +90,7 @@ function invToCSV(rows) {
function invToXLS(rows) {
const data = rows.map(r => {
const capInv = r.capital_total ?? r.montant_investi;
const capRestant = Math.max(0, capInv - (r.capital_rembourse || 0));
const capRestant = Math.max(0, capInv - (r.capital_rembourse || 0) - (r.capital_perdu || 0));
return {
'Date souscription': r.date_souscription,
'Projet': r.nom_projet,
@@ -429,7 +429,11 @@ export default function Investissements() {
const decStart = `${yr}-12-01`;
if (r.date_souscription > cutoff) return false; // pas encore souscrit
const ACTIVE = ['en_cours', 'en_retard', 'procedure'];
if (ACTIVE.includes(r.statut)) return true; // encore actif aujourd'hui
// perte_definitive et prolongation restent "actifs" au sens de ce filtre (comme
// en_retard/procedure dont ils découlent) : le dossier ne doit pas disparaître des
// vues par année sous prétexte que la perte a été déclarée ou qu'une prolongation a
// été négociée — voir project_perte_definitive.md et project_prolongation.md
if (ACTIVE.includes(r.statut) || ['perte_definitive', 'prolongation'].includes(r.statut)) return true; // encore actif aujourd'hui
const fin = r.date_cible ?? lastRembDateMap[r.id] ?? null;
return !!fin && fin >= decStart; // clôturé mais actif en décembre
};
@@ -490,11 +494,12 @@ export default function Investissements() {
acc.cap_remb += capRemb;
acc.int_perc += rembParInv[r.id]?.interets_bruts || 0;
acc.int_perc_net += rembParInv[r.id]?.interets_nets || 0;
const capRestant = Math.max(0, capInv - capRemb);
const capRestant = Math.max(0, capInv - capRemb - (r.capital_perdu || 0));
acc.encours += capRestant;
if (['en_retard', 'procedure'].includes(r.statut)) acc.defaut += capRestant;
acc.pertes += (r.capital_perdu || 0);
return acc;
}, { investi: 0, cap_remb: 0, int_perc: 0, int_perc_net: 0, encours: 0, defaut: 0 }), [kpiRows, capRembParInv, rembParInv, reinvestCumulParInv]);
}, { investi: 0, cap_remb: 0, int_perc: 0, int_perc_net: 0, encours: 0, defaut: 0, pertes: 0 }), [kpiRows, capRembParInv, rembParInv, reinvestCumulParInv]);
/* ── Totaux KPI année N-1 (pour TrendBadge) ── */
const prevTotals = useMemo(() => {
@@ -530,7 +535,7 @@ export default function Investissements() {
acc.cap_remb += capRemb;
acc.int_perc += prevRembParInv[r.id]?.interets_bruts || 0;
acc.int_perc_net += prevRembParInv[r.id]?.interets_nets || 0;
const capRestant = Math.max(0, capInv - capRemb);
const capRestant = Math.max(0, capInv - capRemb - (r.capital_perdu || 0));
acc.encours += capRestant;
if (['en_retard', 'procedure'].includes(r.statut)) acc.defaut += capRestant;
return acc;
@@ -563,7 +568,7 @@ export default function Investissements() {
p.cap_remb += capRemb;
p.int_perc += rembParInv[r.id]?.interets_bruts || 0;
p.int_perc_net += rembParInv[r.id]?.interets_nets || 0;
const capRestant = Math.max(0, capInv - capRemb);
const capRestant = Math.max(0, capInv - capRemb - (r.capital_perdu || 0));
// Sans filtre année : les prêts remboursés ont capRestant=0 → pas d'impact
// Avec filtre année : on veut le capital déployé à la coupure, quel que soit le statut actuel
p.encours += capRestant;
@@ -607,7 +612,7 @@ export default function Investissements() {
.map(r => {
const capInv = r.montant_investi + (reinvestCumulParInv[r.id] || 0);
const capRemb = capRembParInv[r.id] || 0;
const capRestant = Math.max(0, capInv - capRemb);
const capRestant = Math.max(0, capInv - capRemb - (r.capital_perdu || 0));
return { plateforme_nom: r.plateforme_nom, montant: capRestant };
})
.filter(r => r.montant > 0),
@@ -847,6 +852,11 @@ export default function Investissements() {
{platYear && totals.defaut > 0 && <TrendBadge current={totals.defaut} prev={prevTotals.defaut} invert={true} />}
</div>
{platYear && <div style={{ fontSize: '0.8em', color: 'var(--text-muted)', marginTop: 5 }}>{fmtEUR(prevTotals.defaut)} en {Number(platYear) - 1}</div>}
{totals.pertes > 0 && (
<div style={{ fontSize: '0.8em', color: '#7c2d12', marginTop: 4, fontWeight: 600 }}>
dont {fmtEUR(totals.pertes)} déjà déclarés en perte définitive (non compris ci-dessus)
</div>
)}
</div>
{/* 3 — Investissements depuis le début */}
@@ -1148,6 +1158,8 @@ export default function Investissements() {
<option value="en_retard">En retard</option>
<option value="procedure">Procédure</option>
<option value="cloture">Clôturé</option>
<option value="perte_definitive">Perte définitive</option>
<option value="prolongation">Prolongation</option>
</select>
</div>
<div style={{ flex: 1, minWidth: 120 }}>
@@ -1221,7 +1233,7 @@ export default function Investissements() {
)}
{pagedRows.map(r => {
const capInv = r.capital_total ?? r.montant_investi;
const capRestant = Math.max(0, capInv - (r.capital_rembourse || 0));
const capRestant = Math.max(0, capInv - (r.capital_rembourse || 0) - (r.capital_perdu || 0));
return (
<tr key={r.id}
onClick={() => navigate(`/investissements/${r.id}`, { state: { from: { path: '/investissements', search: location.search, label: 'Investissements' } } })}
@@ -1560,6 +1572,7 @@ export default function Investissements() {
<option value="en_retard">En retard</option>
<option value="procedure">Procédure</option>
<option value="cloture">Clôturé</option>
<option value="perte_definitive">Perte définitive</option>
</select>
</div>
)}