197 lines
9.0 KiB
React
197 lines
9.0 KiB
React
import { useState, useEffect } from 'react';
|
|
import { api } from '../../api.js';
|
|
|
|
export default function SecteursInvSection() {
|
|
const [secteursInv, setSecteursInv] = useState([]);
|
|
const [err, setErr] = useState(null);
|
|
const [selectedSectInv, setSelectedSectInv] = useState(null);
|
|
const [editingSectInv, setEditingSectInv] = useState(null);
|
|
const [editingNomSectInv, setEditingNomSectInv] = useState('');
|
|
const [newSectInvNom, setNewSectInvNom] = useState('');
|
|
const [showNewSectInv, setShowNewSectInv] = useState(false);
|
|
const [sectGlobalOpen, setSectGlobalOpen] = useState(false);
|
|
const [sectPrivateOpen, setSectPrivateOpen] = useState(true);
|
|
|
|
useEffect(() => {
|
|
api.get('/secteurs-inv').then(data => {
|
|
setSecteursInv(data.sort((a, b) => b.is_global - a.is_global || a.nom.localeCompare(b.nom)));
|
|
}).catch(() => {});
|
|
}, []);
|
|
|
|
const saveSectInv = async (nom) => {
|
|
if (!nom.trim()) return;
|
|
try {
|
|
const row = await api.post('/secteurs-inv', { nom: nom.trim() });
|
|
setSecteursInv(prev => [...prev, row].sort((a, b) =>
|
|
b.is_global - a.is_global || a.nom.localeCompare(b.nom)));
|
|
setShowNewSectInv(false); setNewSectInvNom(''); setErr(null);
|
|
} catch (e) { setErr(e.message || 'Erreur'); }
|
|
};
|
|
const renameSectInv = async (id, nom) => {
|
|
if (!nom.trim()) return;
|
|
try {
|
|
await api.put(`/secteurs-inv/${id}`, { nom: nom.trim() });
|
|
setSecteursInv(prev => prev.map(s => s.id === id ? { ...s, nom: nom.trim() } : s));
|
|
setEditingSectInv(null);
|
|
} catch (e) { setErr(e.message || 'Erreur'); }
|
|
};
|
|
const delSectInv = async (id) => {
|
|
try {
|
|
await api.del(`/secteurs-inv/${id}`);
|
|
setSecteursInv(prev => prev.filter(s => s.id !== id));
|
|
if (selectedSectInv?.id === id) setSelectedSectInv(null);
|
|
} catch (e) { setErr(e.message || 'Erreur'); }
|
|
};
|
|
|
|
const globalSects = secteursInv.filter(s => s.is_global);
|
|
const privateSects = secteursInv.filter(s => !s.is_global);
|
|
|
|
return (
|
|
<div>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
|
|
<h3 style={{ margin: 0 }}>Mes secteurs d'investissement</h3>
|
|
</div>
|
|
{err && <div className="error" style={{ marginBottom: 12 }}>{err}</div>}
|
|
|
|
{/* Accordéon — Secteurs globalement définis */}
|
|
<div className="card" style={{ marginBottom: 10 }}>
|
|
<button type="button"
|
|
onClick={() => setSectGlobalOpen(o => !o)}
|
|
style={{ width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
background: 'none', border: 'none', cursor: 'pointer', padding: 0, color: 'var(--text)' }}>
|
|
<span style={{ fontWeight: 600, fontSize: 'var(--fs-base)' }}>
|
|
Secteurs globalement définis
|
|
<span style={{ marginLeft: 8, fontWeight: 400, fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
|
({globalSects.length})
|
|
</span>
|
|
</span>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"
|
|
strokeLinecap="round" strokeLinejoin="round"
|
|
style={{ transform: sectGlobalOpen ? 'rotate(180deg)' : 'none', transition: 'transform .2s', flexShrink: 0 }}>
|
|
<polyline points="6 9 12 15 18 9"/>
|
|
</svg>
|
|
</button>
|
|
{sectGlobalOpen && (
|
|
<div style={{ marginTop: 14 }}>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Nom</th>
|
|
<th className="num">Plateformes</th>
|
|
<th className="num">Investissements</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{globalSects.length === 0 && (
|
|
<tr><td colSpan={3} className="text-muted" style={{ textAlign: 'center', padding: 24 }}>Aucun secteur global</td></tr>
|
|
)}
|
|
{globalSects.map(s => (
|
|
<tr key={s.id}>
|
|
<td><span style={{ fontWeight: 600 }}>{s.nom}</span></td>
|
|
<td className="num">{s.nb_plateformes > 0 ? s.nb_plateformes : <span className="text-muted">—</span>}</td>
|
|
<td className="num">{s.nb_investissements > 0 ? s.nb_investissements : <span className="text-muted">—</span>}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Accordéon — Mes propres secteurs */}
|
|
<div className="card">
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<button type="button"
|
|
onClick={() => setSectPrivateOpen(o => !o)}
|
|
style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 8,
|
|
background: 'none', border: 'none', cursor: 'pointer', padding: 0, color: 'var(--text)', textAlign: 'left' }}>
|
|
<span style={{ fontWeight: 600, fontSize: 'var(--fs-base)' }}>
|
|
Mes propres secteurs
|
|
<span style={{ marginLeft: 8, fontWeight: 400, fontSize: 'var(--fs-sm)', color: 'var(--text-muted)' }}>
|
|
({privateSects.length})
|
|
</span>
|
|
</span>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"
|
|
strokeLinecap="round" strokeLinejoin="round"
|
|
style={{ transform: sectPrivateOpen ? 'rotate(180deg)' : 'none', transition: 'transform .2s', flexShrink: 0 }}>
|
|
<polyline points="6 9 12 15 18 9"/>
|
|
</svg>
|
|
</button>
|
|
{sectPrivateOpen && (
|
|
<button className="primary" type="button" style={{ marginLeft: 12, flexShrink: 0 }}
|
|
onClick={() => { setSectPrivateOpen(true); setShowNewSectInv(true); setNewSectInvNom(''); setErr(null); }}>
|
|
+ Ajouter
|
|
</button>
|
|
)}
|
|
</div>
|
|
{sectPrivateOpen && (
|
|
<div style={{ marginTop: 14 }}>
|
|
{showNewSectInv && (
|
|
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
|
<input autoFocus style={{ flex: 1 }} placeholder="Nom du secteur"
|
|
value={newSectInvNom} onChange={e => setNewSectInvNom(e.target.value)}
|
|
onKeyDown={e => { if (e.key === 'Enter') saveSectInv(newSectInvNom); if (e.key === 'Escape') setShowNewSectInv(false); }} />
|
|
<button className="primary" onClick={() => saveSectInv(newSectInvNom)}>Créer</button>
|
|
<button onClick={() => setShowNewSectInv(false)}>Annuler</button>
|
|
</div>
|
|
)}
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Nom</th>
|
|
<th className="num">Plateformes</th>
|
|
<th className="num">Investissements</th>
|
|
<th style={{ width: 80 }}></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{privateSects.length === 0 && (
|
|
<tr><td colSpan={4} className="text-muted" style={{ textAlign: 'center', padding: 24 }}>Aucun secteur personnel</td></tr>
|
|
)}
|
|
{privateSects.map(s => (
|
|
<tr key={s.id}>
|
|
<td>
|
|
{editingSectInv === s.id ? (
|
|
<div style={{ display: 'flex', gap: 6 }}>
|
|
<input autoFocus style={{ flex: 1 }} value={editingNomSectInv}
|
|
onChange={e => setEditingNomSectInv(e.target.value)}
|
|
onKeyDown={e => { if (e.key === 'Enter') renameSectInv(s.id, editingNomSectInv); if (e.key === 'Escape') setEditingSectInv(null); }} />
|
|
<button className="primary" onClick={() => renameSectInv(s.id, editingNomSectInv)}>OK</button>
|
|
<button onClick={() => setEditingSectInv(null)}>✕</button>
|
|
</div>
|
|
) : (
|
|
<span style={{ fontWeight: 600 }}>{s.nom}</span>
|
|
)}
|
|
</td>
|
|
<td className="num">{s.nb_plateformes > 0 ? s.nb_plateformes : <span className="text-muted">—</span>}</td>
|
|
<td className="num">{s.nb_investissements > 0 ? s.nb_investissements : <span className="text-muted">—</span>}</td>
|
|
<td>
|
|
{editingSectInv !== s.id && (
|
|
<div style={{ display: 'flex', gap: 4, justifyContent: 'flex-end' }}>
|
|
<button style={{ fontSize: 12, padding: '2px 8px' }}
|
|
onClick={() => { setEditingSectInv(s.id); setEditingNomSectInv(s.nom); setErr(null); }}>
|
|
Renommer
|
|
</button>
|
|
<button style={{ fontSize: 12, padding: '2px 8px', color: 'var(--danger)', borderColor: 'var(--danger)' }}
|
|
onClick={() => setConfirmDelete({
|
|
title: 'Supprimer le secteur',
|
|
message: `Supprimer le secteur "${s.nom}" ? Il sera retiré de toutes les plateformes et investissements.`,
|
|
confirmLabel: 'Supprimer',
|
|
onConfirm: () => { delSectInv(s.id); setConfirmDelete(null); }
|
|
})}>
|
|
Supprimer
|
|
</button>
|
|
</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|