feat: RLA API v1.0.0 — API complète + exports + thème McKinsey
- 9 endpoints métier : synthese, alertes, en-service, en-cours, par-region, clotures, pilotage-proactif, matrice-risque - Exports PDF (tous rôles) / XLSX PPTX DOCX (superadmin) - services/calc.js : helpers normalisés partagés - services/export-pdf.js : PDF async PDFKit par vue - Thème McKinsey (#1C2B4B / bleu pétrole / gris sobre) - Boutons XLSX/DOCX front (superadmin uniquement) - BASEROW_API_URL → https://baserow.bolbol.tn/api/ - dotenv override: true Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user