commit 2be438f1f1af1c9ab1a739a6bdc8e35956f703a1 Author: Nabil Derouiche Date: Sun Mar 22 22:17:27 2026 +0100 chore: snapshot état initial API Gestion des Marchés RLA — V0 Version initiale de l'API complète : - Frontend : index.html (thème McKinsey, tableau de bord dynamique) - Backend Express : server.js, routes/, services/, middleware/ - Authentification JWT, rôles, logs - Connexion Baserow (NAS 192.168.100.33) - Export PDF/XLSX, statistiques, alertes, pilotage - Docker : Dockerfile, .env.example 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/Fichiers-cibles/Marches_RLA_2025_Zone_Sud_01_2026 - Lecture seule.pptx b/Fichiers-cibles/Marches_RLA_2025_Zone_Sud_01_2026 - Lecture seule.pptx new file mode 100644 index 0000000..d3b076f Binary files /dev/null and b/Fichiers-cibles/Marches_RLA_2025_Zone_Sud_01_2026 - Lecture seule.pptx differ diff --git a/Fichiers-cibles/Marches_RLA_2025_Zone_Sud_02_2026.xlsx b/Fichiers-cibles/Marches_RLA_2025_Zone_Sud_02_2026.xlsx new file mode 100644 index 0000000..6dc13d3 Binary files /dev/null and b/Fichiers-cibles/Marches_RLA_2025_Zone_Sud_02_2026.xlsx differ diff --git a/Fichiers-cibles/Marches_RLA_Marchés_En_Service___Raccordement___Medenine__Sfax_2026-03-12.pdf b/Fichiers-cibles/Marches_RLA_Marchés_En_Service___Raccordement___Medenine__Sfax_2026-03-12.pdf new file mode 100644 index 0000000..da83d81 Binary files /dev/null and b/Fichiers-cibles/Marches_RLA_Marchés_En_Service___Raccordement___Medenine__Sfax_2026-03-12.pdf differ diff --git a/Fichiers-cibles/Rapport_RLA_2025_Zone_Sud_02_2026.docx b/Fichiers-cibles/Rapport_RLA_2025_Zone_Sud_02_2026.docx new file mode 100644 index 0000000..bf9cb46 Binary files /dev/null and b/Fichiers-cibles/Rapport_RLA_2025_Zone_Sud_02_2026.docx differ 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..2daa4b0 --- /dev/null +++ b/data/users.json @@ -0,0 +1,23 @@ +[ + { + "id": 1, + "username": "nabil", + "password": "$2a$10$4nqlMxOSsDV99mJHvVQJzubVLCArz6fnkQ0RSTD9p1CwdGxwibHdq", + "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" + } +] 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..3691fda --- /dev/null +++ b/index.html @@ -0,0 +1,1106 @@ + + + + + + Marchés RLA - Zone Sud | Tunisie Telecom + + + + + + + + + + + +
+
Chargement des données...
+
+ + +
+
+ Tunisie Telecom +
+

Marchés RLA

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

Vue Générale

+
+
Total Marchés
+
Marchés Actifs
Avancement moy. : —
+
Alertes Délais
Critiques (≤45j) : —
+
Clôturés
+
+
+
+
Répartition par statut
+
+
+
+
Marchés en alerte — délais proches
+

Chargement...

+
+
+
+ + +
+

Alertes Délais

+
+
+

Marchés à surveiller

+ 0 alertes +
+
+ + + +
RéférenceEntrepreneurProjetRégionAvt. Phy.Délai Rest.Niveau
Chargement...
+
+
+ +
+ + +
+

Marchés En Service

+
+
+ +
+
+ +
+
+
+
+

En Cours d'Exécution

+ 0 marchés +
+
+ + + +
RéférenceProjetRégionEntrepreneurMontant MaxPériodeAvt. Phy.Délai Rest.
Chargement...
+
+
+
+ + +
+

Pilotage Proactif — Avancement Physique

+
+
Normal
Avt. ≥ seuil standard (70%)
+
Sous Avancement
Avt. physique < seuil (70%)
+
Dépassement
Avt. physique ≥ critique (90%)
+
Non déterminé
Données insuffisantes
+
+
+
+

Détail par marché

+ 0 marchés +
+
+ + + +
RéférenceEntrepreneurProjetRégionAvt. Phy.Délai Rest.Résultat
Chargement...
+
+
+
+ + +
+

Détail par Région

+
+

Chargement...

+
+
+ + +
+

Liste des Marchés

+
+
+
Marchés
+
+ + +
+ + + +
+
+ + + + + + + + + + + + + + +
Référence Région Entrepreneur Projet Statut Avt. Phy. Période Montant
Chargement...
+
+
+
+
+ + +
+

Pipeline Appels d'Offres

+
+
+

Projets en Préparation

+ 0 projets +
+
+ + + +
Description du projetRégionsEstimation (DT)Durée (mois)Date prévisionnelle DCA
Chargement...
+
+
+
+ + +
+

Gestion des Utilisateurs

+
+
+
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..70fe4fe --- /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, selectVal } = 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 en service (non clôturés, observation = "En service") + rows = rows.filter(r => !isCloture(r) && selectVal(r.observation).toLowerCase().includes('en service')); + + // 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..49d0708 --- /dev/null +++ b/routes/export.js @@ -0,0 +1,651 @@ +/** + * 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, selectVal, + 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=selectVal(r.observation)||'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.id_marche||r.reference||'',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': { + const enService = actifs.filter(r => selectVal(r.observation).toLowerCase().includes('en service')); + return { count: enService.length, items: enService.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 allRows = await getMarches(); + const filtered = applyFilters(allRows, req); + const actifs = filtered.filter(r => !isCloture(r)); + const buf = await generateXlsx('all', {}, actifs); + const filename = `Marches_RLA_Zone_Sud_${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 allRows = await getMarches(); + const filtered = applyFilters(allRows, req); + const actifs = filtered.filter(r => !isCloture(r)); + const clotures = filtered.filter(r => isCloture(r)); + const today = new Date().toLocaleDateString('fr-FR'); + const ALL_REGIONS = ['Gabes','Gafsa','Kebili','Medenine','Sfax','Tataouine','Tozeur']; + + const parseN = v => { const n = parseFloat(String(v||'').replace(/\s/g,'').replace(',','.')); return isNaN(n)?0:n; }; + const fmtPct = v => { const n = parseN(v); return n===0?'0%':`${n.toFixed(0)}%`; }; + const fmtMDT = v => { const n = parseN(v); if(!n) return '—'; if(n>=1e6) return `${(n/1e6).toFixed(1)} MDT`; return `${(n/1e3).toFixed(0)} kDT`; }; + const selVal = v => { if(!v) return ''; if(typeof v==='object'&&v.value!==undefined) return String(v.value); return String(v); }; + + const totalBudget = actifs.reduce((s,r)=>s+parseN(r.tot_marche||r.totmarche||r.montant),0); + const phyList = actifs.map(r=>parseN(r.taux_phy||r.avt_phy)).filter(v=>v>0); + const avgPhy = phyList.length ? phyList.reduce((a,b)=>a+b,0)/phyList.length : 0; + const alerteItems = actifs + .map(r=>({...r,_d: (() => { const v=parseInt(String(r.delai_restant||''),10); return !isNaN(v)?v:null; })()})) + .filter(r=>r._d!==null&&r._d<=DELAI_ATTENTION) + .sort((a,b)=>a._d-b._d); + + const SEUIL_STD = parseFloat(process.env.SEUIL_STANDARD||70); + const SEUIL_MOD = parseFloat(process.env.SEUIL_MODERNISATION||50); + const classify = r => { + const t = parseN(r.taux_phy||r.avt_phy); + const s = String(selVal(r.nature)||'').toLowerCase().includes('modern') ? SEUIL_MOD : SEUIL_STD; + if(!t) return 'Non déterminé'; + if(t>=SEUIL_CRITIQUE_PCT) return 'Dépassement'; + if(t>=s) return 'Normal'; + return 'Sous Avancement'; + }; + const normal = actifs.filter(r=>classify(r)==='Normal'); + const sous = actifs.filter(r=>classify(r)==='Sous Avancement'); + const dep = actifs.filter(r=>classify(r)==='Dépassement'); + + const pptx = new PptxGenJS(); + pptx.layout = 'LAYOUT_WIDE'; + pptx.author = 'RLA API'; + pptx.company = 'Tunisie Telecom Zone Sud'; + pptx.subject = 'Marchés RLA Zone Sud'; + + const addFooter = slide => { + slide.addText(`Tunisie Telecom • Zone Sud • ${today}`, { + x: 0, y: 5.3, w: '100%', h: 0.25, + fontSize: 8, color: '64748B', align: 'center', + }); + }; + + // ── Slide 1: Couverture + const s1 = pptx.addSlide(); + s1.background = { color: '002D62' }; + s1.addShape(pptx.ShapeType.rect, { x: 0, y: 0, w: '100%', h: 0.08, fill: { color: '00D4FF' } }); + s1.addText('TUNISIE TELECOM', { x: 0.5, y: 0.4, w: '90%', h: 0.5, fontSize: 14, color: '94A3B8', align: 'center' }); + s1.addText('RAPPORT DE SUIVI DES MARCHÉS RLA', { x: 0.5, y: 1.1, w: '90%', h: 1.0, fontSize: 30, bold: true, color: 'FFFFFF', align: 'center' }); + s1.addText('Zone Sud — Situation Actuelle & Pilotage Proactif', { x: 0.5, y: 2.2, w: '90%', h: 0.5, fontSize: 16, color: '00D4FF', align: 'center' }); + s1.addShape(pptx.ShapeType.rect, { x: 3.5, y: 3.0, w: 6.5, h: 0.05, fill: { color: '00D4FF' }, line: { color: '00D4FF' } }); + s1.addText(`📅 ${today}`, { x: 0.5, y: 3.2, w: '90%', h: 0.4, fontSize: 12, color: 'CBD5E1', align: 'center' }); + s1.addText(`📋 ${actifs.length} marchés actifs │ 💰 ${fmtMDT(totalBudget)}`, { x: 0.5, y: 3.7, w: '90%', h: 0.4, fontSize: 11, color: '94A3B8', align: 'center' }); + s1.addShape(pptx.ShapeType.rect, { x: 0, y: 5.47, w: '100%', h: 0.08, fill: { color: '00D4FF' } }); + + // ── Slide 2: Synthèse globale + const s2 = pptx.addSlide(); + s2.background = { color: '0F172A' }; + s2.addText('SYNTHÈSE GLOBALE', { x: 0.3, y: 0.15, w: '90%', h: 0.45, fontSize: 18, bold: true, color: '00D4FF' }); + s2.addShape(pptx.ShapeType.rect, { x: 0.3, y: 0.6, w: 9.4, h: 0.04, fill: { color: '002D62' } }); + + const kpis = [ + { label: 'Total Marchés', val: String(actifs.length), color: '00D4FF' }, + { label: 'Budget Total', val: fmtMDT(totalBudget), color: '10B981' }, + { label: 'Avt. Phy Moy', val: fmtPct(avgPhy), color: '10B981' }, + { label: 'Alertes', val: String(alerteItems.filter(r=>r._d<=DELAI_CRITIQUE).length), color: 'EF4444' }, + ]; + kpis.forEach((k, i) => { + const x = 0.3 + i * 2.5; + s2.addShape(pptx.ShapeType.roundRect, { x, y: 0.75, w: 2.3, h: 1.3, fill: { color: '1E3A5F' }, line: { color: '002D62' } }); + s2.addText(k.val, { x, y: 0.85, w: 2.3, h: 0.7, fontSize: 26, bold: true, color: k.color, align: 'center' }); + s2.addText(k.label, { x, y: 1.6, w: 2.3, h: 0.35, fontSize: 9, color: '94A3B8', align: 'center' }); + }); + + // Par statut + const parStatut = {}; + for (const r of filtered) { + const s = selVal(r.observation) || 'Inconnu'; + parStatut[s] = (parStatut[s] || 0) + 1; + } + const statRows = Object.entries(parStatut).slice(0, 8).map(([s, n]) => [ + { text: s, options: { color: 'CBD5E1', fontSize: 9 } }, + { text: String(n), options: { color: '00D4FF', bold: true, fontSize: 9, align: 'right' } }, + ]); + if (statRows.length) { + s2.addText('Répartition par Statut', { x: 0.3, y: 2.2, w: 4, h: 0.3, fontSize: 11, bold: true, color: 'FFFFFF' }); + s2.addTable([ + [{ text: 'Statut', options: { bold: true, color: 'FFFFFF', fill: '002D62', fontSize: 9 } }, + { text: 'Nb', options: { bold: true, color: 'FFFFFF', fill: '002D62', fontSize: 9 } }], + ...statRows, + ], { x: 0.3, y: 2.55, w: 4, fontSize: 9, border: { type: 'solid', color: '1E3A5F' }, colW: [3.2, 0.8] }); + } + + // Par région + const regData = ['Gabes','Gafsa','Kebili','Medenine','Sfax','Tataouine','Tozeur'].map(reg => { + const ra = actifs.filter(r=>(r.region||'')===reg); + const pl = ra.map(r=>parseN(r.taux_phy||r.avt_phy)).filter(v=>v>0); + const pm = pl.length ? pl.reduce((a,b)=>a+b,0)/pl.length : 0; + return [ + { text: reg, options: { color: 'CBD5E1', fontSize: 9 } }, + { text: String(ra.length), options: { color: '00D4FF', bold: true, fontSize: 9, align: 'center' } }, + { text: fmtPct(pm), options: { color: pm>=70?'10B981':'EF4444', bold: true, fontSize: 9, align: 'center' } }, + ]; + }); + s2.addText('Par Région', { x: 5.2, y: 2.2, w: 4.5, h: 0.3, fontSize: 11, bold: true, color: 'FFFFFF' }); + s2.addTable([ + [{ text: 'Région', options: { bold: true, color: 'FFFFFF', fill: '002D62', fontSize: 9 } }, + { text: 'Marchés', options: { bold: true, color: 'FFFFFF', fill: '002D62', fontSize: 9 } }, + { text: 'Phy %', options: { bold: true, color: 'FFFFFF', fill: '002D62', fontSize: 9 } }], + ...regData, + ], { x: 5.2, y: 2.55, w: 4.5, fontSize: 9, border: { type: 'solid', color: '1E3A5F' }, colW: [2.5, 1.0, 1.0] }); + addFooter(s2); + + // ── Slide 3: Alertes + const s3 = pptx.addSlide(); + s3.background = { color: '0F172A' }; + s3.addText('ALERTES DÉLAIS', { x: 0.3, y: 0.15, w: '90%', h: 0.45, fontSize: 18, bold: true, color: 'EF4444' }); + s3.addShape(pptx.ShapeType.rect, { x: 0.3, y: 0.6, w: 9.4, h: 0.04, fill: { color: 'EF4444' } }); + s3.addText(`${alerteItems.filter(r=>r._d<=DELAI_CRITIQUE).length} critique(s) • ${alerteItems.length} total`, { + x: 0.3, y: 0.7, w: 9.4, h: 0.3, fontSize: 10, color: '94A3B8', + }); + if (alerteItems.length) { + const alertRows = alerteItems.slice(0, 18).map(r => { + const alColor = r._d <= DELAI_CRITIQUE ? 'EF4444' : 'EA580C'; + return [ + { text: r.id_marche||r.reference||'', options: { fontSize: 8, color: 'CBD5E1' } }, + { text: r.projet||'', options: { fontSize: 8, color: 'CBD5E1' } }, + { text: r.region||'', options: { fontSize: 8, color: 'CBD5E1' } }, + { text: r.entrepreneur||'', options: { fontSize: 8, color: 'CBD5E1' } }, + { text: String(r._d||'—'), options: { fontSize: 8, bold: true, color: alColor, align: 'center' } }, + ]; + }); + s3.addTable([ + ['Référence','Projet','Région','Entrepreneur','Délai (j)'].map(t => ({ + text: t, options: { bold: true, color: 'FFFFFF', fill: '7F1D1D', fontSize: 8 }, + })), + ...alertRows, + ], { x: 0.3, y: 1.1, w: 9.4, fontSize: 8, border: { type: 'solid', color: '1E3A5F' }, colW: [2.0, 2.5, 1.2, 2.5, 1.2] }); + } + addFooter(s3); + + // ── Slide 4: Pilotage proactif + const s4 = pptx.addSlide(); + s4.background = { color: '0F172A' }; + s4.addText('PILOTAGE PROACTIF', { x: 0.3, y: 0.15, w: '90%', h: 0.45, fontSize: 18, bold: true, color: '00D4FF' }); + s4.addShape(pptx.ShapeType.rect, { x: 0.3, y: 0.6, w: 9.4, h: 0.04, fill: { color: '002D62' } }); + + const pilotKpis = [ + { label: 'Normal', val: String(normal.length), color: '10B981' }, + { label: 'Sous Avancement', val: String(sous.length), color: 'EF4444' }, + { label: 'Dépassement', val: String(dep.length), color: 'EA580C' }, + ]; + pilotKpis.forEach((k, i) => { + const x = 0.5 + i * 3.3; + s4.addShape(pptx.ShapeType.roundRect, { x, y: 0.75, w: 3.0, h: 1.0, fill: { color: '1E3A5F' }, line: { color: '002D62' } }); + s4.addText(k.val, { x, y: 0.8, w: 3.0, h: 0.55, fontSize: 28, bold: true, color: k.color, align: 'center' }); + s4.addText(k.label, { x, y: 1.4, w: 3.0, h: 0.25, fontSize: 9, color: '94A3B8', align: 'center' }); + }); + + const pilotItems = [...sous, ...dep].slice(0, 18); + if (pilotItems.length) { + const pilRows = pilotItems.map((r, i) => { + const t = parseN(r.taux_phy||r.avt_phy); + const res = classify(r); + return [ + { text: r.id_marche||r.reference||'', options: { fontSize: 8, color: 'CBD5E1' } }, + { text: r.projet||'', options: { fontSize: 8, color: 'CBD5E1' } }, + { text: r.region||'', options: { fontSize: 8, color: 'CBD5E1' } }, + { text: r.entrepreneur||'', options: { fontSize: 8, color: 'CBD5E1' } }, + { text: fmtPct(t), options: { fontSize: 8, bold: true, color: t>=SEUIL_CRITIQUE_PCT?'EA580C':'EF4444', align: 'center' } }, + { text: res, options: { fontSize: 8, bold: true, color: res==='Dépassement'?'EA580C':'EF4444', align: 'center' } }, + ]; + }); + s4.addText('Marchés Sous Avancement & Dépassement', { x: 0.3, y: 1.9, w: 9.4, h: 0.3, fontSize: 10, bold: true, color: 'FFFFFF' }); + s4.addTable([ + ['Référence','Projet','Région','Entrepreneur','Phy %','Résultat'].map(t => ({ + text: t, options: { bold: true, color: 'FFFFFF', fill: '002D62', fontSize: 8 }, + })), + ...pilRows, + ], { x: 0.3, y: 2.25, w: 9.4, fontSize: 8, border: { type: 'solid', color: '1E3A5F' }, colW: [2.0, 2.3, 1.0, 2.0, 0.9, 1.2] }); + } + addFooter(s4); + + // ── Slide 5: Par région + const s5 = pptx.addSlide(); + s5.background = { color: '0F172A' }; + s5.addText('SYNTHÈSE PAR RÉGION', { x: 0.3, y: 0.15, w: '90%', h: 0.45, fontSize: 18, bold: true, color: '00D4FF' }); + s5.addShape(pptx.ShapeType.rect, { x: 0.3, y: 0.6, w: 9.4, h: 0.04, fill: { color: '002D62' } }); + + const regTableData = ALL_REGIONS.map(reg => { + const ra = actifs.filter(r=>(r.region||'')===reg); + const rc = filtered.filter(r=>(r.region||'')===reg&&isCloture(r)); + const bud = ra.reduce((s,r)=>s+parseN(r.tot_marche||r.totmarche||r.montant),0); + const pl = ra.map(r=>parseN(r.taux_phy||r.avt_phy)).filter(v=>v>0); + const pm = pl.length ? pl.reduce((a,b)=>a+b,0)/pl.length : 0; + const al = ra.map(r=>({_d:parseInt(String(r.delai_restant||''),10)||null})).filter(r=>r._d!==null&&r._d<=DELAI_ATTENTION); + return [ + { text: reg, options: { color: '00D4FF', bold: true, fontSize: 9 } }, + { text: String(ra.length), options: { color: 'CBD5E1', fontSize: 9, align: 'center' } }, + { text: String(rc.length), options: { color: '64748B', fontSize: 9, align: 'center' } }, + { text: fmtMDT(bud), options: { color: 'CBD5E1', fontSize: 9, align: 'right' } }, + { text: fmtPct(pm), options: { color: pm>=70?'10B981':'EF4444', bold: true, fontSize: 9, align: 'center' } }, + { text: String(al.length), options: { color: al.length>0?'EF4444':'10B981', bold: true, fontSize: 9, align: 'center' } }, + ]; + }); + s5.addTable([ + ['Région','Actifs','Clôturés','Budget','Phy Moy','Alertes'].map(t => ({ + text: t, options: { bold: true, color: 'FFFFFF', fill: '002D62', fontSize: 9 }, + })), + ...regTableData, + ], { x: 0.3, y: 0.75, w: 9.4, fontSize: 9, border: { type: 'solid', color: '1E3A5F' }, colW: [2.0, 1.2, 1.2, 1.8, 1.5, 1.7] }); + addFooter(s5); + + const filename = `Marches_RLA_Zone_Sud_${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, PageBreak, + Header, Footer, ImageRun, + } = require('docx'); + + const allRows = await getMarches(); + const filtered = applyFilters(allRows, req); + const actifs = filtered.filter(r => !isCloture(r)); + const clotures = filtered.filter(r => isCloture(r)); + const today = new Date().toLocaleDateString('fr-FR'); + const ALL_REGIONS = ['Gabes','Gafsa','Kebili','Medenine','Sfax','Tataouine','Tozeur']; + + const parseN = v => { const n = parseFloat(String(v||'').replace(/\s/g,'').replace(',','.')); return isNaN(n)?0:n; }; + const fmtPct = v => { const n = parseN(v); return n===0?'0 %':`${n.toFixed(0)} %`; }; + const fmtMDT = v => { const n = parseN(v); if(!n) return '—'; if(n>=1e6) return `${(n/1e6).toFixed(1)} MDT`; return `${(n/1e3).toFixed(0)} kDT`; }; + const selVal = v => { if(!v) return ''; if(typeof v==='object'&&v.value!==undefined) return String(v.value); return String(v); }; + const parseDt = d => { if(!d) return null; const p=String(d).split(/[\/\-]/); if(p.length===3){const[a,b,c]=p; if(a.length===4) return new Date(`${a}-${b}-${c}`); if(c.length===4) return new Date(`${c}-${b}-${a}`);} const dt=new Date(d); return isNaN(dt.getTime())?null:dt; }; + const fmtDate = d => { const dt=parseDt(d); if(!dt) return '—'; return dt.toLocaleDateString('fr-FR',{day:'2-digit',month:'2-digit',year:'numeric'}); }; + + const totalBudget = actifs.reduce((s,r)=>s+parseN(r.tot_marche||r.totmarche||r.montant),0); + const phyList = actifs.map(r=>parseN(r.taux_phy||r.avt_phy)).filter(v=>v>0); + const avgPhy = phyList.length ? phyList.reduce((a,b)=>a+b,0)/phyList.length : 0; + + const SEUIL_STD = parseFloat(process.env.SEUIL_STANDARD||70); + const SEUIL_MOD = parseFloat(process.env.SEUIL_MODERNISATION||50); + const classify = r => { + const t = parseN(r.taux_phy||r.avt_phy); + const s = String(selVal(r.nature)||'').toLowerCase().includes('modern') ? SEUIL_MOD : SEUIL_STD; + if(!t) return 'Non déterminé'; + if(t>=SEUIL_CRITIQUE_PCT) return 'Dépassement'; + if(t>=s) return 'Normal'; + return 'Sous Avancement'; + }; + + // Helpers + const navyFill = { fill: '002D62' }; + const altFill = { fill: 'F1F5F9' }; + const hdr = (texts, opts={}) => new Paragraph({ children: texts, ...opts }); + const tr = (cells, isHeader=false) => new TableRow({ + tableHeader: isHeader, + children: cells.map(([text, width, shade]) => new TableCell({ + children: [new Paragraph({ + children: [new TextRun({ text: String(text??'—'), bold: isHeader, color: isHeader?'FFFFFF':'1E293B', size: 18 })], + alignment: AlignmentType.LEFT, + })], + width: { size: width||1000, type: WidthType.DXA }, + shading: shade || (isHeader ? navyFill : undefined), + })), + }); + const h1 = text => new Paragraph({ + children: [new TextRun({ text, bold: true, color: '002D62', size: 32 })], + spacing: { before: 300, after: 150 }, + border: { bottom: { style: 'single', size: 8, color: '00D4FF' } }, + }); + const h2 = text => new Paragraph({ + children: [new TextRun({ text, bold: true, color: '0F172A', size: 24 })], + spacing: { before: 200, after: 100 }, + }); + const spacer = () => new Paragraph({ text: '', spacing: { before: 80, after: 80 } }); + + const children = []; + + // ── Page de couverture + children.push(new Paragraph({ children: [new TextRun({ text: '', break: 4 })] })); + children.push(new Paragraph({ + children: [new TextRun({ text: 'TUNISIE TELECOM', bold: true, color: '002D62', size: 36, allCaps: true })], + alignment: AlignmentType.CENTER, + spacing: { before: 100, after: 50 }, + })); + children.push(new Paragraph({ + children: [new TextRun({ text: 'Direction Centrale', color: '64748B', size: 24 })], + alignment: AlignmentType.CENTER, + })); + children.push(new Paragraph({ + children: [new TextRun({ text: 'Zone Sud', color: '64748B', size: 24 })], + alignment: AlignmentType.CENTER, + spacing: { after: 200 }, + })); + children.push(new Paragraph({ + children: [new TextRun({ text: 'Rapport de Suivi des Marchés RLA', bold: true, color: '002D62', size: 44 })], + alignment: AlignmentType.CENTER, + spacing: { before: 100, after: 80 }, + })); + children.push(new Paragraph({ + children: [new TextRun({ text: 'Situation Actuelle & Analyse Prospective', color: '0F172A', size: 26 })], + alignment: AlignmentType.CENTER, + spacing: { after: 200 }, + })); + children.push(new Paragraph({ + children: [new TextRun({ text: `📅 ${today} 📋 ${actifs.length} marchés 💰 Budget total : ${fmtMDT(totalBudget)}`, color: '475569', size: 20 })], + alignment: AlignmentType.CENTER, + spacing: { after: 100 }, + })); + children.push(new Paragraph({ children: [new PageBreak()] })); + + // ── Sommaire exécutif + children.push(h1('Sommaire Exécutif')); + children.push(new Paragraph({ + children: [new TextRun({ text: `Ce rapport présente la situation au ${today} des ${actifs.length} marchés actifs de la Zone Sud de Tunisie Telecom. Budget global engagé : ${fmtMDT(totalBudget)}. Avancement physique moyen : ${fmtPct(avgPhy)}.`, size: 20, color: '374151' })], + spacing: { after: 120 }, + })); + + // KPI table + const normal = actifs.filter(r=>classify(r)==='Normal').length; + const sous = actifs.filter(r=>classify(r)==='Sous Avancement').length; + const dep = actifs.filter(r=>classify(r)==='Dépassement').length; + const nd = actifs.filter(r=>classify(r)==='Non déterminé').length; + + children.push(new Paragraph({ children: [new TextRun({ text: 'Indicateurs Clés', bold: true, size: 22, color: '002D62' })], spacing: { before: 150, after: 80 } })); + children.push(new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + rows: [ + tr([['Indicateur',2500,navyFill],['Valeur',2000,navyFill]], true), + tr([['Total marchés actifs', 2500], [String(actifs.length), 2000]]), + tr([['Marchés clôturés', 2500], [String(clotures.length), 2000]], false), + tr([['Budget total', 2500], [fmtMDT(totalBudget), 2000]], false), + tr([['Avancement physique moyen', 2500], [fmtPct(avgPhy), 2000]], false), + tr([['Normal', 2500], [String(normal), 2000]], false), + tr([['Sous Avancement', 2500], [String(sous), 2000]], false), + tr([['Dépassement', 2500], [String(dep), 2000]], false), + ], + })); + children.push(new Paragraph({ children: [new PageBreak()] })); + + // ── Alertes + children.push(h1('Alertes Délais')); + const alerteItems = actifs + .map(r => ({ ...r, _d: (() => { const v=parseInt(String(r.delai_restant||''),10); return !isNaN(v)?v:null; })() })) + .filter(r => r._d !== null && r._d <= DELAI_ATTENTION) + .sort((a, b) => a._d - b._d); + + children.push(new Paragraph({ + children: [new TextRun({ text: `${alerteItems.filter(r=>r._d<=DELAI_CRITIQUE).length} marché(s) en alerte critique (< ${DELAI_CRITIQUE}j) • ${alerteItems.length} total (< ${DELAI_ATTENTION}j)`, size: 20, color: 'EF4444', bold: true })], + spacing: { after: 100 }, + })); + if (alerteItems.length) { + children.push(new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + rows: [ + tr([['Référence',2500,navyFill],['Projet',3000,navyFill],['Région',1200,navyFill],['Délai (j)',1200,navyFill]], true), + ...alerteItems.slice(0,20).map((r, i) => tr([ + [r.id_marche||r.reference||'', 2500, i%2===1?altFill:undefined], + [r.projet||'', 3000, i%2===1?altFill:undefined], + [r.region||'', 1200, i%2===1?altFill:undefined], + [String(r._d||'—'), 1200, i%2===1?altFill:undefined], + ])), + ], + })); + } + children.push(new Paragraph({ children: [new PageBreak()] })); + + // ── Synthèse par région + children.push(h1('Synthèse par Région')); + for (const region of ALL_REGIONS) { + const regActifs = actifs.filter(r => (r.region||'') === region); + if (!regActifs.length) continue; + children.push(h2(`📍 ${region}`)); + const bud = regActifs.reduce((s,r)=>s+parseN(r.tot_marche||r.totmarche||r.montant),0); + const pl = regActifs.map(r=>parseN(r.taux_phy||r.avt_phy)).filter(v=>v>0); + const pm = pl.length ? pl.reduce((a,b)=>a+b,0)/pl.length : 0; + children.push(new Paragraph({ + children: [new TextRun({ text: `${regActifs.length} marchés • Budget : ${fmtMDT(bud)} • Phy moy : ${fmtPct(pm)}`, size: 18, color: '64748B' })], + spacing: { after: 80 }, + })); + children.push(new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + rows: [ + tr([['Référence',2500,navyFill],['Projet',2800,navyFill],['Entrepreneur',2000,navyFill],['Phy %',700,navyFill],['Statut',1000,navyFill]], true), + ...regActifs.map((r, i) => { + const phy = parseN(r.taux_phy||r.avt_phy); + return tr([ + [r.id_marche||r.reference||'', 2500, i%2===1?altFill:undefined], + [r.projet||'', 2800, i%2===1?altFill:undefined], + [r.entrepreneur||'', 2000, i%2===1?altFill:undefined], + [fmtPct(phy), 700, i%2===1?altFill:undefined], + [selVal(r.observation)||'—', 1000, i%2===1?altFill:undefined], + ]); + }), + ], + })); + children.push(spacer()); + } + children.push(new Paragraph({ children: [new PageBreak()] })); + + // ── Pilotage proactif + children.push(h1('Pilotage Proactif')); + children.push(new Paragraph({ + children: [new TextRun({ text: `Normal: ${normal} • Sous Avancement: ${sous} • Dépassement: ${dep} • Non déterminé: ${nd}`, size: 20, color: '374151' })], + spacing: { after: 100 }, + })); + const pilotItems = actifs.map(r => ({ r, res: classify(r), phy: parseN(r.taux_phy||r.avt_phy) })) + .sort((a,b) => a.phy - b.phy); + children.push(new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + rows: [ + tr([['Référence',2200,navyFill],['Projet',2500,navyFill],['Région',1000,navyFill],['Entrepreneur',1800,navyFill],['Phy %',700,navyFill],['Résultat',1300,navyFill]], true), + ...pilotItems.map(({ r, res, phy }, i) => tr([ + [r.id_marche||r.reference||'', 2200, i%2===1?altFill:undefined], + [r.projet||'', 2500, i%2===1?altFill:undefined], + [r.region||'', 1000, i%2===1?altFill:undefined], + [r.entrepreneur||'', 1800, i%2===1?altFill:undefined], + [fmtPct(phy), 700, i%2===1?altFill:undefined], + [res, 1300, i%2===1?altFill:undefined], + ])), + ], + })); + + const doc = new Document({ + creator: 'RLA API', + description: 'Rapport Marchés Tunisie Telecom Zone Sud', + title: 'Rapport RLA Zone Sud', + sections: [{ children }], + }); + const buf = await Packer.toBuffer(doc); + const filename = `Rapport_RLA_Zone_Sud_${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..723b8da --- /dev/null +++ b/routes/pilotage.js @@ -0,0 +1,72 @@ +/** + * GET /api/pilotage-proactif + * Pilotage proactif : référence, entrepreneur, projet, montant max/min/projeté, résultat + */ +const express = require('express'); +const router = express.Router(); +const { getMarches } = require('../services/baserow'); +const { + selectVal, isCloture, normalizeMarche, parseNum, + niveauAvancement, getDelaiRestant, niveauAlerte, resultatPhysique, + SEUIL_STANDARD, SEUIL_MODERNISATION, SEUIL_CRITIQUE_PCT, +} = require('../services/calc'); + +router.get('/', async (req, res) => { + try { + const { region, entrepreneur, nature, niveau, resultat } = 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 => selectVal(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, selectVal(r.nature)), + resultat: resultatPhysique(r), + }; + }); + + const normal = items.filter(r => r.resultat === 'Normal'); + const sous_avancement = items.filter(r => r.resultat === 'Sous Avancement'); + const depassement = items.filter(r => r.resultat === 'Dépassement'); + const non_determine = items.filter(r => r.resultat === 'Non déterminé'); + + // Filtres optionnels + let result = items; + if (niveau === 'normal') result = normal; + else if (niveau === 'sous') result = sous_avancement; + else if (niveau === 'dep') result = depassement; + if (resultat) result = result.filter(r => r.resultat === resultat); + + res.json({ + seuils: { + standard: SEUIL_STANDARD, + modernisation: SEUIL_MODERNISATION, + critique: SEUIL_CRITIQUE_PCT, + }, + resume: { + total: items.length, + normal: normal.length, + sous_avancement: sous_avancement.length, + depassement: depassement.length, + non_determine: non_determine.length, + }, + normal, sous_avancement, depassement, non_determine, + 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..b9de66d --- /dev/null +++ b/routes/stats.js @@ -0,0 +1,51 @@ +/** + * GET /api/stats — compatibilité avec l'ancien front + */ +const express = require('express'); +const router = express.Router(); +const { getMarches } = require('../services/baserow'); +const { + selectVal, parseNum, isCloture, getDelaiRestant, niveauAlerte, + DELAI_ATTENTION, +} = require('../services/calc'); + +router.get('/', async (req, res) => { + try { + const rows = await getMarches(); + const actifs = rows.filter(r => !isCloture(r)); + + const parStatut = {}; + for (const r of rows) { + const s = selectVal(r.observation) || 'Inconnu'; + parStatut[s] = (parStatut[s] || 0) + 1; + } + + 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 alertes = actifs + .map(r => ({ ...r, _delai: getDelaiRestant(r) })) + .filter(r => r._delai !== null && r._delai <= DELAI_ATTENTION) + .map(r => ({ + id: r.id, ref: r.id_marche || r.reference || '', + entrepreneur: r.entrepreneur || '', projet: r.projet || '', region: r.region || '', + delai_restant: r._delai, niveau: niveauAlerte(r._delai), + })); + + 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..ca950fd --- /dev/null +++ b/routes/synthese.js @@ -0,0 +1,120 @@ +/** + * 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 { + selectVal, parseNum, formatMontant, isCloture, + getDelaiRestant, niveauAlerte, + DELAI_CRITIQUE, DELAI_ATTENTION, +} = require('../services/calc'); + +router.get('/', async (req, res) => { + try { + const { region, nature, entrepreneur, projet } = req.query; + const regionFilter = req.regionFilter; + + let rows = await getMarches(); + + 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 => selectVal(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.m_max), 0); + const totalAvtFin = actifs.reduce((s, r) => s + parseNum(r.avt_fin), 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 (observation) + const parStatut = {}; + for (const r of rows) { + const s = selectVal(r.observation) || 'Inconnu'; + parStatut[s] = (parStatut[s] || 0) + 1; + } + + // Par nature (CAPEX/OPEX) + const parNature = {}; + for (const r of actifs) { + const n = selectVal(r.nature) || 'Non défini'; + parNature[n] = (parNature[n] || 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; + } + + // 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.id_marche || 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 + const pilotage = { normal: 0, sous_avancement: 0, depasse: 0 }; + for (const r of actifs) { + const t = parseNum(r.taux_phy || r.avt_phy); + if (t >= 90) pilotage.depasse++; + else if (t >= 70) pilotage.sous_avancement++; + else pilotage.normal++; + } + + res.json({ + total: rows.length, actifs: actifs.length, clotures: clotures.length, + budget: { + total: formatMontant(totalBudget), + total_raw: totalBudget, + consomme: formatMontant(totalAvtFin), + consomme_raw: totalAvtFin, + restant: formatMontant(totalBudget - totalAvtFin), + restant_raw: totalBudget - totalAvtFin, + }, + taux_avancement_moyen: tauxMoyen, + par_statut: parStatut, + par_nature: parNature, + par_region: parRegion, + 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..5287927 --- /dev/null +++ b/services/calc.js @@ -0,0 +1,201 @@ +/** + * services/calc.js + * Helpers partagés : calculs, formatage, seuils métier RLA + * Champs Baserow table 856 : id_marche, nature{value}, region, observation{value}, + * taux_phy, taux_fin, avt_phy, avt_fin, m_min, m_max, tot_marche, + * date_debut, date_fin, delai_restant, debut_marche, date_fin_marche + */ + +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); + +// ─── Helpers Baserow select/multi-select ───────────────────────────────────── + +/** Extrait la valeur d'un champ Baserow (select ou string) */ +function selectVal(v) { + if (!v) return ''; + if (typeof v === 'object' && v.value !== undefined) return String(v.value); + if (Array.isArray(v)) return v.map(x => (x.value !== undefined ? x.value : x)).join(', '); + return String(v); +} + +// ─── Parseurs ──────────────────────────────────────────────────────────────── + +function parseNum(v) { + if (v === null || v === undefined || v === '') return 0; + if (typeof v === 'object') return 0; // objet Baserow non-numérique + const n = parseFloat(String(v).replace(/\s/g, '').replace(',', '.')); + return isNaN(n) ? 0 : n; +} + +function parseDateFR(d) { + if (!d) return null; + const parts = String(d).split(/[\/\-]/); + if (parts.length === 3) { + const [a, b, c] = parts; + if (a.length === 4) return new Date(`${a}-${b}-${c}`); + if (c.length === 4) return new Date(`${c}-${b}-${a}`); + } + 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: 0 }) + ' 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)} %`; +} + +// ─── Statuts métier ─────────────────────────────────────────────────────────── + +function isCloture(r) { + const obs = selectVal(r.observation).toLowerCase(); + return obs.includes('clôtur') || obs.includes('clotur') || !!r.date_cloture; +} + +function getDelaiRestant(r) { + // Champ calculé Baserow + const dField = r.delai_restant; + if (dField !== null && dField !== undefined && dField !== '') { + const v = parseInt(String(dField), 10); + if (!isNaN(v)) return v; + } + // Calculer depuis date_fin + const fin = r.date_fin || r.date_fin_marche || 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 nat = String(nature || '').toLowerCase(); + const seuil = nat.includes('modern') ? SEUIL_MODERNISATION : SEUIL_STANDARD; + if (t >= SEUIL_CRITIQUE_PCT) return 'dépassé'; + if (t >= seuil) return 'sous_avancement'; + return 'normal'; +} + +/** + * Résultat financier du marché (pour pilotage proactif) + * Compare avt_fin (avancement financier en DT) vs m_min / m_max + */ +function resultatFinancier(r) { + const avt = parseNum(r.avt_fin); + const mMin = parseNum(r.m_min); + const mMax = parseNum(r.m_max ?? r.tot_marche); + if (avt === 0 && mMin === 0) return 'Non déterminé'; + if (avt > mMax && mMax > 0) return 'Dépassement'; + if (avt < mMin && mMin > 0) return 'Sous Min'; + return 'Normal'; +} + +/** + * Résultat basé sur l'avancement PHYSIQUE du marché (pilotage proactif) + * Compare taux_phy vs seuils standards / critique + */ +function resultatPhysique(r) { + const taux = parseNum(r.taux_phy || r.avt_phy); + const nat = String(selectVal(r.nature) || '').toLowerCase(); + const seuil = nat.includes('modern') ? SEUIL_MODERNISATION : SEUIL_STANDARD; + if (taux === 0) return 'Non déterminé'; + if (taux >= SEUIL_CRITIQUE_PCT) return 'Dépassement'; + if (taux >= seuil) return 'Normal'; + return 'Sous Avancement'; +} + +function niveauRisque(r) { + const delai = getDelaiRestant(r); + const avt = parseNum(r.taux_phy || r.avt_phy); + 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 obsValue = selectVal(r.observation); + const natureValue = selectVal(r.nature); + const delaiRestant = getDelaiRestant(r); + const tauxPhy = parseNum(r.taux_phy || r.avt_phy); + const tauxFin = parseNum(r.taux_fin || r.avt_fin); + const montant = parseNum(r.tot_marche || r.totmarche || r.montant); + const mMin = parseNum(r.m_min); + const mMax = parseNum(r.m_max || r.tot_marche); + const avt_fin_raw = parseNum(r.avt_fin); + + return { + id: r.id, + ref: r.id_marche || r.reference || String(r.id || ''), + projet: r.projet || '', + region: r.region || r.region_csc || '', + region_csc: r.region_csc || r.region || '', + entrepreneur: r.entrepreneur || '', + nature: natureValue, + statut: obsValue, + observation: obsValue, + lots: r.lots || '', + cloture: isCloture(r), + + date_debut: formatDateFR(r.date_debut || r.debut_marche), + date_fin: formatDateFR(r.date_fin || r.date_fin_marche), + date_cloture: formatDateFR(r.date_cloture), + alerte_echeance: r.Alerte_Echeance || '', + + montant_raw: montant, + montant: formatMontant(montant), + m_min_raw: mMin, + m_min: formatMontant(mMin), + m_max_raw: mMax, + m_max: formatMontant(mMax), + montant_proj_raw: avt_fin_raw, + montant_proj: formatMontant(avt_fin_raw), + + 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(tauxPhy, natureValue), + niveau_risque: niveauRisque(r), + resultat: resultatFinancier(r), + }; +} + +module.exports = { + SEUIL_STANDARD, SEUIL_MODERNISATION, SEUIL_CRITIQUE_PCT, + DELAI_CRITIQUE, DELAI_ATTENTION, + selectVal, parseNum, parseDateFR, + formatMontant, formatDateFR, formatPct, + isCloture, getDelaiRestant, niveauAlerte, + niveauAvancement, resultatFinancier, resultatPhysique, niveauRisque, + normalizeMarche, +}; diff --git a/services/export-pdf.js b/services/export-pdf.js new file mode 100644 index 0000000..5ee04b6 --- /dev/null +++ b/services/export-pdf.js @@ -0,0 +1,303 @@ +/** + * 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 Max','Période','Avt. Phy.','Délai Rest.','Alerte'], + colWidths:[65,140,65,110,90,90,55,55,60], + rows: (data.items||[]).map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.montant, + `${r.date_debut||'—'} → ${r.date_fin||'—'}`,r.taux_phy,r.delai_restant??'—',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 Max','Période','Avt. Phy.','Niveau Avt.'], + colWidths:[65,140,65,110,90,90,60,70], + rows: (data.items||[]).map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.montant, + `${r.date_debut||'—'} → ${r.date_fin||'—'}`,r.taux_phy,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..8ab9db3 --- /dev/null +++ b/services/export-xlsx.js @@ -0,0 +1,450 @@ +/** + * services/export-xlsx.js + * Génération XLSX comprehensive — Situation des Marchés RLA Zone Sud + */ +const ExcelJS = require('exceljs'); + +const C = { + NAVY: 'FF002D62', + WHITE: 'FFFFFFFF', + ACCENT: 'FF00D4FF', + GREEN: 'FF16A34A', + ORANGE: 'FFEA580C', + RED: 'FFDC2626', + YELLOW: 'FFEAB308', + GRAY: 'FF64748B', + ALT: 'FFF1F5F9', + LIGHT: 'FFE2E8F0', + CAPEX: 'FFD1FAE5', + OPEX: 'FFFEF3C7', + TOTAL: 'FFDBEAFE', + HEADER: 'FF0F172A', +}; + +function fill(argb) { return { type: 'pattern', pattern: 'solid', fgColor: { argb } }; } +function font(argb, bold = false, size = 10) { return { color: { argb }, bold, size }; } +function border(color = C.LIGHT) { + const s = { style: 'thin', color: { argb: color } }; + return { top: s, bottom: s, left: s, right: s }; +} + +const ALL_REGIONS = ['Gabes', 'Gafsa', 'Kebili', 'Medenine', 'Sfax', 'Tataouine', 'Tozeur']; + +function parseNum(v) { + if (v === null || v === undefined || v === '') return 0; + if (typeof v === 'object') return 0; + const n = parseFloat(String(v).replace(/\s/g, '').replace(',', '.')); + return isNaN(n) ? 0 : n; +} + +function selectVal(v) { + if (!v) return ''; + if (typeof v === 'object' && v.value !== undefined) return String(v.value); + if (Array.isArray(v)) return v.map(x => x.value !== undefined ? x.value : x).join(', '); + return String(v); +} + +function parseDateFR(d) { + if (!d) return null; + const parts = String(d).split(/[\/\-]/); + if (parts.length === 3) { + const [a, b, c] = parts; + if (a.length === 4) return new Date(`${a}-${b}-${c}`); + if (c.length === 4) return new Date(`${c}-${b}-${a}`); + } + const dt = new Date(d); + return isNaN(dt.getTime()) ? null : dt; +} + +function fmtDate(d) { + const dt = parseDateFR(d); + if (!dt) return '-'; + return dt.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' }); +} + +function fmtMDT(val) { + const n = parseNum(val); + if (n === 0) return '0'; + if (n >= 1000000) return `${(n / 1000000).toFixed(1)} MDT`; + if (n >= 1000) return `${(n / 1000).toFixed(0)} kDT`; + return `${n.toFixed(0)} DT`; +} + +function isCloture(r) { + const obs = selectVal(r.observation).toLowerCase(); + return obs.includes('clôtur') || obs.includes('clotur') || !!r.date_cloture; +} + +function getDelai(r) { + const dField = r.delai_restant; + if (dField !== null && dField !== undefined && dField !== '') { + const v = parseInt(String(dField), 10); + if (!isNaN(v)) return v; + } + const fin = r.date_fin || r.date_fin_marche || r.datefin; + const dt = parseDateFR(fin); + if (!dt) return '-'; + return Math.ceil((dt - new Date()) / 86400000); +} + +async function generateXlsx(view, data, allRows) { + const wb = new ExcelJS.Workbook(); + wb.creator = 'RLA API'; + wb.company = 'Tunisie Telecom Zone Sud'; + wb.created = new Date(); + + const rows = allRows || data.items || data.regions || []; + const actifs = allRows ? rows.filter(r => !isCloture(r)) : rows; + + await buildSheet1(wb, actifs); + await buildSheet2(wb, actifs); + + return wb.xlsx.writeBuffer(); +} + +async function buildSheet1(wb, actifs) { + const ws = wb.addWorksheet('Situation des Marchés'); + ws.views = [{ state: 'frozen', ySplit: 9 }]; + + // Column widths + const cols = [ + { width: 40 }, { width: 20 }, { width: 22 }, { width: 12 }, + { width: 16 }, { width: 14 }, { width: 8 }, { width: 14 }, + { width: 8 }, { width: 14 }, { width: 14 }, { width: 8 }, { width: 22 }, + ]; + ws.columns = cols.map((c, i) => ({ key: String.fromCharCode(65 + i), width: c.width })); + + const today = new Date().toLocaleDateString('fr-FR'); + const capex = actifs.filter(r => String(selectVal(r.nature)).toUpperCase().includes('CAPEX') || + !String(selectVal(r.nature)).toUpperCase().includes('OPEX')); + const opex = actifs.filter(r => String(selectVal(r.nature)).toUpperCase().includes('OPEX')); + const totalBudget = actifs.reduce((s, r) => s + parseNum(r.tot_marche || r.totmarche || r.montant), 0); + + const avgPhy = (() => { + const vals = actifs.map(r => parseNum(r.taux_phy || r.avt_phy)).filter(v => v > 0); + return vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : 0; + })(); + + // Row 1: Title + const r1 = ws.addRow(['📊 SITUATION DES MARCHÉS RLA — ZONE SUD', ...Array(12).fill('')]); + r1.height = 30; + ws.mergeCells('A1:M1'); + const c1 = r1.getCell(1); + c1.fill = fill(C.NAVY); + c1.font = { color: { argb: C.WHITE }, bold: true, size: 16 }; + c1.alignment = { horizontal: 'center', vertical: 'middle' }; + + // Row 2: Subtitle + const r2 = ws.addRow([`Tunisie Telecom • Direction Centrale Achats • Zone Sud`, ...Array(12).fill('')]); + r2.height = 18; + ws.mergeCells('A2:M2'); + r2.getCell(1).fill = fill('FF0F172A'); + r2.getCell(1).font = font('FFCBD5E1', false, 11); + r2.getCell(1).alignment = { horizontal: 'center', vertical: 'middle' }; + + // Row 3: Stats bar + const r3 = ws.addRow([ + `📅 ${today} │ 📋 ${actifs.length} marchés │ 💰 ${fmtMDT(totalBudget)} │ 📈 Phy moy: ${avgPhy.toFixed(0)}%`, + ...Array(12).fill(''), + ]); + r3.height = 16; + ws.mergeCells('A3:M3'); + r3.getCell(1).fill = fill('FF1E3A5F'); + r3.getCell(1).font = font('FF94A3B8', false, 10); + r3.getCell(1).alignment = { horizontal: 'center', vertical: 'middle' }; + + // Row 4: empty + ws.addRow([]); + + // Row 5: KPI headers + const r5 = ws.addRow(['📊 GLOBAL', '', '', '', '', '🟢 CAPEX', '', '', '', '', '🟠 OPEX', '', '']); + r5.height = 22; + ws.mergeCells('A5:E5'); ws.mergeCells('F5:J5'); ws.mergeCells('K5:M5'); + const kpiHdrStyle = (cell, argb) => { + cell.fill = fill(argb); + cell.font = { color: { argb: C.WHITE }, bold: true, size: 11 }; + cell.alignment = { horizontal: 'center', vertical: 'middle' }; + }; + kpiHdrStyle(r5.getCell(1), C.NAVY); + kpiHdrStyle(r5.getCell(6), 'FF16A34A'); + kpiHdrStyle(r5.getCell(11), C.ORANGE); + + const capexBudget = capex.reduce((s, r) => s + parseNum(r.tot_marche || r.totmarche || r.montant), 0); + const opexBudget = opex.reduce((s, r) => s + parseNum(r.tot_marche || r.totmarche || r.montant), 0); + const capexPhy = (() => { const v = capex.map(r => parseNum(r.taux_phy || r.avt_phy)).filter(x => x > 0); return v.length ? v.reduce((a,b)=>a+b,0)/v.length : 0; })(); + const opexPhy = (() => { const v = opex.map(r => parseNum(r.taux_phy || r.avt_phy)).filter(x => x > 0); return v.length ? v.reduce((a,b)=>a+b,0)/v.length : 0; })(); + + // Row 6: KPI values + const r6 = ws.addRow([`${actifs.length} marchés`, '', '', '', '', + `${capex.length} marchés`, '', '', '', '', `${opex.length} marchés`, '', '']); + r6.height = 18; + ws.mergeCells('A6:E6'); ws.mergeCells('F6:J6'); ws.mergeCells('K6:M6'); + [1, 6, 11].forEach(col => { + r6.getCell(col).font = { bold: true, size: 14, color: { argb: C.NAVY } }; + r6.getCell(col).alignment = { horizontal: 'center', vertical: 'middle' }; + }); + + // Row 7: KPI details + const r7 = ws.addRow([ + `Budget: ${fmtMDT(totalBudget)} • Phy moy: ${avgPhy.toFixed(0)}%`, '', '', '', '', + `Budget: ${fmtMDT(capexBudget)} • Phy: ${capexPhy.toFixed(0)}%`, '', '', '', '', + `Budget: ${fmtMDT(opexBudget)} • Phy: ${opexPhy.toFixed(0)}%`, '', '', + ]); + r7.height = 16; + ws.mergeCells('A7:E7'); ws.mergeCells('F7:J7'); ws.mergeCells('K7:M7'); + [1, 6, 11].forEach(col => { + r7.getCell(col).font = { size: 9, color: { argb: C.GRAY } }; + r7.getCell(col).alignment = { horizontal: 'center', vertical: 'middle' }; + }); + + // Row 8: empty + ws.addRow([]); + + // Row 9: Column headers + const HEADERS = ['Référence', 'Projet', 'Entrepreneur', 'Nature', + 'Montant Marché', 'Av. Phy (DT)', 'Phy %', 'Av. Fin (DT)', 'Fin %', + 'Début', 'Fin', 'Délai', 'Observation']; + const r9 = ws.addRow(HEADERS); + r9.height = 22; + r9.eachCell(cell => { + cell.fill = fill(C.HEADER); + cell.font = { color: { argb: C.WHITE }, bold: true, size: 10 }; + cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true }; + cell.border = border(C.NAVY); + }); + + // Per-region data + for (const region of ALL_REGIONS) { + const regRows = actifs.filter(r => (r.region || '') === region); + if (!regRows.length) continue; + + // Region header row + const rh = ws.addRow([`📍 ${region} — ${regRows.length} marchés`, ...Array(12).fill('')]); + rh.height = 18; + ws.mergeCells(`A${rh.number}:M${rh.number}`); + rh.getCell(1).fill = fill('FF1E3A5F'); + rh.getCell(1).font = { bold: true, size: 11, color: { argb: C.ACCENT } }; + rh.getCell(1).alignment = { horizontal: 'left', vertical: 'middle', indent: 1 }; + + let subtotalBudget = 0, subtotalPhy = 0, subtotalFin = 0, phyCount = 0; + + for (let i = 0; i < regRows.length; i++) { + const r = regRows[i]; + const nat = selectVal(r.nature); + const isCapex = nat.toUpperCase().includes('CAPEX'); + const budget = parseNum(r.tot_marche || r.totmarche || r.montant); + const phyDT = parseNum(r.avt_phy); + const phyPct = parseNum(r.taux_phy || r.avt_phy); + const finDT = parseNum(r.avt_fin); + const finPct = parseNum(r.taux_fin); + const delai = getDelai(r); + + subtotalBudget += budget; + if (phyPct > 0) { subtotalPhy += phyPct; phyCount++; } + subtotalFin += finDT; + + const rd = ws.addRow([ + r.id_marche || r.reference || '', + r.projet || '', + r.entrepreneur || '', + nat, + budget || '', + phyDT || '', + phyPct > 0 ? phyPct / 100 : '', + finDT || '', + finPct > 0 ? finPct / 100 : '', + fmtDate(r.date_debut || r.debut_marche), + fmtDate(r.date_fin || r.date_fin_marche), + delai, + selectVal(r.observation), + ]); + rd.height = 15; + + const altFill = i % 2 === 1 ? fill(C.ALT) : undefined; + const natFill = isCapex ? fill(C.CAPEX) : fill(C.OPEX); + + rd.eachCell((cell, col) => { + if (altFill) cell.fill = altFill; + if (col === 4) cell.fill = natFill; + cell.border = { bottom: { style: 'thin', color: { argb: C.LIGHT } } }; + cell.alignment = { vertical: 'middle' }; + if ([5, 6, 8].includes(col)) cell.numFmt = '#,##0'; + if ([7, 9].includes(col)) cell.numFmt = '0%'; + if ([12].includes(col)) cell.alignment = { horizontal: 'center', vertical: 'middle' }; + }); + } + + // Subtotal row + const avgPct = phyCount > 0 ? subtotalPhy / phyCount : 0; + const rst = ws.addRow([ + `Sous-total ${region} (${regRows.length})`, '', '', + '', subtotalBudget, '', avgPct / 100, '', '', + '', '', '', '', + ]); + rst.height = 16; + ws.mergeCells(`A${rst.number}:D${rst.number}`); + rst.eachCell(cell => { + cell.fill = fill('FF1E3A5F'); + cell.font = { bold: true, size: 9, color: { argb: C.WHITE } }; + cell.border = { top: { style: 'medium', color: { argb: C.NAVY } }, bottom: { style: 'medium', color: { argb: C.NAVY } } }; + }); + rst.getCell(5).numFmt = '#,##0'; + rst.getCell(7).numFmt = '0%'; + rst.getCell(1).alignment = { horizontal: 'right', vertical: 'middle' }; + + ws.addRow([]); // spacer + } + + // Grand total row + const gt = ws.addRow([ + `TOTAL ZONE SUD (${actifs.length} marchés)`, '', '', '', + actifs.reduce((s, r) => s + parseNum(r.tot_marche || r.totmarche || r.montant), 0), + '', avgPhy / 100, '', '', '', '', '', '', + ]); + ws.mergeCells(`A${gt.number}:D${gt.number}`); + gt.height = 22; + gt.eachCell(cell => { + cell.fill = fill(C.NAVY); + cell.font = { bold: true, size: 11, color: { argb: C.WHITE } }; + cell.border = { top: { style: 'medium', color: { argb: C.ACCENT } } }; + }); + gt.getCell(5).numFmt = '#,##0'; + gt.getCell(7).numFmt = '0%'; + gt.getCell(1).alignment = { horizontal: 'right', vertical: 'middle' }; +} + +async function buildSheet2(wb, actifs) { + const ws = wb.addWorksheet('Pilotage Proactif'); + ws.views = [{ state: 'frozen', ySplit: 9 }]; + + ws.columns = [ + { width: 40 }, { width: 20 }, { width: 22 }, { width: 16 }, + { width: 10 }, { width: 10 }, { width: 12 }, { width: 12 }, { width: 18 }, + ]; + + const today = new Date().toLocaleDateString('fr-FR'); + + // Title + const r1 = ws.addRow(['📈 PILOTAGE PROACTIF — ZONE SUD', ...Array(8).fill('')]); + r1.height = 30; + ws.mergeCells('A1:I1'); + r1.getCell(1).fill = fill(C.NAVY); + r1.getCell(1).font = { color: { argb: C.WHITE }, bold: true, size: 16 }; + r1.getCell(1).alignment = { horizontal: 'center', vertical: 'middle' }; + + const r2 = ws.addRow([`Tunisie Telecom • Direction Centrale Achats • Zone Sud`, ...Array(8).fill('')]); + r2.height = 18; + ws.mergeCells('A2:I2'); + r2.getCell(1).fill = fill('FF0F172A'); + r2.getCell(1).font = font('FFCBD5E1', false, 11); + r2.getCell(1).alignment = { horizontal: 'center', vertical: 'middle' }; + + const r3 = ws.addRow([`📅 ${today} │ 📋 ${actifs.length} marchés actifs`, ...Array(8).fill('')]); + r3.height = 16; + ws.mergeCells('A3:I3'); + r3.getCell(1).fill = fill('FF1E3A5F'); + r3.getCell(1).font = font('FF94A3B8', false, 10); + r3.getCell(1).alignment = { horizontal: 'center', vertical: 'middle' }; + + ws.addRow([]); + + // KPIs + const SEUIL_STD = parseFloat(process.env.SEUIL_STANDARD || 70); + const SEUIL_CRIT = parseFloat(process.env.SEUIL_CRITIQUE_PCT || 90); + const SEUIL_MOD = parseFloat(process.env.SEUIL_MODERNISATION || 50); + + const classify = r => { + const taux = parseNum(r.taux_phy || r.avt_phy); + const nat = String(selectVal(r.nature) || '').toLowerCase(); + const seuil = nat.includes('modern') ? SEUIL_MOD : SEUIL_STD; + if (taux === 0) return 'Non déterminé'; + if (taux >= SEUIL_CRIT) return 'Dépassement'; + if (taux >= seuil) return 'Normal'; + return 'Sous Avancement'; + }; + + const normal = actifs.filter(r => classify(r) === 'Normal').length; + const sous = actifs.filter(r => classify(r) === 'Sous Avancement').length; + const dep = actifs.filter(r => classify(r) === 'Dépassement').length; + const nd = actifs.filter(r => classify(r) === 'Non déterminé').length; + + const r5 = ws.addRow(['✅ NORMAL', '', '❌ SOUS AVANCEMENT', '', '⚡ DÉPASSEMENT', '', '❓ NON DÉTERMINÉ', '', '📊 TOTAL']); + r5.height = 22; + ws.mergeCells('A5:B5'); ws.mergeCells('C5:D5'); ws.mergeCells('E5:F5'); ws.mergeCells('G5:H5'); + [[1,C.GREEN],[3,'FFDC2626'],[5,C.ORANGE],[7,C.GRAY],[9,C.NAVY]].forEach(([col, argb]) => { + r5.getCell(col).fill = fill(argb); + r5.getCell(col).font = { color: { argb: C.WHITE }, bold: true, size: 10 }; + r5.getCell(col).alignment = { horizontal: 'center', vertical: 'middle' }; + }); + + const r6 = ws.addRow([normal, '', sous, '', dep, '', nd, '', actifs.length]); + r6.height = 20; + ws.mergeCells('A6:B6'); ws.mergeCells('C6:D6'); ws.mergeCells('E6:F6'); ws.mergeCells('G6:H6'); + [1, 3, 5, 7, 9].forEach(col => { + r6.getCell(col).font = { bold: true, size: 18 }; + r6.getCell(col).alignment = { horizontal: 'center', vertical: 'middle' }; + }); + + ws.addRow([]); + ws.addRow([]); + + // Column headers + const HEADERS2 = ['Référence', 'Projet', 'Entrepreneur', 'Région', + 'Phy %', 'Fin %', 'Délai', 'Alerte', 'Résultat']; + const r9 = ws.addRow(HEADERS2); + r9.height = 22; + r9.eachCell(cell => { + cell.fill = fill(C.HEADER); + cell.font = { color: { argb: C.WHITE }, bold: true, size: 10 }; + cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true }; + cell.border = border(C.NAVY); + }); + + const ALERTE_COLOR = { 'critique': 'FFDC2626', 'attention': 'FFEA580C', 'normal': C.GREEN, 'indéterminé': C.GRAY }; + const RESULT_COLOR = { 'Normal': C.GREEN, 'Sous Avancement': 'FFDC2626', 'Dépassement': C.ORANGE, 'Non déterminé': C.GRAY }; + + const DELAI_CRIT = parseInt(process.env.DELAI_CRITIQUE || 45); + const DELAI_ATT = parseInt(process.env.DELAI_ATTENTION || 90); + const niveauAlerte = d => d === null ? 'indéterminé' : d <= DELAI_CRIT ? 'critique' : d <= DELAI_ATT ? 'attention' : 'normal'; + + const sorted = [...actifs].sort((a, b) => { + const ta = parseNum(a.taux_phy || a.avt_phy); + const tb = parseNum(b.taux_phy || b.avt_phy); + return ta - tb; + }); + + for (let i = 0; i < sorted.length; i++) { + const r = sorted[i]; + const phyPct = parseNum(r.taux_phy || r.avt_phy); + const finPct = parseNum(r.taux_fin); + const delai = getDelai(r); + const alerte = niveauAlerte(typeof delai === 'number' ? delai : null); + const result = classify(r); + + const rd = ws.addRow([ + r.id_marche || r.reference || '', + r.projet || '', + r.entrepreneur || '', + r.region || '', + phyPct / 100, + finPct / 100, + typeof delai === 'number' ? delai : '-', + alerte, + result, + ]); + rd.height = 15; + + if (i % 2 === 1) rd.eachCell(cell => { cell.fill = fill(C.ALT); }); + + rd.getCell(5).numFmt = '0%'; + rd.getCell(6).numFmt = '0%'; + rd.getCell(8).font = { color: { argb: ALERTE_COLOR[alerte] || C.GRAY }, bold: true }; + rd.getCell(9).font = { color: { argb: RESULT_COLOR[result] || C.GRAY }, bold: true }; + rd.eachCell(cell => { + cell.border = { bottom: { style: 'thin', color: { argb: C.LIGHT } } }; + cell.alignment = { vertical: 'middle' }; + }); + } +} + +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 };