173 lines
6.2 KiB
TypeScript
173 lines
6.2 KiB
TypeScript
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<GraphConfig> {
|
|
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<string, string> = {};
|
|
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<string> {
|
|
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<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 ──────────────────────────────────────
|
|
|
|
/**
|
|
* 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<void> {
|
|
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}`);
|
|
}
|
|
}
|