"feat-sharepoint-test-button-and-error-feedback"

This commit is contained in:
deploy
2026-04-29 15:40:21 +02:00
parent 0d5ddc7eb0
commit 9adacb17de
5 changed files with 58 additions and 6 deletions
+6 -2
View File
@@ -337,6 +337,7 @@ router.post('/:id/send', wrap(async (req: AuthRequest, res: Response): Promise<v
// ── SharePoint (non bloquant) ───────────────────────────── // ── SharePoint (non bloquant) ─────────────────────────────
let trackingAdded = false; let trackingAdded = false;
let trackingError: string | null = null;
if (invoice.add_to_tracking) { if (invoice.add_to_tracking) {
try { try {
const [year, month, day] = dateStr.split('-'); const [year, month, day] = dateStr.split('-');
@@ -351,8 +352,10 @@ router.post('/:id/send', wrap(async (req: AuthRequest, res: Response): Promise<v
}); });
trackingAdded = true; trackingAdded = true;
} catch (err: any) { } catch (err: any) {
// Non bloquant : l'email est déjà envoyé, on log l'erreur // Non bloquant : l'email est déjà envoyé, mais on remonte l'erreur
// pour que le frontend puisse afficher un avertissement.
console.warn('[SharePoint] Erreur non bloquante :', err.message); console.warn('[SharePoint] Erreur non bloquante :', err.message);
trackingError = err.message;
} }
} }
@@ -365,7 +368,8 @@ router.post('/:id/send', wrap(async (req: AuthRequest, res: Response): Promise<v
[path.join('pdfs', `${invoice.id}.pdf`), pdfFilename, trackingAdded, invoice.id] [path.join('pdfs', `${invoice.id}.pdf`), pdfFilename, trackingAdded, invoice.id]
); );
res.json(await getInvoiceById(invoice.id)); const updated = await getInvoiceById(invoice.id);
res.json({ ...updated, tracking_error: trackingError });
})); }));
/** /**
+11
View File
@@ -140,4 +140,15 @@ router.put('/app', validate(appSettingsSchema), wrap(async (req: AuthRequest, re
res.json({ success: true }); res.json({ success: true });
})); }));
/** POST /api/settings/sharepoint/test — Vérifie la connexion Graph + accès au fichier Excel */
router.post('/sharepoint/test', wrap(async (_req: AuthRequest, res: Response): Promise<void> => {
try {
const { testSharepointConnection } = await import('../services/sharepoint');
await testSharepointConnection();
res.json({ success: true, message: 'Connexion SharePoint OK — fichier Excel accessible.' });
} catch (err: any) {
res.status(400).json({ error: err.message });
}
}));
export default router; export default router;
+21
View File
@@ -76,6 +76,27 @@ async function getAccessToken(cfg: GraphConfig): Promise<string> {
return data.access_token; return data.access_token;
} }
// ─── Test de connexion (sans écriture) ───────────────────────
/**
* Vérifie que le token Graph s'obtient et que la feuille Excel est accessible.
* Utilisé par POST /api/settings/sharepoint/test.
*/
export async function testSharepointConnection(): Promise<void> {
const cfg = await getGraphConfig();
const token = await getAccessToken(cfg);
const baseUrl = `https://graph.microsoft.com/v1.0/sites/${cfg.sharepointSiteId}/drive/items/${cfg.sharepointItemId}/workbook/worksheets/${encodeURIComponent(cfg.sharepointSheet)}`;
const resp = await fetch(`${baseUrl}/usedRange?$select=rowCount`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!resp.ok) {
const body = await resp.text();
throw new Error(`Impossible d'accéder à la feuille "${cfg.sharepointSheet}" (${resp.status}) : ${body.slice(0, 200)}`);
}
}
// ─── Fonction principale ────────────────────────────────────── // ─── Fonction principale ──────────────────────────────────────
/** /**
+4 -1
View File
@@ -138,8 +138,11 @@ export default function NewInvoice() {
const sendRes = await api.post(`/invoices/${createRes.data.id}/send`); const sendRes = await api.post(`/invoices/${createRes.data.id}/send`);
return sendRes.data; return sendRes.data;
}, },
onSuccess: () => { onSuccess: (data: any) => {
toast.success('Facture envoyée avec succès !'); toast.success('Facture envoyée avec succès !');
if (data?.tracking_error) {
toast.error(`⚠️ Suivi SharePoint : ${data.tracking_error}`, { duration: 10000 });
}
qc.invalidateQueries({ queryKey: ['invoices'] }); qc.invalidateQueries({ queryKey: ['invoices'] });
createdInvoiceIdRef.current = null; createdInvoiceIdRef.current = null;
resetForm(); resetForm();
+13
View File
@@ -316,6 +316,12 @@ function GraphSection() {
onError: (e: any) => toast.error(e.response?.data?.error ?? 'Erreur'), 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) { function handleSubmit(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
save.mutate({ save.mutate({
@@ -399,9 +405,16 @@ function GraphSection() {
placeholder={data?.sharepoint_sheet_name ?? 'Feuil1'} placeholder={data?.sharepoint_sheet_name ?? 'Feuil1'}
value={form.sheetName ?? ''} onChange={e => setForm(f => ({ ...f, sheetName: e.target.value }))} /> value={form.sheetName ?? ''} onChange={e => setForm(f => ({ ...f, sheetName: e.target.value }))} />
</div> </div>
<div className="flex gap-2">
<button type="submit" disabled={save.isPending} className="btn-primary py-2 text-sm"> <button type="submit" disabled={save.isPending} className="btn-primary py-2 text-sm">
{save.isPending ? 'Sauvegarde…' : 'Sauvegarder'} {save.isPending ? 'Sauvegarde…' : 'Sauvegarder'}
</button> </button>
<button type="button" onClick={() => testSp.mutate()}
disabled={testSp.isPending || !isConfigured}
className="btn-secondary py-2 text-sm">
{testSp.isPending ? 'Test…' : 'Tester'}
</button>
</div>
</form> </form>
)} )}
</div> </div>