diff --git a/frontend/src/pages/DepotsRetraits.jsx b/frontend/src/pages/DepotsRetraits.jsx
index 3515a05..f5f9510 100644
--- a/frontend/src/pages/DepotsRetraits.jsx
+++ b/frontend/src/pages/DepotsRetraits.jsx
@@ -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}
/>
diff --git a/frontend/src/pages/InvestissementDetail.jsx b/frontend/src/pages/InvestissementDetail.jsx
index 02a6993..b1d6c6a 100644
--- a/frontend/src/pages/InvestissementDetail.jsx
+++ b/frontend/src/pages/InvestissementDetail.jsx
@@ -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 = () =>
diff --git a/frontend/src/pages/Investissements.jsx b/frontend/src/pages/Investissements.jsx
index 78c978e..ff56cf0 100644
--- a/frontend/src/pages/Investissements.jsx
+++ b/frontend/src/pages/Investissements.jsx
@@ -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 && (
+
+
+
+ Porte-monnaie insuffisant
+
+
+ Une fois cet investissement pris en compte, le porte-monnaie de {depositPrompt.plateforme_nom} est
+ à découvert. Voulez-vous créer un dépôt pour le couvrir ?
+