Amélioration de la logique des investissements vs dépôts

This commit is contained in:
ocroguennec committed 2026-08-29 16:03:55 +02:00
1 parent c7bbe5dee6
commit f6829dec9d
4 files changed
+349 -52

No files matched your search

+123 -1
View File
@@ -293,6 +293,11 @@ export default function Investissements() {
const [loading, setLoading] = useState(false);
const [deleteConfirm, setDeleteConfirm] = useState(null);
const [openMenu, setOpenMenu] = useState(null);
// Proposition de dépôt après création d'un investissement si le porte-monnaie ne
// couvre pas l'opération (contrôle création uniquement, cf. submit()).
const [depositPrompt, setDepositPrompt] = useState({
open: false, plateforme_id: null, plateforme_nom: '', montant: 0, investissementId: null,
});
/* ── Chargement (sans filtre — filtrage côté client) ── */
const load = async () => {
@@ -625,6 +630,40 @@ export default function Investissements() {
setModalOpen(true);
}, [plats]); /* eslint-disable-next-line */
/* ── Reprise de la saisie après un aller-retour "Déposer" vers Dépôts/Retraits (bouton
dans InvestissementFormModal, étape 1) : la modale se rouvre avec exactement ce qui
avait été saisi avant le dépôt. */
const pendingResume = useRef(null);
useEffect(() => {
if (searchParams.get('resumeInvestissement') !== '1') return;
const n = new URLSearchParams(searchParams); n.delete('resumeInvestissement');
navigate({ pathname: '/investissements', search: n.toString() ? '?' + n.toString() : '' }, { replace: true });
let draft = null;
try {
const raw = sessionStorage.getItem('cl_investissement_draft');
if (raw) { draft = JSON.parse(raw); sessionStorage.removeItem('cl_investissement_draft'); }
} catch { /* ignore */ }
if (!draft?.form) return;
if (plats.length > 0) {
setEditingId(draft.editingId || null);
setForm(draft.form);
setErr(null);
setModalOpen(true);
} else {
pendingResume.current = draft;
}
}, [searchParams]); /* eslint-disable-next-line */
useEffect(() => {
if (!pendingResume.current || !plats.length) return;
const draft = pendingResume.current;
pendingResume.current = null;
setEditingId(draft.editingId || null);
setForm(draft.form);
setErr(null);
setModalOpen(true);
}, [plats]); /* eslint-disable-next-line */
/* ── CRUD ── */
const openNew = () => {
setEditingId(null);
@@ -747,8 +786,34 @@ export default function Investissements() {
api.put(`/investissements/${created.id}/categories-inv`, { ids: form.categories_inv_ids || [] }),
api.put(`/investissements/${created.id}/secteurs-inv`, { ids: form.secteurs_inv_ids || [] }),
]);
// Contrôle du porte-monnaie — création uniquement (jamais en édition). Le solde
// recalculé APRÈS création tient déjà compte du capital de ce nouvel investissement
// (formule officielle, cf. mémoire project_solde_portefeuille) : s'il ressort négatif,
// le porte-monnaie ne couvrait pas l'opération. On propose alors un dépôt du montant
// du découvert (rectifiable par l'utilisateur) plutôt que de bloquer la création.
let soldeApres = null;
try {
const r = await api.get('/dashboard/solde-historique', { plateforme_id: form.plateforme_id, date: today() });
soldeApres = r?.solde ?? null;
} catch {
// Contrôle indisponible — n'empêche pas la création pour autant.
}
close();
navigate(`/investissements/${created.id}`);
if (soldeApres !== null && soldeApres < -0.005) {
const plat = plats.find(p => String(p.id) === String(form.plateforme_id));
setDepositPrompt({
open: true,
plateforme_id: Number(form.plateforme_id),
plateforme_nom: plat?.nom || '',
montant: Math.round(Math.abs(soldeApres) * 100) / 100,
investissementId: created.id,
});
} else {
navigate(`/investissements/${created.id}`);
}
}
} catch (e) { setErr(e.message); }
};
@@ -1335,6 +1400,63 @@ export default function Investissements() {
onConfirm={deleteConfirm?.onConfirm}
onCancel={() => setDeleteConfirm(null)}
/>
{/* ── Proposition de dépôt si le porte-monnaie ne couvrait pas le nouvel
investissement (cf. submit()) — montant rectifiable avant de créer le dépôt. ── */}
{depositPrompt.open && (
<div style={{
position: 'fixed', inset: 0, zIndex: 1000,
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'rgba(0,0,0,0.4)',
}}>
<div style={{
background: 'var(--surface)', borderRadius: 10,
padding: '24px 28px', width: 420, maxWidth: '90vw',
boxShadow: '0 8px 32px rgba(0,0,0,0.18)',
border: '1px solid var(--border)',
}}>
<div style={{ marginBottom: 6, fontWeight: 700, fontSize: 'var(--fs-base)' }}>
Porte-monnaie insuffisant
</div>
<div style={{ fontSize: 'var(--fs-sm)', color: 'var(--text-muted)', marginBottom: 18, lineHeight: 1.5 }}>
Une fois cet investissement pris en compte, le porte-monnaie de <strong>{depositPrompt.plateforme_nom}</strong> est
à découvert. Voulez-vous créer un dépôt pour le couvrir ?
</div>
<div style={{ marginBottom: 18 }}>
<label style={{ fontSize: 'var(--fs-sm)', fontWeight: 500, display: 'block', marginBottom: 4 }}>
Montant du dépôt (€)
</label>
<input
type="number" step="0.01" min="0"
value={depositPrompt.montant}
onChange={e => setDepositPrompt(p => ({ ...p, montant: e.target.value }))}
style={{ width: '100%', fontSize: 'var(--fs-base)' }}
autoFocus
/>
</div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
<button onClick={() => {
const id = depositPrompt.investissementId;
setDepositPrompt(p => ({ ...p, open: false }));
navigate(`/investissements/${id}`);
}}>
Plus tard
</button>
<button
className="primary"
disabled={!depositPrompt.montant || Number(depositPrompt.montant) <= 0}
onClick={() => {
const { plateforme_id, montant } = depositPrompt;
setDepositPrompt(p => ({ ...p, open: false }));
navigate(`/depots-retraits?new=1&plateforme_id=${plateforme_id}&montant=${encodeURIComponent(montant)}&autoCreate=1`);
}}
>
Créer le dépôt
</button>
</div>
</div>
</div>
)}
</>
);
}