commit 88a0dbe6d2762016c4261596819f036c296b7ce5 Author: Nabil Derouiche Date: Thu Mar 12 23:47:10 2026 +0100 feat: RLA API v1.0.0 — API complète + exports + thème McKinsey - 9 endpoints métier : synthese, alertes, en-service, en-cours, par-region, clotures, pilotage-proactif, matrice-risque - Exports PDF (tous rôles) / XLSX PPTX DOCX (superadmin) - services/calc.js : helpers normalisés partagés - services/export-pdf.js : PDF async PDFKit par vue - Thème McKinsey (#1C2B4B / bleu pétrole / gris sobre) - Boutons XLSX/DOCX front (superadmin uniquement) - BASEROW_API_URL → https://baserow.bolbol.tn/api/ - dotenv override: true Co-Authored-By: Claude Sonnet 4.6 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e526ef3 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +JWT_SECRET=your_jwt_secret_here + +BASEROW_API_URL=https://baserow.bolbol.tn/api/ +BASEROW_TOKEN=your_baserow_token +BASEROW_TABLE_MARCHES=856 +BASEROW_TABLE_PIPELINE=872 + +PORT=3005 + +# Optionnel +SEUIL_STANDARD=70 +SEUIL_CRITIQUE_PCT=90 +DELAI_CRITIQUE=45 +DELAI_ATTENTION=90 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8fbdd8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +logs/*.json +*.log +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f1658b8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,15 @@ +# RLA API — Gestion des Marchés Tunisie Telecom Zone Sud +**Stack cible** : Node.js Express + Baserow + Docker (NAS 192.168.100.33) +**URL prod** : https://nd.i234.me/rla +**Fichiers existants** : index.html, config.js uniquement + +## Règles strictes (abonnement Pro limité en tokens) +- Lire UNIQUEMENT les fichiers demandés explicitement +- 1 tâche par session, pas de mélange de modules +- Ne jamais réécrire ce qui fonctionne déjà +- Réponses courtes et précises, sans blabla +- Travailler fichier par fichier + +## MCP disponibles +- baserow : http://192.168.100.33 (données marchés RLA) +- portainer : http://192.168.100.33:9000 (déploiement Docker) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dc90db6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json ./ +RUN npm install --omit=dev + +COPY . . + +EXPOSE 3005 + +CMD ["node", "server.js"] diff --git a/Nabil.Derouiche.jpg b/Nabil.Derouiche.jpg new file mode 100644 index 0000000..f5edcc4 Binary files /dev/null and b/Nabil.Derouiche.jpg differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f1ee99 --- /dev/null +++ b/README.md @@ -0,0 +1,140 @@ +# RLA API v1 — Marchés Tunisie Telecom Zone Sud + +API REST + tableau de bord pour la gestion des marchés publics Tunisie Telecom Zone Sud. + +**URL production :** `https://rla.bolbol.tn` + +--- + +## Architecture + +``` +index.html → Front-end SPA (9 vues, thèmes, exports) +server.js → Entry point Express (port 3005 par défaut) +routes/ → Contrôleurs HTTP +services/ → Logique métier (Baserow, calculs, exports) +middleware/ → Auth JWT + RBAC +data/users.json → Utilisateurs (bcrypt) +logs/ → Logs connexion JSON +``` + +## Endpoints API + +### Public +| Méthode | Endpoint | Description | +|---------|----------|-------------| +| GET | `/api/health` | Santé du service | +| POST | `/api/auth/login` | Authentification → JWT | + +### Données (auth requise) +| Méthode | Endpoint | Description | +|---------|----------|-------------| +| GET | `/api/synthese` | KPIs globaux + alertes + pilotage | +| GET | `/api/alertes` | Marchés avec alertes délai (≤90j) | +| GET | `/api/en-service` | Marchés actifs en service | +| GET | `/api/en-cours` | Marchés en cours (taux_phy < 100%) | +| GET | `/api/par-region` | Agrégation par région | +| GET | `/api/clotures` | Marchés clôturés | +| GET | `/api/pilotage-proactif` | Niveaux avancement (normal/sous/dépassé) | +| GET | `/api/matrice-risque` | Matrice risque 3×3 (délai × avancement) | +| GET | `/api/marches` | Liste complète marchés | +| GET | `/api/marches/:id` | Détail marché | +| GET | `/api/stats` | Statistiques (compat. ancien front) | + +### Exports (auth requise) +| Méthode | Endpoint | Rôle min. | Description | +|---------|----------|-----------|-------------| +| GET | `/api/export/pdf?view=` | user | PDF par vue | +| GET | `/api/export/xlsx?view=` | superadmin | Excel | +| GET | `/api/export/pptx?view=` | superadmin | PowerPoint | +| GET | `/api/export/docx?view=` | superadmin | Word | + +**Vues disponibles :** `synthese`, `alertes`, `en-service`, `en-cours`, `par-region`, `clotures`, `pilotage`, `matrice-risque` + +### Admin (superadmin) +| Méthode | Endpoint | Description | +|---------|----------|-------------| +| GET | `/api/users` | Liste utilisateurs | +| POST | `/api/users` | Créer utilisateur | +| DELETE | `/api/users/:id` | Supprimer utilisateur | +| GET | `/api/logs` | Logs connexion | +| GET | `/api/pipeline` | Pipeline AO (admin+) | + +## Paramètres de filtres + +Tous les endpoints données acceptent : + +| Paramètre | Type | Exemple | +|-----------|------|---------| +| `region` | string | `?region=Sfax` | +| `entrepreneur` | string | `?entrepreneur=Ent%20X` | +| `projet` | string | `?projet=4G` | +| `nature` | string | `?nature=CAPEX` | +| `statut` | string | `?statut=en+cours` | +| `niveau` | string | `?niveau=critique` (alertes) | + +## Variables d'environnement + +| Variable | Requis | Description | +|----------|--------|-------------| +| `JWT_SECRET` | ✓ | Clé secrète JWT | +| `BASEROW_API_URL` | ✓ | URL Baserow (`https://baserow.bolbol.tn/api/`) | +| `BASEROW_TOKEN` | ✓ | Token d'accès Baserow | +| `BASEROW_TABLE_MARCHES` | ✓ | ID table marchés (ex: 856) | +| `BASEROW_TABLE_PIPELINE` | ✓ | ID table pipeline (ex: 872) | +| `PORT` | — | Port serveur (défaut: 3001) | +| `USERS` | — | JSON utilisateurs (init si data/users.json absent) | +| `SEUIL_STANDARD` | — | Seuil avancement normal (défaut: 70) | +| `SEUIL_CRITIQUE_PCT` | — | Seuil avancement critique (défaut: 90) | +| `DELAI_CRITIQUE` | — | Jours alerte critique (défaut: 45) | +| `DELAI_ATTENTION` | — | Jours alerte attention (défaut: 90) | + +## Rôles + +| Rôle | PDF | PPTX/XLSX/DOCX | Pipeline | Users | Logs | +|------|-----|----------------|----------|-------|------| +| user | ✓ | ✗ | ✗ | ✗ | ✗ | +| admin | ✓ | ✗ | ✓ | ✗ | ✗ | +| superadmin | ✓ | ✓ | ✓ | ✓ | ✓ | + +## Démarrage + +```bash +npm install +cp .env.example .env # configurer les variables +npm start # production +npm run dev # développement (--watch) +``` + +## Structure des fichiers + +``` +routes/ + auth.js → POST /api/auth/login + marches.js → GET /api/marches[/:id] + stats.js → GET /api/stats + synthese.js → GET /api/synthese + alertes.js → GET /api/alertes + en-service.js → GET /api/en-service + en-cours.js → GET /api/en-cours + par-region.js → GET /api/par-region + clotures.js → GET /api/clotures + pilotage.js → GET /api/pilotage-proactif + matrice-risque.js → GET /api/matrice-risque + export.js → GET /api/export/[pdf|xlsx|pptx|docx] + pipeline.js → GET /api/pipeline + users.js → CRUD /api/users + logs.js → GET /api/logs + +services/ + baserow.js → Client Baserow (pagination auto) + calc.js → Calculs métier (alertes, risques, formatage) + export-pdf.js → Générateurs PDF par vue (PDFKit) + export-xlsx.js → Générateur Excel (ExcelJS) + users.js → CRUD fichier users.json + logs.js → Logs connexion JSON + +middleware/ + auth.js → Vérification JWT Bearer + roles.js → RBAC (requireUser/Admin/SuperAdmin + filterByRegion) +``` diff --git a/balise-TT.jpg b/balise-TT.jpg new file mode 100644 index 0000000..2af061f Binary files /dev/null and b/balise-TT.jpg differ diff --git a/config-old-statique-page.js b/config-old-statique-page.js new file mode 100644 index 0000000..79c0edb --- /dev/null +++ b/config-old-statique-page.js @@ -0,0 +1,33 @@ +// config.js - Configuration centralisée +const CONFIG = { + // API Baserow + API_URL: 'https://baserow.bolbol.tn/api/database/rows/table/856/', + API_URL_PIPELINE: 'https://baserow.bolbol.tn/api/database/rows/table/872/', + API_TOKEN: 'zJaDdkttN1gr6oPvd3cxfCXNwzvvwMMF', + + // Rafraîchissement (en minutes) + REFRESH_INTERVAL: 5, + + // Seuils alertes (en %) + SEUIL_STANDARD: 70, + SEUIL_MODERNISATION: 50, + SEUIL_CRITIQUE: 90, + + // Délais alertes (en jours) + DELAI_CRITIQUE: 45, + DELAI_ATTENTION: 90, + + // Thème par défaut + DEFAULT_THEME: 'light', + + // Couleurs régions + REGION_COLORS: { + 'Gabes': '#17A2B8', + 'Gafsa': '#22C55E', + 'Kebili': '#9333EA', + 'Medenine': '#0EA5E9', + 'Sfax': '#002855', + 'Tataouine': '#14B8A6', + 'Tozeur': '#818CF8' + } +}; \ No newline at end of file diff --git a/config.js b/config.js new file mode 100644 index 0000000..43e767e --- /dev/null +++ b/config.js @@ -0,0 +1,28 @@ +// config.js - Configuration centralisée +const CONFIG = { + // Rafraîchissement (en minutes) + REFRESH_INTERVAL: 60, + + // Seuils alertes (en %) + SEUIL_STANDARD: 70, + SEUIL_MODERNISATION: 50, + SEUIL_CRITIQUE: 90, + + // Délais alertes (en jours) + DELAI_CRITIQUE: 45, + DELAI_ATTENTION: 90, + + // Thème par défaut + DEFAULT_THEME: 'light', + + // Couleurs régions + REGION_COLORS: { + 'Gabes': '#17A2B8', + 'Gafsa': '#22C55E', + 'Kebili': '#9333EA', + 'Medenine': '#0EA5E9', + 'Sfax': '#002855', + 'Tataouine': '#14B8A6', + 'Tozeur': '#818CF8' + } +}; \ No newline at end of file diff --git a/data/users.json b/data/users.json new file mode 100644 index 0000000..a4bb25f --- /dev/null +++ b/data/users.json @@ -0,0 +1,23 @@ +[ + { + "id": 1, + "username": "nabil", + "password": "$2a$10$eQjI1sdYu4hgDnO3TanageD3/R.JqdWqAfzPVD4TxSW0Tjit0x8hu", + "role": "superadmin", + "region": "all" + }, + { + "id": 2, + "username": "admin", + "password": "$2a$10$CbmqfDC8gYg0oOzGD7T2seIWQf1.vQQQSsSQbvVjkwOybmtogTTzS", + "role": "admin", + "region": "all" + }, + { + "id": 3, + "username": "ikram", + "password": "$2a$10$pDeN2gaPE3GGXtScS.k9l.PAj99.Cxa637AbVrOlI7HbO9Cd4H2NW", + "role": "admin", + "region": "all" + } +] \ No newline at end of file diff --git a/index-old-statique-page.html b/index-old-statique-page.html new file mode 100644 index 0000000..f2e2925 --- /dev/null +++ b/index-old-statique-page.html @@ -0,0 +1,1360 @@ + + + + + + Marchés RLA - Zone Sud | Tunisie Telecom + + + + + + + + + +
+
Chargement des données...
+
+ + +

Aperçu PDF

+
+
Tunisie Telecom

Marchés RLA

Zone Sud — Tableau de Bord
+
+
+ +
+
Dernière mise à jour
--/--/---- --:--
+
+ +
+ +
+

Cartographie Marchés RLA

+
+
--
Marchés Actifs
--
+
--
CAPEX
--
+
--
OPEX
--
+
--
Alertes
+
--
Clôturés
+
+
+
+ + + + + + + + + Tozeur + Gafsa + Kebili + Sfax + Gabes + Medenine + Tataouine + +
+
+
+
+
+

Avancement vs Objectif - Marchés en Service

+
+
+
+ +
+

Alertes Consommation

+
+

Marchés à Surveiller

0 alertes
+
RéférenceEntrepreneurProjetPériodeAvancementDélaiStatut
+
+

Suivi Modernisation par Région

+
+

Marché en vigueur + Pipeline

0 régions
+
+ + + + + +
RégionMarché en vigueurAv. PhyEstimationStatut Pipeline
+
+

Alerte Lancement

+
+

Marchés à lancer par la Zone

0 marchés
+
RégionRéférenceProjetObservation
+
+

Pipeline de Lancement

+
+

Projets en Préparation (Table 872)

0 projets
+
+ + + + + +
ProjetRégionsEstimationDuréeStatut DCA
+
+
+ + +
+

Marchés En Service

+
+
Filtres Projets
Clic pour (dé)sélectionner — aucune sélection = tous les projets
+
Filtres Régions
Clic pour (dé)sélectionner — aucune sélection = toutes les régions
+
+
+
Entrepreneur
+ + + + +
+
+

En Cours d'Exécution

0 marchés
+
RéférenceProjetEntrepreneurMontant MaxPériodeAv. PhyAv. Fin
+
+
+ + +
+

Pilotage Proactif

+
+
--
Normal
Projection entre Min et Max
+
--
Sous Montant Min
Risque non-atteinte seuil
+
--
Dépassement
Projection > Montant Max
+
--
Indéterminé
Données insuffisantes
+
+
+
Filtres Projets
Clic pour (dé)sélectionner — aucune sélection = tous
+
Filtres Régions
Clic pour (dé)sélectionner — aucune sélection = toutes
+
+
+
Entrepreneur
+ + + + +
+

Synthèse Opérationnelle

+
+

Trajectoire par Région

+
+
Normal
+
Sous Min
+
Critique
+
Indéterminé
+
Épuisement
+
Aujourd'hui
+
+
+
+

Actions Prioritaires

+
+

Matrice de Risque par Région

+
+

Évaluation Risque

+
RégionMarchésBudgetAv. PhyAv. FinÉcart Phy-FinProjectionTendanceRisque
+
+
+ + +
+

Détail par Région

+
+
+ + +
+

Marchés En Cours

+
+

En Attente / Évaluation

0 marchés
+
RéférenceProjetEntrepreneurObservation
+
+
+
+ +
+ + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..cd8854d --- /dev/null +++ b/index.html @@ -0,0 +1,1779 @@ + + + + + + Marchés RLA - Zone Sud | Tunisie Telecom + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + +
+ + +
+
+
+
Vue générale
+
+
+
+
+ +
+ Mis à jour : +
+ +
+
+ + +
+ + +
+
+
Vue générale
+
+ +
+
+ + +
+
+
+
+
Total marchés
+
+
+
+
+
Marchés actifs
+
Avancement moyen : —
+
+
+
+
+
Alertes délais
+
Critiques (≤45j) : —
+
+
+
+
+
Clôturés
+
+
+ + +
+ +
+
+ Répartition par statut +
+
+ +
+
+ + +
+
+ Timeline marchés actifs +
+
+

+ Chargement du diagramme… +

+
+
+
+ + +
+
+ + Marchés en alerte — délais critiques +
+
+

Chargement…

+
+
+
+ + +
+
+
Alertes délais
+
+
+

Chargement…

+
+
+ + +
+
+
Liste des marchés
+
+
+
+
Marchés
+
+ + +
+ + + +
+
+ + + + + + + + + + + + + + + +
Référence Entrepreneur Projet Statut Avancement Période Montant
Chargement…
+
+
+
+
+ + +
+
+
Marchés en service
+
+
+
+ + + + + + + + + + +
RéférenceEntrepreneurRégionAvancementDate fin
Chargement…
+
+
+
+ + +
+
+
Pilotage proactif
+
+
+
+
+ + + + + + + +
RéférenceEntrepreneurRégionAvancementStatut proactif
Chargement…
+
+
+
+ + +
+
+
Vue par région
+
+
+

Chargement…

+
+
+ + +
+
+
Pipeline appels d'offres
+
+
+
+ + + + + + + + + + +
Référence AOProjetRégionStatutDate prévue
Chargement…
+
+
+
+ + +
+
+
Gestion des utilisateurs
+
+ +
+
+
+ +
+ + + + + + + +
#IdentifiantRôleRégionActions
Chargement…
+
+
+
+ + +
+
+
Historique des connexions
+
+
+
+ + + + + + + +
Date & heureUtilisateurRôleIPRésultat
Chargement…
+
+
+
+ +
+
+
+ + +
+ + +
+ + + + diff --git a/logo-TT.png b/logo-TT.png new file mode 100644 index 0000000..8c3e6e6 Binary files /dev/null and b/logo-TT.png differ diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..11c6328 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,17 @@ +const jwt = require('jsonwebtoken'); + +module.exports = function authMiddleware(req, res, next) { + const header = req.headers['authorization'] || ''; + const token = header.startsWith('Bearer ') ? header.slice(7) : null; + + if (!token) { + return res.status(401).json({ error: 'Token manquant' }); + } + + try { + req.user = jwt.verify(token, process.env.JWT_SECRET); + next(); + } catch { + res.status(401).json({ error: 'Token invalide ou expiré' }); + } +}; diff --git a/middleware/roles.js b/middleware/roles.js new file mode 100644 index 0000000..90075c0 --- /dev/null +++ b/middleware/roles.js @@ -0,0 +1,32 @@ +module.exports = { + + requireSuperAdmin(req, res, next) { + if (req.user?.role !== 'superadmin') { + return res.status(403).json({ error: 'Accès réservé au super-administrateur' }); + } + next(); + }, + + requireAdmin(req, res, next) { + if (!['superadmin', 'admin'].includes(req.user?.role)) { + return res.status(403).json({ error: 'Accès réservé aux administrateurs' }); + } + next(); + }, + + requireUser(req, res, next) { + if (!req.user?.role) { + return res.status(403).json({ error: 'Accès non autorisé' }); + } + next(); + }, + + // Injecte req.regionFilter : null = toutes régions, string = région filtrée + filterByRegion(req, res, next) { + req.regionFilter = (req.user?.role === 'user' && req.user.region !== 'all') + ? req.user.region + : null; + next(); + }, + +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7464431 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2364 @@ +{ + "name": "rla-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rla-api", + "version": "1.0.0", + "dependencies": { + "axios": "^1.7.2", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "docx": "^9.6.1", + "dotenv": "^16.4.5", + "exceljs": "^4.4.0", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", + "pdfkit": "^0.17.2", + "pptxgenjs": "^4.0.1" + } + }, + "node_modules/@fast-csv/format": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", + "integrity": "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isboolean": "^3.0.3", + "lodash.isequal": "^4.5.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0" + } + }, + "node_modules/@fast-csv/format/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@fast-csv/parse": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@fast-csv/parse/-/parse-4.3.6.tgz", + "integrity": "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==", + "license": "MIT", + "dependencies": { + "@types/node": "^14.0.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.groupby": "^4.6.0", + "lodash.isfunction": "^3.0.9", + "lodash.isnil": "^4.0.0", + "lodash.isundefined": "^3.0.1", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/@fast-csv/parse/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.19.tgz", + "integrity": "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/brotli": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.1.2" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", + "license": "MIT" + }, + "node_modules/docx": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/docx/-/docx-9.6.1.tgz", + "integrity": "sha512-ZJja9/KBUuFC109sCMzovoq2GR2wCG/AuxivjA+OHj/q0TEgJIm3S7yrlUxIy3B+bV8YDj/BiHfWyrRFmyWpDQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^25.2.3", + "hash.js": "^1.1.7", + "jszip": "^3.10.1", + "nanoid": "^5.1.3", + "xml": "^1.0.1", + "xml-js": "^1.6.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/exceljs": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/exceljs/-/exceljs-4.4.0.tgz", + "integrity": "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==", + "license": "MIT", + "dependencies": { + "archiver": "^5.0.0", + "dayjs": "^1.8.34", + "fast-csv": "^4.3.1", + "jszip": "^3.10.1", + "readable-stream": "^3.6.0", + "saxes": "^5.0.1", + "tmp": "^0.2.0", + "unzipper": "^0.10.11", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=8.3.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-csv": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fast-csv/-/fast-csv-4.3.6.tgz", + "integrity": "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==", + "license": "MIT", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@fast-csv/parse": "4.3.6" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fontkit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", + "integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.12", + "brotli": "^1.3.2", + "clone": "^2.1.2", + "dfa": "^1.2.0", + "fast-deep-equal": "^3.1.3", + "restructure": "^3.0.0", + "tiny-inflate": "^1.0.3", + "unicode-properties": "^1.4.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "license": "ISC" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jpeg-exif": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/linebreak/node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "license": "ISC" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isfunction": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", + "integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnil": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.isundefined": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", + "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz", + "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pdfkit": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz", + "integrity": "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==", + "license": "MIT", + "dependencies": { + "crypto-js": "^4.2.0", + "fontkit": "^2.0.4", + "jpeg-exif": "^1.1.4", + "linebreak": "^1.1.0", + "png-js": "^1.0.0" + } + }, + "node_modules/png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, + "node_modules/pptxgenjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz", + "integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==", + "license": "MIT", + "dependencies": { + "@types/node": "^22.8.1", + "https": "^1.0.0", + "image-size": "^1.2.1", + "jszip": "^3.10.1" + } + }, + "node_modules/pptxgenjs/node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/pptxgenjs/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/restructure": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", + "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unicode-properties": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "license": "MIT" + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "license": "MIT" + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "license": "MIT", + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b42d211 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "rla-api", + "version": "1.0.0", + "description": "API REST Marchés RLA - Tunisie Telecom Zone Sud", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "node --watch server.js" + }, + "dependencies": { + "axios": "^1.7.2", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "docx": "^9.6.1", + "dotenv": "^16.4.5", + "exceljs": "^4.4.0", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", + "pdfkit": "^0.17.2", + "pptxgenjs": "^4.0.1" + } +} diff --git a/routes/alertes.js b/routes/alertes.js new file mode 100644 index 0000000..015b8e6 --- /dev/null +++ b/routes/alertes.js @@ -0,0 +1,46 @@ +/** + * GET /api/alertes + * Alertes délais : marchés dont la date de fin approche + */ +const express = require('express'); +const router = express.Router(); +const { getMarches } = require('../services/baserow'); +const { + isCloture, getDelaiRestant, niveauAlerte, normalizeMarche, + DELAI_ATTENTION, +} = require('../services/calc'); + +router.get('/', async (req, res) => { + try { + const { region, entrepreneur, projet, niveau } = req.query; + const regionFilter = req.regionFilter; + + let rows = await getMarches(); + + // Filtres + if (regionFilter) rows = rows.filter(r => r.region === regionFilter); + else if (region) rows = rows.filter(r => r.region === region); + if (entrepreneur) rows = rows.filter(r => String(r.entrepreneur || '').toLowerCase().includes(entrepreneur.toLowerCase())); + if (projet) rows = rows.filter(r => String(r.projet || '').toLowerCase().includes(projet.toLowerCase())); + + // Uniquement marchés actifs avec alerte (délai ≤ DELAI_ATTENTION) + const actifs = rows.filter(r => !isCloture(r)); + const alertes = actifs + .map(r => ({ ...r, _delai: getDelaiRestant(r) })) + .filter(r => r._delai !== null && r._delai <= DELAI_ATTENTION) + .map(r => ({ ...normalizeMarche(r), delai_restant: r._delai, niveau: niveauAlerte(r._delai) })) + .filter(r => !niveau || r.niveau === niveau) + .sort((a, b) => a.delai_restant - b.delai_restant); + + res.json({ + count: alertes.length, + critique: alertes.filter(a => a.niveau === 'critique').length, + attention: alertes.filter(a => a.niveau === 'attention').length, + items: alertes, + }); + } catch (err) { + res.status(502).json({ error: 'Erreur Baserow', detail: err.message }); + } +}); + +module.exports = router; diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..c2caa8d --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,37 @@ +const express = require('express'); +const router = express.Router(); +const jwt = require('jsonwebtoken'); +const bcrypt = require('bcryptjs'); +const { getUsers } = require('../services/users'); +const { logLogin } = require('../services/logs'); + +// POST /api/auth/login +router.post('/login', async (req, res) => { + const { username, password } = req.body || {}; + const ip = req.ip || req.socket?.remoteAddress || null; + + const user = getUsers().find(u => u.username === username); + + if (!user) { + logLogin({ username: username || '?', role: null, ip, success: false }); + return res.status(401).json({ error: 'Identifiants invalides' }); + } + + const valid = await bcrypt.compare(password || '', user.password); + if (!valid) { + logLogin({ username, role: user.role, ip, success: false }); + return res.status(401).json({ error: 'Identifiants invalides' }); + } + + logLogin({ username, role: user.role, ip, success: true }); + + const token = jwt.sign( + { sub: user.username, id: user.id, username: user.username, role: user.role, region: user.region }, + process.env.JWT_SECRET, + { expiresIn: '8h' } + ); + + res.json({ token }); +}); + +module.exports = router; diff --git a/routes/clotures.js b/routes/clotures.js new file mode 100644 index 0000000..62b8e12 --- /dev/null +++ b/routes/clotures.js @@ -0,0 +1,50 @@ +/** + * GET /api/clotures + * Marchés clôturés + */ +const express = require('express'); +const router = express.Router(); +const { getMarches } = require('../services/baserow'); +const { isCloture, normalizeMarche, parseNum, formatMontant } = require('../services/calc'); + +router.get('/', async (req, res) => { + try { + const { region, entrepreneur, projet, nature } = req.query; + const regionFilter = req.regionFilter; + + let rows = await getMarches(); + + // Uniquement clôturés + rows = rows.filter(r => isCloture(r)); + + // Filtres + if (regionFilter) rows = rows.filter(r => r.region === regionFilter); + else if (region) rows = rows.filter(r => r.region === region); + if (entrepreneur) rows = rows.filter(r => String(r.entrepreneur || '').toLowerCase().includes(entrepreneur.toLowerCase())); + if (projet) rows = rows.filter(r => String(r.projet || '').toLowerCase().includes(projet.toLowerCase())); + if (nature) rows = rows.filter(r => String(r.nature || '').toLowerCase().includes(nature.toLowerCase())); + + const items = rows.map(normalizeMarche); + + const totalBudget = rows.reduce((s, r) => s + parseNum(r.tot_marche ?? r.totmarche ?? r.montant), 0); + + // Par région + const parRegion = {}; + for (const r of rows) { + const reg = r.region || 'Inconnu'; + parRegion[reg] = (parRegion[reg] || 0) + 1; + } + + res.json({ + count: items.length, + budget_total: formatMontant(totalBudget), + budget_total_raw: totalBudget, + par_region: parRegion, + items, + }); + } catch (err) { + res.status(502).json({ error: 'Erreur Baserow', detail: err.message }); + } +}); + +module.exports = router; diff --git a/routes/en-cours.js b/routes/en-cours.js new file mode 100644 index 0000000..6fa1552 --- /dev/null +++ b/routes/en-cours.js @@ -0,0 +1,49 @@ +/** + * GET /api/en-cours + * Marchés en cours (actifs, progress < 100%) + */ +const express = require('express'); +const router = express.Router(); +const { getMarches } = require('../services/baserow'); +const { isCloture, normalizeMarche, parseNum } = require('../services/calc'); + +router.get('/', async (req, res) => { + try { + const { region, entrepreneur, projet, nature, statut } = req.query; + const regionFilter = req.regionFilter; + + let rows = await getMarches(); + + // Actifs dont taux_phy < 100 + rows = rows.filter(r => { + if (isCloture(r)) return false; + const t = parseNum(r.taux_phy ?? r.avt_phy); + return t < 100; + }); + + // Filtres + if (regionFilter) rows = rows.filter(r => r.region === regionFilter); + else if (region) rows = rows.filter(r => r.region === region); + if (entrepreneur) rows = rows.filter(r => String(r.entrepreneur || '').toLowerCase().includes(entrepreneur.toLowerCase())); + if (projet) rows = rows.filter(r => String(r.projet || '').toLowerCase().includes(projet.toLowerCase())); + if (nature) rows = rows.filter(r => String(r.nature || '').toLowerCase().includes(nature.toLowerCase())); + if (statut) rows = rows.filter(r => String(r.statut || '').toLowerCase().includes(statut.toLowerCase())); + + const items = rows.map(normalizeMarche); + + const tauxList = items.map(r => r.taux_phy_raw).filter(v => v > 0); + const tauxMoyen = tauxList.length + ? Math.round(tauxList.reduce((a, b) => a + b, 0) / tauxList.length * 10) / 10 + : 0; + + res.json({ + count: items.length, + taux_avancement_moyen: tauxMoyen, + items, + }); + } catch (err) { + res.status(502).json({ error: 'Erreur Baserow', detail: err.message }); + } +}); + +module.exports = router; diff --git a/routes/en-service.js b/routes/en-service.js new file mode 100644 index 0000000..9c56c30 --- /dev/null +++ b/routes/en-service.js @@ -0,0 +1,48 @@ +/** + * GET /api/en-service + * Marchés actifs en service (non clôturés) + */ +const express = require('express'); +const router = express.Router(); +const { getMarches } = require('../services/baserow'); +const { isCloture, normalizeMarche, parseNum } = require('../services/calc'); + +router.get('/', async (req, res) => { + try { + const { region, entrepreneur, projet, nature, statut } = req.query; + const regionFilter = req.regionFilter; + + let rows = await getMarches(); + + // Uniquement non clôturés + rows = rows.filter(r => !isCloture(r)); + + // Filtres + if (regionFilter) rows = rows.filter(r => r.region === regionFilter); + else if (region) rows = rows.filter(r => r.region === region); + if (entrepreneur) rows = rows.filter(r => String(r.entrepreneur || '').toLowerCase().includes(entrepreneur.toLowerCase())); + if (projet) rows = rows.filter(r => String(r.projet || '').toLowerCase().includes(projet.toLowerCase())); + if (nature) rows = rows.filter(r => String(r.nature || '').toLowerCase().includes(nature.toLowerCase())); + if (statut) rows = rows.filter(r => String(r.statut || '').toLowerCase().includes(statut.toLowerCase())); + + const items = rows.map(normalizeMarche); + + // Agrégats + const totalBudget = rows.reduce((s, r) => s + parseNum(r.tot_marche ?? r.totmarche ?? r.montant), 0); + const tauxList = items.map(r => r.taux_phy_raw).filter(v => v > 0); + const tauxMoyen = tauxList.length + ? Math.round(tauxList.reduce((a, b) => a + b, 0) / tauxList.length * 10) / 10 + : 0; + + res.json({ + count: items.length, + budget_total_raw: totalBudget, + taux_avancement_moyen: tauxMoyen, + items, + }); + } catch (err) { + res.status(502).json({ error: 'Erreur Baserow', detail: err.message }); + } +}); + +module.exports = router; diff --git a/routes/export.js b/routes/export.js new file mode 100644 index 0000000..9a0b876 --- /dev/null +++ b/routes/export.js @@ -0,0 +1,310 @@ +/** + * routes/export.js + * Exports PDF, PPTX, XLSX, DOCX par vue + * + * PDF → tous les rôles authentifiés + * PPTX / XLSX / DOCX → SuperAdmin uniquement (vérifié ici) + */ +const express = require('express'); +const router = express.Router(); + +const { getMarches } = require('../services/baserow'); +const { + isCloture, normalizeMarche, parseNum, formatMontant, + getDelaiRestant, niveauAlerte, niveauAvancement, niveauRisque, + DELAI_CRITIQUE, DELAI_ATTENTION, SEUIL_STANDARD, SEUIL_CRITIQUE_PCT, +} = require('../services/calc'); + +const pdfGen = require('../services/export-pdf'); +const { generateXlsx } = require('../services/export-xlsx'); + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function applyFilters(rows, req) { + const { region, entrepreneur, projet, nature, statut } = req.query; + const regionFilter = req.regionFilter; + let r = rows; + if (regionFilter) r = r.filter(x => x.region === regionFilter); + else if (region) r = r.filter(x => x.region === region); + if (entrepreneur) r = r.filter(x => String(x.entrepreneur || '').toLowerCase().includes(entrepreneur.toLowerCase())); + if (projet) r = r.filter(x => String(x.projet || '').toLowerCase().includes(projet.toLowerCase())); + if (nature) r = r.filter(x => String(x.nature || '').toLowerCase().includes(nature.toLowerCase())); + if (statut) r = r.filter(x => String(x.statut || '').toLowerCase().includes(statut.toLowerCase())); + return r; +} + +async function buildViewData(view, req) { + const allRows = await getMarches(); + let rows = applyFilters(allRows, req); + + const actifs = rows.filter(r => !isCloture(r)); + const clotures = rows.filter(r => isCloture(r)); + + switch (view) { + case 'synthese': { + const tauxList = actifs.map(r => parseNum(r.taux_phy ?? r.avt_phy)).filter(v => v > 0); + const tauxMoyen = tauxList.length ? Math.round(tauxList.reduce((a,b)=>a+b,0)/tauxList.length*10)/10 : 0; + const totalBudget = actifs.reduce((s,r) => s+parseNum(r.tot_marche??r.totmarche??r.montant),0); + const parStatut = {}; + for (const r of rows) { const s=String(r.statut||'Inconnu'); parStatut[s]=(parStatut[s]||0)+1; } + const alertes = actifs + .map(r=>({...r,_d:getDelaiRestant(r)})) + .filter(r=>r._d!==null&&r._d<=DELAI_ATTENTION) + .map(r=>({ref:r.ref||'',projet:r.projet||'',region:r.region||'',entrepreneur:r.entrepreneur||'',delai_restant:r._d,niveau:niveauAlerte(r._d)})) + .sort((a,b)=>a.delai_restant-b.delai_restant); + return { + total: rows.length, actifs: actifs.length, clotures: clotures.length, + taux_avancement_moyen: tauxMoyen, par_statut: parStatut, + budget: { total: formatMontant(totalBudget), total_raw: totalBudget }, + alertes_delais: { count: alertes.length, critique: alertes.filter(a=>a.niveau==='critique').length, items: alertes }, + }; + } + case 'alertes': { + const items = actifs + .map(r=>({...r,_d:getDelaiRestant(r)})) + .filter(r=>r._d!==null&&r._d<=DELAI_ATTENTION) + .map(r=>({...normalizeMarche(r),delai_restant:r._d,niveau:niveauAlerte(r._d),niveau_alerte:niveauAlerte(r._d)})) + .sort((a,b)=>a.delai_restant-b.delai_restant); + return { count: items.length, critique: items.filter(a=>a.niveau==='critique').length, items }; + } + case 'en-service': + return { count: actifs.length, items: actifs.map(normalizeMarche) }; + case 'en-cours': { + const enCours = actifs.filter(r=>parseNum(r.taux_phy??r.avt_phy)<100); + return { count: enCours.length, items: enCours.map(normalizeMarche) }; + } + case 'par-region': { + const ALL_REGIONS = ['Gabes','Gafsa','Kebili','Medenine','Sfax','Tataouine','Tozeur']; + const regions = ALL_REGIONS.map(reg => { + const regActifs = actifs.filter(r=>(r.region||'')=== reg); + const regTotal = rows.filter(r=>(r.region||'')=== reg); + const tauxList = regActifs.map(r=>parseNum(r.taux_phy??r.avt_phy)).filter(v=>v>0); + const tauxMoyen = tauxList.length ? Math.round(tauxList.reduce((a,b)=>a+b,0)/tauxList.length*10)/10 : 0; + const budget = regActifs.reduce((s,r)=>s+parseNum(r.tot_marche??r.totmarche??r.montant),0); + const alertes = regActifs + .map(r=>({...r,_d:getDelaiRestant(r)})) + .filter(r=>r._d!==null&&r._d<=DELAI_ATTENTION); + return { region: reg, actifs: regActifs.length, clotures: regTotal.length-regActifs.length, total: regTotal.length, + taux_moyen: tauxMoyen, budget: formatMontant(budget), alertes_count: alertes.length, + alertes_critique: alertes.filter(r=>r._d<=DELAI_CRITIQUE).length }; + }); + return { count: regions.length, regions }; + } + case 'clotures': { + const totalBudget = clotures.reduce((s,r)=>s+parseNum(r.tot_marche??r.totmarche??r.montant),0); + return { count: clotures.length, budget_total: formatMontant(totalBudget), items: clotures.map(normalizeMarche) }; + } + case 'pilotage': { + const items = actifs.map(r=>{ + const d = getDelaiRestant(r); + return {...normalizeMarche(r), delai_restant:d, niveau_alerte:niveauAlerte(d), niveau_avancement:niveauAvancement(r.taux_phy??r.avt_phy,r.nature)}; + }); + const normal = items.filter(r=>r.niveau_avancement==='normal'); + const sous = items.filter(r=>r.niveau_avancement==='sous_avancement'); + const dep = items.filter(r=>r.niveau_avancement==='dépassé'); + return { resume:{total:items.length,normal:normal.length,sous_avancement:sous.length,depasse:dep.length}, + normal, sous_avancement:sous, depasse:dep, items }; + } + case 'matrice-risque': { + const items = actifs.map(r=>{ + const d=getDelaiRestant(r); + return {...normalizeMarche(r),delai_restant:d,niveau_alerte:niveauAlerte(d),niveau_risque:niveauRisque(r), + score_delai: d===null?1:d<=DELAI_CRITIQUE?3:d<=DELAI_ATTENTION?2:1, + score_avancement: parseNum(r.taux_phy??r.avt_phy)>=SEUIL_CRITIQUE_PCT?3:parseNum(r.taux_phy??r.avt_phy)>=SEUIL_STANDARD?2:1, + }; + }); + const pn={critique:0,élevé:0,moyen:0,faible:0}; + for(const i of items){ if(pn[i.niveau_risque]!==undefined) pn[i.niveau_risque]++; else pn[i.niveau_risque]=1; } + return { total:items.length, par_niveau:pn, items }; + } + default: + return { items: actifs.map(normalizeMarche) }; + } +} + +// ─── Route PDF ──────────────────────────────────────────────────────────────── + +router.get('/pdf', async (req, res) => { + try { + const view = req.query.view || 'synthese'; + const data = await buildViewData(view, req); + + let buf; + switch (view) { + case 'synthese': buf = await pdfGen.generateSynthese(data); break; + case 'alertes': buf = await pdfGen.generateAlertes(data); break; + case 'en-service': buf = await pdfGen.generateEnService(data); break; + case 'en-cours': buf = await pdfGen.generateEnCours(data); break; + case 'par-region': buf = await pdfGen.generateParRegion(data); break; + case 'clotures': buf = await pdfGen.generateClotures(data); break; + case 'pilotage': buf = await pdfGen.generatePilotage(data); break; + case 'matrice-risque': buf = await pdfGen.generateMatriceRisque(data); break; + default: buf = await pdfGen.generateGeneric(view, data); break; + } + + const filename = `RLA_${view}_${new Date().toISOString().slice(0,10)}.pdf`; + res.set({ + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${filename}"`, + 'Content-Length': buf.length, + }); + res.end(buf); + } catch (err) { + res.status(502).json({ error: 'Erreur génération PDF', detail: err.message }); + } +}); + +// ─── Route XLSX (SuperAdmin) ────────────────────────────────────────────────── + +router.get('/xlsx', async (req, res) => { + if (req.user?.role !== 'superadmin') { + return res.status(403).json({ error: 'Accès réservé au SuperAdmin' }); + } + try { + const view = req.query.view || 'synthese'; + const data = await buildViewData(view, req); + const buf = await generateXlsx(view, data); + const filename = `RLA_${view}_${new Date().toISOString().slice(0,10)}.xlsx`; + res.set({ + 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'Content-Disposition': `attachment; filename="${filename}"`, + }); + res.end(buf); + } catch (err) { + res.status(502).json({ error: 'Erreur génération XLSX', detail: err.message }); + } +}); + +// ─── Route PPTX (SuperAdmin) ────────────────────────────────────────────────── + +router.get('/pptx', async (req, res) => { + if (req.user?.role !== 'superadmin') { + return res.status(403).json({ error: 'Accès réservé au SuperAdmin' }); + } + try { + const PptxGenJS = require('pptxgenjs'); + const view = req.query.view || 'synthese'; + const data = await buildViewData(view, req); + + const pptx = new PptxGenJS(); + pptx.layout = 'LAYOUT_WIDE'; + pptx.author = 'RLA API'; + pptx.company = 'Tunisie Telecom Zone Sud'; + pptx.subject = `RLA — ${view}`; + + // Slide de titre + const slide1 = pptx.addSlide(); + slide1.background = { color: '002D62' }; + slide1.addText(`RLA — ${view.toUpperCase()}`, { + x: 0.5, y: 2, w: '90%', h: 1.2, + fontSize: 36, bold: true, color: 'FFFFFF', align: 'center', + }); + slide1.addText('Marchés Tunisie Telecom Zone Sud', { + x: 0.5, y: 3.4, w: '90%', h: 0.5, + fontSize: 16, color: 'B3C5E0', align: 'center', + }); + slide1.addText(new Date().toLocaleDateString('fr-FR'), { + x: 0.5, y: 4, w: '90%', h: 0.4, + fontSize: 12, color: 'E31837', align: 'center', + }); + + // Slide données + const slide2 = pptx.addSlide(); + slide2.addText(`Données — ${view}`, { + x: 0.3, y: 0.2, w: '95%', h: 0.5, + fontSize: 18, bold: true, color: '002D62', + }); + + // Table si items + const items = data.items || data.regions || []; + if (items.length) { + const sample = items[0]; + const keys = Object.keys(sample).filter(k => !k.endsWith('_raw') && k!=='id' && typeof sample[k]!=='object').slice(0,7); + const tableData = [ + keys.map(k => ({ text: k, options: { bold: true, color: 'FFFFFF', fill: '002D62' } })), + ...items.slice(0, 20).map(item => + keys.map(k => ({ text: String(item[k] ?? '—') })) + ), + ]; + slide2.addTable(tableData, { + x: 0.3, y: 0.9, w: 9.4, + fontSize: 9, + border: { type: 'solid', color: 'E2E8F0' }, + colW: keys.map(() => +(9.4 / keys.length).toFixed(2)), + }); + } + + const filename = `RLA_${view}_${new Date().toISOString().slice(0,10)}.pptx`; + const buf = await pptx.write({ outputType: 'nodebuffer' }); + res.set({ + 'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'Content-Disposition': `attachment; filename="${filename}"`, + }); + res.end(buf); + } catch (err) { + res.status(502).json({ error: 'Erreur génération PPTX', detail: err.message }); + } +}); + +// ─── Route DOCX (SuperAdmin) ────────────────────────────────────────────────── + +router.get('/docx', async (req, res) => { + if (req.user?.role !== 'superadmin') { + return res.status(403).json({ error: 'Accès réservé au SuperAdmin' }); + } + try { + const { Document, Packer, Paragraph, Table, TableRow, TableCell, TextRun, HeadingLevel, AlignmentType, WidthType, BorderStyle } = require('docx'); + const view = req.query.view || 'synthese'; + const data = await buildViewData(view, req); + const items = data.items || data.regions || []; + + const children = [ + new Paragraph({ + text: `RLA — ${view.toUpperCase()}`, + heading: HeadingLevel.HEADING_1, + }), + new Paragraph({ + text: `Marchés Tunisie Telecom Zone Sud — Édité le ${new Date().toLocaleDateString('fr-FR')}`, + children: [new TextRun({ text: '', break: 1 })], + }), + ]; + + if (items.length) { + const sample = items[0]; + const keys = Object.keys(sample).filter(k => !k.endsWith('_raw') && k!=='id' && typeof sample[k]!=='object').slice(0,7); + + const tableRows = [ + new TableRow({ + children: keys.map(k => new TableCell({ + children: [new Paragraph({ children: [new TextRun({ text: k, bold: true, color: 'FFFFFF' })], alignment: AlignmentType.CENTER })], + shading: { fill: '002D62' }, + })), + }), + ...items.slice(0, 50).map((item, i) => new TableRow({ + children: keys.map(k => new TableCell({ + children: [new Paragraph(String(item[k] ?? '—'))], + shading: i % 2 === 1 ? { fill: 'F1F5F9' } : undefined, + })), + })), + ]; + + children.push(new Table({ + rows: tableRows, + width: { size: 100, type: WidthType.PERCENTAGE }, + })); + } + + const doc = new Document({ sections: [{ children }] }); + const buf = await Packer.toBuffer(doc); + const filename = `RLA_${view}_${new Date().toISOString().slice(0,10)}.docx`; + res.set({ + 'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'Content-Disposition': `attachment; filename="${filename}"`, + }); + res.end(buf); + } catch (err) { + res.status(502).json({ error: 'Erreur génération DOCX', detail: err.message }); + } +}); + +module.exports = router; diff --git a/routes/logs.js b/routes/logs.js new file mode 100644 index 0000000..3ab79eb --- /dev/null +++ b/routes/logs.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); +const { getLogs } = require('../services/logs'); + +// GET /api/logs +router.get('/', (req, res) => { + res.json(getLogs(100)); +}); + +module.exports = router; diff --git a/routes/marches.js b/routes/marches.js new file mode 100644 index 0000000..1cbc78a --- /dev/null +++ b/routes/marches.js @@ -0,0 +1,26 @@ +const express = require('express'); +const router = express.Router(); +const { getMarches, getMarcheById } = require('../services/baserow'); + +// GET /api/marches +router.get('/', async (req, res) => { + try { + const rows = await getMarches(); + res.json({ count: rows.length, results: rows }); + } catch (err) { + res.status(502).json({ error: 'Erreur Baserow', detail: err.message }); + } +}); + +// GET /api/marches/:id +router.get('/:id', async (req, res) => { + try { + const row = await getMarcheById(req.params.id); + res.json(row); + } catch (err) { + const status = err.response?.status === 404 ? 404 : 502; + res.status(status).json({ error: 'Marché introuvable', detail: err.message }); + } +}); + +module.exports = router; diff --git a/routes/matrice-risque.js b/routes/matrice-risque.js new file mode 100644 index 0000000..d3687c9 --- /dev/null +++ b/routes/matrice-risque.js @@ -0,0 +1,90 @@ +/** + * GET /api/matrice-risque + * Matrice de risque : probabilité × impact (délai × avancement) + */ +const express = require('express'); +const router = express.Router(); +const { getMarches } = require('../services/baserow'); +const { + isCloture, normalizeMarche, + getDelaiRestant, niveauAlerte, niveauRisque, + parseNum, SEUIL_STANDARD, SEUIL_CRITIQUE_PCT, + DELAI_CRITIQUE, DELAI_ATTENTION, +} = require('../services/calc'); + +/** + * Score délai : 1 (ok) → 3 (critique) + */ +function scoreDelai(delai) { + if (delai === null) return 1; // inconnu → faible + if (delai <= DELAI_CRITIQUE) return 3; + if (delai <= DELAI_ATTENTION) return 2; + return 1; +} + +/** + * Score avancement : 1 (ok) → 3 (dépassé) + */ +function scoreAvancement(tauxPhy) { + const t = parseNum(tauxPhy); + if (t >= SEUIL_CRITIQUE_PCT) return 3; + if (t >= SEUIL_STANDARD) return 2; + return 1; +} + +router.get('/', async (req, res) => { + try { + const { region, entrepreneur, nature } = req.query; + const regionFilter = req.regionFilter; + + let rows = await getMarches(); + rows = rows.filter(r => !isCloture(r)); + + if (regionFilter) rows = rows.filter(r => r.region === regionFilter); + else if (region) rows = rows.filter(r => r.region === region); + if (entrepreneur) rows = rows.filter(r => String(r.entrepreneur || '').toLowerCase().includes(entrepreneur.toLowerCase())); + if (nature) rows = rows.filter(r => String(r.nature || '').toLowerCase().includes(nature.toLowerCase())); + + // Matrice 3×3 : delai (1-3) × avancement (1-3) + const matrice = {}; + for (let d = 1; d <= 3; d++) { + for (let a = 1; a <= 3; a++) { + matrice[`${d}_${a}`] = { score_delai: d, score_avancement: a, items: [] }; + } + } + + const items = rows.map(r => { + const delai = getDelaiRestant(r); + const sd = scoreDelai(delai); + const sa = scoreAvancement(r.taux_phy ?? r.avt_phy); + const risque = niveauRisque(r); + const m = normalizeMarche(r); + return { ...m, score_delai: sd, score_avancement: sa, niveau_risque: risque }; + }); + + // Remplissage matrice + for (const item of items) { + const key = `${item.score_delai}_${item.score_avancement}`; + if (matrice[key]) matrice[key].items.push(item); + } + + // Comptage par niveau de risque + const parNiveau = { critique: 0, élevé: 0, moyen: 0, faible: 0 }; + for (const item of items) { + const n = item.niveau_risque; + if (parNiveau[n] !== undefined) parNiveau[n]++; + else parNiveau[n] = 1; + } + + res.json({ + total: items.length, + par_niveau: parNiveau, + matrice, // grille 3×3 + items, // liste complète triée par risque décroissant + }); + } catch (err) { + res.status(502).json({ error: 'Erreur Baserow', detail: err.message }); + } +}); + +module.exports = router; diff --git a/routes/par-region.js b/routes/par-region.js new file mode 100644 index 0000000..1358711 --- /dev/null +++ b/routes/par-region.js @@ -0,0 +1,73 @@ +/** + * GET /api/par-region + * Agrégation des marchés par région + */ +const express = require('express'); +const router = express.Router(); +const { getMarches } = require('../services/baserow'); +const { + isCloture, parseNum, formatMontant, getDelaiRestant, + niveauAlerte, DELAI_ATTENTION, +} = require('../services/calc'); + +const ALL_REGIONS = ['Gabes', 'Gafsa', 'Kebili', 'Medenine', 'Sfax', 'Tataouine', 'Tozeur']; + +router.get('/', async (req, res) => { + try { + const { nature, entrepreneur, statut } = req.query; + const regionFilter = req.regionFilter; + + let rows = await getMarches(); + + // Filtres complémentaires + if (nature) rows = rows.filter(r => String(r.nature || '').toLowerCase().includes(nature.toLowerCase())); + if (entrepreneur) rows = rows.filter(r => String(r.entrepreneur || '').toLowerCase().includes(entrepreneur.toLowerCase())); + if (statut) rows = rows.filter(r => String(r.statut || '').toLowerCase().includes(statut.toLowerCase())); + + const activeRows = rows.filter(r => !isCloture(r)); + + const regions = (regionFilter ? [regionFilter] : ALL_REGIONS).map(reg => { + const regRows = activeRows.filter(r => (r.region || r.region_csc || '') === reg); + const regTotal = rows.filter(r => (r.region || r.region_csc || '') === reg); + const clotures = regTotal.filter(r => isCloture(r)).length; + + const budget = regRows.reduce((s, r) => s + parseNum(r.tot_marche ?? r.totmarche ?? r.montant), 0); + const tauxList = regRows.map(r => parseNum(r.taux_phy ?? r.avt_phy)).filter(v => v > 0); + const tauxMoyen = tauxList.length + ? Math.round(tauxList.reduce((a, b) => a + b, 0) / tauxList.length * 10) / 10 + : 0; + + const alertes = regRows + .map(r => ({ ...r, _delai: getDelaiRestant(r) })) + .filter(r => r._delai !== null && r._delai <= DELAI_ATTENTION) + .map(r => ({ id: r.id, ref: r.ref || '', projet: r.projet || '', delai_restant: r._delai, niveau: niveauAlerte(r._delai) })) + .sort((a, b) => a.delai_restant - b.delai_restant); + + const parNature = {}; + for (const r of regRows) { + const n = r.nature || 'Non défini'; + parNature[n] = (parNature[n] || 0) + 1; + } + + return { + region: reg, + actifs: regRows.length, + clotures, + total: regTotal.length, + budget_raw: budget, + budget: formatMontant(budget), + taux_moyen: tauxMoyen, + alertes_count: alertes.length, + alertes_critique: alertes.filter(a => a.niveau === 'critique').length, + alertes, + par_nature: parNature, + }; + }); + + res.json({ count: regions.length, regions }); + } catch (err) { + res.status(502).json({ error: 'Erreur Baserow', detail: err.message }); + } +}); + +module.exports = router; diff --git a/routes/pilotage.js b/routes/pilotage.js new file mode 100644 index 0000000..dda2a7f --- /dev/null +++ b/routes/pilotage.js @@ -0,0 +1,73 @@ +/** + * GET /api/pilotage-proactif + * Pilotage proactif : classement par niveau d'avancement vs seuils + */ +const express = require('express'); +const router = express.Router(); +const { getMarches } = require('../services/baserow'); +const { + isCloture, normalizeMarche, parseNum, + niveauAvancement, getDelaiRestant, niveauAlerte, + SEUIL_STANDARD, SEUIL_MODERNISATION, SEUIL_CRITIQUE_PCT, +} = require('../services/calc'); + +router.get('/', async (req, res) => { + try { + const { region, entrepreneur, nature, niveau } = req.query; + const regionFilter = req.regionFilter; + + let rows = await getMarches(); + + rows = rows.filter(r => !isCloture(r)); + + // Filtres + if (regionFilter) rows = rows.filter(r => r.region === regionFilter); + else if (region) rows = rows.filter(r => r.region === region); + if (entrepreneur) rows = rows.filter(r => String(r.entrepreneur || '').toLowerCase().includes(entrepreneur.toLowerCase())); + if (nature) rows = rows.filter(r => String(r.nature || '').toLowerCase().includes(nature.toLowerCase())); + + const items = rows.map(r => { + const m = normalizeMarche(r); + const delai = getDelaiRestant(r); + return { + ...m, + delai_restant: delai, + niveau_alerte: niveauAlerte(delai), + niveau_avancement: niveauAvancement(r.taux_phy ?? r.avt_phy, r.nature), + }; + }); + + // Groupement + const normal = items.filter(r => r.niveau_avancement === 'normal'); + const sous_avancement = items.filter(r => r.niveau_avancement === 'sous_avancement'); + const depasse = items.filter(r => r.niveau_avancement === 'dépassé'); + + // Si filtre sur niveau + let result = items; + if (niveau === 'normal') result = normal; + else if (niveau === 'sous') result = sous_avancement; + else if (niveau === 'depasse') result = depasse; + + res.json({ + seuils: { + standard: SEUIL_STANDARD, + modernisation: SEUIL_MODERNISATION, + critique: SEUIL_CRITIQUE_PCT, + }, + resume: { + normal: normal.length, + sous_avancement: sous_avancement.length, + depasse: depasse.length, + total: items.length, + }, + normal, + sous_avancement, + depasse, + items: result, + }); + } catch (err) { + res.status(502).json({ error: 'Erreur Baserow', detail: err.message }); + } +}); + +module.exports = router; diff --git a/routes/pipeline.js b/routes/pipeline.js new file mode 100644 index 0000000..c36bf91 --- /dev/null +++ b/routes/pipeline.js @@ -0,0 +1,15 @@ +const express = require('express'); +const router = express.Router(); +const { getPipeline } = require('../services/baserow'); + +// GET /api/pipeline +router.get('/', async (req, res) => { + try { + const rows = await getPipeline(); + res.json({ count: rows.length, results: rows }); + } catch (err) { + res.status(502).json({ error: 'Erreur Baserow', detail: err.message }); + } +}); + +module.exports = router; diff --git a/routes/stats.js b/routes/stats.js new file mode 100644 index 0000000..458570b --- /dev/null +++ b/routes/stats.js @@ -0,0 +1,80 @@ +const express = require('express'); +const router = express.Router(); +const { getMarches } = require('../services/baserow'); + +const DELAI_CRITIQUE = 45; +const DELAI_ATTENTION = 90; + +function parseNum(v) { + const n = parseFloat(String(v || '').replace(',', '.')); + return isNaN(n) ? 0 : n; +} + +function getDelaiRestant(r) { + if (r.delai_restant != null) return parseInt(r.delai_restant, 10); + const fin = r.date_fin || r.datefin; + if (!fin) return null; + const d = new Date(fin); + if (isNaN(d.getTime())) return null; + return Math.ceil((d - new Date()) / 86400000); +} + +function isCloture(r) { + const obs = String(r.observation || '').toLowerCase(); + return obs.includes('clôtur') || obs.includes('clotur') || !!r.date_cloture; +} + +// GET /api/stats +router.get('/', async (req, res) => { + try { + const rows = await getMarches(); + + const actifs = rows.filter(r => !isCloture(r)); + + // Nb marchés par statut + const parStatut = {}; + for (const r of rows) { + const s = String(r.statut || 'Inconnu'); + parStatut[s] = (parStatut[s] || 0) + 1; + } + + // Taux d'avancement physique moyen (marchés actifs) + const tauxList = actifs.map(r => parseNum(r.taux_phy)).filter(v => v > 0); + const tauxMoyen = tauxList.length + ? Math.round(tauxList.reduce((a, b) => a + b, 0) / tauxList.length * 10) / 10 + : 0; + + // Alertes délais + const alertes = actifs + .map(r => ({ ...r, _delai: getDelaiRestant(r) })) + .filter(r => r._delai !== null && r._delai <= DELAI_ATTENTION) + .map(r => ({ + id: r.id, + ref: r.ref || r.reference || '', + entrepreneur: r.entrepreneur || '', + projet: r.projet || '', + region: r.region || '', + avt_fin: parseNum(r.avt_fin ?? r.avtfin), + delai_restant: r._delai, + niveau: r._delai <= DELAI_CRITIQUE ? 'critique' : 'attention', + })); + + res.json({ + total: rows.length, + actifs: actifs.length, + clotures: rows.length - actifs.length, + par_statut: parStatut, + taux_avancement_moyen: tauxMoyen, + alertes_delais: { + count: alertes.length, + critique: alertes.filter(a => a.niveau === 'critique').length, + attention: alertes.filter(a => a.niveau === 'attention').length, + items: alertes, + }, + }); + } catch (err) { + res.status(502).json({ error: 'Erreur Baserow', detail: err.message }); + } +}); + +module.exports = router; diff --git a/routes/synthese.js b/routes/synthese.js new file mode 100644 index 0000000..f099fe0 --- /dev/null +++ b/routes/synthese.js @@ -0,0 +1,122 @@ +/** + * GET /api/synthese + * Vue synthèse globale : KPIs, répartitions, alertes, projections + */ +const express = require('express'); +const router = express.Router(); +const { getMarches } = require('../services/baserow'); +const { + parseNum, formatMontant, isCloture, + getDelaiRestant, niveauAlerte, niveauAvancement, + DELAI_CRITIQUE, DELAI_ATTENTION, +} = require('../services/calc'); + +router.get('/', async (req, res) => { + try { + const { region, nature, entrepreneur, projet } = req.query; + const regionFilter = req.regionFilter; // set by filterByRegion middleware + + let rows = await getMarches(); + + // Filtres + if (regionFilter) rows = rows.filter(r => r.region === regionFilter); + else if (region) rows = rows.filter(r => r.region === region); + if (nature) rows = rows.filter(r => String(r.nature || '').toLowerCase().includes(nature.toLowerCase())); + if (entrepreneur) rows = rows.filter(r => String(r.entrepreneur || '').toLowerCase().includes(entrepreneur.toLowerCase())); + if (projet) rows = rows.filter(r => String(r.projet || '').toLowerCase().includes(projet.toLowerCase())); + + const actifs = rows.filter(r => !isCloture(r)); + const clotures = rows.filter(r => isCloture(r)); + + // Montants + const totalBudget = actifs.reduce((s, r) => s + parseNum(r.tot_marche ?? r.totmarche ?? r.montant), 0); + const totalConsomme = actifs.reduce((s, r) => s + parseNum(r.consomme ?? r.montant_consomme ?? 0), 0); + + // Avancement moyen physique + const tauxList = actifs.map(r => parseNum(r.taux_phy ?? r.avt_phy)).filter(v => v > 0); + const tauxMoyen = tauxList.length + ? Math.round(tauxList.reduce((a, b) => a + b, 0) / tauxList.length * 10) / 10 + : 0; + + // Par statut + const parStatut = {}; + for (const r of rows) { + const s = String(r.statut || 'Inconnu'); + parStatut[s] = (parStatut[s] || 0) + 1; + } + + // Par région + const parRegion = {}; + for (const r of actifs) { + const reg = r.region || 'Inconnu'; + if (!parRegion[reg]) parRegion[reg] = { count: 0, taux_sum: 0, taux_count: 0 }; + parRegion[reg].count++; + const t = parseNum(r.taux_phy ?? r.avt_phy); + if (t > 0) { parRegion[reg].taux_sum += t; parRegion[reg].taux_count++; } + } + for (const reg of Object.keys(parRegion)) { + const d = parRegion[reg]; + parRegion[reg].taux_moyen = d.taux_count ? Math.round(d.taux_sum / d.taux_count * 10) / 10 : 0; + } + + // Par nature (CAPEX/OPEX) + const parNature = {}; + for (const r of actifs) { + const n = r.nature || 'Non défini'; + parNature[n] = (parNature[n] || 0) + 1; + } + + // Alertes délais + const alertes = actifs + .map(r => ({ ...r, _delai: getDelaiRestant(r) })) + .filter(r => r._delai !== null && r._delai <= DELAI_ATTENTION) + .map(r => ({ + id: r.id, + ref: r.ref || r.reference || '', + projet: r.projet || '', + region: r.region || '', + entrepreneur: r.entrepreneur || '', + delai_restant: r._delai, + niveau: niveauAlerte(r._delai), + })) + .sort((a, b) => a.delai_restant - b.delai_restant); + + // Pilotage proactif (niveaux d'avancement) + const pilotage = { normal: 0, sous_avancement: 0, depasse: 0 }; + for (const r of actifs) { + const n = niveauAvancement(r.taux_phy ?? r.avt_phy, r.nature); + if (n === 'normal') pilotage.normal++; + else if (n === 'sous_avancement') pilotage.sous_avancement++; + else pilotage.depasse++; + } + + res.json({ + total: rows.length, + actifs: actifs.length, + clotures: clotures.length, + budget: { + total: formatMontant(totalBudget), + total_raw: totalBudget, + consomme: formatMontant(totalConsomme), + consomme_raw: totalConsomme, + restant: formatMontant(totalBudget - totalConsomme), + restant_raw: totalBudget - totalConsomme, + }, + taux_avancement_moyen: tauxMoyen, + par_statut: parStatut, + par_region: parRegion, + par_nature: parNature, + alertes_delais: { + count: alertes.length, + critique: alertes.filter(a => a.niveau === 'critique').length, + attention: alertes.filter(a => a.niveau === 'attention').length, + items: alertes, + }, + pilotage, + }); + } catch (err) { + res.status(502).json({ error: 'Erreur Baserow', detail: err.message }); + } +}); + +module.exports = router; diff --git a/routes/users.js b/routes/users.js new file mode 100644 index 0000000..1fe1b99 --- /dev/null +++ b/routes/users.js @@ -0,0 +1,53 @@ +const express = require('express'); +const router = express.Router(); +const bcrypt = require('bcryptjs'); +const { getUsers, saveUsers } = require('../services/users'); + +// GET /api/users — liste sans mot de passe +router.get('/', (req, res) => { + const users = getUsers().map(({ password, ...u }) => u); + res.json(users); +}); + +// POST /api/users — créer un utilisateur +router.post('/', async (req, res) => { + const { username, password, role = 'user', region = 'all' } = req.body || {}; + + if (!username || !password) { + return res.status(400).json({ error: 'username et password requis' }); + } + + const users = getUsers(); + if (users.find(u => u.username === username)) { + return res.status(409).json({ error: 'Identifiant déjà utilisé' }); + } + + const id = Math.max(0, ...users.map(u => u.id || 0)) + 1; + const hash = await bcrypt.hash(password, 10); + const newUser = { id, username, password: hash, role, region }; + + users.push(newUser); + saveUsers(users); + + res.status(201).json({ id, username, role, region }); +}); + +// DELETE /api/users/:id — supprimer un utilisateur +router.delete('/:id', (req, res) => { + const id = parseInt(req.params.id, 10); + const users = getUsers(); + const idx = users.findIndex(u => u.id === id); + + if (idx === -1) { + return res.status(404).json({ error: 'Utilisateur introuvable' }); + } + if (users[idx].username === req.user.username) { + return res.status(400).json({ error: 'Impossible de supprimer son propre compte' }); + } + + users.splice(idx, 1); + saveUsers(users); + res.json({ ok: true }); +}); + +module.exports = router; diff --git a/server.js b/server.js new file mode 100644 index 0000000..bdf2bc7 --- /dev/null +++ b/server.js @@ -0,0 +1,51 @@ +require('dotenv').config({ override: true }); +const express = require('express'); +const cors = require('cors'); + +const auth = require('./middleware/auth'); +const { requireSuperAdmin, requireAdmin, requireUser, filterByRegion } = require('./middleware/roles'); + +const app = express(); +const PORT = process.env.PORT || 3001; + +app.use(cors()); +app.use(express.json()); +app.use(express.static(__dirname)); + +// ─── Public ────────────────────────────────────────────────────────────────── + +app.get('/api/health', (req, res) => { + res.json({ status: 'ok', project: 'RLA API v1', date: new Date().toISOString() }); +}); + +app.use('/api/auth', require('./routes/auth')); + +// ─── Protégées (user+) ─────────────────────────────────────────────────────── + +app.use('/api/marches', auth, requireUser, filterByRegion, require('./routes/marches')); +app.use('/api/stats', auth, requireUser, filterByRegion, require('./routes/stats')); +app.use('/api/synthese', auth, requireUser, filterByRegion, require('./routes/synthese')); +app.use('/api/alertes', auth, requireUser, filterByRegion, require('./routes/alertes')); +app.use('/api/en-service', auth, requireUser, filterByRegion, require('./routes/en-service')); +app.use('/api/en-cours', auth, requireUser, filterByRegion, require('./routes/en-cours')); +app.use('/api/par-region', auth, requireUser, filterByRegion, require('./routes/par-region')); +app.use('/api/clotures', auth, requireUser, filterByRegion, require('./routes/clotures')); +app.use('/api/pilotage-proactif',auth, requireUser, filterByRegion, require('./routes/pilotage')); +app.use('/api/matrice-risque', auth, requireUser, filterByRegion, require('./routes/matrice-risque')); +app.use('/api/export', auth, requireUser, filterByRegion, require('./routes/export')); + +// ─── Protégées (admin+) ────────────────────────────────────────────────────── + +app.use('/api/pipeline', auth, requireAdmin, require('./routes/pipeline')); + +// ─── Protégées (superadmin) ────────────────────────────────────────────────── + +app.use('/api/users', auth, requireSuperAdmin, require('./routes/users')); +app.use('/api/logs', auth, requireSuperAdmin, require('./routes/logs')); + +// ─── Start ─────────────────────────────────────────────────────────────────── + +app.listen(PORT, () => { + console.log(`RLA API v1 démarrée sur le port ${PORT}`); + console.log(`Endpoints disponibles sur http://localhost:${PORT}/api/`); +}); diff --git a/services/baserow.js b/services/baserow.js new file mode 100644 index 0000000..1492cc1 --- /dev/null +++ b/services/baserow.js @@ -0,0 +1,44 @@ +const axios = require('axios'); + +const BASE_URL = process.env.BASEROW_API_URL; +const TOKEN = process.env.BASEROW_TOKEN; +const TABLE_MARCHES = process.env.BASEROW_TABLE_MARCHES; +const TABLE_PIPELINE = process.env.BASEROW_TABLE_PIPELINE; + +const client = axios.create({ + baseURL: BASE_URL, + headers: { Authorization: `Token ${TOKEN}` }, +}); + +// Récupère toutes les lignes d'une table (gestion pagination) +async function fetchAllRows(tableId, filters = {}) { + const rows = []; + let page = 1; + let hasMore = true; + + while (hasMore) { + const { data } = await client.get(`/database/rows/table/${tableId}/`, { + params: { page, size: 200, user_field_names: true, ...filters }, + }); + rows.push(...data.results); + hasMore = data.next !== null; + page++; + } + + return rows; +} + +// Récupère une seule ligne par ID +async function fetchRow(tableId, rowId) { + const { data } = await client.get( + `/database/rows/table/${tableId}/${rowId}/`, + { params: { user_field_names: true } } + ); + return data; +} + +module.exports = { + getMarches: (filters) => fetchAllRows(TABLE_MARCHES, filters), + getMarcheById: (id) => fetchRow(TABLE_MARCHES, id), + getPipeline: (filters) => fetchAllRows(TABLE_PIPELINE, filters), +}; diff --git a/services/calc.js b/services/calc.js new file mode 100644 index 0000000..8daba26 --- /dev/null +++ b/services/calc.js @@ -0,0 +1,155 @@ +/** + * services/calc.js + * Helpers partagés : calculs, formatage, seuils métier RLA + */ + +const SEUIL_STANDARD = parseFloat(process.env.SEUIL_STANDARD || 70); +const SEUIL_MODERNISATION = parseFloat(process.env.SEUIL_MODERNISATION || 50); +const SEUIL_CRITIQUE_PCT = parseFloat(process.env.SEUIL_CRITIQUE_PCT || 90); +const DELAI_CRITIQUE = parseInt(process.env.DELAI_CRITIQUE || 45, 10); +const DELAI_ATTENTION = parseInt(process.env.DELAI_ATTENTION || 90, 10); + +// ─── Parseurs ─────────────────────────────────────────────────────────────── + +function parseNum(v) { + const n = parseFloat(String(v ?? '').replace(/\s/g, '').replace(',', '.')); + return isNaN(n) ? 0 : n; +} + +function parseDateFR(d) { + if (!d) return null; + // ISO or FR dd/mm/yyyy + const parts = String(d).split(/[\/\-]/); + if (parts.length === 3) { + const [a, b, c] = parts; + if (a.length === 4) return new Date(`${a}-${b}-${c}`); // YYYY-MM-DD + if (c.length === 4) return new Date(`${c}-${b}-${a}`); // DD/MM/YYYY + } + const dt = new Date(d); + return isNaN(dt.getTime()) ? null : dt; +} + +// ─── Formatage ─────────────────────────────────────────────────────────────── + +function formatMontant(v) { + const n = parseNum(v); + if (n === 0) return '—'; + return n.toLocaleString('fr-TN', { minimumFractionDigits: 0, maximumFractionDigits: 3 }) + ' DT'; +} + +function formatDateFR(d) { + const dt = parseDateFR(d); + if (!dt) return '—'; + return dt.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' }); +} + +function formatPct(v) { + const n = parseNum(v); + return n === 0 ? '0 %' : `${n.toFixed(1)} %`; +} + +// ─── Détermination de statuts métier ───────────────────────────────────────── + +function isCloture(r) { + const obs = String(r.observation || r.statut || '').toLowerCase(); + return obs.includes('clôtur') || obs.includes('clotur') || !!r.date_cloture; +} + +function getDelaiRestant(r) { + if (r.delai_restant != null && r.delai_restant !== '') { + const v = parseInt(r.delai_restant, 10); + return isNaN(v) ? null : v; + } + const fin = r.date_fin || r.datefin; + const dt = parseDateFR(fin); + if (!dt) return null; + return Math.ceil((dt - new Date()) / 86400000); +} + +function niveauAlerte(delai) { + if (delai === null) return 'indéterminé'; + if (delai <= DELAI_CRITIQUE) return 'critique'; + if (delai <= DELAI_ATTENTION) return 'attention'; + return 'normal'; +} + +function niveauAvancement(tauxPhy, nature) { + const t = parseNum(tauxPhy); + const seuil = String(nature || '').toLowerCase().includes('modern') ? SEUIL_MODERNISATION : SEUIL_STANDARD; + if (t >= SEUIL_CRITIQUE_PCT) return 'dépassé'; + if (t >= seuil) return 'sous_avancement'; + return 'normal'; +} + +// ─── Niveau de risque global ───────────────────────────────────────────────── + +function niveauRisque(r) { + const delai = getDelaiRestant(r); + const avt = parseNum(r.taux_phy || r.avt_fin); + const nd = niveauAlerte(delai); + if (nd === 'critique' || avt >= SEUIL_CRITIQUE_PCT) return 'critique'; + if (nd === 'attention') return 'élevé'; + if (avt >= SEUIL_STANDARD) return 'moyen'; + return 'faible'; +} + +// ─── Normalisation d'un marché ─────────────────────────────────────────────── + +function normalizeMarche(r) { + const delaiRestant = getDelaiRestant(r); + const tauxPhy = parseNum(r.taux_phy ?? r.avt_phy ?? r.avancement_physique); + const tauxFin = parseNum(r.taux_fin ?? r.avt_fin ?? r.avancement_financier); + const montant = parseNum(r.tot_marche ?? r.totmarche ?? r.montant); + const consomme = parseNum(r.consomme ?? r.montant_consomme ?? (montant * tauxFin / 100)); + const restant = montant - consomme; + + return { + id: r.id, + ref: r.ref || r.reference || r.id_marche || '', + projet: r.projet || '', + region: r.region || r.region_csc || '', + entrepreneur: r.entrepreneur || '', + nature: r.nature || '', + statut: r.statut || '', + observation: r.observation || '', + cloture: isCloture(r), + date_debut: formatDateFR(r.date_debut), + date_fin: formatDateFR(r.date_fin || r.datefin), + date_cloture: formatDateFR(r.date_cloture), + montant_raw: montant, + montant: formatMontant(montant), + consomme_raw: consomme, + consomme: formatMontant(consomme), + restant_raw: restant, + restant: formatMontant(restant), + taux_phy_raw: tauxPhy, + taux_phy: formatPct(tauxPhy), + taux_fin_raw: tauxFin, + taux_fin: formatPct(tauxFin), + delai_restant: delaiRestant, + niveau_alerte: niveauAlerte(delaiRestant), + niveau_avancement: niveauAvancement(r.taux_phy ?? r.avt_phy, r.nature), + niveau_risque: niveauRisque(r), + }; +} + +// ─── Seuils exportés ───────────────────────────────────────────────────────── + +module.exports = { + SEUIL_STANDARD, + SEUIL_MODERNISATION, + SEUIL_CRITIQUE_PCT, + DELAI_CRITIQUE, + DELAI_ATTENTION, + parseNum, + parseDateFR, + formatMontant, + formatDateFR, + formatPct, + isCloture, + getDelaiRestant, + niveauAlerte, + niveauAvancement, + niveauRisque, + normalizeMarche, +}; diff --git a/services/export-pdf.js b/services/export-pdf.js new file mode 100644 index 0000000..f9f026b --- /dev/null +++ b/services/export-pdf.js @@ -0,0 +1,301 @@ +/** + * services/export-pdf.js + * Génération de PDF par vue avec PDFKit (async/Promise) + */ +const PDFDocument = require('pdfkit'); + +// Palette RLA / McKinsey +const C = { + primary: '#002D62', + accent: '#E31837', + success: '#10b981', + warning: '#f59e0b', + danger: '#ef4444', + muted: '#6b7280', + light: '#f8fafc', + border: '#e2e8f0', + text: '#1e293b', + white: '#ffffff', +}; + +function hex(h) { + const s = h.replace('#', ''); + return [parseInt(s.slice(0,2),16), parseInt(s.slice(2,4),16), parseInt(s.slice(4,6),16)]; +} +const fill = (d, h) => d.fillColor(hex(h)); +const stroke = (d, h) => d.strokeColor(hex(h)); + +// ─── Collect PDF to Buffer ──────────────────────────────────────────────────── + +function pdfToBuffer(doc, writeFn) { + return new Promise((resolve, reject) => { + const chunks = []; + doc.on('data', c => chunks.push(c)); + doc.on('end', () => resolve(Buffer.concat(chunks))); + doc.on('error', err => reject(err)); + try { writeFn(doc); doc.end(); } catch (e) { reject(e); } + }); +} + +// ─── Header / Footer ───────────────────────────────────────────────────────── + +function header(doc, title, subtitle) { + fill(doc, C.primary); + doc.rect(0, 0, doc.page.width, 68).fill(); + fill(doc, C.white); + doc.fontSize(17).font('Helvetica-Bold').text(title || '', 40, 18, { width: 500 }); + if (subtitle) doc.fontSize(9).font('Helvetica').text(subtitle, 40, 42, { width: 500 }); + const now = new Date().toLocaleDateString('fr-FR', { day:'2-digit', month:'2-digit', year:'numeric' }); + doc.fontSize(8).text(`Édité le ${now}`, 0, 50, { align:'right', width: doc.page.width - 40 }); + doc.y = 88; +} + +function footer(doc, n) { + const y = doc.page.height - 38; + fill(doc, C.border); + doc.rect(0, y - 4, doc.page.width, 1).fill(); + fill(doc, C.muted); + doc.fontSize(7.5).font('Helvetica') + .text('RLA — Marchés Tunisie Telecom Zone Sud', 40, y) + .text(`Page ${n}`, 0, y, { align:'right', width: doc.page.width - 40 }); +} + +// ─── KPI Box ───────────────────────────────────────────────────────────────── + +function kpiBox(doc, x, y, w, h, label, value, color) { + fill(doc, C.light); + doc.rect(x, y, w, h).fill(); + fill(doc, color || C.primary); + doc.rect(x, y, 4, h).fill(); + fill(doc, C.muted); + doc.fontSize(7.5).font('Helvetica').text(label, x+10, y+7, { width: w-14 }); + fill(doc, C.text); + doc.fontSize(16).font('Helvetica-Bold').text(String(value ?? '—'), x+10, y+20, { width: w-14 }); +} + +// ─── Table ──────────────────────────────────────────────────────────────────── + +function table(doc, { title, headers, rows, colWidths }) { + const pageW = doc.page.width - 80; + const totalW = colWidths.reduce((a, b) => a + b, 0); + const scale = pageW / totalW; + const widths = colWidths.map(w => Math.round(w * scale)); + let y = doc.y; + + if (title) { + fill(doc, C.text); + doc.fontSize(10).font('Helvetica-Bold').text(title, 40, y); + y += 16; + } + + function drawHeader() { + fill(doc, C.primary); + doc.rect(40, y, pageW, 17).fill(); + fill(doc, C.white); + doc.fontSize(7.5).font('Helvetica-Bold'); + let x = 40; + for (let i = 0; i < headers.length; i++) { + doc.text(headers[i], x + 3, y + 4, { width: widths[i] - 6, ellipsis: true }); + x += widths[i]; + } + y += 17; + } + drawHeader(); + + let alt = false; + for (const row of rows) { + if (y > doc.page.height - 75) { + footer(doc, '—'); + doc.addPage(); + header(doc, '', ''); + y = doc.y; + drawHeader(); + alt = false; + } + const rowH = 15; + fill(doc, alt ? '#f1f5f9' : C.white); + doc.rect(40, y, pageW, rowH).fill(); + fill(doc, C.text); + doc.fontSize(7).font('Helvetica'); + let x = 40; + for (let i = 0; i < row.length; i++) { + doc.text(String(row[i] ?? '—'), x + 3, y + 4, { width: widths[i] - 6, ellipsis: true }); + x += widths[i]; + } + stroke(doc, C.border); + doc.moveTo(40, y + rowH).lineTo(40 + pageW, y + rowH).stroke(); + y += rowH; + alt = !alt; + } + doc.y = y + 8; +} + +const NL = n => ({ critique:'CRITIQUE', attention:'ATTENTION', élevé:'ÉLEVÉ', moyen:'MOYEN', faible:'FAIBLE', normal:'NORMAL', sous_avancement:'SOUS-AVT' }[n] || String(n||'').toUpperCase()); + +// ─── Vues ───────────────────────────────────────────────────────────────────── + +function generateSynthese(data) { + const doc = new PDFDocument({ margin:40, size:'A4' }); + return pdfToBuffer(doc, d => { + header(d, 'Synthèse Globale — Marchés RLA', 'Tunisie Telecom Zone Sud'); + const kpis = [ + { l:'Total Marchés', v: data.total, c: C.primary }, + { l:'Actifs', v: data.actifs, c: C.success }, + { l:'Clôturés', v: data.clotures,c: C.muted }, + { l:'Alertes', v: data.alertes_delais?.count||0, c: C.warning }, + { l:'Avt. Moy.(%)', v:`${data.taux_avancement_moyen||0}%`, c: C.accent }, + ]; + let kx = 40; + for (const k of kpis) { kpiBox(d, kx, d.y, 95, 48, k.l, k.v, k.c); kx += 101; } + d.y += 58; + + if (data.budget) { + fill(d, C.text); d.fontSize(10).font('Helvetica-Bold').text('Budget', 40, d.y); d.y += 12; + for (const [l, v] of [['Total',data.budget.total],['Consommé',data.budget.consomme],['Restant',data.budget.restant]]) { + fill(d, C.muted); d.fontSize(8.5).font('Helvetica').text(l+' :', 40, d.y, {width:110}); + fill(d, C.text); d.fontSize(8.5).font('Helvetica-Bold').text(v, 155, d.y, {width:250}); d.y += 13; + } + } + if (data.par_statut) { + d.y += 6; + table(d, { title:'Répartition par Statut', headers:['Statut','Nombre'], colWidths:[350,100], + rows: Object.entries(data.par_statut).map(([s,n])=>[s,n]) }); + } + if (data.alertes_delais?.items?.length) { + const items = data.alertes_delais.items.slice(0,10); + table(d, { title:`Top ${items.length} Alertes Délais`, headers:['Réf.','Projet','Région','Entrepreneur','J. Rest.','Niveau'], + colWidths:[70,140,65,120,55,55], rows: items.map(a=>[a.ref,a.projet,a.region,a.entrepreneur,a.delai_restant,NL(a.niveau)]) }); + } + footer(d, 1); + }); +} + +function generateAlertes(data) { + const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape' }); + return pdfToBuffer(doc, d => { + header(d, 'Alertes Délais — Marchés RLA', `${data.count||0} alerte(s) — dont ${data.critique||0} critique(s)`); + if (data.items?.length) { + table(d, { title:'Liste des Alertes', headers:['Réf.','Projet','Région','Entrepreneur','Taux Phy.','Date Fin','J. Rest.','Niveau'], + colWidths:[70,155,65,125,55,65,55,55], rows: data.items.map(a=>[a.ref,a.projet,a.region,a.entrepreneur,a.taux_phy,a.date_fin,a.delai_restant,NL(a.niveau_alerte||a.niveau)]) }); + } else { fill(d, C.success); d.fontSize(14).font('Helvetica-Bold').text('Aucune alerte active.', {align:'center'}); } + footer(d, 1); + }); +} + +function generateEnService(data) { + const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape' }); + return pdfToBuffer(doc, d => { + header(d, 'Marchés en Service — RLA', `${data.count||0} marché(s) actif(s)`); + table(d, { title:'Liste des Marchés en Service', + headers:['Réf.','Projet','Région','Entrepreneur','Montant','Taux Phy.','Taux Fin.','Date Fin','Alerte'], + colWidths:[60,140,65,115,90,50,50,65,55], + rows: (data.items||[]).map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.montant,r.taux_phy,r.taux_fin,r.date_fin,NL(r.niveau_alerte)]) }); + footer(d, 1); + }); +} + +function generateEnCours(data) { + const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape' }); + return pdfToBuffer(doc, d => { + header(d, 'Marchés en Cours — RLA', `${data.count||0} marché(s) en cours`); + table(d, { title:'Liste des Marchés en Cours', + headers:['Réf.','Projet','Région','Entrepreneur','Montant','Taux Phy.','Taux Fin.','Date Fin','Niveau Avt.'], + colWidths:[60,135,65,115,90,50,50,65,60], + rows: (data.items||[]).map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.montant,r.taux_phy,r.taux_fin,r.date_fin,r.niveau_avancement]) }); + footer(d, 1); + }); +} + +function generateParRegion(data) { + const doc = new PDFDocument({ margin:40, size:'A4' }); + return pdfToBuffer(doc, d => { + header(d, 'Vue par Région — Marchés RLA', `${data.count||0} région(s)`); + for (const reg of data.regions||[]) { + if (d.y > d.page.height - 120) { footer(d, '—'); d.addPage(); header(d,'',''); } + fill(d, C.primary); d.rect(40, d.y, d.page.width-80, 22).fill(); + fill(d, C.white); d.fontSize(11).font('Helvetica-Bold').text(reg.region, 52, d.y+5); d.y += 30; + const kpis = [ + {l:'Actifs',v:reg.actifs},{l:'Clôturés',v:reg.clotures}, + {l:'Alertes',v:reg.alertes_count,c:C.warning},{l:'Critiques',v:reg.alertes_critique,c:C.danger}, + {l:'Taux moy.',v:`${reg.taux_moyen}%`}, + ]; + let kx=40; for(const k of kpis){kpiBox(d,kx,d.y,92,40,k.l,k.v,k.c||C.primary);kx+=98;} + d.y+=50; d.moveDown(0.3); + } + footer(d, 1); + }); +} + +function generateClotures(data) { + const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape' }); + return pdfToBuffer(doc, d => { + header(d, 'Marchés Clôturés — RLA', `${data.count||0} marché(s) — Budget : ${data.budget_total||'—'}`); + table(d, { title:'Liste des Marchés Clôturés', + headers:['Réf.','Projet','Région','Entrepreneur','Montant','Taux Phy.','Date Clôture'], + colWidths:[70,155,65,130,100,60,75], + rows: (data.items||[]).map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.montant,r.taux_phy,r.date_cloture]) }); + footer(d, 1); + }); +} + +function generatePilotage(data) { + const doc = new PDFDocument({ margin:40, size:'A4' }); + return pdfToBuffer(doc, d => { + const r = data.resume||{}; + header(d, 'Pilotage Proactif — Marchés RLA', `Total actifs : ${r.total||0}`); + const kpis = [ + {l:'Dans les normes',v:r.normal||0,c:C.success}, + {l:'Sous avancement',v:r.sous_avancement||0,c:C.warning}, + {l:'Dépassé',v:r.depasse||0,c:C.danger}, + ]; + let kx=40; for(const k of kpis){kpiBox(d,kx,d.y,148,50,k.l,k.v,k.c);kx+=156;} + d.y+=62; + const problematic = [...(data.depasse||[]),...(data.sous_avancement||[])]; + if (problematic.length) { + table(d, { title:'Marchés à surveiller', headers:['Réf.','Projet','Région','Entrepreneur','Taux Phy.','Niveau'], + colWidths:[70,155,65,130,60,75], rows: problematic.map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.taux_phy,r.niveau_avancement]) }); + } + footer(d, 1); + }); +} + +function generateMatriceRisque(data) { + const doc = new PDFDocument({ margin:40, size:'A4' }); + return pdfToBuffer(doc, d => { + const pn = data.par_niveau||{}; + header(d, 'Matrice de Risque — Marchés RLA', `${data.total||0} marchés analysés`); + const kpis = [ + {l:'Critique',v:pn.critique||0,c:C.danger}, + {l:'Élevé',v:pn['élevé']||0,c:C.warning}, + {l:'Moyen',v:pn.moyen||0,c:'#6366f1'}, + {l:'Faible',v:pn.faible||0,c:C.success}, + ]; + let kx=40; for(const k of kpis){kpiBox(d,kx,d.y,110,50,k.l,k.v,k.c);kx+=118;} + d.y+=62; + if (data.items?.length) { + const sorted = [...data.items].sort((a,b)=>(b.score_delai+b.score_avancement)-(a.score_delai+a.score_avancement)); + table(d, { title:'Marchés classés par niveau de risque', headers:['Réf.','Projet','Région','Entrepreneur','Taux Phy.','J. Rest.','Risque'], + colWidths:[70,145,65,125,55,50,55], rows: sorted.map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.taux_phy,r.delai_restant??'—',NL(r.niveau_risque)]) }); + } + footer(d, 1); + }); +} + +function generateGeneric(title, data) { + const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape' }); + return pdfToBuffer(doc, d => { + header(d, title, ''); + const items = data.items||[]; + if (items.length) { + const keys = Object.keys(items[0]).filter(k=>!k.endsWith('_raw')&&k!=='id'&&typeof items[0][k]!=='object').slice(0,8); + table(d, { headers:keys, colWidths:keys.map(()=>Math.floor(700/keys.length)), + rows: items.slice(0,100).map(r=>keys.map(k=>r[k])) }); + } + footer(d, 1); + }); +} + +module.exports = { + generateSynthese, generateAlertes, generateEnService, generateEnCours, + generateParRegion, generateClotures, generatePilotage, generateMatriceRisque, generateGeneric, +}; diff --git a/services/export-xlsx.js b/services/export-xlsx.js new file mode 100644 index 0000000..f6a25bb --- /dev/null +++ b/services/export-xlsx.js @@ -0,0 +1,84 @@ +/** + * services/export-xlsx.js + * Génération XLSX avec ExcelJS (SuperAdmin uniquement) + */ +const ExcelJS = require('exceljs'); + +const HEADER_FILL = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF002D62' } }; +const HEADER_FONT = { color: { argb: 'FFFFFFFF' }, bold: true, size: 10 }; +const ALT_FILL = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } }; + +function styleHeader(row) { + row.eachCell(cell => { + cell.fill = HEADER_FILL; + cell.font = HEADER_FONT; + cell.alignment = { vertical: 'middle', horizontal: 'center' }; + cell.border = { bottom: { style: 'thin', color: { argb: 'FFE2E8F0' } } }; + }); + row.height = 22; +} + +function styleDataRow(row, alt) { + if (alt) { + row.eachCell(cell => { cell.fill = ALT_FILL; }); + } + row.height = 16; +} + +async function generateXlsx(view, data) { + const wb = new ExcelJS.Workbook(); + wb.creator = 'RLA API'; + wb.created = new Date(); + + const titles = { + synthese: 'Synthèse Globale', + alertes: 'Alertes Délais', + 'en-service': 'Marchés en Service', + 'en-cours': 'Marchés en Cours', + 'par-region': 'Par Région', + clotures: 'Marchés Clôturés', + pilotage: 'Pilotage Proactif', + 'matrice-risque': 'Matrice de Risque', + }; + + const title = titles[view] || view; + const ws = wb.addWorksheet(title.slice(0, 31)); + + const items = data.items || data.regions || []; + + if (!items.length) { + ws.addRow(['Aucune donnée disponible.']); + return wb.xlsx.writeBuffer(); + } + + const sample = items[0]; + const keys = Object.keys(sample).filter(k => !k.endsWith('_raw') && k !== 'id' && typeof sample[k] !== 'object'); + + // En-tête + ws.columns = keys.map(k => ({ header: k, key: k, width: 20 })); + styleHeader(ws.getRow(1)); + + // Données + items.forEach((item, i) => { + const row = ws.addRow(keys.map(k => item[k] ?? '')); + styleDataRow(row, i % 2 === 1); + }); + + // Freeze header + ws.views = [{ state: 'frozen', ySplit: 1 }]; + + // Onglet résumé si synthèse + if (view === 'synthese' && data.par_statut) { + const ws2 = wb.addWorksheet('Par Statut'); + ws2.columns = [{ header: 'Statut', key: 'statut', width: 30 }, { header: 'Nombre', key: 'nb', width: 15 }]; + styleHeader(ws2.getRow(1)); + Object.entries(data.par_statut).forEach(([s, n], i) => { + const row = ws2.addRow({ statut: s, nb: n }); + styleDataRow(row, i % 2 === 1); + }); + } + + return wb.xlsx.writeBuffer(); +} + +module.exports = { generateXlsx }; diff --git a/services/logs.js b/services/logs.js new file mode 100644 index 0000000..938d1b0 --- /dev/null +++ b/services/logs.js @@ -0,0 +1,31 @@ +const fs = require('fs'); +const path = require('path'); + +const LOGS_FILE = path.join(__dirname, '..', 'logs', 'connexions.json'); +const MAX_LOGS = 500; + +function readLogs() { + try { + return JSON.parse(fs.readFileSync(LOGS_FILE, 'utf8')); + } catch (_) { + return []; + } +} + +function writeLogs(logs) { + fs.mkdirSync(path.dirname(LOGS_FILE), { recursive: true }); + fs.writeFileSync(LOGS_FILE, JSON.stringify(logs, null, 2), 'utf8'); +} + +function logLogin({ username, role = null, ip = null, success }) { + const logs = readLogs(); + logs.unshift({ timestamp: new Date().toISOString(), username, role, ip, success: !!success }); + if (logs.length > MAX_LOGS) logs.length = MAX_LOGS; + writeLogs(logs); +} + +function getLogs(limit = 100) { + return readLogs().slice(0, limit); +} + +module.exports = { logLogin, getLogs }; diff --git a/services/users.js b/services/users.js new file mode 100644 index 0000000..ecfad41 --- /dev/null +++ b/services/users.js @@ -0,0 +1,30 @@ +const fs = require('fs'); +const path = require('path'); + +const DATA_FILE = path.join(__dirname, '..', 'data', 'users.json'); + +function initUsersFile() { + if (fs.existsSync(DATA_FILE)) return; + fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true }); + try { + const envUsers = JSON.parse(process.env.USERS || '[]'); + fs.writeFileSync(DATA_FILE, JSON.stringify(envUsers, null, 2), 'utf8'); + console.log(`[users] Fichier initialisé depuis .env (${envUsers.length} utilisateurs)`); + } catch (_) { + fs.writeFileSync(DATA_FILE, '[]', 'utf8'); + } +} + +function getUsers() { + initUsersFile(); + try { + return JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')); + } catch (_) { return []; } +} + +function saveUsers(users) { + fs.mkdirSync(path.dirname(DATA_FILE), { recursive: true }); + fs.writeFileSync(DATA_FILE, JSON.stringify(users, null, 2), 'utf8'); +} + +module.exports = { getUsers, saveUsers };