Initial commit
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
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); }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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>
|
||||
<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={6} 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>
|
||||
<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)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user