Ajout du paneau mois en cours + correction bug validation en masse
This commit is contained in:
1 parent
b78fbbb101
commit
6f8f7bab01
6 files changed
+779
-406
No files matched your search
@@ -424,6 +424,8 @@ export default function Dashboard() {
|
||||
const [capitalMensuelData, setCapitalMensuelData] = useState([]);
|
||||
const [plateformes, setPlateformes] = useState([]);
|
||||
const [platLoaded, setPlatLoaded] = useState(false);
|
||||
const [investissements, setInvestissements] = useState([]);
|
||||
const [drillRefreshKey, setDrillRefreshKey] = useState(0);
|
||||
|
||||
/* ── drillCell : cellule sélectionnée dans le TIP — par défaut mois courant toutes plateformes ── */
|
||||
const _now = new Date();
|
||||
@@ -466,14 +468,30 @@ export default function Dashboard() {
|
||||
api.get('/plateformes').then(d => { setPlateformes(d); setPlatLoaded(true); }).catch(() => { setPlatLoaded(true); });
|
||||
}, []);
|
||||
|
||||
const loadDashboardData = () => {
|
||||
if (!activeId && activeView !== 'all') return Promise.resolve();
|
||||
const scopeParams = activeView === 'all' ? { scope: 'all' } : undefined;
|
||||
return Promise.all([
|
||||
api.get('/dashboard', scopeParams).then(setData),
|
||||
api.get('/investissements', scopeParams).then(setInvestissements).catch(() => setInvestissements([])),
|
||||
]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeId && activeView !== 'all') return;
|
||||
setData(null);
|
||||
setLoading(true);
|
||||
const params = activeView === 'all' ? { scope: 'all' } : undefined;
|
||||
api.get('/dashboard', params).then(setData).finally(() => setLoading(false));
|
||||
loadDashboardData().finally(() => setLoading(false));
|
||||
/* eslint-disable-next-line */
|
||||
}, [activeView, activeId]);
|
||||
|
||||
/* Rafraîchit le tableau de bord + le panneau de détail après une validation en masse
|
||||
de projections (bulk) depuis le DrillCellPanel — sans quoi l'écran restait figé sur
|
||||
les anciennes projections malgré des remboursements bien créés en base. */
|
||||
const onDrillBulkDone = () => {
|
||||
loadDashboardData();
|
||||
setDrillRefreshKey(k => k + 1);
|
||||
};
|
||||
|
||||
const ready = activeView === 'all' ? investisseurs.length > 0 : !!activeId;
|
||||
if (!ready) return <div className="card text-muted">Sélectionnez un compte investisseur.</div>;
|
||||
if (loading || !data) return <div className="card text-muted">Chargement…</div>;
|
||||
@@ -505,7 +523,7 @@ export default function Dashboard() {
|
||||
<>
|
||||
<div className="topbar"><h2><PageIcon name="dashboard" />Tableau de bord — {viewTitle}</h2></div>
|
||||
|
||||
<InteretsChartProvider netMode={netMode} pfuRates={pfuRates} activeView={activeView} activeId={activeId}>
|
||||
<InteretsChartProvider key={drillRefreshKey} netMode={netMode} pfuRates={pfuRates} activeView={activeView} activeId={activeId}>
|
||||
|
||||
{/* ── KPI + sélecteur année ── */}
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'stretch', marginBottom: 16 }}>
|
||||
@@ -545,8 +563,9 @@ export default function Dashboard() {
|
||||
activeView={activeView}
|
||||
activeId={activeId}
|
||||
plateformes={plateformes}
|
||||
investissements={[]}
|
||||
onBulkDone={() => {}}
|
||||
investissements={investissements}
|
||||
refreshKey={drillRefreshKey}
|
||||
onBulkDone={onDrillBulkDone}
|
||||
onEditRecu={(r) => {
|
||||
const q = new URLSearchParams({
|
||||
'edit-remb': r.id,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useInvestisseur } from '../context/InvestisseurContext.jsx';
|
||||
import { useUi } from '../context/UiContext.jsx';
|
||||
import Modal from '../components/Modal.jsx';
|
||||
import ConfirmModal from '../components/ConfirmModal.jsx';
|
||||
import RembFormModal from '../components/RembFormModal.jsx';
|
||||
import CountrySelect, { COUNTRIES, FlagIcon } from '../components/CountrySelect.jsx';
|
||||
import { fmtEUR, fmtPct, fmtDate, today } from '../utils/format.js';
|
||||
import { xirr } from '../utils/xirr.js';
|
||||
@@ -2194,177 +2195,24 @@ export default function InvestissementDetail() {
|
||||
</Modal>
|
||||
|
||||
{/* ── Modal saisie remboursement (depuis projection) ────────── */}
|
||||
{(() => {
|
||||
const rates = getRatesForYear(rembForm.date_remb);
|
||||
const fmtRate = n => n != null ? n.toFixed(1).replace('.', ',') + ' %' : '…';
|
||||
const labelPS = `Prélèvements sociaux${rates ? ` (${fmtRate(rates.prelev_sociaux)})` : ''} (€)`;
|
||||
const currentPlat = plats.find(p => p.id === inv?.plateforme_id);
|
||||
const isChoixOuvert = currentPlat?.methode_remboursement === 'choix_investisseur';
|
||||
const hasLocalTax = currentPlat?.fiscalite === 'avec_fiscalite_locale' && currentPlat?.taux_fiscalite_locale;
|
||||
const labelIR = `Impôt sur le revenu${rates ? ` (${fmtRate(rates.impot_revenu)})` : ''} (€)`;
|
||||
const interetsNets = computeInteretsNets(rembForm);
|
||||
const netRecu = computeNet(rembForm);
|
||||
const isExonere = inv?.fiscalite_override === 'exonere';
|
||||
// Indicatif = plateforme étrangère (non flat_tax) OU investissement exonéré PEA-PME.
|
||||
// Dans ce cas, les prélèvements sont affichés en lecture seule et le montant versé
|
||||
// ne les déduit pas (l'investisseur reçoit le brut intégral).
|
||||
const isIndicatif = isExonere || currentPlat?.fiscalite !== 'flat_tax';
|
||||
const montantRembourse = isIndicatif
|
||||
? round2((Number(rembForm.capital)||0) + (Number(rembForm.cashback)||0) + (Number(rembForm.interets_bruts)||0))
|
||||
: netRecu;
|
||||
return (
|
||||
<Modal
|
||||
open={rembModalOpen}
|
||||
title={editingRembId ? 'Modifier le remboursement' : 'Saisir le remboursement'}
|
||||
onClose={closeRembModal}
|
||||
width={900}
|
||||
footer={
|
||||
<div style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{editingRembId && !confirmingRembDelete && (
|
||||
<button className="danger" onClick={() => setConfirmingRembDelete(true)}>
|
||||
Supprimer
|
||||
</button>
|
||||
)}
|
||||
{confirmingRembDelete && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<span style={{ fontSize: 13, color: 'var(--danger, #ef4444)' }}>
|
||||
Supprimer ce remboursement ?
|
||||
</span>
|
||||
<button onClick={() => setConfirmingRembDelete(false)}>Non</button>
|
||||
<button className="danger" onClick={deleteRemb}>Oui, supprimer</button>
|
||||
</div>
|
||||
)}
|
||||
{!confirmingRembDelete && (
|
||||
<div style={{ display: 'flex', gap: 8, marginLeft: 'auto' }}>
|
||||
<button onClick={closeRembModal}>Annuler</button>
|
||||
<button className="primary" onClick={submitRemb}>Enregistrer</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<form onSubmit={submitRemb}>
|
||||
{rembErr && <div className="error">{rembErr}</div>}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px 16px' }}>
|
||||
<div style={{ gridColumn: 'span 3', borderTop: '1px solid var(--border)', paddingTop: 4, marginTop: 4 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-muted)' }}>Remboursement</span>
|
||||
</div>
|
||||
{hasLocalTax && (
|
||||
<div style={{ gridColumn: 'span 2' }}>
|
||||
<label>Intérêts bruts avant retenue locale ({currentPlat.taux_fiscalite_locale} %) (€)</label>
|
||||
<input type="number" step="0.01" value={rembForm.interets_bruts_avant_local ?? 0}
|
||||
onChange={e => setRembField('interets_bruts_avant_local', e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
{hasLocalTax && (
|
||||
<div>
|
||||
<label>Retenue à la source locale (€)</label>
|
||||
<input type="number" step="0.01" value={rembForm.taxe_locale ?? 0}
|
||||
onChange={e => setRembField('taxe_locale', e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label>Capital (€)</label>
|
||||
<input type="number" step="0.01" value={rembForm.capital ?? 0}
|
||||
onChange={e => setRembField('capital', e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
{hasLocalTax ? 'Intérêts bruts après retenue locale (€)' : 'Intérêts bruts (€)'}
|
||||
</label>
|
||||
<input type="number" step="0.01" value={rembForm.interets_bruts ?? 0}
|
||||
readOnly={!!hasLocalTax}
|
||||
style={hasLocalTax ? { background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' } : undefined}
|
||||
onChange={e => !hasLocalTax && setRembField('interets_bruts', e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Cashback (€)</label>
|
||||
<input type="number" step="0.01" value={rembForm.cashback ?? 0}
|
||||
onChange={e => setRembField('cashback', e.target.value)} />
|
||||
</div>
|
||||
<div style={{ gridColumn: 'span 3', borderTop: '1px solid var(--border)', paddingTop: 4, marginTop: 4 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-muted)' }}>Imposition{isIndicatif ? ' — indicatif' : ''}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>{labelPS}</label>
|
||||
<input type="number" step="0.01" value={rembForm.prelev_sociaux ?? 0}
|
||||
readOnly={isIndicatif}
|
||||
style={isIndicatif ? { background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' } : undefined}
|
||||
onChange={e => !isIndicatif && setRembField('prelev_sociaux', e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label>{labelIR}</label>
|
||||
<input type="number" step="0.01" value={rembForm.prelev_forfaitaire ?? 0}
|
||||
readOnly={isIndicatif}
|
||||
style={isIndicatif ? { background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' } : undefined}
|
||||
onChange={e => !isIndicatif && setRembField('prelev_forfaitaire', e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Total prélèvements (€)</label>
|
||||
<input type="number" step="0.01"
|
||||
value={round2((Number(rembForm.prelev_sociaux) || 0) + (Number(rembForm.prelev_forfaitaire) || 0))}
|
||||
readOnly
|
||||
style={{ background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' }} />
|
||||
</div>
|
||||
<div style={{ gridColumn: 'span 3', display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Montant des intérêts après imposition</span>
|
||||
<span style={{ fontWeight: 600, color: 'var(--text)' }}>{fmtEUR(interetsNets)}</span>
|
||||
</div>
|
||||
<div style={{ gridColumn: 'span 3', borderTop: '1px solid var(--border)', paddingTop: 4, marginTop: 4 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-muted)' }}>Versement</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>Date *</label>
|
||||
<input type="date" required value={rembForm.date_remb || ''}
|
||||
onChange={e => setRembField('date_remb', e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Méthode de remboursement</label>
|
||||
<select value={rembForm.methode_remboursement || 'portefeuille'}
|
||||
onChange={e => {
|
||||
const m = e.target.value;
|
||||
const def = m === 'compte_courant' ? (comptesRembInvestisseur.find(c => c.type === 'compte_courant') ?? comptesRembInvestisseur[0]) : null;
|
||||
setRembForm(f => ({ ...f, methode_remboursement: m, compte_id: def ? String(def.id) : '' }));
|
||||
}}>
|
||||
<option value="portefeuille">Porte-monnaie de la plateforme</option>
|
||||
<option value="compte_courant">Compte courant de l'investisseur</option>
|
||||
</select>
|
||||
</div>
|
||||
{rembForm.methode_remboursement === 'compte_courant' && (
|
||||
<div>
|
||||
<label>Compte de réception</label>
|
||||
{comptesRembInvestisseur.length > 0 ? (
|
||||
<select value={rembForm.compte_id || ''}
|
||||
onChange={e => setRembForm(f => ({ ...f, compte_id: e.target.value }))}>
|
||||
<option value="">— Choisir un compte —</option>
|
||||
{comptesRembInvestisseur.map(c => (
|
||||
<option key={c.id} value={c.id}>{c.nom}{c.banque ? ` — ${c.banque}` : ''}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<p className="text-muted" style={{ fontSize: 'var(--fs-sm)', margin: '4px 0' }}>
|
||||
Aucun compte défini. <a href="/settings?section=comptes" target="_blank" rel="noreferrer">Créer un compte →</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label>Montant Versé (€)</label>
|
||||
<input type="number" step="0.01" value={montantRembourse} readOnly
|
||||
style={{ background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' }} />
|
||||
</div>
|
||||
<div style={{ gridColumn: 'span 3', display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
|
||||
<span style={{ color: 'var(--text-muted)' }}>
|
||||
{isIndicatif
|
||||
? "L'imposition n'est pas déduite du montant versé (fiscalité appliquée à titre indicatif)."
|
||||
: "L'imposition est déduite du montant versé (capital + cashback + intérêts nets)."}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
})()}
|
||||
<RembFormModal
|
||||
open={rembModalOpen}
|
||||
editingId={editingRembId}
|
||||
onClose={closeRembModal}
|
||||
onSubmit={submitRemb}
|
||||
onDelete={deleteRemb}
|
||||
confirmingDelete={confirmingRembDelete}
|
||||
setConfirmingDelete={setConfirmingRembDelete}
|
||||
err={rembErr}
|
||||
form={rembForm}
|
||||
onFieldChange={setRembField}
|
||||
onFormPatch={patch => setRembForm(f => ({ ...f, ...patch }))}
|
||||
pfuRates={pfuRates}
|
||||
comptesInvestisseur={comptesRembInvestisseur}
|
||||
investissements={inv ? [inv] : []}
|
||||
plateformes={plats}
|
||||
locked
|
||||
/>
|
||||
|
||||
{/* ── Modal réinvestissement ────────────────────────────────── */}
|
||||
<Modal
|
||||
|
||||
@@ -8,6 +8,9 @@ import EmptyState from '../components/EmptyState.jsx';
|
||||
import InvChart from '../components/InvChart.jsx';
|
||||
import InvMensuelTable from '../components/InvMensuelTable.jsx';
|
||||
import SuiviObjectifs from '../components/SuiviObjectifs.jsx';
|
||||
import DrillCellPanel from '../components/DrillCellPanel.jsx';
|
||||
import RembFormModal from '../components/RembFormModal.jsx';
|
||||
import { InteretsChartProvider } from '../context/InteretsChartContext.jsx';
|
||||
import { fmtEUR, fmtDate, fmtStatut, memberLabel, today } from '../utils/format.js';
|
||||
import { xirr } from '../utils/xirr.js';
|
||||
import { usePagination } from '../hooks/usePagination.js';
|
||||
@@ -362,11 +365,165 @@ export default function Plateformes() {
|
||||
setAllCorrections(corrections);
|
||||
setAllSimulRembs(Array.isArray(simulRembs) ? simulRembs : []);
|
||||
setPfuRates(Array.isArray(pfu) ? pfu : []);
|
||||
} finally { setLoading(false); }
|
||||
} finally { setLoading(false); setDrillRefreshKey(k => k + 1); }
|
||||
};
|
||||
|
||||
useEffect(() => { load(); /* eslint-disable-next-line */ }, [activeId, activeView]);
|
||||
|
||||
/* ── Section "Mois en cours" (DrillCellPanel) — saisie/édition de remboursements ── */
|
||||
const [drillRefreshKey, setDrillRefreshKey] = useState(0);
|
||||
const [rembModalOpen, setRembModalOpen] = useState(false);
|
||||
const [rembEditingId, setRembEditingId] = useState(null);
|
||||
const [rembInv, setRembInv] = useState(null); // investissement lié au remb en cours d'édition
|
||||
const [rembForm, setRembForm] = useState({});
|
||||
const [rembErr, setRembErr] = useState(null);
|
||||
const [rembConfirmingDelete, setRembConfirmingDelete] = useState(false);
|
||||
const [comptesRembInvestisseur, setComptesRembInvestisseur] = useState([]);
|
||||
|
||||
const round2 = (n) => Math.round(n * 100) / 100;
|
||||
|
||||
const getRatesForYear = (dateStr) => {
|
||||
if (!dateStr || !pfuRates.length) return null;
|
||||
const year = parseInt(dateStr.slice(0, 4), 10);
|
||||
return pfuRates.find(r => r.annee === year)
|
||||
?? pfuRates.reduce((best, r) => r.annee > best.annee ? r : best, pfuRates[0]);
|
||||
};
|
||||
|
||||
const loadComptesRemb = (investisseurId) => {
|
||||
if (!investisseurId) { setComptesRembInvestisseur([]); return; }
|
||||
api.get(`/investissements/comptes-par-investisseur/${investisseurId}`)
|
||||
.then(setComptesRembInvestisseur)
|
||||
.catch(() => setComptesRembInvestisseur([]));
|
||||
};
|
||||
|
||||
const setRembField = (k, v) => {
|
||||
setRembForm(prev => {
|
||||
const next = { ...prev, [k]: v };
|
||||
const plat = plats.find(p => p.id === rembInv?.plateforme_id);
|
||||
const hasLocalTax = plat?.fiscalite === 'avec_fiscalite_locale' && plat?.taux_fiscalite_locale;
|
||||
|
||||
if (k === 'interets_bruts_avant_local') {
|
||||
const taux = hasLocalTax ? (plat.taux_fiscalite_locale || 0) : 0;
|
||||
next.taxe_locale = round2(Number(v) * taux / 100);
|
||||
next.interets_bruts = round2(Number(v) - next.taxe_locale);
|
||||
}
|
||||
if (k === 'taxe_locale') {
|
||||
next.interets_bruts = round2(Number(next.interets_bruts_avant_local) - Number(v));
|
||||
}
|
||||
if (k === 'date_remb' || k === 'interets_bruts' || k === 'interets_bruts_avant_local' || k === 'taxe_locale') {
|
||||
const dateStr = k === 'date_remb' ? v : next.date_remb;
|
||||
const bruts = Number(next.interets_bruts) || 0;
|
||||
const rates = getRatesForYear(dateStr);
|
||||
if (rates) {
|
||||
next.prelev_sociaux = round2(bruts * rates.prelev_sociaux / 100);
|
||||
next.prelev_forfaitaire = round2(bruts * rates.impot_revenu / 100);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const openEditRecu = (r) => {
|
||||
const inv = allRows.find(i => i.id === r.investissement_id);
|
||||
setRembInv(inv || null);
|
||||
setRembEditingId(r.id);
|
||||
setRembForm({
|
||||
date_remb: r.date_remb,
|
||||
capital: r.capital ?? 0,
|
||||
cashback: r.cashback ?? 0,
|
||||
interets_bruts_avant_local: r.interets_bruts_avant_local ?? 0,
|
||||
taxe_locale: r.taxe_locale ?? 0,
|
||||
interets_bruts: r.interets_bruts ?? 0,
|
||||
prelev_sociaux: r.prelev_sociaux ?? 0,
|
||||
prelev_forfaitaire: r.prelev_forfaitaire ?? 0,
|
||||
statut: r.statut,
|
||||
notes: r.notes || '',
|
||||
methode_remboursement: r.methode_remboursement || 'portefeuille',
|
||||
compte_id: r.compte_id || '',
|
||||
});
|
||||
setRembErr(null);
|
||||
setRembConfirmingDelete(false);
|
||||
setRembModalOpen(true);
|
||||
loadComptesRemb(inv?.investisseur_id);
|
||||
};
|
||||
|
||||
const openFromProjet = (p) => {
|
||||
const inv = allRows.find(i => i.id === p.investissement_id);
|
||||
const plat = inv ? plats.find(pl => pl.id === inv.plateforme_id) : null;
|
||||
const bruts = p.interets_prevus || 0;
|
||||
const rates = getRatesForYear(p.date_prevue);
|
||||
const methode = plat && plat.methode_remboursement !== 'choix_investisseur'
|
||||
? plat.methode_remboursement
|
||||
: (inv?.methode_remboursement || 'portefeuille');
|
||||
const hasLocalTax = plat?.fiscalite === 'avec_fiscalite_locale' && plat?.taux_fiscalite_locale;
|
||||
const taxe_locale = hasLocalTax ? round2(bruts * plat.taux_fiscalite_locale / 100) : 0;
|
||||
const brutsApresLocal = hasLocalTax ? round2(bruts - taxe_locale) : bruts;
|
||||
setRembInv(inv || null);
|
||||
setRembEditingId(null);
|
||||
setRembForm({
|
||||
date_remb: p.date_prevue,
|
||||
capital: p.capital_prevu || 0,
|
||||
cashback: 0,
|
||||
interets_bruts_avant_local: hasLocalTax ? bruts : 0,
|
||||
taxe_locale,
|
||||
interets_bruts: brutsApresLocal,
|
||||
prelev_sociaux: rates ? round2(brutsApresLocal * rates.prelev_sociaux / 100) : 0,
|
||||
prelev_forfaitaire: rates ? round2(brutsApresLocal * rates.impot_revenu / 100) : 0,
|
||||
statut: 'paye',
|
||||
notes: '',
|
||||
methode_remboursement: methode,
|
||||
compte_id: methode === 'compte_courant' ? (inv?.compte_id || '') : '',
|
||||
});
|
||||
setRembErr(null);
|
||||
setRembConfirmingDelete(false);
|
||||
setRembModalOpen(true);
|
||||
loadComptesRemb(inv?.investisseur_id);
|
||||
};
|
||||
|
||||
const closeRembModal = () => {
|
||||
setRembModalOpen(false);
|
||||
setRembEditingId(null);
|
||||
setRembInv(null);
|
||||
setRembForm({});
|
||||
setRembErr(null);
|
||||
setRembConfirmingDelete(false);
|
||||
};
|
||||
|
||||
const submitRemb = async (e) => {
|
||||
e?.preventDefault?.();
|
||||
setRembErr(null);
|
||||
try {
|
||||
const payload = {
|
||||
type: 'normal',
|
||||
investissement_id: rembInv?.id,
|
||||
date_remb: rembForm.date_remb,
|
||||
capital: Number(rembForm.capital || 0),
|
||||
cashback: Number(rembForm.cashback || 0),
|
||||
interets_bruts_avant_local: Number(rembForm.interets_bruts_avant_local || 0),
|
||||
taxe_locale: Number(rembForm.taxe_locale || 0),
|
||||
interets_bruts: Number(rembForm.interets_bruts || 0),
|
||||
prelev_sociaux: Number(rembForm.prelev_sociaux || 0),
|
||||
prelev_forfaitaire: Number(rembForm.prelev_forfaitaire || 0),
|
||||
statut: rembForm.statut,
|
||||
notes: rembForm.notes || undefined,
|
||||
methode_remboursement: rembForm.methode_remboursement || 'portefeuille',
|
||||
compte_id: rembForm.methode_remboursement === 'compte_courant' && rembForm.compte_id ? Number(rembForm.compte_id) : null,
|
||||
};
|
||||
if (rembEditingId) await api.put(`/remboursements/${rembEditingId}`, payload);
|
||||
else await api.post('/remboursements', payload);
|
||||
closeRembModal();
|
||||
await load();
|
||||
} catch (e) { setRembErr(e.message); }
|
||||
};
|
||||
|
||||
const onDeleteRemb = async () => {
|
||||
try {
|
||||
await api.del(`/remboursements/${rembEditingId}`);
|
||||
closeRembModal();
|
||||
await load();
|
||||
} catch (e) { setRembErr(e.message); }
|
||||
};
|
||||
|
||||
/* ── Icônes bibliothèque ── */
|
||||
useEffect(() => {
|
||||
api.get('/icons').then(rows => {
|
||||
@@ -449,6 +606,22 @@ export default function Plateformes() {
|
||||
return new Set(matching.filter(p => String(p.investisseur_id) === String(selectedDetenteurId)).map(p => p.id));
|
||||
}, [plats, selectedPlatName, selectedDetenteurId]);
|
||||
|
||||
/* ── Cellule "mois en cours" pour la section DrillCellPanel ── */
|
||||
const MOIS_LONG_CELL = ['Janvier','Février','Mars','Avril','Mai','Juin','Juillet','Août','Septembre','Octobre','Novembre','Décembre'];
|
||||
const moisEnCoursCell = useMemo(() => {
|
||||
const ids = [...filteredPlatIds];
|
||||
if (!selectedPlatName || ids.length === 0) return null;
|
||||
const now = new Date();
|
||||
return {
|
||||
platId: ids.length === 1 ? ids[0] : null,
|
||||
platIds: ids,
|
||||
platNom: selectedPlatName,
|
||||
annee: now.getFullYear(),
|
||||
mois: now.getMonth() + 1,
|
||||
moisLabel: MOIS_LONG_CELL[now.getMonth()],
|
||||
};
|
||||
}, [filteredPlatIds, selectedPlatName]);
|
||||
|
||||
/* ── Lignes filtrées (plateforme + détenteur) ── */
|
||||
const filteredRows = useMemo(() => {
|
||||
if (selectedDetenteurId === 'all' || !detenteurOptions) return platRows;
|
||||
@@ -831,6 +1004,8 @@ export default function Plateformes() {
|
||||
<div className="dr-tabs">
|
||||
<button className={`dr-tab${activeTab === 'remboursements' ? ' active' : ''}`}
|
||||
onClick={() => setActiveTab('remboursements')}>Remboursements</button>
|
||||
<button className={`dr-tab${activeTab === 'mois-en-cours' ? ' active' : ''}`}
|
||||
onClick={() => setActiveTab('mois-en-cours')}>Mois en cours</button>
|
||||
<button className={`dr-tab${activeTab === 'depots-retraits' ? ' active' : ''}`}
|
||||
onClick={() => setActiveTab('depots-retraits')}>Dépôts / Retraits</button>
|
||||
<button className={`dr-tab${activeTab === 'capital-investi' ? ' active' : ''}`}
|
||||
@@ -1253,6 +1428,43 @@ export default function Plateformes() {
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* ====== ONGLET MOIS EN COURS ====== */}
|
||||
{activeTab === 'mois-en-cours' && (
|
||||
<div style={{ padding: '0 24px' }}>
|
||||
{moisEnCoursCell ? (
|
||||
<div className="solde-chart-wrap" style={{ padding: '20px 24px 24px' }}>
|
||||
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'baseline', gap: 8 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '.06em', color: 'var(--text-muted)' }}>
|
||||
Mois en cours
|
||||
</span>
|
||||
<span style={{ fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
||||
{moisEnCoursCell.moisLabel} {moisEnCoursCell.annee}
|
||||
</span>
|
||||
</div>
|
||||
<InteretsChartProvider netMode={netMode} pfuRates={pfuRates} activeView={activeView} activeId={activeId}>
|
||||
<DrillCellPanel
|
||||
cell={moisEnCoursCell}
|
||||
alwaysOpen
|
||||
pfuRates={pfuRates}
|
||||
activeView={activeView}
|
||||
activeId={activeId}
|
||||
onEditRecu={openEditRecu}
|
||||
onEditProjet={openFromProjet}
|
||||
refreshKey={drillRefreshKey}
|
||||
investissements={platRows}
|
||||
plateformes={plats.filter(p => filteredPlatIds.has(p.id))}
|
||||
onBulkDone={load}
|
||||
/>
|
||||
</InteretsChartProvider>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: '24px 0', textAlign: 'center', color: 'var(--text-muted)', fontSize: 'var(--fs-sm)' }}>
|
||||
Aucune plateforme sélectionnée.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ====== ONGLET DÉPÔTS / RETRAITS ====== */}
|
||||
{activeTab === 'depots-retraits' && (() => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
@@ -1678,6 +1890,26 @@ export default function Plateformes() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Modal saisie/édition remboursement (section "Mois en cours") ── */}
|
||||
<RembFormModal
|
||||
open={rembModalOpen}
|
||||
editingId={rembEditingId}
|
||||
onClose={closeRembModal}
|
||||
onSubmit={submitRemb}
|
||||
onDelete={onDeleteRemb}
|
||||
confirmingDelete={rembConfirmingDelete}
|
||||
setConfirmingDelete={setRembConfirmingDelete}
|
||||
err={rembErr}
|
||||
form={rembForm}
|
||||
onFieldChange={setRembField}
|
||||
onFormPatch={patch => setRembForm(f => ({ ...f, ...patch }))}
|
||||
pfuRates={pfuRates}
|
||||
comptesInvestisseur={comptesRembInvestisseur}
|
||||
investissements={rembInv ? [rembInv] : []}
|
||||
plateformes={plats}
|
||||
locked
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -7,12 +7,12 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { api } from '../api.js';
|
||||
import { useInvestisseur } from '../context/InvestisseurContext.jsx';
|
||||
import { useUi } from '../context/UiContext.jsx';
|
||||
import Modal from '../components/Modal.jsx';
|
||||
import ConfirmModal from '../components/ConfirmModal.jsx';
|
||||
import InteretsChart from '../components/InteretsChart.jsx';
|
||||
import InteretsDistributionChart from '../components/InteretsDistributionChart.jsx';
|
||||
import TableauInteretsPlateforme from '../components/TableauInteretsPlateforme.jsx';
|
||||
import DrillCellPanel from '../components/DrillCellPanel.jsx';
|
||||
import RembFormModal from '../components/RembFormModal.jsx';
|
||||
import { InteretsChartProvider } from '../context/InteretsChartContext.jsx';
|
||||
import { fmtEUR, fmtDate, today, memberLabel } from '../utils/format.js';
|
||||
import * as XLSX from 'xlsx';
|
||||
@@ -195,6 +195,18 @@ export default function Remboursements() {
|
||||
const drillPanelRef = useRef(null);
|
||||
const tipTableRef = useRef(null);
|
||||
|
||||
/* ── Cellule "mois en cours" (onglet dédié, toutes plateformes) ── */
|
||||
const moisEnCoursCell = useMemo(() => {
|
||||
const now = new Date();
|
||||
return {
|
||||
platId: null,
|
||||
platNom: null,
|
||||
annee: now.getFullYear(),
|
||||
mois: now.getMonth() + 1,
|
||||
moisLabel: MOIS_FR[now.getMonth()],
|
||||
};
|
||||
}, []);
|
||||
|
||||
/* ── Navigation retour Dashboard ── */
|
||||
const autoOpenRef = useRef(false); // garantit l'ouverture auto une seule fois
|
||||
const backToDashboardRef = useRef(null); // URL de retour quand from=dashboard
|
||||
@@ -837,6 +849,8 @@ export default function Remboursements() {
|
||||
onClick={() => setActiveTab('plateformes')}>Plateformes</button>
|
||||
<button className={`dr-tab${activeTab === 'vision-mensuelle' ? ' active' : ''}`}
|
||||
onClick={() => setActiveTab('vision-mensuelle')}>Vision mensuelle</button>
|
||||
<button className={`dr-tab${activeTab === 'mois-en-cours' ? ' active' : ''}`}
|
||||
onClick={() => setActiveTab('mois-en-cours')}>Mois en cours</button>
|
||||
<button className={`dr-tab${activeTab === 'remboursements' ? ' active' : ''}`}
|
||||
onClick={() => setActiveTab('remboursements')}>Remboursements</button>
|
||||
<button className={`dr-tab${activeTab === 'projections' ? ' active' : ''}`}
|
||||
@@ -1025,6 +1039,27 @@ export default function Remboursements() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ══ ONGLET MOIS EN COURS ══ */}
|
||||
{activeTab === 'mois-en-cours' && (
|
||||
<div style={{ padding: '0 24px' }}>
|
||||
<InteretsChartProvider netMode={netMode} pfuRates={pfuRates} activeView={activeView} activeId={activeId}>
|
||||
<DrillCellPanel
|
||||
cell={moisEnCoursCell}
|
||||
alwaysOpen
|
||||
pfuRates={pfuRates}
|
||||
activeView={activeView}
|
||||
activeId={activeId}
|
||||
onEditRecu={openEdit}
|
||||
onEditProjet={openFromSimul}
|
||||
refreshKey={drillRefreshKey}
|
||||
investissements={investissements}
|
||||
plateformes={plateformes}
|
||||
onBulkDone={load}
|
||||
/>
|
||||
</InteretsChartProvider>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ══ ONGLET REMBOURSEMENTS ══ */}
|
||||
{activeTab === 'remboursements' && (
|
||||
<div style={{ padding: '0 24px' }}>
|
||||
@@ -1409,12 +1444,6 @@ export default function Remboursements() {
|
||||
|
||||
{/* ══ MODAL ══ */}
|
||||
{(() => {
|
||||
const pfuYear = getRatesForYear(form.date_remb);
|
||||
const fmtRate = (n) => n != null ? n.toFixed(1).replace('.', ',') + ' %' : '…';
|
||||
const labelPS = `Prélèvements sociaux${pfuYear ? ` (${fmtRate(pfuYear.prelev_sociaux)})` : ''} (€)`;
|
||||
const labelIR = `Impôt sur le revenu${pfuYear ? ` (${fmtRate(pfuYear.impot_revenu)})` : ''} (€)`;
|
||||
const interetsNets = computeInteretsNets(form);
|
||||
|
||||
// Liste d'investissements disponibles dans le select
|
||||
const invListForSelect = (editingId
|
||||
? investissements.filter(i => i.statut !== 'rembourse' || i.id === Number(form.investissement_id))
|
||||
@@ -1426,212 +1455,26 @@ export default function Remboursements() {
|
||||
return (a.nom_projet || '').localeCompare(b.nom_projet || '', 'fr');
|
||||
});
|
||||
|
||||
// Méthode de remboursement — plateforme de l'investissement sélectionné
|
||||
const currentInv = !isBonus ? investissements.find(i => i.id === Number(form.investissement_id)) : null;
|
||||
const currentPlat = currentInv ? plateformes.find(p => p.id === currentInv.plateforme_id) : null;
|
||||
const isChoixOuvert = currentPlat?.methode_remboursement === 'choix_investisseur';
|
||||
const hasLocalTax = !isBonus && currentPlat?.fiscalite === 'avec_fiscalite_locale' && currentPlat?.taux_fiscalite_locale;
|
||||
const isExonere = !isBonus && currentInv?.fiscalite_override === 'exonere';
|
||||
const isIndicatif = isExonere || (!isBonus && !!currentPlat && currentPlat?.fiscalite !== 'flat_tax');
|
||||
const netRecu = isIndicatif
|
||||
? round2(Number(form.capital||0) + Number(form.cashback||0) + Number(form.interets_bruts||0))
|
||||
: computeNet(form);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<RembFormModal
|
||||
open={modalOpen}
|
||||
title={editingId ? 'Modifier le remboursement' : 'Nouveau remboursement'}
|
||||
editingId={editingId}
|
||||
onClose={close}
|
||||
width={900}
|
||||
footer={
|
||||
<div style={{ display: 'flex', alignItems: 'center', width: '100%', gap: 8 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
{editingId && (
|
||||
!confirmingDelete ? (
|
||||
<button className="danger" onClick={() => setConfirmingDelete(true)}>
|
||||
Supprimer
|
||||
</button>
|
||||
) : (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontSize: 13, color: 'var(--danger, #ef4444)', whiteSpace: 'nowrap' }}>
|
||||
Supprimer définitivement ?
|
||||
</span>
|
||||
<button onClick={() => setConfirmingDelete(false)}>Non</button>
|
||||
<button className="danger" onClick={onDelete}>Oui, supprimer</button>
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<button onClick={close}>Annuler</button>
|
||||
<button className="primary" onClick={submit}>{editingId ? 'Enregistrer' : 'Créer'}</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<form onSubmit={submit}>
|
||||
{err && <div className="error">{err}</div>}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px 16px' }}>
|
||||
{/* ── Sélecteur d'investissement ── */}
|
||||
<div style={{ gridColumn: isBonus ? 'span 1' : 'span 3' }}>
|
||||
<label>Investissement *</label>
|
||||
<select required value={form.investissement_id}
|
||||
onChange={e => setField('investissement_id', e.target.value)}>
|
||||
<option value="">—</option>
|
||||
<option value="BONUS_PARRAINAGE">🎁 Bonus Parrainage</option>
|
||||
<option value="BONUS_PLATEFORME">🏆 Bonus Plateforme</option>
|
||||
<optgroup label="──────────────────" />
|
||||
{invListForSelect.map(i =>
|
||||
<option key={i.id} value={i.id}>{i.plateforme_nom} — {i.nom_projet} ({fmtEUR(i.montant_investi)})</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
{isBonus && (
|
||||
<div style={{ gridColumn: 'span 2' }}>
|
||||
<label>Plateforme associée (bonus)</label>
|
||||
<select value={form.bonus_plateforme_id} onChange={e => {
|
||||
const plat = plateformes.find(p => String(p.id) === e.target.value);
|
||||
setForm({ ...form, bonus_plateforme_id: e.target.value, bonus_investisseur_id: plat?.investisseur_id ? String(plat.investisseur_id) : '' });
|
||||
}}>
|
||||
<option value="">— Aucune —</option>
|
||||
{plateformes.map(p => <option key={p.id} value={p.id}>{p.nom}{multiDetenteur && p.investisseur_nom ? ` — ${p.investisseur_nom}` : ''}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Champs remboursement normal ── */}
|
||||
{!isBonus && (<>
|
||||
<div style={{ gridColumn: 'span 3', borderTop: '1px solid var(--border)', paddingTop: 4, marginTop: 4 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-muted)' }}>Remboursement</span>
|
||||
</div>
|
||||
{hasLocalTax && (
|
||||
<div style={{ gridColumn: 'span 2' }}>
|
||||
<label>Intérêts bruts avant retenue locale ({currentPlat.taux_fiscalite_locale} %) (€)</label>
|
||||
<input type="number" step="0.01" value={form.interets_bruts_avant_local}
|
||||
onChange={e => setField('interets_bruts_avant_local', e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
{hasLocalTax && (
|
||||
<div>
|
||||
<label>Retenue à la source locale (€)</label>
|
||||
<input type="number" step="0.01" value={form.taxe_locale}
|
||||
onChange={e => setField('taxe_locale', e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label>Capital (€)</label>
|
||||
<input type="number" step="0.01" value={form.capital}
|
||||
onChange={e => setField('capital', e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label>{hasLocalTax ? 'Intérêts bruts après retenue locale (€)' : 'Intérêts bruts (€)'}</label>
|
||||
<input type="number" step="0.01" value={form.interets_bruts}
|
||||
readOnly={!!hasLocalTax}
|
||||
style={hasLocalTax ? { background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' } : undefined}
|
||||
onChange={e => !hasLocalTax && setField('interets_bruts', e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Cashback (€)</label>
|
||||
<input type="number" step="0.01" value={form.cashback}
|
||||
onChange={e => setField('cashback', e.target.value)} />
|
||||
</div>
|
||||
<div style={{ gridColumn: 'span 3', borderTop: '1px solid var(--border)', paddingTop: 4, marginTop: 4 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-muted)' }}>Imposition{isIndicatif ? ' — indicatif' : ''}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>{labelPS}</label>
|
||||
<input type="number" step="0.01" value={form.prelev_sociaux}
|
||||
readOnly={isIndicatif}
|
||||
style={isIndicatif ? { background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' } : undefined}
|
||||
onChange={e => !isIndicatif && setForm({ ...form, prelev_sociaux: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<label>{labelIR}</label>
|
||||
<input type="number" step="0.01" value={form.prelev_forfaitaire}
|
||||
readOnly={isIndicatif}
|
||||
style={isIndicatif ? { background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' } : undefined}
|
||||
onChange={e => !isIndicatif && setForm({ ...form, prelev_forfaitaire: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Total prélèvements (€)</label>
|
||||
<input type="number" step="0.01"
|
||||
value={round2((Number(form.prelev_sociaux) || 0) + (Number(form.prelev_forfaitaire) || 0))}
|
||||
readOnly
|
||||
style={{ background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' }} />
|
||||
</div>
|
||||
<div style={{ gridColumn: 'span 3', display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Montant des intérêts après imposition</span>
|
||||
<span style={{ fontWeight: 600, color: 'var(--text)' }}>{fmtEUR(interetsNets)}</span>
|
||||
</div>
|
||||
<div style={{ gridColumn: 'span 3', borderTop: '1px solid var(--border)', paddingTop: 4, marginTop: 4 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-muted)' }}>Versement</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>Date *</label>
|
||||
<input type="date" required value={form.date_remb}
|
||||
onChange={e => setField('date_remb', e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Méthode de remboursement</label>
|
||||
<select value={form.methode_remboursement || 'portefeuille'}
|
||||
onChange={e => {
|
||||
const m = e.target.value;
|
||||
const def = m === 'compte_courant' ? (comptesInvestisseur.find(c => c.type === 'compte_courant') ?? comptesInvestisseur[0]) : null;
|
||||
setForm(f => ({ ...f, methode_remboursement: m, compte_id: def ? String(def.id) : '' }));
|
||||
}}>
|
||||
<option value="portefeuille">Porte-monnaie de la plateforme</option>
|
||||
<option value="compte_courant">Compte courant de l'investisseur</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
{form.methode_remboursement === 'compte_courant' && (
|
||||
<>
|
||||
<label>Compte de réception</label>
|
||||
{comptesInvestisseur.length > 0 ? (
|
||||
<select value={form.compte_id || ''}
|
||||
onChange={e => setForm(f => ({ ...f, compte_id: e.target.value }))}>
|
||||
<option value="">— Choisir un compte —</option>
|
||||
{comptesInvestisseur.map(c => (
|
||||
<option key={c.id} value={c.id}>{c.nom}{c.banque ? ` — ${c.banque}` : ''}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<p className="text-muted" style={{ fontSize: 'var(--fs-sm)', margin: '4px 0' }}>
|
||||
Aucun compte défini. <a href="/settings?section=comptes" target="_blank" rel="noreferrer">Créer un compte →</a>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<label>Montant versé (€)</label>
|
||||
<input type="number" step="0.01" value={netRecu} readOnly
|
||||
style={{ background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' }} />
|
||||
</div>
|
||||
<div style={{ gridColumn: 'span 3', display: 'flex', alignItems: 'center', gap: 8, fontSize: 13 }}>
|
||||
<span style={{ color: 'var(--text-muted)' }}>
|
||||
{isIndicatif
|
||||
? "L'imposition n'est pas déduite du montant versé (fiscalité appliquée à titre indicatif)."
|
||||
: "L'imposition est déduite du montant versé (capital + cashback + intérêts nets)."}
|
||||
</span>
|
||||
</div>
|
||||
</>)}
|
||||
|
||||
{/* ── Bonus ── */}
|
||||
{isBonus && (
|
||||
<div>
|
||||
<label>Date *</label>
|
||||
<input type="date" required value={form.date_remb}
|
||||
onChange={e => setField('date_remb', e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
{isBonus && (
|
||||
<div style={{ gridColumn: 'span 2' }}>
|
||||
<label>Montant (€)</label>
|
||||
<input type="number" step="0.01" value={form.cashback}
|
||||
onChange={e => setField('cashback', e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
onSubmit={submit}
|
||||
onDelete={onDelete}
|
||||
confirmingDelete={confirmingDelete}
|
||||
setConfirmingDelete={setConfirmingDelete}
|
||||
err={err}
|
||||
form={form}
|
||||
onFieldChange={setField}
|
||||
onFormPatch={patch => setForm(f => ({ ...f, ...patch }))}
|
||||
pfuRates={pfuRates}
|
||||
comptesInvestisseur={comptesInvestisseur}
|
||||
investissements={investissements}
|
||||
plateformes={plateformes}
|
||||
investissementOptions={invListForSelect}
|
||||
multiDetenteur={multiDetenteur}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
{openMenu && (
|
||||
|
||||
Reference in new issue
Block a user