Files
crowdlending-app/frontend/src/pages/settings/ComptesSection.jsx
T

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)}
/>
</>
);
}