/** * 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;