/** * Page Paramètres — 4 sections : * 1. SMTP (config email par utilisateur) * 2. Sociétés (nom + email de remboursement) * 3. Catégories * 4. Microsoft 365 / SharePoint (Azure App Registration + fichier Excel commun) */ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import toast from 'react-hot-toast'; import { useRef } from 'react'; import api from '../api/client'; import type { Company, Category, SmtpConfig, AppSettings, Contact } from '../types'; // ─── Section SMTP ──────────────────────────────────────────── function SmtpSection() { const qc = useQueryClient(); const { data, isLoading } = useQuery({ queryKey: ['smtp'], queryFn: () => api.get('/settings/smtp').then((r) => r.data), }); const [form, setForm] = useState & { smtp_pass?: string }>({}); const [open, setOpen] = useState(false); const save = useMutation({ mutationFn: (payload: object) => api.put('/settings/smtp', payload), onSuccess: () => { toast.success('SMTP sauvegardé'); qc.invalidateQueries({ queryKey: ['smtp'] }); setOpen(false); }, onError: (e: any) => toast.error(e.response?.data?.error ?? 'Erreur'), }); const test = useMutation({ mutationFn: () => api.post('/settings/smtp/test'), onSuccess: () => toast.success('Email de test envoyé !'), onError: (e: any) => toast.error(e.response?.data?.error ?? 'Échec SMTP'), }); function handleSubmit(e: React.FormEvent) { e.preventDefault(); const merged = { ...data, ...form, smtp_port: Number(form.smtp_port ?? data?.smtp_port ?? 587) }; save.mutate(merged); } const cfg = { ...data, ...form }; return (
{open && !isLoading && (
setForm(f => ({ ...f, smtp_host: e.target.value }))} />
setForm(f => ({ ...f, smtp_port: Number(e.target.value) }))} />
setForm(f => ({ ...f, smtp_user: e.target.value }))} />
setForm(f => ({ ...f, smtp_pass: e.target.value }))} />
setForm(f => ({ ...f, smtp_from_name: e.target.value }))} />
setForm(f => ({ ...f, smtp_from_email: e.target.value }))} />
)}
); } // ─── Section Sociétés ──────────────────────────────────────── function CompaniesSection() { const qc = useQueryClient(); const { data: companies = [] } = useQuery({ queryKey: ['companies'], queryFn: () => api.get('/companies').then(r => r.data), }); const [editing, setEditing] = useState | null>(null); const [open, setOpen] = useState(false); const save = useMutation({ mutationFn: (c: Partial) => 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); }, onError: (e: any) => toast.error(e.response?.data?.error ?? 'Erreur'), }); const del = useMutation({ mutationFn: (id: number) => api.delete(`/companies/${id}`), onSuccess: () => { toast.success('Société supprimée'); qc.invalidateQueries({ queryKey: ['companies'] }); }, }); function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (editing) save.mutate(editing); } return (
{open && (
{/* Liste */} {companies.map(c => (

{c.name}

{c.email}

))} {/* Formulaire ajout/modif */} {editing !== null ? (

{editing.id ? 'Modifier la société' : 'Nouvelle société'}

setEditing(p => ({ ...p, name: e.target.value }))} /> setEditing(p => ({ ...p, email: e.target.value }))} />
) : (
)}
)}
); } // ─── Section Catégories ────────────────────────────────────── function CategoriesSection() { const qc = useQueryClient(); const { data: categories = [] } = useQuery({ queryKey: ['categories'], queryFn: () => api.get('/categories').then(r => r.data), }); const [newName, setNewName] = useState(''); const [open, setOpen] = useState(false); const add = useMutation({ mutationFn: (name: string) => api.post('/categories', { name }), onSuccess: () => { toast.success('Catégorie ajoutée'); qc.invalidateQueries({ queryKey: ['categories'] }); setNewName(''); }, }); const del = useMutation({ mutationFn: (id: number) => api.delete(`/categories/${id}`), onSuccess: () => { qc.invalidateQueries({ queryKey: ['categories'] }); }, }); return (
{open && (
{categories.map(c => (
{c.name}
))}
setNewName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); if (newName.trim()) add.mutate(newName.trim()); }}} />
)}
); } // ─── Section Microsoft Graph ───────────────────────────────── function GraphSection() { const qc = useQueryClient(); const { data } = useQuery({ queryKey: ['app-settings'], queryFn: () => api.get('/settings/app').then(r => r.data), }); const [form, setForm] = useState<{ tenant?: string; client?: string; secret?: string; siteId?: string; itemId?: string; sheetName?: string; }>({}); const [open, setOpen] = useState(false); const save = useMutation({ mutationFn: (payload: object) => api.put('/settings/app', payload), onSuccess: () => { toast.success('Configuration Graph sauvegardée'); qc.invalidateQueries({ queryKey: ['app-settings'] }); setOpen(false); }, onError: (e: any) => toast.error(e.response?.data?.error ?? 'Erreur'), }); const testSp = useMutation({ mutationFn: () => api.post('/settings/sharepoint/test'), onSuccess: (r: any) => toast.success(r.data?.message ?? 'SharePoint OK'), onError: (e: any) => toast.error(e.response?.data?.error ?? 'Erreur SharePoint', { duration: 8000 }), }); function handleSubmit(e: React.FormEvent) { e.preventDefault(); save.mutate({ graph_tenant_id: form.tenant || undefined, graph_client_id: form.client || undefined, graph_client_secret: form.secret || undefined, sharepoint_site_id: form.siteId || undefined, sharepoint_item_id: form.itemId || undefined, sharepoint_sheet_name: form.sheetName || undefined, }); } const isConfigured = data?.graph_tenant_id && data?.graph_client_id && data?.has_secret === 'true' && data?.sharepoint_site_id && data?.sharepoint_item_id; return (
{open && (

Configuration Azure App Registration

Voir le README pour créer l'App Registration et obtenir ces valeurs.

Azure App Registration

{([ { key: 'tenant', label: 'Tenant ID', ph: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', cur: data?.graph_tenant_id }, { key: 'client', label: 'Client ID', ph: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', cur: data?.graph_client_id }, ] as { key: string; label: string; ph: string; cur?: string }[]).map(({ key, label, ph, cur }) => (
setForm(f => ({ ...f, [key]: e.target.value }))} />
))}
setForm(f => ({ ...f, secret: e.target.value }))} />

Fichier Excel SharePoint (partagé entre toutes les sociétés)

setForm(f => ({ ...f, siteId: e.target.value }))} />
setForm(f => ({ ...f, itemId: e.target.value }))} />
setForm(f => ({ ...f, sheetName: e.target.value }))} />
)}
); } // ─── Section Contacts ──────────────────────────────────────── function ContactsSection() { const qc = useQueryClient(); const fileRef = useRef(null); const [open, setOpen] = useState(false); const [newName, setNewName] = useState(''); const [newCompany, setNewCompany] = useState(''); const [importing, setImporting] = useState(false); const { data: contacts = [] } = useQuery({ queryKey: ['contacts'], queryFn: () => api.get('/contacts').then(r => r.data), }); const add = useMutation({ mutationFn: (payload: { name: string; company: string }) => api.post('/contacts', payload), onSuccess: () => { toast.success('Contact ajouté'); qc.invalidateQueries({ queryKey: ['contacts'] }); setNewName(''); setNewCompany(''); }, onError: (e: any) => toast.error(e.response?.data?.error ?? 'Erreur'), }); const del = useMutation({ mutationFn: (id: number) => api.delete(`/contacts/${id}`), onSuccess: () => qc.invalidateQueries({ queryKey: ['contacts'] }), onError: (e: any) => toast.error(e.response?.data?.error ?? 'Erreur'), }); async function handleFileImport(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; e.target.value = ''; setImporting(true); const formData = new FormData(); formData.append('file', file); try { const r = await api.post('/contacts/import', formData, { headers: { 'Content-Type': 'multipart/form-data' }, }); const { inserted, skipped, total } = r.data; toast.success(`${inserted} contact${inserted !== 1 ? 's' : ''} importé${inserted !== 1 ? 's' : ''}${skipped > 0 ? ` (${skipped} doublon${skipped !== 1 ? 's' : ''} ignoré${skipped !== 1 ? 's' : ''})` : ''} sur ${total}`); qc.invalidateQueries({ queryKey: ['contacts'] }); } catch (err: any) { toast.error(err.response?.data?.error ?? 'Erreur lors de l\'import'); } finally { setImporting(false); } } return (
{open && (
{/* Bouton import */}

Colonnes : Nom, Société (optionnel)

{/* Liste des contacts */} {contacts.length > 0 && (
{contacts.map(c => (
{c.name} {c.company && {c.company}}
))}
)} {/* Ajout manuel */}

Ajouter manuellement

setNewName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && newName.trim()) { e.preventDefault(); add.mutate({ name: newName.trim(), company: newCompany.trim() }); }}} /> setNewCompany(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && newName.trim()) { e.preventDefault(); add.mutate({ name: newName.trim(), company: newCompany.trim() }); }}} />
)}
); } // ─── Page principale ───────────────────────────────────────── export default function Settings() { return (

Paramètres

); }