235 lines
11 KiB
JavaScript
235 lines
11 KiB
JavaScript
/**
|
|
* 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,
|
|
buildRef,
|
|
} = require('../services/calc');
|
|
|
|
const pdfGen = require('../services/export-pdf');
|
|
const { generateXlsx } = require('../services/export-xlsx');
|
|
const { generatePptx } = require('../services/export-pptx');
|
|
const { generateDocx } = require('../services/export-docx');
|
|
|
|
// ─── 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:buildRef(r),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 allRows = await getMarches();
|
|
const filtered = applyFilters(allRows, req);
|
|
const actifs = filtered.filter(r => !isCloture(r));
|
|
const clotures = filtered.filter(r => isCloture(r));
|
|
|
|
const filename = `Marches_RLA_Zone_Sud_${new Date().toISOString().slice(0,10)}.pptx`;
|
|
const buf = await generatePptx(actifs, clotures, filtered);
|
|
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 { getPipeline } = require('../services/baserow');
|
|
const allRows = await getMarches();
|
|
const pipelineRows = await getPipeline();
|
|
const filtered = applyFilters(allRows, req);
|
|
const actifs = filtered.filter(r => !isCloture(r));
|
|
const clotures = filtered.filter(r => isCloture(r));
|
|
|
|
const buf = await generateDocx({ actifs, clotures, filtered, pipelineRows });
|
|
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;
|