Ajoutez des fichiers projet.

This commit is contained in:
2026-04-30 12:51:31 +02:00
parent d17cbe4df0
commit 72f4beeb61
57 changed files with 26895 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
AZURE_CLIENT_ID=51a2c5b3-4cea-4752-93d0-bc59ea33be29
AZURE_CLIENT_SECRET=uPD8Q~CJbl26DDrRWRgKXvvqwVVm0DtRhzkEqcyT
AZURE_TENANT_ID=9840a2a0-6ae1-4688-b03d-d2ec291be0f9
JWT_SECRET=un_secret_aleatoire_securise
AZURE_GROUP_ID=c1ea877c-6bca-4f47-bfad-f223640813a0
DB_SERVER=192.168.0.3
DB_USER=ndf_app
DB_PASSWORD=P@ssw0rd2026!
DB_NAME=NDF
DB_PORT=1433
RESPONSABLE_PAIEMENT_EMAIL=aagromayor@ensup.eu
MAIL_FROM=ndfnoreply@ensup.eu
OAUTH_REDIRECT_URI=https://myndf.ensup-adm.net/api/auth/callback
PORT=3024
SHAREPOINT_SITE_ID=ensup.sharepoint.com,d94abc08-28eb-47ce-8e12-fbbd6f16b9ea,a052c325-d33a-40e3-9e7b-7896a2ea7ab7
SHAREPOINT_DRIVE_ID=b!CLxK2esozkeOEvu9bxa56iXDUqA60-NAnnt4lqLqerfjKRsFHmtxSbj0s5KCssZK
IBAN_ENCRYPTION_KEY=a3f8c2d1e4b7096f5a2e1d8c3b4f7a90e2d1c8b5f3a6e9d0c7b4a1f8e5d2c9b6
IBAN_HASH_SALT=b4c7e2a1f9d3086e5c4a2b8f1e7d3c9a
COMPANY_NAME=ENSUP GROUP
COMPANY_IBAN=FR76XXXXXXXXXXXXXXXXXXXXXXXXX
COMPANY_BIC=BNPAFRPP
COMPANY_ADDRESS=1 RUE DE LA PAIX
COMPANY_CP=75009
COMPANY_VILLE=PARIS 09
COMPANY_PAYS=FR
+18
View File
@@ -0,0 +1,18 @@
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies (including pdfkit)
RUN npm ci --only=production
# Copy application code
COPY . .
# Expose port
EXPOSE 3024
# Start the server
CMD ["node", "server.js"]
+27
View File
@@ -0,0 +1,27 @@
require('dotenv').config();
const msalConfig = {
auth: {
clientId: process.env.AZURE_CLIENT_ID,
authority: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`,
clientSecret: process.env.AZURE_CLIENT_SECRET,
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: 'Info',
},
},
};
const REDIRECT_URI = process.env.AZURE_REDIRECT_URI;
const POST_LOGOUT_REDIRECT_URI = process.env.FRONTEND_URL;
module.exports = {
msalConfig,
REDIRECT_URI,
POST_LOGOUT_REDIRECT_URI,
};
+485
View File
@@ -0,0 +1,485 @@
// ══════════════════════════════════════════════════════════════════════════════
// ndfPdfGenerator.js — v4 (pdfkit pur, 0% Python)
// Génère la fiche Note de Frais au format exact du modèle ENSUP
// avec signatures électroniques intégrées.
// v4 : Tarif km et Sous-total km intégrés dans le tableau après colonne Km
//
// Prérequis : pdfkit déjà installé (npm install pdfkit)
// Copier dans le même dossier que server.js.
// ══════════════════════════════════════════════════════════════════════════════
import PDFDocument from 'pdfkit';
// ─────────────────────────────────────────────────────────────────────────────
// CONSTANTES
// ─────────────────────────────────────────────────────────────────────────────
const C = {
blue: '#1B4F8A', header: '#2563EB',
totalBg: '#DBEAFE', altRow: '#EFF6FF',
amountBg: '#EEF2FF', greenBg: '#F0FDF4',
greenText: '#15803D', headerRow: '#E2E8F0',
border: '#CBD5E1', dark: '#0F172A',
grey: '#64748B', light: '#94A3B8',
white: '#FFFFFF',
collabBg: '#F0FDF4', collabBorder: '#A7F3D0', collabText: '#059669',
validBg: '#EEF2FF', validBorder: '#C7D2FE', validText: '#4F46E5',
refusBg: '#FEF2F2', refusBorder: '#FCA5A5', refusText: '#DC2626',
waitBg: '#F8FAFC', waitBorder: '#E2E8F0',
kmBg: '#F5F3FF', // fond violet clair pour colonne tarif km
kmBorder: '#DDD6FE', // bordure violet clair
kmText: '#7C3AED', // texte violet
kmTotalBg: '#EDE9FE', // fond sous-total km
};
// Colonnes tableau (largeurs en points)
// ── v4 : 'tarifKm' et 'sousKm' insérées après 'km' ──
const COLS = [
{ key: 'num', label: 'N°pièce', w: 34, align: 'center' },
{ key: 'date', label: 'Date', w: 58, align: 'left' },
{ key: 'nature', label: 'Nature', w: 70, align: 'left' },
{ key: 'lib', label: 'Libellé', w: 140, align: 'left' },
{ key: 'km', label: 'Km', w: 36, align: 'right' },
{ key: 'tarifKm', label: 'Tarif €/km', w: 46, align: 'right' },
{ key: 'sousKm', label: 'S/Total Km', w: 50, align: 'right' },
{ key: 'ttc', label: 'TTC', w: 50, align: 'right' },
{ key: 'tva21', label: 'TVA 2,1%', w: 46, align: 'right' },
{ key: 'tva55', label: 'TVA 5,5%', w: 46, align: 'right' },
{ key: 'tva10', label: 'TVA 10%', w: 46, align: 'right' },
{ key: 'tva20', label: 'TVA 20%', w: 46, align: 'right' },
{ key: 'ht', label: 'HT', w: 50, align: 'right' },
];
// Colonnes km (pour coloration spéciale)
const KM_COLS = ['km', 'tarifKm', 'sousKm'];
const MARGIN = 30;
const ROW_H = 16;
const HEAD_H = 20;
const PAGE_W = 841.89; // A4 largeur
const PAGE_H = 595.28; // A4 hauteur
// ─────────────────────────────────────────────────────────────────────────────
// HELPERS
// ─────────────────────────────────────────────────────────────────────────────
const f2 = v => (parseFloat(v) || 0).toFixed(2);
const f3 = v => (parseFloat(v) || 0).toFixed(3);
function fmtDate(s) {
if (!s) return '';
try {
const d = new Date(s);
if (isNaN(d)) return String(s).slice(0, 10);
return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
} catch { return ''; }
}
function fmtDateTime(s) {
if (!s) return '';
try {
const d = new Date(s);
if (isNaN(d)) return String(s).slice(0, 16);
return d.toLocaleString('fr-FR', {
timeZone: 'Europe/Paris', day: '2-digit', month: '2-digit',
year: 'numeric', hour: '2-digit', minute: '2-digit',
});
} catch { return ''; }
}
function drawRect(doc, x, y, w, h, fill, stroke = null, lw = 0.3) {
doc.save();
if (stroke) {
doc.lineWidth(lw).rect(x, y, w, h).fillAndStroke(fill, stroke);
} else {
doc.rect(x, y, w, h).fill(fill);
}
doc.restore();
}
function drawCellText(doc, text, x, y, w, h, font, size, color, align, padX = 3) {
text = String(text ?? '');
const maxW = w - padX * 2;
doc.save().font(font).fontSize(size).fillColor(color);
while (text.length > 1 && doc.widthOfString(text) > maxW) text = text.slice(0, -1);
const ty = y + h * 0.28;
if (align === 'right') {
doc.text(text, x + padX, ty, { width: maxW, align: 'right', lineBreak: false });
} else if (align === 'center') {
doc.text(text, x + padX, ty, { width: maxW, align: 'center', lineBreak: false });
} else {
doc.text(text, x + padX, ty, { width: maxW, align: 'left', lineBreak: false });
}
doc.restore();
}
function drawVLine(doc, x, y1, y2) {
doc.save().strokeColor(C.border).lineWidth(0.4)
.moveTo(x, y1).lineTo(x, y2).stroke().restore();
}
function drawHLine(doc, x1, x2, y) {
doc.save().strokeColor(C.border).lineWidth(0.3)
.moveTo(x1, y).lineTo(x2, y).stroke().restore();
}
// ─────────────────────────────────────────────────────────────────────────────
// preparerLignesPDF — convertit lignes formulaire → lignes PDF
// ─────────────────────────────────────────────────────────────────────────────
export function preparerLignesPDF(lignesParsed, tarifKm = TARIF_KM_DEFAULT) {
return (lignesParsed || []).map((l, idx) => {
const isKm = (l.categorie || '').toLowerCase().includes('kilom');
const km = parseFloat(l.km) || 0;
const ttc = isKm ? 0 : (parseFloat(l.montant) || 0);
const taux = parseFloat(l.tauxTVA) || 0;
let ht = ttc, tva21 = 0, tva55 = 0, tva10 = 0, tva20 = 0;
if (!isKm && taux > 0 && ttc > 0) {
ht = parseFloat((ttc / (1 + taux / 100)).toFixed(2));
const tvaM = parseFloat((ttc - ht).toFixed(2));
if (Math.abs(taux - 2.1) < 0.01) tva21 = tvaM;
else if (Math.abs(taux - 5.5) < 0.01) tva55 = tvaM;
else if (Math.abs(taux - 10) < 0.01) tva10 = tvaM;
else if (Math.abs(taux - 20) < 0.01) tva20 = tvaM;
}
const indemniteKm = isKm ? parseFloat((km * tarifKm).toFixed(2)) : 0;
return {
numPiece: idx + 1,
date: l.date,
nature: l.categorie || '',
libelle: l.libelle || '',
km: isKm ? km : 0,
tarifKmVal: isKm ? tarifKm : 0, // ← valeur numérique tarif
montantTTC: isKm ? 0 : ttc,
tva21, tva55, tva10, tva20,
montantHT: isKm ? 0 : ht,
indemniteKm,
};
});
}
// ─────────────────────────────────────────────────────────────────────────────
// generateFicheSignee — point d'entrée appelé depuis server.js
// ─────────────────────────────────────────────────────────────────────────────
export async function generateFicheSignee(note, signatures = []) {
const tarifKm = parseFloat(note.tarifKm) || TARIF_KM_DEFAULT;
let lignesPDF = [];
if (note.lignesJson) {
try {
const parsed = typeof note.lignesJson === 'string'
? JSON.parse(note.lignesJson) : note.lignesJson;
lignesPDF = preparerLignesPDF(parsed, tarifKm);
} catch (e) { console.error('ndfPdfGenerator — Parse lignesJson:', e.message); }
} else if (note.lignes && Array.isArray(note.lignes)) {
lignesPDF = preparerLignesPDF(note.lignes, tarifKm);
} else {
const isKm = !!(note.km && parseFloat(note.km) > 0);
const km = isKm ? parseFloat(note.km) : 0;
lignesPDF = [{
numPiece: 1, date: note.date, nature: note.categorie || '', libelle: note.libelle || '',
km: isKm ? km : 0,
tarifKmVal: isKm ? tarifKm : 0,
montantTTC: isKm ? 0 : parseFloat(note.montant || 0),
tva21: 0, tva55: 0, tva10: 0, tva20: 0,
montantHT: isKm ? 0 : parseFloat(note.montantHT || note.montant || 0),
indemniteKm: isKm ? parseFloat((km * tarifKm).toFixed(2)) : 0,
}];
}
let mois = note.mois || '';
if (!mois && note.date) {
const d = new Date(note.date);
const m = d.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
mois = m.charAt(0).toUpperCase() + m.slice(1);
}
return _buildPDF({
reference: note.reference || '',
nomPrenom: note.nomPrenom || note.collaborateur || '',
mois,
departement: note.departement || '',
lignes: lignesPDF,
tarifKm,
signatures,
statut: note.statut || 'enattente',
});
}
// ─────────────────────────────────────────────────────────────────────────────
// _buildPDF — génère le Buffer PDF
// ─────────────────────────────────────────────────────────────────────────────
function _buildPDF({ reference, nomPrenom, mois, departement, lignes, tarifKm, signatures }) {
return new Promise((resolve, reject) => {
const doc = new PDFDocument({
size: 'A4',
layout: 'landscape',
margin: 0,
info: {
Title: `Note de Frais ${reference}`,
Author: `ENSUP — ${nomPrenom}`,
Subject: `NDF ${reference}`,
Creator: 'NDF ENSUP v4',
},
});
const chunks = [];
doc.on('data', c => chunks.push(c));
doc.on('end', () => resolve(Buffer.concat(chunks)));
doc.on('error', e => reject(e));
// ── Positions X colonnes ─────────────────────────────────────
const colX = {};
let cx = MARGIN;
for (const col of COLS) { colX[col.key] = cx; cx += col.w; }
const tableW = cx - MARGIN;
// ── 2. BANDEAU ───────────────────────────────────────────────
const bandY = 44;
drawRect(doc, MARGIN, bandY, tableW, 18, C.header);
doc.font('Helvetica-Bold').fontSize(11).fillColor(C.white)
.text('NOTE DE FRAIS', MARGIN, bandY + 3.5, { width: tableW, align: 'center', lineBreak: false });
// ── 3. INFOS COLLAB ──────────────────────────────────────────
const infoY = bandY + 23;
doc.font('Helvetica-Bold').fontSize(9).fillColor(C.dark)
.text(`NOM : ${nomPrenom}`, MARGIN, infoY, { lineBreak: false });
if (departement)
doc.font('Helvetica').fontSize(8).fillColor(C.grey)
.text(`Service : ${departement}`, MARGIN + 200, infoY, { lineBreak: false });
doc.font('Helvetica').fontSize(8).fillColor(C.grey)
.text('Repas, déplacement, autres', MARGIN + 380, infoY, { lineBreak: false });
doc.font('Helvetica-Bold').fontSize(9).fillColor(C.dark)
.text(`Mois : ${mois}`, MARGIN, infoY + 13, { lineBreak: false });
// ── 4. EN-TÊTE COLONNES ──────────────────────────────────────
const tableTop = infoY + 27;
// Fond de base
drawRect(doc, MARGIN, tableTop, tableW, HEAD_H, C.headerRow, C.border, 0.5);
// Fond spécial violet pour les 3 colonnes km dans l'en-tête
for (const key of KM_COLS) {
drawRect(doc, colX[key], tableTop, COLS.find(c => c.key === key).w, HEAD_H, C.kmBg);
}
for (const col of COLS) {
const isKmCol = KM_COLS.includes(col.key);
drawCellText(
doc, col.label,
colX[col.key], tableTop, col.w, HEAD_H,
'Helvetica-Bold', isKmCol ? 6.5 : 7,
isKmCol ? C.kmText : C.dark,
col.align
);
drawVLine(doc, colX[col.key], tableTop, tableTop + HEAD_H);
}
drawVLine(doc, MARGIN + tableW, tableTop, tableTop + HEAD_H);
drawHLine(doc, MARGIN, MARGIN + tableW, tableTop);
drawHLine(doc, MARGIN, MARGIN + tableW, tableTop + HEAD_H);
// ── 5. LIGNES DONNÉES ────────────────────────────────────────
const MIN_ROWS = 18;
const totalRows = Math.max(MIN_ROWS, lignes.length);
let y = tableTop + HEAD_H;
let totKm = 0, totTTC = 0, totT21 = 0, totT55 = 0, totT10 = 0, totT20 = 0, totHT = 0, totSousKm = 0;
for (let i = 0; i < totalRows; i++) {
const lig = lignes[i] || null;
// Fond de ligne alterné
drawRect(doc, MARGIN, y, tableW, ROW_H, i % 2 === 1 ? C.altRow : C.white);
// Fond violet léger sur les 3 colonnes km (toutes lignes)
for (const key of KM_COLS) {
const col = COLS.find(c => c.key === key);
drawRect(doc, colX[key], y, col.w, ROW_H,
i % 2 === 1 ? '#F3F0FF' : '#FAF8FF');
}
if (lig) {
const km = parseFloat(lig.km) || 0;
const tarif = parseFloat(lig.tarifKmVal) || 0;
const sousKm = parseFloat(lig.indemniteKm) || 0;
const ttc = parseFloat(lig.montantTTC) || 0;
const t21 = parseFloat(lig.tva21) || 0;
const t55 = parseFloat(lig.tva55) || 0;
const t10 = parseFloat(lig.tva10) || 0;
const t20 = parseFloat(lig.tva20) || 0;
const ht = parseFloat(lig.montantHT) || 0;
totKm += km;
totTTC += ttc;
totT21 += t21; totT55 += t55; totT10 += t10; totT20 += t20;
totHT += ht;
totSousKm += sousKm;
const r = {
num: String(lig.numPiece || i + 1),
date: fmtDate(lig.date),
nature: lig.nature || '',
lib: lig.libelle || '',
km: km > 0 ? f2(km) : '',
tarifKm: tarif > 0 ? f3(tarif) : '', // ex: 0.697
sousKm: sousKm > 0 ? f2(sousKm) : '',
ttc: f2(ttc),
tva21: f2(t21), tva55: f2(t55),
tva10: f2(t10), tva20: f2(t20),
ht: f2(ht),
};
for (const col of COLS) {
const isKmCol = KM_COLS.includes(col.key);
drawCellText(
doc, r[col.key],
colX[col.key], y, col.w, ROW_H,
'Helvetica', 7,
isKmCol ? C.kmText : C.dark,
col.align
);
}
} else {
// Ligne vide — zéros en gris sur colonnes numériques
for (const col of COLS) {
if (['ttc', 'tva21', 'tva55', 'tva10', 'tva20', 'ht'].includes(col.key))
drawCellText(doc, '0.00', colX[col.key], y, col.w, ROW_H, 'Helvetica', 7, C.border, 'right');
}
}
// Bordures ligne
drawHLine(doc, MARGIN, MARGIN + tableW, y + ROW_H);
for (const col of COLS) drawVLine(doc, colX[col.key], y, y + ROW_H);
drawVLine(doc, MARGIN + tableW, y, y + ROW_H);
y += ROW_H;
}
// ── 6. LIGNE TOTAL ───────────────────────────────────────────
const totalY = y;
drawRect(doc, MARGIN, totalY, tableW, ROW_H + 2, C.totalBg, C.border, 0.5);
// Fond violet sur les colonnes km dans la ligne total
for (const key of KM_COLS) {
const col = COLS.find(c => c.key === key);
drawRect(doc, colX[key], totalY, col.w, ROW_H + 2, C.kmTotalBg);
}
doc.font('Helvetica-Bold').fontSize(8).fillColor(C.dark)
.text('Total', MARGIN + 3, totalY + 4, { lineBreak: false });
const totMap = {
km: totKm > 0 ? f2(totKm) : '',
tarifKm: '', // pas de somme de tarifs
sousKm: totSousKm > 0 ? f2(totSousKm) : '',
ttc: f2(totTTC),
tva21: f2(totT21), tva55: f2(totT55),
tva10: f2(totT10), tva20: f2(totT20),
ht: f2(totHT),
};
for (const col of COLS) {
if (!totMap[col.key] && totMap[col.key] !== '0.00') continue;
if (totMap[col.key] === '') continue;
const isKmCol = KM_COLS.includes(col.key);
drawCellText(
doc, totMap[col.key],
colX[col.key], totalY, col.w, ROW_H + 2,
'Helvetica-Bold', 8,
isKmCol ? C.kmText : C.dark,
'right'
);
}
// ── 7. ZONE BAS (simplifiée — les infos km sont dans le tableau) ──
const footY = totalY + ROW_H + 12;
const montantR = parseFloat((totTTC + totSousKm).toFixed(2));
const bw = 64;
// Montant à rembourser (simplifié)
doc.font('Helvetica-Bold').fontSize(9).fillColor(C.dark)
.text('Montant total à rembourser', MARGIN, footY + 4, { lineBreak: false });
drawRect(doc, MARGIN + 180, footY, bw + 10, 18, C.amountBg, C.border, 0.5);
doc.font('Helvetica-Bold').fontSize(11).fillColor(C.blue)
.text(f2(montantR) + ' €', MARGIN + 182, footY + 3.5, { width: bw + 6, align: 'right', lineBreak: false });
// Rappel tarif utilisé (petit, discret)
doc.font('Helvetica').fontSize(6.5).fillColor(C.grey)
.text(`Tarif km appliqué : ${tarifKm.toFixed(3)} €/km — Sous-total km : ${f2(totSousKm)} € — TTC hors km : ${f2(totTTC)}`,
MARGIN, footY + 24, { lineBreak: false });
// ── 8. SIGNATURES ─────────────────────────────────────────────
const sigStartX = MARGIN + 310;
const sigW = (tableW - 313) / 2 - 4;
const sigH = 52;
const sigY = footY - 2;
const sigCollab = signatures.find(s => s.niveau === 'COLLAB');
const sigManager = signatures.find(s => ['N1', 'N2'].includes(s.niveau));
_drawSigBox(doc, sigCollab, sigStartX, sigY, sigW, sigH, 'Date et signature Collaborateur', false);
_drawSigBox(doc, sigManager, sigStartX + sigW + 6, sigY, sigW, sigH, 'Date et signature', true);
// ── 9. PIED DE PAGE ───────────────────────────────────────────
doc.font('Helvetica').fontSize(6).fillColor(C.light)
.text(
`Réf. ${reference} — Généré le ${new Date().toLocaleDateString('fr-FR')} — NDF ENSUP Groupe — Document électronique`,
MARGIN, PAGE_H - 13, { width: tableW, align: 'center', lineBreak: false }
);
doc.end();
});
}
// ─────────────────────────────────────────────────────────────────────────────
// _drawSigBox — boîte signature avec ou sans contenu
// ─────────────────────────────────────────────────────────────────────────────
function _drawSigBox(doc, sig, x, y, w, h, label, isManager) {
let bg, border, accent, icon;
if (sig) {
const a = sig.action || '';
if (a === 'refuser' || a === 'refuse') {
bg = C.refusBg; border = C.refusBorder; accent = C.refusText; icon = '✗ REFUSÉ';
} else if (isManager) {
bg = C.validBg; border = C.validBorder; accent = C.validText; icon = '✓ VALIDÉ';
} else {
bg = C.collabBg; border = C.collabBorder; accent = C.collabText; icon = '✓ SOUMIS';
}
} else {
bg = C.waitBg; border = C.waitBorder; accent = C.grey; icon = null;
}
drawRect(doc, x, y, w, h, bg, border, 1);
// Label haut
doc.font('Helvetica-Bold').fontSize(6.5).fillColor(C.grey)
.text(label, x + 4, y + 4, { width: w - 8, lineBreak: false });
if (sig) {
let nom = String(sig.nomPrenom || '');
const ds = fmtDateTime(sig.date);
const comment = String(sig.commentaire || '');
doc.font('Helvetica-Bold').fontSize(8).fillColor(accent)
.text(icon, x + 4, y + 14, { lineBreak: false });
doc.font('Helvetica-Bold').fontSize(9).fillColor(C.dark);
while (nom.length > 1 && doc.widthOfString(nom) > w - 10) nom = nom.slice(0, -1);
doc.text(nom, x + 4, y + 25, { width: w - 8, lineBreak: false });
doc.font('Helvetica').fontSize(7).fillColor(C.grey)
.text(`Le ${ds}`, x + 4, y + 36, { width: w - 8, lineBreak: false });
if (comment)
doc.font('Helvetica-Oblique').fontSize(6.5).fillColor(C.grey)
.text(comment, x + 4, y + 45, { width: w - 8, lineBreak: false });
doc.save().strokeColor(border).lineWidth(0.5)
.moveTo(x + 3, y + h - 9).lineTo(x + w - 3, y + h - 9).stroke().restore();
doc.font('Helvetica').fontSize(6).fillColor(C.grey)
.text('Signature — NDF ENSUP', x + 4, y + h - 7, { width: w - 8, align: 'center', lineBreak: false });
} else {
doc.font('Helvetica').fontSize(8).fillColor(C.grey)
.text('En attente de signature', x + 4, y + h / 2 - 5, { width: w - 8, align: 'center', lineBreak: false });
}
}
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
{
"name": "ndf-backend",
"version": "1.0.0",
"type": "module",
"main": "server.js",
"scripts": {
"start": "cross-env TZ=Europe/Paris node server.js",
"dev": "cross-env TZ=Europe/Paris nodemon server.js"
},
"dependencies": {
"@azure/msal-node": "^2.16.3",
"@microsoft/microsoft-graph-client": "^3.0.7",
"axios": "^1.13.5",
"cors": "^2.8.6",
"dotenv": "^16.6.1",
"express": "^4.22.1",
"isomorphic-fetch": "^3.0.0",
"jsonwebtoken": "^9.0.3",
"mssql": "^11.0.1",
"multer": "^2.0.2",
"pdf-lib": "^1.17.1",
"pdfkit": "^0.17.2"
},
"devDependencies": {
"@types/node": "^25.2.3",
"cross-env": "^10.1.0",
"ts-node": "^10.9.2",
"tsx": "^4.21.0"
}
}
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 865 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB