import { db } from '../db'; import { decrypt } from '../crypto'; // ─── Types ─────────────────────────────────────────────────── interface GraphConfig { tenantId: string; clientId: string; clientSecret: string; sharepointSiteId: string; sharepointItemId: string; sharepointSheet: string; } export interface SharepointRowData { category: string; companyName: string; comment: string; guests: Array<{ name: string; company?: string | null }>; date: string; // format JJ/MM/AAAA amount: number; userName: string; // 'Greg' ou 'Gaël' } // ─── Helpers ───────────────────────────────────────────────── async function getGraphConfig(): Promise { const result = await db.query( `SELECT key, value FROM app_settings WHERE key IN ( 'graph_tenant_id', 'graph_client_id', 'graph_client_secret_enc', 'sharepoint_site_id', 'sharepoint_item_id', 'sharepoint_sheet_name' )` ); const cfg: Record = {}; for (const row of result.rows) cfg[row.key] = row.value; if (!cfg.graph_tenant_id || !cfg.graph_client_id || !cfg.graph_client_secret_enc) { throw new Error('Microsoft Graph non configuré (tenant_id, client_id ou client_secret manquant)'); } if (!cfg.sharepoint_site_id || !cfg.sharepoint_item_id) { throw new Error('Fichier Excel SharePoint non configuré (site_id ou item_id manquant dans Paramètres → Microsoft 365)'); } return { tenantId: cfg.graph_tenant_id, clientId: cfg.graph_client_id, clientSecret: decrypt(cfg.graph_client_secret_enc), sharepointSiteId: cfg.sharepoint_site_id, sharepointItemId: cfg.sharepoint_item_id, sharepointSheet: cfg.sharepoint_sheet_name ?? 'Feuil1', }; } async function getAccessToken(cfg: GraphConfig): Promise { const response = await fetch( `https://login.microsoftonline.com/${cfg.tenantId}/oauth2/v2.0/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'client_credentials', client_id: cfg.clientId, client_secret: cfg.clientSecret, scope: 'https://graph.microsoft.com/.default', }), } ); if (!response.ok) { const body = await response.text(); throw new Error(`Obtention token Graph échouée (${response.status}) : ${body}`); } const data = (await response.json()) as { access_token: string }; 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 { 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 ────────────────────────────────────── /** * Ajoute une ligne dans le fichier Excel SharePoint commun selon le mapping : * A : Catégorie * B : Société facturée * C : Commentaire + invités * D : Date (JJ/MM/AAAA) * E : Montant si Greg * F : Montant si Gaël */ export async function addRowToExcel(row: SharepointRowData): Promise { const cfg = await getGraphConfig(); const token = await getAccessToken(cfg); // Construction de la cellule C (commentaire + invités) let cellComment = row.comment || ''; if (row.guests.length > 0) { const guestStr = row.guests .map((g) => (g.company ? `${g.name} — ${g.company}` : g.name)) .join(' ; '); cellComment = cellComment ? `${cellComment}. Invités : ${guestStr}` : `Invités : ${guestStr}`; } // Colonnes E/F selon l'utilisateur const nameLower = row.userName.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, ''); const isGreg = nameLower === 'greg'; const isGael = nameLower === 'gael'; const values = [[ row.category, // A row.companyName, // B cellComment, // C row.date, // D isGreg ? row.amount : null, // E — Greg isGael ? row.amount : null, // F — Gaël ]]; // ── Trouver la première ligne vide ─────────────────────────── const baseUrl = `https://graph.microsoft.com/v1.0/sites/${cfg.sharepointSiteId}/drive/items/${cfg.sharepointItemId}/workbook/worksheets/${encodeURIComponent(cfg.sharepointSheet)}`; const rangeResp = await fetch(`${baseUrl}/usedRange`, { headers: { Authorization: `Bearer ${token}` }, }); if (!rangeResp.ok) { const body = await rangeResp.text(); throw new Error(`Graph usedRange échoué (${rangeResp.status}) : ${body}`); } const rangeData = (await rangeResp.json()) as { rowCount: number }; const nextRow = rangeData.rowCount + 1; // ── Écriture de la nouvelle ligne ──────────────────────────── const writeResp = await fetch( `${baseUrl}/range(address='A${nextRow}:F${nextRow}')`, { method: 'PATCH', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ values }), } ); if (!writeResp.ok) { const body = await writeResp.text(); throw new Error(`Graph écriture ligne échouée (${writeResp.status}) : ${body}`); } }