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

+233 -1
View File
@@ -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
/>
</>
);
}