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
@@ -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
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user