MAJ
This commit is contained in:
1 parent
14cf4e5a19
commit
26197fc411
15 files changed
+1776
-29
No files matched your search
@@ -9,7 +9,7 @@ import InvestissementPeFormModal from '../components/InvestissementPeFormModal.j
|
||||
import FraisFormModal from '../components/FraisFormModal.jsx';
|
||||
import { FRAIS_CATEGORIE_LABELS } from '../components/MouvementsShared.jsx';
|
||||
import { useAsyncAction } from '../hooks/useAsyncAction.js';
|
||||
import { fmtEUR, fmtDate, fmtOctets, today } from '../utils/format.js';
|
||||
import { fmtEUR, fmtDate, fmtOctets, today, MODE_DETENTION_LABELS } from '../utils/format.js';
|
||||
import { emptyDealForm, AUTO_VAL_NOTE, AUTO_VAL_NOTE_LEGACY } from './InvestissementsPe.jsx';
|
||||
|
||||
/**
|
||||
@@ -44,12 +44,6 @@ const STATUT_LABELS = {
|
||||
perte_definitive: 'Perte définitive',
|
||||
};
|
||||
|
||||
const MODE_DETENTION_LABELS = {
|
||||
direct: 'Direct',
|
||||
pea_pme: 'PEA / PEA-PME',
|
||||
assurance_vie: 'Assurance-vie',
|
||||
};
|
||||
|
||||
const FRAIS_MODE_LABELS = {
|
||||
source: 'À la source',
|
||||
portefeuille: 'Porte-monnaie',
|
||||
@@ -356,6 +350,63 @@ export default function InvestissementPeDetail() {
|
||||
} catch (e) { setErr(e.message); }
|
||||
};
|
||||
|
||||
/* ── Déclaration de perte définitive (20/09/26, chantier "Fiscalité PE") ──────────
|
||||
* Même principe que InvestissementDetail.jsx côté crowdlending (openPerteModal/submitPerte/
|
||||
* cancelPerteDeclaration) mais adapté : un deal PE n'a pas de statut intermédiaire de
|
||||
* recouvrement (en_retard/procedure), donc l'action est proposée pour tout statut autre que
|
||||
* 'perte_definitive' plutôt que réservée à un sous-ensemble (cf. POST /:id/perte-definitive
|
||||
* côté backend, même garde-fou). Montant préfilli avec le capital pas encore restitué
|
||||
* (montant investi/investissable − distributions déjà classées "capital"), mais reste éditable
|
||||
* (perte partielle possible si une partie a déjà été récupérée).
|
||||
*/
|
||||
const [perteModalOpen, setPerteModalOpen] = useState(false);
|
||||
const [perteForm, setPerteForm] = useState({ date_effet: today(), montant_perte: '', motif: '' });
|
||||
const [perteErr, setPerteErr] = useState(null);
|
||||
const [perteSaving, setPerteSaving] = useState(false);
|
||||
const [confirmingPerteCancel, setConfirmingPerteCancel] = useState(false);
|
||||
|
||||
const openPerteModal = () => {
|
||||
const capInvestissable = deal?.capital_investissable ?? deal?.montant_investi ?? 0;
|
||||
const capNonRestitue = Math.max(0, capInvestissable - (deal?.capital_restitue_cumule || 0));
|
||||
setPerteForm({ date_effet: today(), montant_perte: String(Math.round(capNonRestitue * 100) / 100), motif: '' });
|
||||
setPerteErr(null);
|
||||
setPerteModalOpen(true);
|
||||
};
|
||||
const closePerteModal = () => { setPerteModalOpen(false); setPerteErr(null); };
|
||||
|
||||
const submitPerte = async (e) => {
|
||||
e?.preventDefault?.();
|
||||
setPerteErr(null);
|
||||
if (!perteForm.date_effet) { setPerteErr("La date d'effet est requise."); return; }
|
||||
if (perteForm.montant_perte === '' || Number(perteForm.montant_perte) < 0) {
|
||||
setPerteErr('Le montant de la perte doit être un nombre positif ou nul.');
|
||||
return;
|
||||
}
|
||||
if (!perteForm.motif.trim()) { setPerteErr('Le motif est obligatoire.'); return; }
|
||||
setPerteSaving(true);
|
||||
try {
|
||||
await api.post(`/investissements-pe/${id}/perte-definitive`, {
|
||||
date_effet: perteForm.date_effet,
|
||||
montant_perte: Number(perteForm.montant_perte),
|
||||
motif: perteForm.motif.trim(),
|
||||
});
|
||||
setPerteModalOpen(false);
|
||||
await load();
|
||||
} catch (e) {
|
||||
const fieldErrors = e.details?.fieldErrors;
|
||||
const detail = fieldErrors ? Object.values(fieldErrors).flat().filter(Boolean).join(' ') : '';
|
||||
setPerteErr(detail || e.message);
|
||||
} finally { setPerteSaving(false); }
|
||||
};
|
||||
|
||||
const cancelPerteDeclaration = async () => {
|
||||
try {
|
||||
await api.del(`/investissements-pe/${id}/perte-definitive`);
|
||||
setConfirmingPerteCancel(false);
|
||||
await load();
|
||||
} catch (e) { setConfirmingPerteCancel(false); }
|
||||
};
|
||||
|
||||
/* ── Valorisations ── */
|
||||
const [valForm, setValForm] = useState(emptyValForm);
|
||||
const [valErr, setValErr] = useState(null);
|
||||
@@ -1010,6 +1061,72 @@ export default function InvestissementPeDetail() {
|
||||
onCancel={() => setDocDeleteTarget(null)}
|
||||
/>
|
||||
|
||||
{/* ── Modal déclaration de perte définitive (20/09/26, chantier "Fiscalité PE") ──
|
||||
Même contenu que la modale équivalente côté crowdlending (InvestissementDetail.jsx),
|
||||
adapté au vocabulaire PE ("deal" plutôt que "dossier", case 2TY via taxreport-pe). */}
|
||||
<Modal
|
||||
open={perteModalOpen}
|
||||
title="Déclarer une perte définitive"
|
||||
onClose={closePerteModal}
|
||||
width={520}
|
||||
busy={perteSaving}
|
||||
footer={
|
||||
<>
|
||||
<button className="ghost" disabled={perteSaving} onClick={closePerteModal}>Annuler</button>
|
||||
<button className="danger" onClick={submitPerte} disabled={perteSaving}>
|
||||
{perteSaving ? 'Enregistrement…' : 'Déclarer la perte'}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p className="text-muted" style={{ fontSize: 13, marginTop: 0 }}>
|
||||
À utiliser uniquement lorsque le capital est réellement irrécouvrable (fonds liquidé
|
||||
sans distribution, société sous-jacente radiée…) — pas une simple baisse de
|
||||
valorisation latente. Une fois déclaré, ce deal sort du capital investi/en risque et
|
||||
le montant est reporté dans votre synthèse fiscale PE (case 2TY), pour l'année de la
|
||||
date d'effet ci-dessous.
|
||||
</p>
|
||||
<form onSubmit={submitPerte}>
|
||||
<label>Date d'effet (constat de la perte)</label>
|
||||
<input
|
||||
type="date"
|
||||
value={perteForm.date_effet}
|
||||
onChange={e => setPerteForm(f => ({ ...f, date_effet: e.target.value }))}
|
||||
required
|
||||
/>
|
||||
<label style={{ marginTop: 12, display: 'block' }}>
|
||||
Montant de la perte
|
||||
<span className="text-muted"> — capital pas encore restitué : {fmtEUR(Math.max(0, (deal?.capital_investissable ?? deal?.montant_investi ?? 0) - (deal?.capital_restitue_cumule || 0)))}</span>
|
||||
</label>
|
||||
<input
|
||||
type="number" step="0.01" min="0" required
|
||||
value={perteForm.montant_perte}
|
||||
onChange={e => setPerteForm(f => ({ ...f, montant_perte: e.target.value }))}
|
||||
/>
|
||||
<div className="text-muted" style={{ fontSize: 11, marginTop: 4 }}>
|
||||
Préfilli avec le capital pas encore restitué — ajustez si une partie a déjà été
|
||||
récupérée (perte partielle).
|
||||
</div>
|
||||
<label style={{ marginTop: 12, display: 'block' }}>Motif</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
placeholder="Ex : liquidation du fonds, société sous-jacente radiée…"
|
||||
value={perteForm.motif}
|
||||
onChange={e => setPerteForm(f => ({ ...f, motif: e.target.value }))}
|
||||
required
|
||||
/>
|
||||
{perteErr && <div className="form-error" style={{ marginTop: 10 }}>{perteErr}</div>}
|
||||
</form>
|
||||
</Modal>
|
||||
<ConfirmModal
|
||||
open={confirmingPerteCancel}
|
||||
title="Annuler la déclaration de perte ?"
|
||||
message="Le deal retrouve son statut précédent et le montant sort de votre synthèse fiscale PE."
|
||||
confirmLabel="Annuler la déclaration"
|
||||
onConfirm={cancelPerteDeclaration}
|
||||
onCancel={() => setConfirmingPerteCancel(false)}
|
||||
/>
|
||||
|
||||
<FraisFormModal
|
||||
open={fraisModalOpen}
|
||||
onClose={closeFraisModal}
|
||||
@@ -1041,6 +1158,21 @@ export default function InvestissementPeDetail() {
|
||||
items={[
|
||||
{ label: 'Modifier', icon: <><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></>, action: () => { setCardMenu(null); openEditDeal(); } },
|
||||
{ label: 'Exporter', icon: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></>, action: () => { setCardMenu(null); exportDeal(); } },
|
||||
// Déclarer/annuler une perte définitive (20/09/26, chantier "Fiscalité PE") — même
|
||||
// paire d'actions que le menu "⋮" crowdlending (InvestissementDetail.jsx).
|
||||
deal && deal.statut !== 'perte_definitive' && {
|
||||
label: 'Déclarer une perte définitive',
|
||||
icon: <><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></>,
|
||||
danger: true,
|
||||
separatorBefore: true,
|
||||
action: () => { setCardMenu(null); openPerteModal(); },
|
||||
},
|
||||
deal && deal.statut === 'perte_definitive' && {
|
||||
label: 'Annuler la déclaration de perte',
|
||||
icon: <><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-9.36L1 10"/></>,
|
||||
separatorBefore: true,
|
||||
action: () => { setCardMenu(null); setConfirmingPerteCancel(true); },
|
||||
},
|
||||
{ label: 'Supprimer', danger: true, separatorBefore: true, icon: <><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></>, action: () => { setCardMenu(null); setDeleteConfirm(true); } },
|
||||
]}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user