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
@@ -14,6 +14,19 @@ function hexToRgba(hex, a) {
|
||||
|
||||
const round2 = v => Math.round(v * 100) / 100;
|
||||
|
||||
/* Petite bulle de couleur — indicateur vert/orange/gris à côté du nom du projet */
|
||||
function Bubble({ color, title }) {
|
||||
return (
|
||||
<span
|
||||
title={title}
|
||||
style={{
|
||||
display: 'inline-block', width: 8, height: 8, borderRadius: '50%',
|
||||
background: color, marginLeft: 7, flex: '0 0 auto',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* DrillCellPanel
|
||||
*
|
||||
@@ -45,6 +58,7 @@ export default function DrillCellPanel({
|
||||
} = useInteretsChart();
|
||||
|
||||
const [data, setData] = useState(null);
|
||||
const [prevData, setPrevData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
/* filterPlatId : '' = toutes, sinon l'id (string) de la plateforme sélectionnée dans le dropdown */
|
||||
const [filterPlatId, setFilterPlatId] = useState('');
|
||||
@@ -57,6 +71,7 @@ export default function DrillCellPanel({
|
||||
const [bulkProcessing, setBulkProcessing] = useState(false);
|
||||
const [bulkProgress, setBulkProgress] = useState(0);
|
||||
const [bulkDone, setBulkDone] = useState(false);
|
||||
const [bulkErrors, setBulkErrors] = useState([]);
|
||||
|
||||
/* ── Reset filtres quand la cellule change ── */
|
||||
useEffect(() => {
|
||||
@@ -94,6 +109,32 @@ export default function DrillCellPanel({
|
||||
.finally(() => setLoading(false));
|
||||
}, [cell?.annee, cell?.mois, filterPlatId, activeView, activeId, refreshKey]);
|
||||
|
||||
/* ── Fetch du mois précédent (pour l'indicateur vert/orange/gris) ── */
|
||||
useEffect(() => {
|
||||
if (!cell) { setPrevData(null); return; }
|
||||
|
||||
const isInitialFilter = cell.platId ? filterPlatId === String(cell.platId) : !filterPlatId;
|
||||
const consolidatedIds = isInitialFilter && cell.platIds?.length > 1
|
||||
? cell.platIds.join(',')
|
||||
: null;
|
||||
|
||||
let prevMois = cell.mois - 1;
|
||||
let prevAnnee = cell.annee;
|
||||
if (prevMois < 1) { prevMois = 12; prevAnnee -= 1; }
|
||||
|
||||
const params = {
|
||||
annee: prevAnnee,
|
||||
mois: prevMois,
|
||||
...(consolidatedIds
|
||||
? { plateforme_ids: consolidatedIds }
|
||||
: filterPlatId ? { plateforme_id: filterPlatId } : {}),
|
||||
...(activeView === 'all' ? { scope: 'all' } : {}),
|
||||
};
|
||||
api.get('/dashboard/detail-cellule', params)
|
||||
.then(d => setPrevData(d))
|
||||
.catch(() => setPrevData({ recus: [], projetes: [] }));
|
||||
}, [cell?.annee, cell?.mois, filterPlatId, activeView, activeId, refreshKey]);
|
||||
|
||||
/* ── Taux PFU pour l'année de la cellule ── */
|
||||
const pfuReduction = useMemo(() => {
|
||||
if (!pfuRates?.length || !cell) return 0;
|
||||
@@ -113,6 +154,22 @@ export default function DrillCellPanel({
|
||||
return [...data.projetes].sort((a, b) => (a.date_prevue || '').localeCompare(b.date_prevue || ''));
|
||||
}, [data]);
|
||||
|
||||
/* ── Agrégation des reçus du mois précédent, par investissement ──
|
||||
Sert de référence pour l'indicateur vert/orange/gris à côté du nom du projet. */
|
||||
const prevRecusByInv = useMemo(() => {
|
||||
const map = new Map();
|
||||
if (!prevData) return map;
|
||||
for (const r of prevData.recus) {
|
||||
const acc = map.get(r.investissement_id) || { interets_bruts: 0, interets_nets: 0, capital: 0, cashback: 0 };
|
||||
acc.interets_bruts += r.interets_bruts || 0;
|
||||
acc.interets_nets += r.interets_nets || 0;
|
||||
acc.capital += r.capital || 0;
|
||||
acc.cashback += r.cashback || 0;
|
||||
map.set(r.investissement_id, acc);
|
||||
}
|
||||
return map;
|
||||
}, [prevData]);
|
||||
|
||||
if (!cell) return null;
|
||||
|
||||
/* ── Afficher la colonne Plateforme quand on est en mode "toutes" ── */
|
||||
@@ -136,6 +193,36 @@ export default function DrillCellPanel({
|
||||
return v;
|
||||
};
|
||||
|
||||
/* ── Valeur du mois précédent pour un investissement (mêmes critères actifs) ──
|
||||
undefined = aucun remboursement reçu ce mois-là pour cet investissement */
|
||||
const prevRecuValue = (investissementId) => {
|
||||
const acc = prevRecusByInv.get(investissementId);
|
||||
if (!acc) return undefined;
|
||||
let v = 0;
|
||||
if (inclureInterets) v += netMode ? acc.interets_nets : acc.interets_bruts;
|
||||
if (inclureCashback) v += acc.cashback;
|
||||
if (inclureCapital) v += acc.capital;
|
||||
return v;
|
||||
};
|
||||
|
||||
/* ── Indicateur visuel (bulle) — reçus : vert si ≥ mois précédent ou si rien n'existait, orange sinon ── */
|
||||
const recuIndicator = (r) => {
|
||||
const prev = prevRecuValue(r.investissement_id);
|
||||
const cur = recuRowValue(r);
|
||||
if (prev === undefined) return { color: 'var(--success)', title: 'Aucun remboursement le mois précédent' };
|
||||
if (round2(cur) >= round2(prev)) return { color: 'var(--success)', title: `≥ mois précédent (${fmtEUR(prev)})` };
|
||||
return { color: 'var(--warning)', title: `< mois précédent (${fmtEUR(prev)})` };
|
||||
};
|
||||
|
||||
/* ── Indicateur visuel (bulle) — projetés : vert si = reçu du mois précédent, gris si rien n'existait, orange sinon ── */
|
||||
const projIndicator = (p) => {
|
||||
const prev = prevRecuValue(p.investissement_id);
|
||||
const cur = projRowValue(p);
|
||||
if (prev === undefined) return { color: 'var(--text-muted)', title: 'Aucun remboursement le mois précédent' };
|
||||
if (round2(cur) === round2(prev)) return { color: 'var(--success)', title: `= mois précédent (${fmtEUR(prev)})` };
|
||||
return { color: 'var(--warning)', title: `≠ mois précédent (${fmtEUR(prev)})` };
|
||||
};
|
||||
|
||||
/* ── Totaux ── */
|
||||
const totalRecus = recus.reduce((s, r) => s + recuRowValue(r), 0);
|
||||
const totalProjetes = projetes.reduce((s, p) => s + projRowValue(p), 0);
|
||||
@@ -195,7 +282,7 @@ export default function DrillCellPanel({
|
||||
statut: 'paye',
|
||||
notes: '',
|
||||
methode_remboursement: methode,
|
||||
compte_id: methode === 'compte_courant' ? (inv?.compte_id || '') : '',
|
||||
compte_id: methode === 'compte_courant' && inv?.compte_id ? inv.compte_id : null,
|
||||
};
|
||||
});
|
||||
setBulkItems(items);
|
||||
@@ -208,12 +295,18 @@ export default function DrillCellPanel({
|
||||
const runBulk = async () => {
|
||||
setBulkProcessing(true);
|
||||
let done = 0;
|
||||
const errors = [];
|
||||
for (const item of bulkItems) {
|
||||
const { _label, _plat, _date, _capital, _interets, _total, ...payload } = item;
|
||||
try { await api.post('/remboursements', payload); } catch (e) { /* continuer */ }
|
||||
try {
|
||||
await api.post('/remboursements', payload);
|
||||
} catch (e) {
|
||||
errors.push({ label: _label, plat: _plat, date: _date, message: e.message });
|
||||
}
|
||||
done++;
|
||||
setBulkProgress(done);
|
||||
}
|
||||
setBulkErrors(errors);
|
||||
setBulkProcessing(false);
|
||||
setBulkDone(true);
|
||||
if (onBulkDone) onBulkDone();
|
||||
@@ -225,6 +318,7 @@ export default function DrillCellPanel({
|
||||
setBulkProgress(0);
|
||||
setBulkDone(false);
|
||||
setBulkProcessing(false);
|
||||
setBulkErrors([]);
|
||||
};
|
||||
|
||||
/* ── Rendu ── */
|
||||
@@ -356,7 +450,12 @@ export default function DrillCellPanel({
|
||||
>
|
||||
<td style={tdStyle}>{fmtDate(r.date_remb)}</td>
|
||||
{showPlatCol && <td style={{ ...tdStyle, fontWeight: 500 }}>{r.plateforme_nom || '—'}</td>}
|
||||
<td style={tdStyle}>{r.nom_projet}</td>
|
||||
<td style={tdStyle}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
|
||||
{r.nom_projet}
|
||||
<Bubble {...recuIndicator(r)} />
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ ...tdStyle, color: 'var(--text-muted)', fontSize: '0.8em' }}>{r.detenteur_nom || '—'}</td>
|
||||
{cols.map(c => {
|
||||
let v;
|
||||
@@ -434,7 +533,12 @@ export default function DrillCellPanel({
|
||||
>
|
||||
<td style={{ ...tdStyle, fontStyle: 'italic', color: 'var(--text-muted)' }}>{fmtDate(p.date_prevue)}</td>
|
||||
{showPlatCol && <td style={{ ...tdStyle, fontStyle: 'italic', fontWeight: 500 }}>{p.plateforme_nom || '—'}</td>}
|
||||
<td style={{ ...tdStyle, fontStyle: 'italic' }}>{p.nom_projet}</td>
|
||||
<td style={{ ...tdStyle, fontStyle: 'italic' }}>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
|
||||
{p.nom_projet}
|
||||
<Bubble {...projIndicator(p)} />
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ ...tdStyle, color: 'var(--text-muted)', fontSize: '0.8em' }}>{p.detenteur_nom || '—'}</td>
|
||||
{cols.map(c => {
|
||||
let v;
|
||||
@@ -529,21 +633,60 @@ export default function DrillCellPanel({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{bulkDone && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10, padding: '12px 0' }}>
|
||||
<div style={{ width: 48, height: 48, borderRadius: '50%', background: 'var(--success)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
{bulkDone && (() => {
|
||||
const successCount = bulkItems.length - bulkErrors.length;
|
||||
const allOk = bulkErrors.length === 0;
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10, padding: '12px 0' }}>
|
||||
<div style={{
|
||||
width: 48, height: 48, borderRadius: '50%',
|
||||
background: allOk ? 'var(--success)' : 'var(--warning)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
{allOk ? (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" y1="8" x2="12" y2="13"/><line x1="12" y1="16.5" x2="12.01" y2="16.5"/>
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<p style={{ margin: 0, fontWeight: 600, fontSize: '1rem' }}>
|
||||
{allOk
|
||||
? `${bulkItems.length} remboursement${bulkItems.length > 1 ? 's' : ''} enregistré${bulkItems.length > 1 ? 's' : ''}`
|
||||
: `${successCount}/${bulkItems.length} remboursement${bulkItems.length > 1 ? 's' : ''} enregistré${successCount > 1 ? 's' : ''} — ${bulkErrors.length} échec${bulkErrors.length > 1 ? 's' : ''}`}
|
||||
</p>
|
||||
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.88rem' }}>
|
||||
{cell.moisLabel} {cell.annee}
|
||||
</p>
|
||||
{!allOk && (
|
||||
<div style={{ width: '100%', marginTop: 8, border: '1px solid var(--danger)', borderRadius: 6, overflow: 'hidden' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.82rem' }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'var(--surface-2)' }}>
|
||||
<th style={{ ...thStyle, color: 'var(--danger)' }}>Projet</th>
|
||||
<th style={{ ...thStyle, color: 'var(--danger)' }}>Date</th>
|
||||
<th style={{ ...thStyle, color: 'var(--danger)' }}>Erreur</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{bulkErrors.map((e, i) => (
|
||||
<tr key={i} style={{ borderTop: '1px solid var(--border)' }}>
|
||||
<td style={tdStyle}>{e.plat ? `${e.plat} — ` : ''}{e.label}</td>
|
||||
<td style={tdStyle}>{fmtDate(e.date)}</td>
|
||||
<td style={{ ...tdStyle, color: 'var(--danger)' }}>{e.message}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p style={{ margin: 0, fontWeight: 600, fontSize: '1rem' }}>
|
||||
{bulkItems.length} remboursement{bulkItems.length > 1 ? 's' : ''} enregistré{bulkItems.length > 1 ? 's' : ''}
|
||||
</p>
|
||||
<p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.88rem' }}>
|
||||
{cell.moisLabel} {cell.annee}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
{!bulkDone && (
|
||||
<>
|
||||
<p style={{ margin: '0 0 12px', color: 'var(--text-muted)', fontSize: '0.88rem' }}>
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
import Modal from './Modal.jsx';
|
||||
import { fmtEUR } from '../utils/format.js';
|
||||
|
||||
/* ── Helpers purs (dupliqués volontairement, pas d'état) ── */
|
||||
const round2 = (n) => Math.round(n * 100) / 100;
|
||||
const fmtRate = (n) => n != null ? n.toFixed(1).replace('.', ',') + ' %' : '…';
|
||||
|
||||
const computeInteretsNets = (f) =>
|
||||
round2(Number(f.interets_bruts || 0) - Number(f.prelev_sociaux || 0) - Number(f.prelev_forfaitaire || 0));
|
||||
|
||||
const computeNet = (f) =>
|
||||
round2(Number(f.capital || 0) + Number(f.cashback || 0) + computeInteretsNets(f));
|
||||
|
||||
const getRatesForYear = (dateStr, pfuRates) => {
|
||||
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 BONUS_VALUES = ['BONUS_PARRAINAGE', 'BONUS_PLATEFORME'];
|
||||
|
||||
/**
|
||||
* RembFormModal — formulaire partagé de saisie/édition d'un remboursement.
|
||||
*
|
||||
* Utilisé par Remboursements.jsx (mode complet, sélecteur d'investissement + bonus),
|
||||
* InvestissementDetail.jsx et Plateformes.jsx (mode `locked` : investissement déjà déterminé
|
||||
* par le contexte, pas de sélecteur ni de bonus).
|
||||
*
|
||||
* Le composant est volontairement "bête" côté état : le parent garde la propriété du form
|
||||
* (state, effets de bord au changement de champ comme le chargement des comptes du détenteur,
|
||||
* héritage de la méthode de remboursement, calcul de la fiscalité locale…). Ce composant se
|
||||
* contente de dériver les valeurs d'affichage (labels PFU, fiscalité indicative, montant versé…)
|
||||
* et de restituer le JSX.
|
||||
*
|
||||
* Props :
|
||||
* open, onClose — état d'ouverture
|
||||
* editingId — id en édition, null si création
|
||||
* title — titre optionnel (sinon déduit de editingId/locked)
|
||||
* form — objet form courant (voir `empty` dans les pages appelantes)
|
||||
* onFieldChange(key, value) — pour les champs à effets de bord (déléqué au setField du parent)
|
||||
* onFormPatch(partialObj) — pour les mises à jour directes sans effet de bord (setForm({...}))
|
||||
* err — message d'erreur à afficher
|
||||
* onSubmit — handler de soumission du formulaire
|
||||
* onDelete — handler de suppression (optionnel, affiche le bouton si fourni)
|
||||
* pfuRates — taux PFU par année
|
||||
* comptesInvestisseur — comptes du détenteur courant (pour le select compte de réception)
|
||||
* investissements — liste des investissements (pour résoudre l'investissement courant
|
||||
* à partir de form.investissement_id). En mode locked, passer [investissement].
|
||||
* plateformes — liste des plateformes (fiscalité locale, méthode héritée, dropdown bonus)
|
||||
* locked — bool : masque le sélecteur d'investissement et les champs bonus
|
||||
* investissementOptions — liste pour le <select> investissement (mode non-locked uniquement)
|
||||
* multiDetenteur — bool, affiche le détenteur dans le dropdown bonus (mode non-locked)
|
||||
* confirmingDelete, setConfirmingDelete — état de confirmation de suppression (géré par le parent,
|
||||
* remis à false dans son propre onClose)
|
||||
*/
|
||||
export default function RembFormModal({
|
||||
open, onClose, editingId, title,
|
||||
form, onFieldChange, onFormPatch,
|
||||
err, onSubmit, onDelete,
|
||||
pfuRates, comptesInvestisseur,
|
||||
investissements = [], plateformes = [],
|
||||
locked = false,
|
||||
investissementOptions = [],
|
||||
multiDetenteur = false,
|
||||
confirmingDelete, setConfirmingDelete,
|
||||
}) {
|
||||
const isBonus = !locked && BONUS_VALUES.includes(form.investissement_id);
|
||||
|
||||
const rates = getRatesForYear(form.date_remb, pfuRates);
|
||||
const labelPS = `Prélèvements sociaux${rates ? ` (${fmtRate(rates.prelev_sociaux)})` : ''} (€)`;
|
||||
const labelIR = `Impôt sur le revenu${rates ? ` (${fmtRate(rates.impot_revenu)})` : ''} (€)`;
|
||||
const interetsNets = computeInteretsNets(form);
|
||||
|
||||
const currentInv = locked
|
||||
? investissements[0]
|
||||
: (!isBonus ? investissements.find(i => i.id === Number(form.investissement_id)) : null);
|
||||
const currentPlat = currentInv ? plateformes.find(p => p.id === currentInv.plateforme_id) : null;
|
||||
const hasLocalTax = !isBonus && currentPlat?.fiscalite === 'avec_fiscalite_locale' && currentPlat?.taux_fiscalite_locale;
|
||||
const isExonere = !isBonus && currentInv?.fiscalite_override === 'exonere';
|
||||
const isIndicatif = Boolean(isExonere || (!isBonus && currentPlat && currentPlat.fiscalite !== 'flat_tax'));
|
||||
const montantVerse = isIndicatif
|
||||
? round2(Number(form.capital || 0) + Number(form.cashback || 0) + Number(form.interets_bruts || 0))
|
||||
: computeNet(form);
|
||||
|
||||
const modalTitle = title ?? (editingId ? 'Modifier le remboursement' : (locked ? 'Saisir le remboursement' : 'Nouveau remboursement'));
|
||||
|
||||
const handleMethodeChange = (m) => {
|
||||
const def = m === 'compte_courant' ? (comptesInvestisseur?.find(c => c.type === 'compte_courant') ?? comptesInvestisseur?.[0]) : null;
|
||||
onFormPatch({ methode_remboursement: m, compte_id: def ? String(def.id) : '' });
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title={modalTitle}
|
||||
onClose={onClose}
|
||||
width={900}
|
||||
footer={
|
||||
<div style={{ display: 'flex', alignItems: 'center', width: '100%', gap: 8 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
{editingId && onDelete && (
|
||||
!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={onClose}>Annuler</button>
|
||||
<button className="primary" onClick={onSubmit}>{editingId ? 'Enregistrer' : 'Créer'}</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<form onSubmit={onSubmit}>
|
||||
{err && <div className="error">{err}</div>}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px 16px' }}>
|
||||
|
||||
{/* ── Sélecteur d'investissement (mode non verrouillé uniquement) ── */}
|
||||
{!locked && (
|
||||
<div style={{ gridColumn: isBonus ? 'span 1' : 'span 3' }}>
|
||||
<label>Investissement *</label>
|
||||
<select required value={form.investissement_id}
|
||||
onChange={e => onFieldChange('investissement_id', e.target.value)}>
|
||||
<option value="">—</option>
|
||||
<option value="BONUS_PARRAINAGE">🎁 Bonus Parrainage</option>
|
||||
<option value="BONUS_PLATEFORME">🏆 Bonus Plateforme</option>
|
||||
<optgroup label="──────────────────" />
|
||||
{investissementOptions.map(i =>
|
||||
<option key={i.id} value={i.id}>{i.plateforme_nom} — {i.nom_projet} ({fmtEUR(i.montant_investi)})</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
{!locked && 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);
|
||||
onFormPatch({ 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 ?? 0}
|
||||
onChange={e => onFieldChange('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 ?? 0}
|
||||
onChange={e => onFieldChange('taxe_locale', e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label>Capital (€)</label>
|
||||
<input type="number" step="0.01" value={form.capital ?? 0}
|
||||
onChange={e => onFieldChange('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 ?? 0}
|
||||
readOnly={!!hasLocalTax}
|
||||
style={hasLocalTax ? { background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' } : undefined}
|
||||
onChange={e => !hasLocalTax && onFieldChange('interets_bruts', e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Cashback (€)</label>
|
||||
<input type="number" step="0.01" value={form.cashback ?? 0}
|
||||
onChange={e => onFieldChange('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 ?? 0}
|
||||
readOnly={isIndicatif}
|
||||
style={isIndicatif ? { background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' } : undefined}
|
||||
onChange={e => !isIndicatif && onFormPatch({ prelev_sociaux: Number(e.target.value) })} />
|
||||
</div>
|
||||
<div>
|
||||
<label>{labelIR}</label>
|
||||
<input type="number" step="0.01" value={form.prelev_forfaitaire ?? 0}
|
||||
readOnly={isIndicatif}
|
||||
style={isIndicatif ? { background: 'var(--surface-2)', color: 'var(--text-muted)', cursor: 'not-allowed' } : undefined}
|
||||
onChange={e => !isIndicatif && onFormPatch({ 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 => onFieldChange('date_remb', e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<label>Méthode de remboursement</label>
|
||||
<select value={form.methode_remboursement || 'portefeuille'}
|
||||
onChange={e => handleMethodeChange(e.target.value)}>
|
||||
<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 => onFormPatch({ 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={montantVerse} 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 (mode non verrouillé uniquement) ── */}
|
||||
{!locked && isBonus && (
|
||||
<div>
|
||||
<label>Date *</label>
|
||||
<input type="date" required value={form.date_remb || ''}
|
||||
onChange={e => onFieldChange('date_remb', e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
{!locked && isBonus && (
|
||||
<div style={{ gridColumn: 'span 2' }}>
|
||||
<label>Montant (€)</label>
|
||||
<input type="number" step="0.01" value={form.cashback ?? 0}
|
||||
onChange={e => onFieldChange('cashback', e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user