331 lines
13 KiB
React
331 lines
13 KiB
React
import { useEffect, useRef, useState } from 'react';
|
|
import { api } from '../api.js';
|
|
import { memberInitials, memberDisplayName } from '../utils/format.js';
|
|
import { useInvestisseur } from '../context/InvestisseurContext.jsx';
|
|
import Modal from '../components/Modal.jsx';
|
|
import ConfirmModal from '../components/ConfirmModal.jsx';
|
|
|
|
/* ── Avatar ─────────────────────────────────────────────────── */
|
|
function MemberAvatar({ membre, size = 40 }) {
|
|
const initials = memberInitials(membre);
|
|
const bg = membre.type === 'entreprise'
|
|
? 'linear-gradient(135deg, #4f46e5 0%, #3730a3 100%)'
|
|
: 'linear-gradient(135deg, #1e40af 0%, #1e3a8a 100%)';
|
|
return (
|
|
<div style={{
|
|
width: size, height: size, borderRadius: '50%',
|
|
background: bg,
|
|
color: 'white',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
fontWeight: 700, fontSize: Math.round(size * 0.35),
|
|
flexShrink: 0, letterSpacing: '.03em', userSelect: 'none',
|
|
}}>
|
|
{initials}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ── Menu "···" par membre ──────────────────────────────────── */
|
|
function MemberMenu({ onEdit, onDelete, isPrincipal, isOnly }) {
|
|
const [open, setOpen] = useState(false);
|
|
const ref = useRef(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const h = (e) => { if (!ref.current?.contains(e.target)) setOpen(false); };
|
|
document.addEventListener('mousedown', h);
|
|
return () => document.removeEventListener('mousedown', h);
|
|
}, [open]);
|
|
|
|
return (
|
|
<div ref={ref} style={{ position: 'relative', flexShrink: 0 }}>
|
|
<button className="member-dots-btn" onClick={() => setOpen(o => !o)}
|
|
aria-label="Actions" aria-haspopup="menu">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
<circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/>
|
|
</svg>
|
|
</button>
|
|
{open && (
|
|
<div className="member-dots-menu" role="menu">
|
|
<button className="member-dots-menu-item" role="menuitem" onClick={() => { setOpen(false); onEdit(); }}>
|
|
Modifier
|
|
</button>
|
|
{!isPrincipal && (
|
|
<button className="member-dots-menu-item danger-item" role="menuitem"
|
|
disabled={isOnly}
|
|
title={isOnly ? 'Impossible de supprimer le dernier profil' : ''}
|
|
onClick={() => { setOpen(false); onDelete(); }}>
|
|
Supprimer
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ── Ligne "Ajouter" ─────────────────────────────────────────── */
|
|
function AddRow({ label, onClick }) {
|
|
return (
|
|
<div className="member-add-row" onClick={onClick} role="button" tabIndex={0}
|
|
onKeyDown={e => e.key === 'Enter' && onClick()}>
|
|
<div className="member-add-circle">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
|
|
stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
|
</svg>
|
|
</div>
|
|
<span>{label}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ── Composant principal ─────────────────────────────────────── */
|
|
export default function FamilleEntreprises() {
|
|
const { reload: reloadCtx } = useInvestisseur();
|
|
const [membres, setMembres] = useState([]);
|
|
const [tab, setTab] = useState('famille');
|
|
const [err, setErr] = useState(null);
|
|
|
|
/* Modals */
|
|
const [modalFamille, setModalFamille] = useState(false);
|
|
const [modalEntreprise, setModalEntreprise] = useState(false);
|
|
const [editTarget, setEditTarget] = useState(null); // membre à éditer
|
|
|
|
/* Formulaires */
|
|
const emptyFam = { prenom: '', nom_famille: '' };
|
|
const emptyEnt = { nom: '', type_fiscal: 'PM' };
|
|
const [famForm, setFamForm] = useState(emptyFam);
|
|
const [entForm, setEntForm] = useState(emptyEnt);
|
|
const [saving, setSaving] = useState(false);
|
|
const [deleteConfirm, setDeleteConfirm] = useState(null);
|
|
|
|
const load = async () => {
|
|
const list = await api.get('/investisseurs');
|
|
setMembres(list);
|
|
};
|
|
|
|
useEffect(() => { load(); }, []);
|
|
|
|
const famille = membres.filter(m => m.type === 'famille');
|
|
const entreprises = membres.filter(m => m.type === 'entreprise');
|
|
const totalCount = membres.length;
|
|
|
|
/* ── Ouvrir modal édition ─────────────────────────────────── */
|
|
const openEdit = (m) => {
|
|
setEditTarget(m);
|
|
if (m.type === 'famille') {
|
|
const restNom = m.prenom ? m.nom.replace(m.prenom, '').trim() : m.nom;
|
|
setFamForm({ prenom: m.prenom || '', nom_famille: restNom });
|
|
setModalFamille(true);
|
|
} else {
|
|
setEntForm({ nom: m.nom, type_fiscal: m.type_fiscal || 'PM' });
|
|
setModalEntreprise(true);
|
|
}
|
|
};
|
|
|
|
const closeModals = () => {
|
|
setModalFamille(false); setModalEntreprise(false);
|
|
setEditTarget(null);
|
|
setFamForm(emptyFam); setEntForm(emptyEnt);
|
|
setErr(null);
|
|
};
|
|
|
|
/* ── Sauvegarde famille ────────────────────────────────────── */
|
|
const saveFamille = async (e) => {
|
|
e.preventDefault(); setErr(null); setSaving(true);
|
|
try {
|
|
const fullName = [famForm.prenom.trim(), famForm.nom_famille.trim()].filter(Boolean).join(' ');
|
|
if (!fullName) throw new Error('Veuillez renseigner au moins un prénom ou un nom.');
|
|
const payload = {
|
|
nom: fullName,
|
|
prenom: famForm.prenom.trim() || null,
|
|
type: 'famille',
|
|
type_fiscal: 'PP',
|
|
};
|
|
if (editTarget) {
|
|
await api.put(`/investisseurs/${editTarget.id}`, payload);
|
|
} else {
|
|
await api.post('/investisseurs', payload);
|
|
}
|
|
await load(); await reloadCtx();
|
|
closeModals();
|
|
} catch (e) { setErr(e.message); }
|
|
finally { setSaving(false); }
|
|
};
|
|
|
|
/* ── Sauvegarde entreprise ─────────────────────────────────── */
|
|
const saveEntreprise = async (e) => {
|
|
e.preventDefault(); setErr(null); setSaving(true);
|
|
try {
|
|
const payload = {
|
|
nom: entForm.nom.trim(),
|
|
prenom: null,
|
|
type: 'entreprise',
|
|
type_fiscal: entForm.type_fiscal,
|
|
};
|
|
if (editTarget) {
|
|
await api.put(`/investisseurs/${editTarget.id}`, payload);
|
|
} else {
|
|
await api.post('/investisseurs', payload);
|
|
}
|
|
await load(); await reloadCtx();
|
|
closeModals();
|
|
} catch (e) { setErr(e.message); }
|
|
finally { setSaving(false); }
|
|
};
|
|
|
|
/* ── Suppression ──────────────────────────────────────────── */
|
|
const deleteMembre = (m) => {
|
|
setDeleteConfirm({
|
|
message: `Supprimer "${memberDisplayName(m)}" ? Tous les investissements associés seront effacés.`,
|
|
onConfirm: async () => {
|
|
try {
|
|
await api.del(`/investisseurs/${m.id}`);
|
|
await load(); await reloadCtx();
|
|
} catch (e) { setErr(e.message); }
|
|
finally { setDeleteConfirm(null); }
|
|
},
|
|
});
|
|
};
|
|
|
|
/* ── Render ───────────────────────────────────────────────── */
|
|
const currentList = tab === 'famille' ? famille : entreprises;
|
|
|
|
return (
|
|
<div className="famille-wrap">
|
|
{/* Tabs */}
|
|
<div className="famille-tabs">
|
|
<button
|
|
className={`famille-tab${tab === 'famille' ? ' active' : ''}`}
|
|
onClick={() => setTab('famille')}
|
|
>
|
|
Famille
|
|
<span className="famille-tab-count">{famille.length}</span>
|
|
</button>
|
|
<button
|
|
className={`famille-tab${tab === 'entreprise' ? ' active' : ''}`}
|
|
onClick={() => setTab('entreprise')}
|
|
>
|
|
Entreprises
|
|
<span className="famille-tab-count">{entreprises.length}</span>
|
|
</button>
|
|
</div>
|
|
|
|
{err && <div className="error" style={{ marginBottom: 12 }}>{err}</div>}
|
|
|
|
{/* Liste */}
|
|
<div className="membre-list">
|
|
{currentList.map(m => (
|
|
<div key={m.id} className="membre-row">
|
|
<MemberAvatar membre={m} size={42} />
|
|
<div className="membre-info">
|
|
<span className="membre-name">{memberDisplayName(m)}</span>
|
|
{m.type === 'famille' && (
|
|
<span className="membre-role">
|
|
{m.is_principal ? '(compte principal)' : '(Membre de la famille)'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{m.type_fiscal && m.type === 'entreprise' && (
|
|
<span className="membre-badge">{m.type_fiscal}</span>
|
|
)}
|
|
<MemberMenu
|
|
onEdit={() => openEdit(m)}
|
|
onDelete={() => deleteMembre(m)}
|
|
isPrincipal={!!m.is_principal}
|
|
isOnly={totalCount <= 1}
|
|
/>
|
|
</div>
|
|
))}
|
|
|
|
{/* Ligne d'ajout */}
|
|
{tab === 'famille' && (
|
|
<AddRow label="Ajouter une personne"
|
|
onClick={() => { setEditTarget(null); setFamForm(emptyFam); setModalFamille(true); }} />
|
|
)}
|
|
{tab === 'entreprise' && (
|
|
<AddRow label="Ajouter une entreprise"
|
|
onClick={() => { setEditTarget(null); setEntForm(emptyEnt); setModalEntreprise(true); }} />
|
|
)}
|
|
</div>
|
|
|
|
{/* ── Modal famille ──────────────────────────────────────── */}
|
|
<Modal
|
|
open={modalFamille}
|
|
title={editTarget ? 'Modifier le membre' : 'Ajouter une personne'}
|
|
onClose={closeModals}
|
|
footer={
|
|
<>
|
|
<button className="ghost" type="button" onClick={closeModals}>Annuler</button>
|
|
<button className="primary" form="form-famille" type="submit" disabled={saving}>
|
|
{saving ? '…' : 'Enregistrer'}
|
|
</button>
|
|
</>
|
|
}
|
|
>
|
|
<form id="form-famille" onSubmit={saveFamille}
|
|
style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
{err && <div className="error">{err}</div>}
|
|
<div className="modal-field">
|
|
<label>Prénom</label>
|
|
<input autoFocus value={famForm.prenom}
|
|
onChange={e => setFamForm({ ...famForm, prenom: e.target.value })}
|
|
placeholder="Olivier" />
|
|
</div>
|
|
<div className="modal-field">
|
|
<label>Nom de famille <span className="text-muted" style={{ fontWeight: 400 }}>(optionnel)</span></label>
|
|
<input value={famForm.nom_famille}
|
|
onChange={e => setFamForm({ ...famForm, nom_famille: e.target.value })}
|
|
placeholder="CROGUENNEC" />
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
|
|
{/* ── Modal entreprise ───────────────────────────────────── */}
|
|
<Modal
|
|
open={modalEntreprise}
|
|
title={editTarget ? "Modifier l'entreprise" : 'Ajouter une entreprise'}
|
|
onClose={closeModals}
|
|
footer={
|
|
<>
|
|
<button className="ghost" type="button" onClick={closeModals}>Annuler</button>
|
|
<button className="primary" form="form-entreprise" type="submit" disabled={saving}>
|
|
{saving ? '…' : 'Enregistrer'}
|
|
</button>
|
|
</>
|
|
}
|
|
>
|
|
<form id="form-entreprise" onSubmit={saveEntreprise}
|
|
style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
{err && <div className="error">{err}</div>}
|
|
<div className="modal-field">
|
|
<label>Nom de l'entreprise *</label>
|
|
<input autoFocus required value={entForm.nom}
|
|
onChange={e => setEntForm({ ...entForm, nom: e.target.value })}
|
|
placeholder="SCI Famille Croguennec" />
|
|
</div>
|
|
<div className="modal-field">
|
|
<label>Forme juridique</label>
|
|
<select value={entForm.type_fiscal}
|
|
onChange={e => setEntForm({ ...entForm, type_fiscal: e.target.value })}>
|
|
<option value="PM">Personne morale</option>
|
|
<option value="SCI">SCI</option>
|
|
<option value="SCPI">SCPI</option>
|
|
<option value="SARL">SARL</option>
|
|
<option value="SAS">SAS</option>
|
|
<option value="SA">SA</option>
|
|
</select>
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
<ConfirmModal
|
|
open={!!deleteConfirm}
|
|
message={deleteConfirm?.message}
|
|
onConfirm={deleteConfirm?.onConfirm}
|
|
onCancel={() => setDeleteConfirm(null)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|