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:
Nabil Derouiche
2026-03-12 23:47:10 +01:00
co-authored by Claude Sonnet 4.6
commit 88a0dbe6d2
39 changed files with 7622 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
/**
* services/export-xlsx.js
* Génération XLSX avec ExcelJS (SuperAdmin uniquement)
*/
const ExcelJS = require('exceljs');
const HEADER_FILL = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF002D62' } };
const HEADER_FONT = { color: { argb: 'FFFFFFFF' }, bold: true, size: 10 };
const ALT_FILL = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
function styleHeader(row) {
row.eachCell(cell => {
cell.fill = HEADER_FILL;
cell.font = HEADER_FONT;
cell.alignment = { vertical: 'middle', horizontal: 'center' };
cell.border = { bottom: { style: 'thin', color: { argb: 'FFE2E8F0' } } };
});
row.height = 22;
}
function styleDataRow(row, alt) {
if (alt) {
row.eachCell(cell => { cell.fill = ALT_FILL; });
}
row.height = 16;
}
async function generateXlsx(view, data) {
const wb = new ExcelJS.Workbook();
wb.creator = 'RLA API';
wb.created = new Date();
const titles = {
synthese: 'Synthèse Globale',
alertes: 'Alertes Délais',
'en-service': 'Marchés en Service',
'en-cours': 'Marchés en Cours',
'par-region': 'Par Région',
clotures: 'Marchés Clôturés',
pilotage: 'Pilotage Proactif',
'matrice-risque': 'Matrice de Risque',
};
const title = titles[view] || view;
const ws = wb.addWorksheet(title.slice(0, 31));
const items = data.items || data.regions || [];
if (!items.length) {
ws.addRow(['Aucune donnée disponible.']);
return wb.xlsx.writeBuffer();
}
const sample = items[0];
const keys = Object.keys(sample).filter(k => !k.endsWith('_raw') && k !== 'id' && typeof sample[k] !== 'object');
// En-tête
ws.columns = keys.map(k => ({ header: k, key: k, width: 20 }));
styleHeader(ws.getRow(1));
// Données
items.forEach((item, i) => {
const row = ws.addRow(keys.map(k => item[k] ?? ''));
styleDataRow(row, i % 2 === 1);
});
// Freeze header
ws.views = [{ state: 'frozen', ySplit: 1 }];
// Onglet résumé si synthèse
if (view === 'synthese' && data.par_statut) {
const ws2 = wb.addWorksheet('Par Statut');
ws2.columns = [{ header: 'Statut', key: 'statut', width: 30 }, { header: 'Nombre', key: 'nb', width: 15 }];
styleHeader(ws2.getRow(1));
Object.entries(data.par_statut).forEach(([s, n], i) => {
const row = ws2.addRow({ statut: s, nb: n });
styleDataRow(row, i % 2 === 1);
});
}
return wb.xlsx.writeBuffer();
}
module.exports = { generateXlsx };