Ajout du paneau mois en cours + correction bug validation en masse

This commit is contained in:
ocroguennec committed 2026-08-15 11:10:27 +02:00
1 parent b78fbbb101
commit 6f8f7bab01
6 files changed
+779 -406

No files matched your search

+53 -210
View File
@@ -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 && (