import Modal from './Modal.jsx'; import InvestissementSelect from './InvestissementSelect.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('.', ',') + ' %' : '…'; // Le frais ne réduit interets_nets que lorsqu'il est retenu SUR ce remboursement (mode // 'remboursement', par défaut) — un frais réglé à part ('portefeuille'/'compte_courant', // Étape 5 bis, 01/09/26 — cas Baltis) n'a aucun impact ici, cf. computeChamps() côté backend. const fraisReduitBase = (f) => !f.frais_mode_reglement || f.frais_mode_reglement === 'remboursement'; const computeInteretsNets = (f) => round2(Number(f.interets_bruts || 0) - (fraisReduitBase(f) ? Number(f.frais_ttc || 0) : 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']; const INTERETS_PLATEFORME_VALUE = 'INTERETS_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 { 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) : '' }); }}> {plateformes.map(p => )} )} {!locked && isPlateformeMode && dealOptions.length > 0 && (
)} {!locked && isBonus && (
onFieldChange('date_remb', e.target.value)} />
)} {/* ── Champs remboursement normal / intérêts plateforme ── */} {!isBonus && (<>
Remboursement
{hasLocalTax && (
onFieldChange('interets_bruts_avant_local', e.target.value)} />
)} {hasLocalTax && (
onFieldChange('taxe_locale', e.target.value)} />
)} {!isInteretsPlateforme && (
onFieldChange('capital', e.target.value)} />
)}
!hasLocalTax && onFieldChange('interets_bruts', e.target.value)} />
{!isInteretsPlateforme && (
onFieldChange('cashback', e.target.value)} />
)} {showFraisSection && (<>
Frais
onFieldChange('frais_ttc', e.target.value)} />
{!isInteretsPlateforme ? (<>
{form.frais_mode_reglement === 'compte_courant' && (
{comptesInvestisseur?.length > 0 ? ( ) : (

Aucun compte défini. Créer un compte →

)}
)}
{form.frais_mode_reglement === 'compte_courant' || form.frais_mode_reglement === 'portefeuille' ? "Frais réglé à part (comme pour la souscription) — aucun impact sur l'imposition ni le montant versé de ce remboursement ; compté séparément dans le Frais payés et le XIRR net." : "Frais d'entrée/de gestion prélevés par la plateforme sur cette distribution (ex. avis d'opéré) — vient réduire la base imposable des intérêts bruts avant calcul des prélèvements."}
) : (
Frais d'entrée/de gestion prélevés par la plateforme sur cette distribution (ex. avis d'opéré) — vient réduire la base imposable des intérêts bruts avant calcul des prélèvements.
)} )}
Imposition{isIndicatif ? ' — indicatif' : ''}
!isIndicatif && onFormPatch({ prelev_sociaux: Number(e.target.value) })} />
!isIndicatif && onFormPatch({ prelev_forfaitaire: Number(e.target.value) })} />
Montant des intérêts après imposition {fmtEUR(interetsNets)}
Versement
onFieldChange('date_remb', e.target.value)} />
{!isInteretsPlateforme && (
)}
{!isInteretsPlateforme && form.methode_remboursement === 'compte_courant' && ( <> {comptesInvestisseur?.length > 0 ? ( ) : (

Aucun compte défini. Créer un compte →

)} )}
{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)."}
)} {/* ── Bonus (mode non verrouillé uniquement) ── */} {!locked && isBonus && (
onFieldChange('cashback', e.target.value)} />
)} {!locked && isPlateformeMode && (
onFieldChange('notes', e.target.value)} />
)} ); }