feat: intégration table 872 — Pipeline AO enrichi + Modernisation succession
1. routes/pipeline.js — enrichissement AO - Phase calculée (Préparation/Ouvert/Dépouillé/Évaluation/Attribué) - Jours restants avant date-limite - Régions avec couleurs config - Estimation totale agrégée 2. routes/modernisation.js (nouveau) - Croise table 856 (nature Modernisation) avec table 872 (pipeline) - Jointure par région - Retourne actuel + suivant(s) par région 3. server.js — route /api/modernisation (admin+) 4. index.html - Slide 6 Pipeline AO : 8 colonnes, phases badges, région tags, jours alerte - Slide 4 Par Région : bloc "AO en lancement" sous chaque carte région - Slide 9 Modernisation (nouveau) : vue chaîne actuel → suivant par région - Nav : bouton Modernisation (admin+) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7ca0eccb96
commit
0a0ffc31cf
195
index.html
195
index.html
|
|
@ -285,6 +285,38 @@
|
|||
.spinner{width:46px;height:46px;border:4px solid var(--border-color);border-top-color:var(--accent);border-radius:50%;animation:spin 0.9s linear infinite;}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
|
||||
/* ── PIPELINE AO ── */
|
||||
.phase-badge{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:12px;font-size:0.73em;font-weight:700;color:white;}
|
||||
.pipeline-total{padding:11px 16px;text-align:right;border-top:1px solid var(--border-color);font-size:0.85em;color:var(--text-muted);}
|
||||
.pipeline-total strong{color:var(--accent);font-size:1.05em;}
|
||||
.region-tag{display:inline-flex;align-items:center;padding:2px 8px;border-radius:10px;font-size:0.72em;font-weight:700;color:white;margin:1px;}
|
||||
|
||||
/* ── REGION CARD — AO SUIVANT ── */
|
||||
.region-suivant{margin-top:10px;padding-top:10px;border-top:1px dashed var(--border-color);}
|
||||
.region-suivant-title{font-size:0.72em;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;display:flex;align-items:center;gap:5px;}
|
||||
.region-suivant-item{font-size:0.78em;color:var(--text);padding:5px 8px;background:var(--table-header);border-radius:6px;margin-bottom:4px;display:flex;justify-content:space-between;align-items:center;gap:8px;}
|
||||
|
||||
/* ── MODERNISATION SUCCESSION ── */
|
||||
.moderni-region-block{background:var(--bg-card);border-radius:14px;border:1px solid var(--border-color);margin-bottom:18px;overflow:hidden;backdrop-filter:blur(10px);}
|
||||
.moderni-region-header{background:linear-gradient(90deg,var(--primary),var(--primary-light));padding:11px 16px;display:flex;align-items:center;gap:10px;color:white;font-weight:700;}
|
||||
.moderni-region-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;}
|
||||
.moderni-body{display:grid;grid-template-columns:1fr 1fr;gap:0;}
|
||||
@media(max-width:900px){.moderni-body{grid-template-columns:1fr;}}
|
||||
.moderni-col{padding:14px 16px;}
|
||||
.moderni-col.actuel{border-right:1px solid var(--border-color);}
|
||||
@media(max-width:900px){.moderni-col.actuel{border-right:none;border-bottom:1px solid var(--border-color);}}
|
||||
.moderni-col-title{font-size:0.75em;font-weight:700;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:10px;display:flex;align-items:center;gap:6px;}
|
||||
.moderni-col-title.actuel{color:var(--success);}
|
||||
.moderni-col-title.suivant{color:#8b5cf6;}
|
||||
.moderni-card{background:var(--table-header);border-radius:8px;padding:10px 12px;margin-bottom:8px;font-size:0.83em;}
|
||||
.moderni-card:last-child{margin-bottom:0;}
|
||||
.moderni-card .mc-ref{font-weight:700;color:var(--accent);margin-bottom:4px;font-size:0.9em;}
|
||||
.moderni-card .mc-row{display:flex;justify-content:space-between;color:var(--text-muted);margin-top:3px;font-size:0.88em;}
|
||||
.moderni-card .mc-row span:last-child{color:var(--text);font-weight:600;}
|
||||
.moderni-empty{color:var(--text-muted);font-size:0.83em;font-style:italic;padding:8px 0;}
|
||||
.moderni-arrow{display:flex;align-items:center;justify-content:center;font-size:1.4em;color:var(--border-color);padding:0 4px;}
|
||||
@media(max-width:900px){.moderni-arrow{display:none;}}
|
||||
|
||||
/* ── FOOTER ── */
|
||||
.footer{text-align:center;padding:22px;color:var(--text-muted);font-size:0.83em;border-top:1px solid var(--border-color);margin-top:28px;}
|
||||
.footer-avatar{width:42px;height:42px;border-radius:50%;border:2px solid var(--accent);margin-bottom:7px;display:inline-flex;align-items:center;justify-content:center;background:var(--primary);color:var(--accent);font-size:1.1em;font-weight:700;}
|
||||
|
|
@ -387,6 +419,7 @@
|
|||
<button id="btn-slide-4" onclick="showSlide(4)"><i class="fas fa-map-marker-alt"></i> Par Région</button>
|
||||
<button id="btn-slide-5" onclick="showSlide(5)"><i class="fas fa-list-alt"></i> Marchés</button>
|
||||
<button id="btn-slide-6" class="nav-hidden" onclick="showSlide(6)"><i class="fas fa-stream"></i> Pipeline AO</button>
|
||||
<button id="btn-slide-9" class="nav-hidden" onclick="showSlide(9)"><i class="fas fa-link"></i> Modernisation</button>
|
||||
<button id="btn-slide-7" class="nav-hidden" onclick="showSlide(7)"><i class="fas fa-users-cog"></i> Utilisateurs</button>
|
||||
<button id="btn-slide-8" class="nav-hidden" onclick="showSlide(8)"><i class="fas fa-history"></i> Logs</button>
|
||||
<span class="nav-separator"></span>
|
||||
|
|
@ -613,18 +646,19 @@
|
|||
<h2 class="section-title"><i class="fas fa-stream" style="color:#6366F1"></i> Pipeline Appels d'Offres</h2>
|
||||
<div class="table-container">
|
||||
<div class="table-header" style="background:linear-gradient(90deg,#4F46E5,#6366F1);">
|
||||
<h3><i class="fas fa-rocket"></i> Projets en Préparation</h3>
|
||||
<h3><i class="fas fa-rocket"></i> AO en cours de lancement</h3>
|
||||
<span class="badge" id="pipeline-count">0 projets</span>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Description du projet</th><th>Régions</th><th>Estimation (DT)</th>
|
||||
<th>Durée (mois)</th><th>Date prévisionnelle DCA</th>
|
||||
<th>N° AO</th><th>Description</th><th>Phase</th><th>Régions</th>
|
||||
<th>Estimation (DT)</th><th>Durée</th><th>Date limite</th><th>Jours rest.</th>
|
||||
</tr></thead>
|
||||
<tbody id="pipelineBody"><tr><td colspan="5" style="text-align:center;color:var(--text-muted);padding:28px;">Chargement...</td></tr></tbody>
|
||||
<tbody id="pipelineBody"><tr><td colspan="8" style="text-align:center;color:var(--text-muted);padding:28px;">Chargement...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pipeline-total">Estimation totale : <strong id="pipelineTotalEstimation">—</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -666,6 +700,15 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── SLIDE 9 : MODERNISATION ── -->
|
||||
<section class="slide" id="slide-9">
|
||||
<h2 class="section-title"><i class="fas fa-link" style="color:#8b5cf6"></i> Modernisation — Succession des Marchés</h2>
|
||||
<p style="color:var(--text-muted);font-size:0.85em;margin-bottom:18px;">
|
||||
Croisement entre les marchés <strong>Modernisation actifs</strong> (table 856) et les <strong>AO en cours de lancement</strong> (table 872), par région.
|
||||
</p>
|
||||
<div id="modernisationGrid"><p style="color:var(--text-muted);">Chargement...</p></div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
|
|
@ -824,6 +867,7 @@ function applyRoleUI() {
|
|||
const showAdmin = role !== 'user';
|
||||
const showSuper = role === 'superadmin';
|
||||
document.getElementById('btn-slide-6').classList.toggle('nav-hidden', !showAdmin);
|
||||
document.getElementById('btn-slide-9').classList.toggle('nav-hidden', !showAdmin);
|
||||
document.getElementById('btn-slide-7').classList.toggle('nav-hidden', !showSuper);
|
||||
document.getElementById('btn-slide-8').classList.toggle('nav-hidden', !showSuper);
|
||||
document.getElementById('btnExportPPTX').classList.toggle('nav-hidden', !showSuper);
|
||||
|
|
@ -852,28 +896,31 @@ function handle401() {
|
|||
}
|
||||
|
||||
/* ── DATA ── */
|
||||
let allData = [], filteredData = [], pipelineData = [], proactifData = null, statsData = null;
|
||||
let allData = [], filteredData = [], pipelineData = [], proactifData = null, statsData = null, modernisationData = null;
|
||||
let sortField = null, sortAsc = true, currentPage = 1, pageSize = 25;
|
||||
|
||||
async function loadData() {
|
||||
showLoading(true);
|
||||
try {
|
||||
const isUser = currentUser?.role === 'user';
|
||||
const [rMarches, rStats, rPilotage, rPipeline] = await Promise.all([
|
||||
const [rMarches, rStats, rPilotage, rPipeline, rModerni] = await Promise.all([
|
||||
fetch(`${API_BASE}/marches`, { headers: apiHeaders() }),
|
||||
fetch(`${API_BASE}/stats`, { headers: apiHeaders() }),
|
||||
fetch(`${API_BASE}/pilotage-proactif`, { headers: apiHeaders() }),
|
||||
isUser ? null : fetch(`${API_BASE}/pipeline`, { headers: apiHeaders() }),
|
||||
isUser ? null : fetch(`${API_BASE}/modernisation`, { headers: apiHeaders() }),
|
||||
]);
|
||||
if (rMarches.status === 401) { handle401(); return; }
|
||||
if (!rMarches.ok) throw new Error('Erreur marchés ' + rMarches.status);
|
||||
const marchesJson = await rMarches.json();
|
||||
statsData = rStats?.ok ? await rStats.json() : null;
|
||||
proactifData = rPilotage?.ok ? await rPilotage.json() : null;
|
||||
const pipelineJson = (!isUser && rPipeline?.ok) ? await rPipeline.json() : { results: [] };
|
||||
const pipelineJson = (!isUser && rPipeline?.ok) ? await rPipeline.json() : { count:0, total_estimation:0, results:[] };
|
||||
modernisationData = (!isUser && rModerni?.ok) ? await rModerni.json() : null;
|
||||
allData = (marchesJson.results || marchesJson).map(normalizeMarche);
|
||||
filteredData = [...allData];
|
||||
pipelineData = pipelineJson.results || pipelineJson;
|
||||
if (pipelineJson.total_estimation) pipelineData._total_estimation = pipelineJson.total_estimation;
|
||||
document.getElementById('lastUpdate').textContent =
|
||||
new Date().toLocaleTimeString('fr-FR', { hour:'2-digit', minute:'2-digit' });
|
||||
renderAll();
|
||||
|
|
@ -884,7 +931,7 @@ async function loadData() {
|
|||
|
||||
function renderAll() {
|
||||
renderKPIs(); renderSynthese(); renderService(); renderProactif();
|
||||
renderRegions(); renderMarches(); renderPipeline(); updateBadges();
|
||||
renderRegions(); renderMarches(); renderPipeline(); renderModernisation(); updateBadges();
|
||||
}
|
||||
|
||||
/* ── KPIs ── */
|
||||
|
|
@ -1089,6 +1136,23 @@ function renderRegions() {
|
|||
const budget = rows.reduce((s,r) => s + r.tot_marche, 0);
|
||||
const alertes = rows.filter(r => { const d = getDelaiRestant(r); return d !== null && d <= 90; });
|
||||
const color = REGION_COLORS[reg] || '#888';
|
||||
// AO en lancement pour cette région
|
||||
const aoReg = pipelineData.filter(p => (p._regions||[]).some(rg => rg.name === reg));
|
||||
const aoHtml = aoReg.length > 0
|
||||
? `<div class="region-suivant">
|
||||
<div class="region-suivant-title"><i class="fas fa-rocket"></i> AO en lancement</div>
|
||||
${aoReg.map(p => {
|
||||
const phase = p._phase || {};
|
||||
const jours = p._jours_limite;
|
||||
const joursStr = jours === null ? '' : jours < 0 ? ' — passé' : ` — ${jours}j`;
|
||||
return `<div class="region-suivant-item">
|
||||
<span>${escapeHtml(p['num-ao']||'—')} · ${escapeHtml(p['Description du projet']||'')}</span>
|
||||
<span class="phase-badge" style="background:${phase.color||'#888'};font-size:0.68em">${phase.label||''}${joursStr}</span>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
return `<div class="region-card">
|
||||
<div class="region-header">
|
||||
<div class="region-dot" style="background:${color}"></div>
|
||||
|
|
@ -1101,6 +1165,7 @@ function renderRegions() {
|
|||
<div class="region-stat"><div class="value" style="color:var(--danger)">${alertes.length}</div><div class="label">Alertes délais</div></div>
|
||||
<div class="region-stat"><div class="value">${allData.filter(r => isCloture(r) && r.region === reg).length}</div><div class="label">Clôturés</div></div>
|
||||
</div>
|
||||
${aoHtml}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
|
@ -1214,29 +1279,111 @@ function goPage(p) { currentPage = p; renderMarchesTable(); }
|
|||
function changePageSize(n) { pageSize = parseInt(n,10); currentPage = 1; renderMarchesTable(); }
|
||||
|
||||
/* ── PIPELINE ── */
|
||||
function pipelineRegions(r) {
|
||||
const v = r['Regions'] || r.Regions || [];
|
||||
if (Array.isArray(v)) return v.map(x => (typeof x === 'object' ? x.value : x)).filter(Boolean).join(', ');
|
||||
return String(v || '—');
|
||||
}
|
||||
function renderPipeline() {
|
||||
const total = pipelineData._total_estimation ?? 0;
|
||||
document.getElementById('pipeline-count').textContent = `${pipelineData.length} projets`;
|
||||
document.getElementById('pipelineTotalEstimation').textContent =
|
||||
total > 0 ? parseNum(total).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g,' ') + ' DT' : '—';
|
||||
|
||||
document.getElementById('pipelineBody').innerHTML = pipelineData.length
|
||||
? pipelineData.map(r => {
|
||||
const desc = r['Description du projet'] || r.description || r.projet || '—';
|
||||
const regs = pipelineRegions(r) || '—';
|
||||
const est = r['Estimation'] ?? r.estimation ?? '';
|
||||
const dur = r['Duree'] ?? r.Duree ?? r.duree ?? '';
|
||||
const date = r['Date_previsionnelle_de_la_communication_du_projet_a_la_DCA'] || r.date_prevue || '';
|
||||
const phase = r._phase || {};
|
||||
const jours = r._jours_limite;
|
||||
const regs = (r._regions || []);
|
||||
const est = r._estimation || parseFloat(r.Estimation || 0) || 0;
|
||||
const numAO = r['num-ao'] || '—';
|
||||
const desc = r['Description du projet'] || r.description || '—';
|
||||
const dur = r['Duree'] || r.duree || '—';
|
||||
const dateLim = r['date-limite'] ? formatDateFR(r['date-limite']) : '—';
|
||||
|
||||
const joursHtml = jours === null ? '—'
|
||||
: jours < 0 ? `<span class="status-badge muted">Passé</span>`
|
||||
: jours <= 7 ? `<strong style="color:var(--danger)">${jours}j</strong>`
|
||||
: jours <= 30? `<strong style="color:var(--warning)">${jours}j</strong>`
|
||||
: `<span style="color:var(--success)">${jours}j</span>`;
|
||||
|
||||
const regHtml = regs.map(rg =>
|
||||
`<span class="region-tag" style="background:${rg.color}">${escapeHtml(rg.name)}</span>`
|
||||
).join('');
|
||||
|
||||
return `<tr>
|
||||
<td><strong>${escapeHtml(desc)}</strong></td>
|
||||
<td>${escapeHtml(regs)}</td>
|
||||
<td>${est ? escapeHtml(String(est)) : '—'}</td>
|
||||
<td>${dur ? escapeHtml(String(dur)) : '—'}</td>
|
||||
<td>${formatDateFR(date)}</td>
|
||||
<td><strong>${escapeHtml(numAO)}</strong></td>
|
||||
<td>${escapeHtml(desc)}</td>
|
||||
<td><span class="phase-badge" style="background:${phase.color||'#888'}">${escapeHtml(phase.label||'—')}</span></td>
|
||||
<td>${regHtml || '—'}</td>
|
||||
<td>${est > 0 ? est.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g,' ') + ' DT' : '—'}</td>
|
||||
<td style="font-size:0.82em">${escapeHtml(dur)}</td>
|
||||
<td style="white-space:nowrap">${dateLim}</td>
|
||||
<td>${joursHtml}</td>
|
||||
</tr>`;
|
||||
}).join('')
|
||||
: '<tr><td colspan="5" style="text-align:center;color:var(--text-muted);padding:28px;">Pipeline vide.</td></tr>';
|
||||
: '<tr><td colspan="8" style="text-align:center;color:var(--text-muted);padding:28px;">Pipeline vide.</td></tr>';
|
||||
}
|
||||
|
||||
/* ── MODERNISATION ── */
|
||||
function renderModernisation() {
|
||||
const grid = document.getElementById('modernisationGrid');
|
||||
if (!grid) return;
|
||||
if (!modernisationData) {
|
||||
grid.innerHTML = '<p style="color:var(--text-muted);font-size:0.85em;">Données non disponibles (accès admin requis).</p>';
|
||||
return;
|
||||
}
|
||||
const regions = modernisationData.regions || [];
|
||||
if (!regions.length) {
|
||||
grid.innerHTML = '<p style="color:var(--text-muted);">Aucun marché modernisation trouvé.</p>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = regions.map(reg => {
|
||||
const color = REGION_COLORS[reg.region] || '#888';
|
||||
|
||||
const actuelsHtml = reg.actuels.length
|
||||
? reg.actuels.map(m => `
|
||||
<div class="moderni-card">
|
||||
<div class="mc-ref">${escapeHtml(m.ref||'—')}</div>
|
||||
<div class="mc-row"><span>Projet</span><span>${escapeHtml(m.projet||'—')}</span></div>
|
||||
<div class="mc-row"><span>Entrepreneur</span><span>${escapeHtml(m.entrepreneur||'—')}</span></div>
|
||||
<div class="mc-row"><span>Avt. physique</span><span>${escapeHtml(String(m.taux_phy||'—'))}</span></div>
|
||||
<div class="mc-row"><span>Délai restant</span><span style="color:${(m.delai_restant??999)<=45?'var(--danger)':(m.delai_restant??999)<=90?'var(--warning)':'var(--success)'}">${m.delai_restant!=null?m.delai_restant+'j':'—'}</span></div>
|
||||
<div class="mc-row"><span>Fin</span><span>${escapeHtml(m.date_fin||'—')}</span></div>
|
||||
<div class="mc-row"><span>Montant</span><span>${escapeHtml(m.montant||'—')}</span></div>
|
||||
</div>`).join('')
|
||||
: '<div class="moderni-empty">Aucun marché modernisation actif</div>';
|
||||
|
||||
const suivantsHtml = reg.suivants.length
|
||||
? reg.suivants.map(s => {
|
||||
const jours = s.jours_limite;
|
||||
const joursStyle = jours===null?'' : jours<0?'color:var(--text-muted)' : jours<=7?'color:var(--danger)' : jours<=30?'color:var(--warning)':'color:var(--success)';
|
||||
return `
|
||||
<div class="moderni-card">
|
||||
<div class="mc-ref" style="color:#8b5cf6">${escapeHtml(s.num_ao||'—')}</div>
|
||||
<div class="mc-row"><span>Description</span><span>${escapeHtml(s.description||'—')}</span></div>
|
||||
<div class="mc-row"><span>Phase</span><span><span class="phase-badge" style="background:${s.phase?.color||'#888'};font-size:0.7em">${s.phase?.label||'—'}</span></span></div>
|
||||
<div class="mc-row"><span>Estimation</span><span>${s.estimation>0?(s.estimation).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g,' ')+' DT':'—'}</span></div>
|
||||
<div class="mc-row"><span>Durée</span><span>${escapeHtml(s.duree||'—')}</span></div>
|
||||
<div class="mc-row"><span>Date limite</span><span style="${joursStyle}">${escapeHtml(s.date_limite||'—')}${jours!==null?' ('+jours+'j)':''}</span></div>
|
||||
${s.date_ouverture?`<div class="mc-row"><span>Ouverture</span><span>${escapeHtml(s.date_ouverture)}</span></div>`:''}
|
||||
</div>`;
|
||||
}).join('')
|
||||
: '<div class="moderni-empty">Aucun AO en cours de lancement</div>';
|
||||
|
||||
return `<div class="moderni-region-block">
|
||||
<div class="moderni-region-header">
|
||||
<div class="moderni-region-dot" style="background:${color}"></div>
|
||||
<span>${reg.region}</span>
|
||||
<span style="margin-left:auto;opacity:0.75;font-size:0.82em">${reg.actuels.length} actuel${reg.actuels.length>1?'s':''} · ${reg.suivants.length} AO suivant${reg.suivants.length>1?'s':''}</span>
|
||||
</div>
|
||||
<div class="moderni-body">
|
||||
<div class="moderni-col actuel">
|
||||
<div class="moderni-col-title actuel"><i class="fas fa-play-circle"></i> Marché actuel</div>
|
||||
${actuelsHtml}
|
||||
</div>
|
||||
<div class="moderni-col suivant">
|
||||
<div class="moderni-col-title suivant"><i class="fas fa-arrow-right"></i> AO en préparation / lancement</div>
|
||||
${suivantsHtml}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* charts supprimés — remplacés par jauges CSS dans renderSynthese() */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* routes/modernisation.js
|
||||
* Vue "Succession des marchés Modernisation"
|
||||
* Croise table 856 (marchés actifs, nature=Modernisation) avec table 872 (AO en lancement)
|
||||
* Lien : region commune entre les deux tables
|
||||
*/
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getMarches, getPipeline } = require('../services/baserow');
|
||||
const { normalizeMarche, isCloture, selectVal } = require('../services/calc');
|
||||
|
||||
const ALL_REGIONS = ['Gabes','Gafsa','Kebili','Medenine','Sfax','Tataouine','Tozeur'];
|
||||
|
||||
function isModernisation(r) {
|
||||
const nature = selectVal(r.nature);
|
||||
return nature && nature.toLowerCase().includes('moderni');
|
||||
}
|
||||
|
||||
function pipelineRegions(r) {
|
||||
const v = r['Regions'] || r.regions || [];
|
||||
if (!Array.isArray(v)) return [];
|
||||
return v.map(x => (typeof x === 'object' ? x.value : x)).filter(Boolean);
|
||||
}
|
||||
|
||||
function phaseAO(r) {
|
||||
const now = new Date();
|
||||
const limit = r['date-limite'] ? new Date(r['date-limite']) : null;
|
||||
const ouv = r['date-ouverture-adm-tech'] ? new Date(r['date-ouverture-adm-tech']) : null;
|
||||
const clos = r['date-cloture-evaluation'] ? new Date(r['date-cloture-evaluation']) : null;
|
||||
|
||||
if (clos && now > clos) return { label:'Attribué', code:'attribue', color:'#6b7280' };
|
||||
if (ouv && now > ouv) return { label:'Évaluation', code:'evaluation', color:'#8b5cf6' };
|
||||
if (limit && now > limit) return { label:'Dépouillé', code:'depouille', color:'#f59e0b' };
|
||||
if (limit) return { label:'Ouvert', code:'ouvert', color:'#10b981' };
|
||||
return { label:'Préparation', code:'preparation', color:'#3b82f6' };
|
||||
}
|
||||
|
||||
function fmtDate(d) {
|
||||
if (!d) return null;
|
||||
const dt = new Date(d);
|
||||
if (isNaN(dt.getTime())) return String(d);
|
||||
return `${String(dt.getDate()).padStart(2,'0')}/${String(dt.getMonth()+1).padStart(2,'0')}/${dt.getFullYear()}`;
|
||||
}
|
||||
|
||||
// GET /api/modernisation
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const [rawMarches, rawPipeline] = await Promise.all([getMarches(), getPipeline()]);
|
||||
|
||||
// Marchés modernisation actifs par région (non clôturés)
|
||||
const modActifs = rawMarches
|
||||
.filter(r => !isCloture(r) && isModernisation(r))
|
||||
.map(normalizeMarche);
|
||||
|
||||
// Pour chaque région, construire la chaîne actuel → suivant
|
||||
const regions = ALL_REGIONS.map(reg => {
|
||||
const actuels = modActifs.filter(r => (r.region || '') === reg);
|
||||
const suivants = rawPipeline.filter(r => pipelineRegions(r).includes(reg));
|
||||
|
||||
return {
|
||||
region: reg,
|
||||
actuels: actuels.map(r => ({
|
||||
ref: r.ref,
|
||||
projet: r.projet,
|
||||
entrepreneur: r.entrepreneur,
|
||||
taux_phy: r.taux_phy,
|
||||
taux_fin: r.taux_fin,
|
||||
date_fin: r.date_fin,
|
||||
delai_restant: r.delai_restant,
|
||||
montant: r.montant,
|
||||
statut: r.statut,
|
||||
})),
|
||||
suivants: suivants.map(r => ({
|
||||
num_ao: r['num-ao'] || '',
|
||||
description: r['Description du projet'] || '',
|
||||
estimation: parseFloat(r.Estimation || 0) || 0,
|
||||
duree: r['Duree'] || '',
|
||||
date_limite: fmtDate(r['date-limite']),
|
||||
date_ouverture: fmtDate(r['date-ouverture-adm-tech']),
|
||||
date_evaluation: fmtDate(r['date-cloture-evaluation']),
|
||||
phase: phaseAO(r),
|
||||
jours_limite: r['date-limite']
|
||||
? Math.ceil((new Date(r['date-limite']) - new Date()) / 86400000)
|
||||
: null,
|
||||
})),
|
||||
};
|
||||
}).filter(r => r.actuels.length > 0 || r.suivants.length > 0);
|
||||
|
||||
res.json({ count: regions.length, regions });
|
||||
} catch (err) {
|
||||
res.status(502).json({ error: 'Erreur modernisation', detail: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -2,11 +2,53 @@ const express = require('express');
|
|||
const router = express.Router();
|
||||
const { getPipeline } = require('../services/baserow');
|
||||
|
||||
const REGION_COLORS = {
|
||||
Gabes:'#17A2B8', Gafsa:'#22C55E', Kebili:'#9333EA',
|
||||
Medenine:'#0EA5E9', Sfax:'#002855', Tataouine:'#14B8A6', Tozeur:'#818CF8',
|
||||
};
|
||||
|
||||
function phaseAO(r) {
|
||||
const now = new Date();
|
||||
const limit = r['date-limite'] ? new Date(r['date-limite']) : null;
|
||||
const ouv = r['date-ouverture-adm-tech'] ? new Date(r['date-ouverture-adm-tech']) : null;
|
||||
const clos = r['date-cloture-evaluation'] ? new Date(r['date-cloture-evaluation']) : null;
|
||||
|
||||
if (clos && now > clos) return { label:'Attribué', code:'attribue', color:'#6b7280' };
|
||||
if (ouv && now > ouv) return { label:'Évaluation', code:'evaluation', color:'#8b5cf6' };
|
||||
if (limit && now > limit) return { label:'Dépouillé', code:'depouille', color:'#f59e0b' };
|
||||
if (limit) return { label:'Ouvert', code:'ouvert', color:'#10b981' };
|
||||
return { label:'Préparation', code:'preparation', color:'#3b82f6' };
|
||||
}
|
||||
|
||||
function joursAvantLimite(r) {
|
||||
if (!r['date-limite']) return null;
|
||||
const d = new Date(r['date-limite']);
|
||||
if (isNaN(d.getTime())) return null;
|
||||
return Math.ceil((d - new Date()) / 86400000);
|
||||
}
|
||||
|
||||
function formatRegions(r) {
|
||||
const v = r['Regions'] || r.regions || [];
|
||||
if (!Array.isArray(v)) return [];
|
||||
return v.map(x => ({
|
||||
name: typeof x === 'object' ? (x.value || '') : x,
|
||||
color: REGION_COLORS[typeof x === 'object' ? x.value : x] || '#888',
|
||||
})).filter(x => x.name);
|
||||
}
|
||||
|
||||
// GET /api/pipeline
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const rows = await getPipeline();
|
||||
res.json({ count: rows.length, results: rows });
|
||||
const enriched = rows.map(r => ({
|
||||
...r,
|
||||
_phase: phaseAO(r),
|
||||
_jours_limite: joursAvantLimite(r),
|
||||
_regions: formatRegions(r),
|
||||
_estimation: parseFloat(r.Estimation || r.estimation || 0) || 0,
|
||||
}));
|
||||
const totalEstimation = enriched.reduce((s, r) => s + r._estimation, 0);
|
||||
res.json({ count: enriched.length, total_estimation: totalEstimation, results: enriched });
|
||||
} catch (err) {
|
||||
res.status(502).json({ error: 'Erreur Baserow', detail: err.message });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ app.use('/api/export', auth, requireUser, filterByRegion, require('./r
|
|||
// ─── Protégées (admin+) ──────────────────────────────────────────────────────
|
||||
|
||||
app.use('/api/pipeline', auth, requireAdmin, require('./routes/pipeline'));
|
||||
app.use('/api/modernisation', auth, requireAdmin, require('./routes/modernisation'));
|
||||
|
||||
// ─── Protégées (superadmin) ──────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue