Amélioration de la logique des investissements vs dépôts
This commit is contained in:
1 parent
c7bbe5dee6
commit
f6829dec9d
4 files changed
+349
-52
No files matched your search
@@ -653,30 +653,100 @@ export default function DepotsRetraits() {
|
||||
return plat?.investisseur_id ? String(plat.investisseur_id) : defaultInvestisseurId();
|
||||
};
|
||||
|
||||
/* Ouverture auto depuis le bouton global "Nouveau dépôt/retrait" */
|
||||
/* Ouverture auto depuis le bouton global "Nouveau dépôt/retrait", ou depuis le formulaire
|
||||
investissement — soit le bouton "Déposer" (ajoute ?plateforme_id=… pour présélectionner
|
||||
la plateforme plutôt que de retomber sur plats[0], et ?resumeInvestissement=1 pour
|
||||
demander un retour automatique vers l'investissement une fois le dépôt créé — cf.
|
||||
pendingReturnActive/maybeReturnToInvestissement plus bas), soit la proposition de dépôt
|
||||
après création d'un investissement dont le porte-monnaie ne couvrait pas le montant
|
||||
(ajoute ?montant=… pour pré-remplir le montant du dépôt, et ?autoCreate=1 pour créer le
|
||||
dépôt directement sans repasser par la modale — l'utilisateur a déjà validé/rectifié le
|
||||
montant dans la proposition affichée côté formulaire investissement). */
|
||||
const pendingNew = useRef(false);
|
||||
const pendingNewPlatId = useRef(null);
|
||||
const pendingNewMontant = useRef(null);
|
||||
const pendingNewAutoCreate = useRef(false);
|
||||
const pendingReturnActive = useRef(false);
|
||||
useEffect(() => {
|
||||
if (new URLSearchParams(search).get('new') !== '1') return;
|
||||
const params = new URLSearchParams(search);
|
||||
if (params.get('new') !== '1') return;
|
||||
const platParam = params.get('plateforme_id');
|
||||
const montantParam = params.get('montant');
|
||||
const autoCreateParam = params.get('autoCreate') === '1';
|
||||
if (params.get('resumeInvestissement') === '1') pendingReturnActive.current = true;
|
||||
navigate('/depots-retraits', { replace: true });
|
||||
if (plats.length > 0) {
|
||||
setEditingId(null);
|
||||
const firstId = plats[0]?.id || '';
|
||||
setForm({ ...emptyMouvementForm, plateforme_id: firstId, investisseur_id: investisseurForPlat(firstId) });
|
||||
setErr(null);
|
||||
setModalOpen(true);
|
||||
const firstId = (platParam && plats.some(p => String(p.id) === platParam)) ? platParam : (plats[0]?.id || '');
|
||||
const built = { ...emptyMouvementForm, plateforme_id: firstId, investisseur_id: investisseurForPlat(firstId), montant: montantParam || '' };
|
||||
if (autoCreateParam && montantParam) {
|
||||
setErr(null);
|
||||
doCreateDeposit(built).catch(err => {
|
||||
// Création auto échouée (réseau, validation serveur…) — on retombe sur la modale
|
||||
// pré-remplie pour que l'utilisateur voie l'erreur et puisse retenter manuellement.
|
||||
setForm(built);
|
||||
setErr(err.message);
|
||||
setModalOpen(true);
|
||||
});
|
||||
} else {
|
||||
setForm(built);
|
||||
setErr(null);
|
||||
setModalOpen(true);
|
||||
}
|
||||
} else {
|
||||
pendingNew.current = true;
|
||||
pendingNewPlatId.current = platParam;
|
||||
pendingNewMontant.current = montantParam;
|
||||
pendingNewAutoCreate.current = autoCreateParam;
|
||||
}
|
||||
}, [search]); // eslint-disable-line
|
||||
|
||||
/** Si le dépôt en cours de création vient du bouton "Déposer" du formulaire investissement
|
||||
* (cf. ci-dessus), navigue vers la page d'origine avec la saisie restaurée depuis
|
||||
* sessionStorage. Ne fait rien sinon (comportement normal, inchangé). À appeler après
|
||||
* que le dépôt est effectivement créé (pas à l'annulation) — les 3 points de sortie
|
||||
* possibles de la création (sans correction de solde / correction confirmée / correction
|
||||
* "Passer") appellent tous cette fonction. */
|
||||
const maybeReturnToInvestissement = () => {
|
||||
if (!pendingReturnActive.current) return;
|
||||
pendingReturnActive.current = false;
|
||||
try {
|
||||
const raw = sessionStorage.getItem('cl_investissement_draft');
|
||||
if (!raw) return;
|
||||
sessionStorage.removeItem('cl_investissement_draft');
|
||||
const draft = JSON.parse(raw);
|
||||
if (!draft?.returnTo) return;
|
||||
const sep = draft.returnTo.includes('?') ? '&' : '?';
|
||||
navigate(`${draft.returnTo}${sep}resumeInvestissement=1`);
|
||||
} catch {
|
||||
// sessionStorage/JSON invalide — pas de retour auto, on reste simplement sur cette page.
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingNew.current || !plats.length) return;
|
||||
pendingNew.current = false;
|
||||
setEditingId(null);
|
||||
const firstId = plats[0]?.id || '';
|
||||
setForm({ ...emptyMouvementForm, plateforme_id: firstId, investisseur_id: investisseurForPlat(firstId) });
|
||||
setErr(null);
|
||||
setModalOpen(true);
|
||||
const platParam = pendingNewPlatId.current;
|
||||
pendingNewPlatId.current = null;
|
||||
const montantParam = pendingNewMontant.current;
|
||||
pendingNewMontant.current = null;
|
||||
const autoCreateParam = pendingNewAutoCreate.current;
|
||||
pendingNewAutoCreate.current = false;
|
||||
const firstId = (platParam && plats.some(p => String(p.id) === platParam)) ? platParam : (plats[0]?.id || '');
|
||||
const built = { ...emptyMouvementForm, plateforme_id: firstId, investisseur_id: investisseurForPlat(firstId), montant: montantParam || '' };
|
||||
if (autoCreateParam && montantParam) {
|
||||
setErr(null);
|
||||
doCreateDeposit(built).catch(err => {
|
||||
setForm(built);
|
||||
setErr(err.message);
|
||||
setModalOpen(true);
|
||||
});
|
||||
} else {
|
||||
setForm(built);
|
||||
setErr(null);
|
||||
setModalOpen(true);
|
||||
}
|
||||
}, [plats]); // eslint-disable-line
|
||||
|
||||
/* CRUD */
|
||||
@@ -762,54 +832,71 @@ export default function DepotsRetraits() {
|
||||
|
||||
const close = () => { setModalOpen(false); setEditingId(null); setForm(emptyMouvementForm); setErr(null); };
|
||||
|
||||
/** Crée un dépôt/retrait à partir d'un objet form donné. Extrait de submit() pour être
|
||||
* réutilisable par la création automatique déclenchée depuis ?new=1&autoCreate=1 (cf.
|
||||
* effets ci-dessus) — l'utilisateur y a déjà validé/rectifié le montant en amont, donc pas
|
||||
* besoin de repasser par la modale. Les erreurs remontent à l'appelant (try/catch). */
|
||||
const doCreateDeposit = async (f) => {
|
||||
const payload = {
|
||||
investisseur_id: Number(f.investisseur_id) || undefined,
|
||||
plateforme_id: Number(f.plateforme_id),
|
||||
date_operation: f.date_operation,
|
||||
type: f.type,
|
||||
montant: Number(f.montant),
|
||||
libelle: f.libelle || undefined,
|
||||
reference: f.reference || undefined,
|
||||
notes: f.notes || undefined,
|
||||
};
|
||||
await api.post('/depots-retraits', payload);
|
||||
close();
|
||||
|
||||
// Vérifier si la plateforme est flat_tax → proposer correction de solde
|
||||
const plat = plats.find(p => String(p.id) === String(f.plateforme_id));
|
||||
if (plat?.fiscalite === 'flat_tax') {
|
||||
// Solde attendu = solde actuel ± montant de l'opération (avant reload)
|
||||
const currentPlatData = dashData?.cashByPlatform?.find(
|
||||
p => String(p.plateforme_id) === String(f.plateforme_id)
|
||||
);
|
||||
const currentBalance = currentPlatData?.solde_portefeuille ?? 0;
|
||||
const montant = Number(f.montant);
|
||||
const expectedBalance = Math.round(
|
||||
(f.type === 'retrait' ? currentBalance - montant : currentBalance + montant) * 100
|
||||
) / 100;
|
||||
|
||||
setCorrModal({
|
||||
open: true,
|
||||
plateforme_nom: plat.nom,
|
||||
type_operation: f.type,
|
||||
computedBalance: expectedBalance,
|
||||
date: f.date_operation,
|
||||
platId: Number(f.plateforme_id),
|
||||
investisseurId: Number(f.investisseur_id) || null,
|
||||
});
|
||||
} else {
|
||||
await Promise.all([load(), loadDashDataAsOf()]);
|
||||
maybeReturnToInvestissement();
|
||||
}
|
||||
};
|
||||
|
||||
const submit = async (e) => {
|
||||
e?.preventDefault?.(); setErr(null);
|
||||
try {
|
||||
const payload = {
|
||||
investisseur_id: Number(form.investisseur_id) || undefined,
|
||||
plateforme_id: Number(form.plateforme_id),
|
||||
date_operation: form.date_operation,
|
||||
type: form.type,
|
||||
montant: Number(form.montant),
|
||||
libelle: form.libelle || undefined,
|
||||
reference: form.reference || undefined,
|
||||
notes: form.notes || undefined,
|
||||
};
|
||||
if (editingId) {
|
||||
const payload = {
|
||||
investisseur_id: Number(form.investisseur_id) || undefined,
|
||||
plateforme_id: Number(form.plateforme_id),
|
||||
date_operation: form.date_operation,
|
||||
type: form.type,
|
||||
montant: Number(form.montant),
|
||||
libelle: form.libelle || undefined,
|
||||
reference: form.reference || undefined,
|
||||
notes: form.notes || undefined,
|
||||
};
|
||||
await api.put(`/depots-retraits/${editingId}`, payload);
|
||||
close(); await Promise.all([load(), loadDashDataAsOf()]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Nouvelle opération
|
||||
await api.post('/depots-retraits', payload);
|
||||
close();
|
||||
|
||||
// Vérifier si la plateforme est flat_tax → proposer correction de solde
|
||||
const plat = plats.find(p => String(p.id) === String(form.plateforme_id));
|
||||
if (plat?.fiscalite === 'flat_tax') {
|
||||
// Solde attendu = solde actuel ± montant de l'opération (avant reload)
|
||||
const currentPlatData = dashData?.cashByPlatform?.find(
|
||||
p => String(p.plateforme_id) === String(form.plateforme_id)
|
||||
);
|
||||
const currentBalance = currentPlatData?.solde_portefeuille ?? 0;
|
||||
const montant = Number(form.montant);
|
||||
const expectedBalance = Math.round(
|
||||
(form.type === 'retrait' ? currentBalance - montant : currentBalance + montant) * 100
|
||||
) / 100;
|
||||
|
||||
setCorrModal({
|
||||
open: true,
|
||||
plateforme_nom: plat.nom,
|
||||
type_operation: form.type,
|
||||
computedBalance: expectedBalance,
|
||||
date: form.date_operation,
|
||||
platId: Number(form.plateforme_id),
|
||||
investisseurId: Number(form.investisseur_id) || null,
|
||||
});
|
||||
} else {
|
||||
await Promise.all([load(), loadDashDataAsOf()]);
|
||||
}
|
||||
await doCreateDeposit(form);
|
||||
} catch (e) { setErr(e.message); }
|
||||
};
|
||||
|
||||
@@ -831,6 +918,7 @@ export default function DepotsRetraits() {
|
||||
}
|
||||
setCorrModal(m => ({ ...m, open: false }));
|
||||
await Promise.all([load(), loadDashDataAsOf()]);
|
||||
maybeReturnToInvestissement();
|
||||
};
|
||||
|
||||
const openRowMenu = (e, row) => {
|
||||
@@ -1623,7 +1711,7 @@ export default function DepotsRetraits() {
|
||||
type_operation={corrModal.type_operation}
|
||||
computedBalance={corrModal.computedBalance}
|
||||
date={corrModal.date}
|
||||
onClose={() => { setCorrModal(m => ({ ...m, open: false })); load(); }}
|
||||
onClose={() => { setCorrModal(m => ({ ...m, open: false })); load(); maybeReturnToInvestissement(); }}
|
||||
onConfirm={handleCorrectionConfirm}
|
||||
/>
|
||||
|
||||
|
||||
@@ -223,6 +223,28 @@ export default function InvestissementDetail() {
|
||||
/* eslint-disable-next-line */
|
||||
}, [inv, pfuRates, searchParams.get('remb-date')]);
|
||||
|
||||
// Reprise de la saisie après un aller-retour "Déposer" vers Dépôts/Retraits (bouton dans
|
||||
// InvestissementFormModal, étape 1) : la modale d'édition se rouvre avec exactement ce
|
||||
// qui avait été saisi avant le dépôt.
|
||||
useEffect(() => {
|
||||
if (searchParams.get('resumeInvestissement') !== '1' || !inv) return;
|
||||
setSearchParams(p => { p.delete('resumeInvestissement'); return p; }, { replace: true });
|
||||
try {
|
||||
const raw = sessionStorage.getItem('cl_investissement_draft');
|
||||
if (raw) {
|
||||
const draft = JSON.parse(raw);
|
||||
sessionStorage.removeItem('cl_investissement_draft');
|
||||
if (draft?.form) {
|
||||
setForm(draft.form);
|
||||
lastValidSouscriptionRef.current = draft.form.date_souscription || '';
|
||||
setErr(null);
|
||||
setModalOpen(true);
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
/* eslint-disable-next-line */
|
||||
}, [inv, searchParams.get('resumeInvestissement')]);
|
||||
|
||||
// ── Helpers PFU / calculs remboursement ────────────────────────
|
||||
const round2 = n => Math.round(n * 100) / 100;
|
||||
const getLastKnownRates = () =>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user