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,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 };
|
||||
Reference in New Issue
Block a user