Initial commit

This commit is contained in:
Olivier CROGUENNEC
2026-06-13 14:57:15 +02:00
commit 48ed7fe65e
209 changed files with 49979 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
import { useEffect, useState } from 'react';
import { api } from '../api.js';
import { useInvestisseur } from '../context/InvestisseurContext.jsx';
import { fmtEUR, fmtDate } from '../utils/format.js';
export default function SimulRemboursements() {
const { activeId } = useInvestisseur();
const [investissements, setInvestissements] = useState([]);
const [selected, setSelected] = useState('');
const [echeances, setEcheances] = useState([]);
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState(null);
useEffect(() => {
if (!activeId) return;
api.get('/investissements').then(setInvestissements);
}, [activeId]);
useEffect(() => {
if (!selected) { setEcheances([]); return; }
api.get('/simul', { investissement_id: selected }).then(setEcheances);
}, [selected]);
const generate = async () => {
if (!selected) return;
setBusy(true); setMsg(null);
try {
const r = await api.post('/simul/generate', {
investissement_id: Number(selected),
replace: true,
});
setMsg(`${r.inserted} échéances générées.`);
const e = await api.get('/simul', { investissement_id: selected });
setEcheances(e);
} catch (e) {
setMsg(`${e.message}`);
} finally { setBusy(false); }
};
const inv = investissements.find(i => i.id === Number(selected));
const totals = echeances.reduce((acc, e) => {
acc.capital += e.capital_prevu;
acc.interets += e.interets_prevus;
acc.total += e.total_prevu;
return acc;
}, { capital: 0, interets: 0, total: 0 });
return (
<>
<div className="topbar"><h2>Projections Remboursements</h2></div>
<div className="card">
<div className="row">
<div style={{ flex: 2 }}>
<label>Investissement</label>
<select value={selected} onChange={e => setSelected(e.target.value)}>
<option value=""> Choisir </option>
{investissements.map(i =>
<option key={i.id} value={i.id}>
{i.nom_projet} ({fmtEUR(i.montant_investi)} {i.taux_interet ?? '?'}% {i.duree_mois ?? '?'}m {i.type_remb || '?'})
</option>
)}
</select>
</div>
<div style={{ flex: 1 }}>
<button className="primary" onClick={generate} disabled={!selected || busy} style={{ width: '100%' }}>
{busy ? '…' : 'Générer / Régénérer'}
</button>
</div>
</div>
{msg && <div className={msg.startsWith('✔') ? 'success-msg' : 'error'} style={{ marginTop: 12 }}>{msg}</div>}
{inv && (!inv.taux_interet || !inv.duree_mois) && (
<div className="error" style={{ marginTop: 12 }}>
Cet investissement n'a pas de <strong>taux</strong> et/ou de <strong>durée</strong>. Renseignez-les dans la fiche.
</div>
)}
</div>
{echeances.length > 0 && (
<div className="card">
<div className="kpi-grid" style={{ marginBottom: 12 }}>
<div className="kpi"><div className="label">Capital prévu</div><div className="value">{fmtEUR(totals.capital)}</div></div>
<div className="kpi"><div className="label">Intérêts prévus</div><div className="value success">{fmtEUR(totals.interets)}</div></div>
<div className="kpi"><div className="label">Total prévu</div><div className="value">{fmtEUR(totals.total)}</div></div>
</div>
<table>
<thead>
<tr>
<th>#</th><th>Date prévue</th>
<th className="num">Capital</th><th className="num">Intérêts</th>
<th className="num">Total échéance</th>
</tr>
</thead>
<tbody>
{echeances.map(e => (
<tr key={e.id}>
<td>{e.numero_echeance}</td>
<td>{fmtDate(e.date_prevue)}</td>
<td className="num">{fmtEUR(e.capital_prevu)}</td>
<td className="num">{fmtEUR(e.interets_prevus)}</td>
<td className="num">{fmtEUR(e.total_prevu)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</>
);
}