253 lines
9.7 KiB
React
253 lines
9.7 KiB
React
import { useState, useEffect, useCallback } from 'react';
|
|
import { api } from '../../api.js';
|
|
import { memberLabel } from '../../utils/format.js';
|
|
import ConfirmModal from '../../components/ConfirmModal.jsx';
|
|
import Modal from '../../components/Modal.jsx';
|
|
|
|
const EXONERATION_LABELS = {
|
|
aucune: 'Aucune',
|
|
pfnl_5ans: 'PFnl 5 ans',
|
|
};
|
|
const TYPE_COMPTE_LABELS = {
|
|
compte_courant: 'Compte courant',
|
|
pea_pme: 'PEA-PME',
|
|
};
|
|
|
|
const EMPTY_COMPTE = { nom: '', type: 'compte_courant', investisseur_id: null, banque: '', exoneration_fiscale: 'aucune' };
|
|
|
|
function compteInvestisseur(c) {
|
|
if (!c.investisseur_id) return null;
|
|
return { id: c.investisseur_id, nom: c.investisseur_nom, prenom: c.investisseur_prenom, type: c.investisseur_type, type_fiscal: c.investisseur_type_fiscal };
|
|
}
|
|
|
|
function CompteFormFields({ state, setter, investisseurs }) {
|
|
return (
|
|
<>
|
|
<div>
|
|
<label>Nom *</label>
|
|
<input required value={state.nom}
|
|
onChange={e => setter({ ...state, nom: e.target.value })}
|
|
placeholder="ex. Compte courant BNP" />
|
|
</div>
|
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
|
|
<div>
|
|
<label>Type *</label>
|
|
<select value={state.type} onChange={e => setter({ ...state, type: e.target.value })}>
|
|
<option value="compte_courant">Compte courant</option>
|
|
<option value="pea_pme">PEA-PME</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label>Banque</label>
|
|
<input value={state.banque}
|
|
onChange={e => setter({ ...state, banque: e.target.value })}
|
|
placeholder="ex. BNP Paribas" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label>Détenteur</label>
|
|
<select value={state.investisseur_id ?? ''}
|
|
onChange={e => setter({ ...state, investisseur_id: e.target.value ? Number(e.target.value) : null })}>
|
|
<option value="">— Non renseigné —</option>
|
|
{investisseurs.map(inv => (
|
|
<option key={inv.id} value={inv.id}>{memberLabel(inv)}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label>Exonération fiscale</label>
|
|
<select value={state.exoneration_fiscale ?? 'aucune'}
|
|
onChange={e => setter({ ...state, exoneration_fiscale: e.target.value })}>
|
|
<option value="aucune">Aucune exonération fiscale</option>
|
|
<option value="pfnl_5ans">Exonération Impôts sur le revenu (PFNL) si détention 5 ans</option>
|
|
</select>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function ComptesSection() {
|
|
const [comptes, setComptes] = useState([]);
|
|
const [investisseurs, setInvestisseurs] = useState([]);
|
|
|
|
const reload = useCallback(async () => {
|
|
const [cpts, invs] = await Promise.all([
|
|
api.get('/comptes'),
|
|
api.get('/investisseurs'),
|
|
]);
|
|
setComptes(cpts);
|
|
setInvestisseurs(invs);
|
|
}, []);
|
|
|
|
useEffect(() => { reload(); }, [reload]);
|
|
const [showNew, setShowNew] = useState(false);
|
|
const [newCompte, setNewCompte] = useState(EMPTY_COMPTE);
|
|
const [editCompte, setEditCompte] = useState(null);
|
|
const [err, setErr] = useState(null);
|
|
const [confirmDel, setConfirmDel] = useState(null);
|
|
|
|
const openNew = () => { setNewCompte(EMPTY_COMPTE); setErr(null); setShowNew(true); };
|
|
|
|
const addCompte = async (e) => {
|
|
e.preventDefault(); setErr(null);
|
|
try {
|
|
await api.post('/comptes', {
|
|
nom: newCompte.nom,
|
|
type: newCompte.type,
|
|
banque: newCompte.banque || null,
|
|
investisseur_id: newCompte.investisseur_id ? Number(newCompte.investisseur_id) : null,
|
|
exoneration_fiscale: newCompte.exoneration_fiscale ?? 'aucune',
|
|
});
|
|
setShowNew(false); setNewCompte(EMPTY_COMPTE); reload();
|
|
} catch (ex) { setErr(ex.message); }
|
|
};
|
|
|
|
const saveEdit = async (e) => {
|
|
e.preventDefault(); setErr(null);
|
|
try {
|
|
await api.put(`/comptes/${editCompte.id}`, {
|
|
nom: editCompte.nom,
|
|
type: editCompte.type,
|
|
banque: editCompte.banque || null,
|
|
investisseur_id: editCompte.investisseur_id ? Number(editCompte.investisseur_id) : null,
|
|
exoneration_fiscale: editCompte.exoneration_fiscale ?? 'aucune',
|
|
});
|
|
setEditCompte(null); reload();
|
|
} catch (ex) { setErr(ex.message); }
|
|
};
|
|
|
|
const openEdit = (c) => {
|
|
setErr(null);
|
|
setEditCompte({ id: c.id, nom: c.nom, type: c.type, banque: c.banque || '', investisseur_id: c.investisseur_id ?? null, exoneration_fiscale: c.exoneration_fiscale ?? 'aucune' });
|
|
};
|
|
|
|
const del = (c) => {
|
|
setConfirmDel({
|
|
message: `Supprimer le compte "${c.nom}" ?`,
|
|
onConfirm: async () => {
|
|
try { await api.del(`/comptes/${c.id}`); reload(); }
|
|
catch (ex) { setErr(ex.message); }
|
|
finally { setConfirmDel(null); }
|
|
},
|
|
});
|
|
};
|
|
|
|
const multiDetenteur = investisseurs.length > 1;
|
|
|
|
return (
|
|
<>
|
|
<div className="card">
|
|
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 16 }}>
|
|
<div>
|
|
<h3 style={{ margin: 0 }}>Mes comptes courants</h3>
|
|
<p className="text-muted" style={{ fontSize: 'var(--fs-sm)', margin: '4px 0 0' }}>
|
|
Comptes bancaires et enveloppes financières associés à vos investisseurs.
|
|
</p>
|
|
</div>
|
|
<button className="primary" style={{ whiteSpace: 'nowrap' }} onClick={openNew}>
|
|
+ Nouveau compte
|
|
</button>
|
|
</div>
|
|
|
|
{err && <div className="error" style={{ marginBottom: 10 }}>{err}</div>}
|
|
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Nom</th>
|
|
<th style={{ width: '14%' }}>Type</th>
|
|
{multiDetenteur && <th style={{ width: '22%' }}>Détenteur</th>}
|
|
<th style={{ width: '16%' }}>Banque</th>
|
|
<th style={{ width: '10%' }}>Exonération</th>
|
|
<th style={{ width: 80 }} />
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{comptes.length === 0 && (
|
|
<tr><td colSpan={multiDetenteur ? 6 : 5} className="text-muted" style={{ textAlign: 'center', fontStyle: 'italic' }}>
|
|
Aucun compte défini.
|
|
</td></tr>
|
|
)}
|
|
{comptes.map(c => {
|
|
const inv = compteInvestisseur(c);
|
|
return (
|
|
<tr key={c.id}>
|
|
<td style={{ fontWeight: 500 }}>{c.nom}</td>
|
|
<td><span className="badge">{TYPE_COMPTE_LABELS[c.type] ?? c.type}</span></td>
|
|
{multiDetenteur && <td>{inv ? memberLabel(inv) : <span className="text-muted">—</span>}</td>}
|
|
<td>{c.banque || <span className="text-muted">—</span>}</td>
|
|
<td>
|
|
{c.exoneration_fiscale && c.exoneration_fiscale !== 'aucune' ? (
|
|
<span
|
|
title={EXONERATION_LABELS[c.exoneration_fiscale] ?? c.exoneration_fiscale}
|
|
style={{ cursor: 'help', color: 'var(--success)', fontWeight: 600, fontSize: 'var(--fs-sm)' }}
|
|
>Oui</span>
|
|
) : (
|
|
<span className="text-muted" style={{ fontSize: 'var(--fs-sm)' }}>Non</span>
|
|
)}
|
|
</td>
|
|
<td>
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
<button style={{ padding: '3px 10px', fontSize: 11 }} onClick={() => openEdit(c)}>Modifier</button>
|
|
<button className="danger" style={{ padding: '3px 10px', fontSize: 11 }} onClick={() => del(c)}>✕</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* ── Modal création ── */}
|
|
<Modal
|
|
open={showNew}
|
|
title="Nouveau compte"
|
|
onClose={() => { setShowNew(false); setNewCompte(EMPTY_COMPTE); setErr(null); }}
|
|
footer={
|
|
<div style={{ display: 'flex', justifyContent: 'flex-end', width: '100%', gap: 8 }}>
|
|
<button type="button" onClick={() => { setShowNew(false); setNewCompte(EMPTY_COMPTE); setErr(null); }}>Annuler</button>
|
|
<button className="primary" form="form-new-compte" type="submit">Créer</button>
|
|
</div>
|
|
}
|
|
>
|
|
<form id="form-new-compte" onSubmit={addCompte} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
{err && <div className="error">{err}</div>}
|
|
<CompteFormFields state={newCompte} setter={setNewCompte} investisseurs={investisseurs} />
|
|
</form>
|
|
</Modal>
|
|
|
|
{/* ── Modal édition ── */}
|
|
<Modal
|
|
open={!!editCompte}
|
|
title="Modifier le compte"
|
|
onClose={() => { setEditCompte(null); setErr(null); }}
|
|
footer={
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
|
<button className="danger" type="button" onClick={() => { setEditCompte(null); del(comptes.find(c => c.id === editCompte?.id) ?? editCompte); }}>
|
|
Supprimer
|
|
</button>
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<button type="button" onClick={() => { setEditCompte(null); setErr(null); }}>Annuler</button>
|
|
<button className="primary" form="form-edit-compte" type="submit">Enregistrer</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
>
|
|
<form id="form-edit-compte" onSubmit={saveEdit} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
{err && <div className="error">{err}</div>}
|
|
<CompteFormFields state={editCompte} setter={setEditCompte} investisseurs={investisseurs} />
|
|
</form>
|
|
</Modal>
|
|
|
|
{/* ── ConfirmModal suppression ── */}
|
|
<ConfirmModal
|
|
open={!!confirmDel}
|
|
message={confirmDel?.message}
|
|
onConfirm={confirmDel?.onConfirm}
|
|
onCancel={() => setConfirmDel(null)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|