feat: multi-destinataires email par societe
This commit is contained in:
@@ -234,14 +234,20 @@ function CompaniesSection() {
|
||||
queryFn: () => api.get('/companies').then(r => r.data),
|
||||
});
|
||||
|
||||
const [editing, setEditing] = useState<Partial<Company> | null>(null);
|
||||
const [editing, setEditing] = useState<(Partial<Company> & { extra_emails: string[] }) | null>(null);
|
||||
const [extraInput, setExtraInput] = useState('');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: (c: Partial<Company>) => c.id
|
||||
mutationFn: (c: Partial<Company> & { extra_emails: string[] }) => c.id
|
||||
? api.put(`/companies/${c.id}`, c)
|
||||
: api.post('/companies', c),
|
||||
onSuccess: () => { toast.success('Société sauvegardée'); qc.invalidateQueries({ queryKey: ['companies'] }); setEditing(null); },
|
||||
onSuccess: () => {
|
||||
toast.success('Société sauvegardée');
|
||||
qc.invalidateQueries({ queryKey: ['companies'] });
|
||||
setEditing(null);
|
||||
setExtraInput('');
|
||||
},
|
||||
onError: (e: any) => toast.error(e.response?.data?.error ?? 'Erreur'),
|
||||
});
|
||||
|
||||
@@ -250,6 +256,24 @@ function CompaniesSection() {
|
||||
onSuccess: () => { toast.success('Société supprimée'); qc.invalidateQueries({ queryKey: ['companies'] }); },
|
||||
});
|
||||
|
||||
function startEdit(c: Company) {
|
||||
setEditing({ ...c, extra_emails: c.extra_emails ?? [] });
|
||||
setExtraInput('');
|
||||
}
|
||||
|
||||
function addExtra() {
|
||||
const v = extraInput.trim().toLowerCase();
|
||||
if (!v || !v.includes('@')) return;
|
||||
if (editing && !editing.extra_emails.includes(v)) {
|
||||
setEditing(p => p ? { ...p, extra_emails: [...p.extra_emails, v] } : p);
|
||||
}
|
||||
setExtraInput('');
|
||||
}
|
||||
|
||||
function removeExtra(email: string) {
|
||||
setEditing(p => p ? { ...p, extra_emails: p.extra_emails.filter(e => e !== email) } : p);
|
||||
}
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (editing) save.mutate(editing);
|
||||
@@ -280,22 +304,27 @@ function CompaniesSection() {
|
||||
<div className="border-t border-gray-50">
|
||||
{/* Liste */}
|
||||
{companies.map(c => (
|
||||
<div key={c.id} className="flex items-center gap-3 px-4 py-3 border-b border-gray-50 last:border-0">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">{c.name}</p>
|
||||
<p className="text-xs text-gray-400 truncate">{c.email}</p>
|
||||
<div key={c.id} className="px-4 py-3 border-b border-gray-50 last:border-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">{c.name}</p>
|
||||
<p className="text-xs text-gray-400 truncate">{c.email}
|
||||
{(c.extra_emails?.length ?? 0) > 0 &&
|
||||
<span className="ml-1 text-indigo-400">+{c.extra_emails.length}</span>}
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={() => startEdit(c)} className="p-1.5 text-gray-300 hover:text-indigo-500">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onClick={() => { if (confirm(`Supprimer ${c.name} ?`)) del.mutate(c.id); }}
|
||||
className="p-1.5 text-gray-300 hover:text-red-500">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button onClick={() => setEditing(c)} className="p-1.5 text-gray-300 hover:text-indigo-500">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button onClick={() => { if (confirm(`Supprimer ${c.name} ?`)) del.mutate(c.id); }}
|
||||
className="p-1.5 text-gray-300 hover:text-red-500">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -306,19 +335,56 @@ function CompaniesSection() {
|
||||
{editing.id ? 'Modifier la société' : 'Nouvelle société'}
|
||||
</p>
|
||||
<input className="form-input" placeholder="Nom *" required
|
||||
value={editing.name ?? ''} onChange={e => setEditing(p => ({ ...p, name: e.target.value }))} />
|
||||
<input className="form-input" type="email" placeholder="Email destinataire *" required
|
||||
value={editing.email ?? ''} onChange={e => setEditing(p => ({ ...p, email: e.target.value }))} />
|
||||
<div className="flex gap-2">
|
||||
value={editing.name ?? ''} onChange={e => setEditing(p => p ? { ...p, name: e.target.value } : p)} />
|
||||
|
||||
{/* Email principal */}
|
||||
<div>
|
||||
<label className="form-label">Email principal *</label>
|
||||
<input className="form-input" type="email" placeholder="rh@société.fr" required
|
||||
value={editing.email ?? ''}
|
||||
onChange={e => setEditing(p => p ? { ...p, email: e.target.value } : p)} />
|
||||
</div>
|
||||
|
||||
{/* Emails supplémentaires */}
|
||||
<div>
|
||||
<label className="form-label">Emails supplémentaires</label>
|
||||
{/* Chips */}
|
||||
{editing.extra_emails.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mb-2">
|
||||
{editing.extra_emails.map(em => (
|
||||
<span key={em}
|
||||
className="inline-flex items-center gap-1 px-2 py-1 bg-indigo-50 text-indigo-700
|
||||
text-xs rounded-lg font-medium">
|
||||
{em}
|
||||
<button type="button" onClick={() => removeExtra(em)}
|
||||
className="text-indigo-400 hover:text-indigo-700 leading-none">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<input className="form-input text-sm flex-1" type="email"
|
||||
placeholder="autre@société.fr"
|
||||
value={extraInput}
|
||||
onChange={e => setExtraInput(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); addExtra(); } }} />
|
||||
<button type="button" onClick={addExtra}
|
||||
className="px-3 py-2 bg-indigo-600 text-white text-sm font-semibold rounded-xl disabled:opacity-40"
|
||||
disabled={!extraInput.trim()}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-1">
|
||||
<button type="submit" disabled={save.isPending} className="btn-primary py-2 text-sm">
|
||||
{save.isPending ? '…' : 'Sauvegarder'}
|
||||
</button>
|
||||
<button type="button" onClick={() => setEditing(null)} className="btn-secondary py-2 text-sm">Annuler</button>
|
||||
<button type="button" onClick={() => { setEditing(null); setExtraInput(''); }}
|
||||
className="btn-secondary py-2 text-sm">Annuler</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="p-4">
|
||||
<button onClick={() => setEditing({})} className="btn-secondary py-2 text-sm">
|
||||
<button onClick={() => setEditing({ extra_emails: [] })} className="btn-secondary py-2 text-sm">
|
||||
+ Ajouter une société
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface Company {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
extra_emails: string[];
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user