/** * GET /api/synthese * Vue synthèse globale : KPIs, répartitions, alertes, projections */ const express = require('express'); const router = express.Router(); const { getMarches } = require('../services/baserow'); const { selectVal, parseNum, formatMontant, isCloture, getDelaiRestant, niveauAlerte, DELAI_CRITIQUE, DELAI_ATTENTION, } = require('../services/calc'); router.get('/', async (req, res) => { try { const { region, nature, entrepreneur, projet } = req.query; const regionFilter = req.regionFilter; let rows = await getMarches(); if (regionFilter) rows = rows.filter(r => r.region === regionFilter); else if (region) rows = rows.filter(r => r.region === region); if (nature) rows = rows.filter(r => selectVal(r.nature).toLowerCase().includes(nature.toLowerCase())); if (entrepreneur) rows = rows.filter(r => String(r.entrepreneur || '').toLowerCase().includes(entrepreneur.toLowerCase())); if (projet) rows = rows.filter(r => String(r.projet || '').toLowerCase().includes(projet.toLowerCase())); const actifs = rows.filter(r => !isCloture(r)); const clotures = rows.filter(r => isCloture(r)); // Montants const totalBudget = actifs.reduce((s, r) => s + parseNum(r.tot_marche || r.m_max), 0); const totalAvtFin = actifs.reduce((s, r) => s + parseNum(r.avt_fin), 0); // Avancement moyen physique 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; // Par statut (observation) const parStatut = {}; for (const r of rows) { const s = selectVal(r.observation) || 'Inconnu'; parStatut[s] = (parStatut[s] || 0) + 1; } // Par nature (CAPEX/OPEX) const parNature = {}; for (const r of actifs) { const n = selectVal(r.nature) || 'Non défini'; parNature[n] = (parNature[n] || 0) + 1; } // Par région const parRegion = {}; for (const r of actifs) { const reg = r.region || 'Inconnu'; if (!parRegion[reg]) parRegion[reg] = { count: 0, taux_sum: 0, taux_count: 0 }; parRegion[reg].count++; const t = parseNum(r.taux_phy || r.avt_phy); if (t > 0) { parRegion[reg].taux_sum += t; parRegion[reg].taux_count++; } } for (const reg of Object.keys(parRegion)) { const d = parRegion[reg]; parRegion[reg].taux_moyen = d.taux_count ? Math.round(d.taux_sum / d.taux_count * 10) / 10 : 0; } // Alertes délais const alertes = actifs .map(r => ({ ...r, _delai: getDelaiRestant(r) })) .filter(r => r._delai !== null && r._delai <= DELAI_ATTENTION) .map(r => ({ id: r.id, ref: r.id_marche || r.reference || '', projet: r.projet || '', region: r.region || '', entrepreneur: r.entrepreneur || '', delai_restant: r._delai, niveau: niveauAlerte(r._delai), })) .sort((a, b) => a.delai_restant - b.delai_restant); // Pilotage const pilotage = { normal: 0, sous_avancement: 0, depasse: 0 }; for (const r of actifs) { const t = parseNum(r.taux_phy || r.avt_phy); if (t >= 90) pilotage.depasse++; else if (t >= 70) pilotage.sous_avancement++; else pilotage.normal++; } res.json({ total: rows.length, actifs: actifs.length, clotures: clotures.length, budget: { total: formatMontant(totalBudget), total_raw: totalBudget, consomme: formatMontant(totalAvtFin), consomme_raw: totalAvtFin, restant: formatMontant(totalBudget - totalAvtFin), restant_raw: totalBudget - totalAvtFin, }, taux_avancement_moyen: tauxMoyen, par_statut: parStatut, par_nature: parNature, par_region: parRegion, alertes_delais: { count: alertes.length, critique: alertes.filter(a => a.niveau === 'critique').length, attention: alertes.filter(a => a.niveau === 'attention').length, items: alertes, }, pilotage, }); } catch (err) { res.status(502).json({ error: 'Erreur Baserow', detail: err.message }); } }); module.exports = router;