fix(ui/export): centrage login, vitesse ticker, alignement KPI/budget/hauteur table PDF, désignation PPTX
This commit is contained in:
+8
-6
@@ -48,10 +48,12 @@
|
|||||||
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box;}
|
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box;}
|
||||||
body{font-family:'Inter',system-ui,-apple-system,sans-serif;background:var(--bg-body);min-height:100vh;color:var(--text);overflow-x:hidden;transition:background var(--transition),color var(--transition);font-size:14px;line-height:1.5;}
|
body{font-family:'Inter',system-ui,-apple-system,sans-serif;background:var(--bg-body);min-height:100vh;color:var(--text);overflow-x:hidden;transition:background var(--transition),color var(--transition);font-size:14px;line-height:1.5;}
|
||||||
|
|
||||||
<style>
|
|
||||||
/* ── LOGIN ── */
|
/* ── LOGIN ── */
|
||||||
.login-container{position:fixed;inset:0;background:var(--bg-body);display:flex;justify-content:center;align-items:center;z-index:10000;}
|
html,body{width:100%;margin:0;padding:0;}
|
||||||
.login-box{background:var(--bg-card);padding:40px;border-radius:20px;box-shadow:0 8px 32px rgba(0,0,0,0.12);border:1px solid var(--border-color);width:100%;max-width:400px;text-align:center;}
|
.login-container{position:fixed;top:0;right:0;bottom:0;left:0;width:100%;height:100%;background:var(--bg-body);display:flex;justify-content:center;align-items:center;z-index:10000;padding:16px;box-sizing:border-box;}
|
||||||
|
.login-box{background:var(--bg-card);padding:40px 30px;border-radius:20px;box-shadow:0 8px 32px rgba(0,0,0,0.12);border:1px solid var(--border-color);width:100%;max-width:400px;text-align:center;box-sizing:border-box;}
|
||||||
|
.login-box, .login-box * { box-sizing: border-box; }
|
||||||
|
@media(max-width:480px){.login-box{padding:28px 18px;border-radius:16px;max-width:340px;}}
|
||||||
.login-logo-wrap{height:70px;margin-bottom:20px;display:flex;align-items:center;justify-content:center;}
|
.login-logo-wrap{height:70px;margin-bottom:20px;display:flex;align-items:center;justify-content:center;}
|
||||||
.login-logo-wrap img{height:70px;object-fit:contain;}
|
.login-logo-wrap img{height:70px;object-fit:contain;}
|
||||||
.login-box h2{color:var(--primary);margin-bottom:8px;font-size:1.5em;font-weight:700;}
|
.login-box h2{color:var(--primary);margin-bottom:8px;font-size:1.5em;font-weight:700;}
|
||||||
@@ -327,7 +329,7 @@
|
|||||||
.log-failure{color:var(--danger);font-weight:600;}
|
.log-failure{color:var(--danger);font-weight:600;}
|
||||||
|
|
||||||
/* ── MODAL ── */
|
/* ── MODAL ── */
|
||||||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.5);display:none;justify-content:center;align-items:center;z-index:9000;backdrop-filter:blur(4px);}
|
.modal-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:rgba(0,0,0,0.5);display:none;justify-content:center;align-items:center;z-index:9000;backdrop-filter:blur(4px);}
|
||||||
.modal-overlay.active{display:flex;}
|
.modal-overlay.active{display:flex;}
|
||||||
.modal-box{background:var(--bg-card);border:1px solid var(--border-color);border-radius:16px;padding:28px;width:100%;max-width:420px;box-shadow:0 20px 60px rgba(0,0,0,0.2);}
|
.modal-box{background:var(--bg-card);border:1px solid var(--border-color);border-radius:16px;padding:28px;width:100%;max-width:420px;box-shadow:0 20px 60px rgba(0,0,0,0.2);}
|
||||||
.modal-title{font-size:1.05em;font-weight:700;margin-bottom:18px;display:flex;align-items:center;gap:9px;color:var(--text);}
|
.modal-title{font-size:1.05em;font-weight:700;margin-bottom:18px;display:flex;align-items:center;gap:9px;color:var(--text);}
|
||||||
@@ -345,7 +347,7 @@
|
|||||||
.toast.warning{background:#d97706;color:white;}
|
.toast.warning{background:#d97706;color:white;}
|
||||||
|
|
||||||
/* ── LOADING ── */
|
/* ── LOADING ── */
|
||||||
.loading-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.6);display:none;justify-content:center;align-items:center;flex-direction:column;gap:16px;z-index:9998;backdrop-filter:blur(3px);}
|
.loading-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:rgba(0,0,0,0.6);display:none;justify-content:center;align-items:center;flex-direction:column;gap:16px;z-index:9998;backdrop-filter:blur(3px);}
|
||||||
.loading-overlay.active{display:flex;}
|
.loading-overlay.active{display:flex;}
|
||||||
.spinner{width:44px;height:44px;border:3px solid rgba(255,255,255,0.2);border-top-color:#2563eb;border-radius:50%;animation:spin 0.8s linear infinite;}
|
.spinner{width:44px;height:44px;border:3px solid rgba(255,255,255,0.2);border-top-color:#2563eb;border-radius:50%;animation:spin 0.8s linear infinite;}
|
||||||
@keyframes spin{to{transform:rotate(360deg)}}
|
@keyframes spin{to{transform:rotate(360deg)}}
|
||||||
@@ -1682,7 +1684,7 @@ function updateTicker() {
|
|||||||
`⚠ ${x.r.region} — ${x.r.id_marche} : ${x.delai} jours restants`
|
`⚠ ${x.r.region} — ${x.r.id_marche} : ${x.delai} jours restants`
|
||||||
).join(' • ');
|
).join(' • ');
|
||||||
content.textContent = text;
|
content.textContent = text;
|
||||||
const duration = Math.min(45, Math.max(15, Math.ceil(text.length / 80)));
|
const duration = Math.min(70, Math.max(25, Math.ceil(text.length / 14)));
|
||||||
content.style.animationDuration = `${duration}s`;
|
content.style.animationDuration = `${duration}s`;
|
||||||
ticker.classList.add('active');
|
ticker.classList.add('active');
|
||||||
document.body.classList.add('has-ticker');
|
document.body.classList.add('has-ticker');
|
||||||
|
|||||||
+9
-1
@@ -48,6 +48,7 @@ async function buildViewData(view, req) {
|
|||||||
const tauxList = actifs.map(r => parseNum(r.taux_phy ?? r.avt_phy)).filter(v => v > 0);
|
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 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 totalBudget = actifs.reduce((s,r) => s+parseNum(r.tot_marche??r.totmarche??r.montant),0);
|
||||||
|
const totalAvtFin = actifs.reduce((s,r) => s+parseNum(r.avt_fin),0);
|
||||||
const parStatut = {};
|
const parStatut = {};
|
||||||
for (const r of rows) { const s=selectVal(r.observation)||'Inconnu'; parStatut[s]=(parStatut[s]||0)+1; }
|
for (const r of rows) { const s=selectVal(r.observation)||'Inconnu'; parStatut[s]=(parStatut[s]||0)+1; }
|
||||||
const alertes = actifs
|
const alertes = actifs
|
||||||
@@ -58,7 +59,14 @@ async function buildViewData(view, req) {
|
|||||||
return {
|
return {
|
||||||
total: rows.length, actifs: actifs.length, clotures: clotures.length,
|
total: rows.length, actifs: actifs.length, clotures: clotures.length,
|
||||||
taux_avancement_moyen: tauxMoyen, par_statut: parStatut,
|
taux_avancement_moyen: tauxMoyen, par_statut: parStatut,
|
||||||
budget: { total: formatMontant(totalBudget), total_raw: totalBudget },
|
budget: {
|
||||||
|
total: formatMontant(totalBudget),
|
||||||
|
total_raw: totalBudget,
|
||||||
|
consomme: formatMontant(totalAvtFin),
|
||||||
|
consomme_raw: totalAvtFin,
|
||||||
|
restant: formatMontant(totalBudget - totalAvtFin),
|
||||||
|
restant_raw: totalBudget - totalAvtFin,
|
||||||
|
},
|
||||||
alertes_delais: { count: alertes.length, critique: alertes.filter(a=>a.niveau==='critique').length, items: alertes },
|
alertes_delais: { count: alertes.length, critique: alertes.filter(a=>a.niveau==='critique').length, items: alertes },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ function parseDateFR(d) {
|
|||||||
function formatMontant(v) {
|
function formatMontant(v) {
|
||||||
const n = parseNum(v);
|
const n = parseNum(v);
|
||||||
if (n === 0) return '—';
|
if (n === 0) return '—';
|
||||||
return n.toLocaleString('fr-TN', { minimumFractionDigits: 0, maximumFractionDigits: 0 }) + ' DT';
|
return Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + ' DT';
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateFR(d) {
|
function formatDateFR(d) {
|
||||||
|
|||||||
+46
-38
@@ -35,7 +35,17 @@ function pdfToBuffer(doc, writeFn) {
|
|||||||
doc.on('data', c => chunks.push(c));
|
doc.on('data', c => chunks.push(c));
|
||||||
doc.on('end', () => resolve(Buffer.concat(chunks)));
|
doc.on('end', () => resolve(Buffer.concat(chunks)));
|
||||||
doc.on('error', err => reject(err));
|
doc.on('error', err => reject(err));
|
||||||
try { writeFn(doc); doc.end(); } catch (e) { reject(e); }
|
try {
|
||||||
|
writeFn(doc);
|
||||||
|
const range = doc.bufferedPageRange();
|
||||||
|
for (let i = range.start; i < range.start + range.count; i++) {
|
||||||
|
doc.switchToPage(i);
|
||||||
|
footer(doc, i + 1, range.count);
|
||||||
|
}
|
||||||
|
doc.end();
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,14 +62,18 @@ function header(doc, title, subtitle) {
|
|||||||
doc.y = 88;
|
doc.y = 88;
|
||||||
}
|
}
|
||||||
|
|
||||||
function footer(doc, n) {
|
function footer(doc, n, totalPages) {
|
||||||
const y = doc.page.height - 38;
|
const oldBottom = doc.page.margins.bottom;
|
||||||
fill(doc, C.border);
|
doc.page.margins.bottom = 0;
|
||||||
doc.rect(0, y - 4, doc.page.width, 1).fill();
|
const y = doc.page.height - 30;
|
||||||
|
stroke(doc, C.border);
|
||||||
|
doc.moveTo(40, y - 6).lineTo(doc.page.width - 40, y - 6).stroke();
|
||||||
fill(doc, C.muted);
|
fill(doc, C.muted);
|
||||||
|
const pageStr = totalPages && totalPages > 1 ? `Page ${n} / ${totalPages}` : `Page ${n}`;
|
||||||
doc.fontSize(7.5).font('Helvetica')
|
doc.fontSize(7.5).font('Helvetica')
|
||||||
.text('RLA — Marchés Tunisie Telecom Zone Sud', 40, y)
|
.text('RLA — Marchés Tunisie Telecom Zone Sud', 40, y, { lineBreak: false })
|
||||||
.text(`Page ${n}`, 0, y, { align:'right', width: doc.page.width - 40 });
|
.text(pageStr, 0, y, { align:'right', width: doc.page.width - 40, lineBreak: false });
|
||||||
|
doc.page.margins.bottom = oldBottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── KPI Box ─────────────────────────────────────────────────────────────────
|
// ─── KPI Box ─────────────────────────────────────────────────────────────────
|
||||||
@@ -97,7 +111,7 @@ function table(doc, { title, headers, rows, colWidths }) {
|
|||||||
doc.fontSize(7.5).font('Helvetica-Bold');
|
doc.fontSize(7.5).font('Helvetica-Bold');
|
||||||
let x = 40;
|
let x = 40;
|
||||||
for (let i = 0; i < headers.length; i++) {
|
for (let i = 0; i < headers.length; i++) {
|
||||||
doc.text(headers[i], x + 3, y + 4, { width: widths[i] - 6, ellipsis: true });
|
doc.text(headers[i], x + 3, y + 4, { width: widths[i] - 6, height: 11, ellipsis: true });
|
||||||
x += widths[i];
|
x += widths[i];
|
||||||
}
|
}
|
||||||
y += 17;
|
y += 17;
|
||||||
@@ -107,7 +121,6 @@ function table(doc, { title, headers, rows, colWidths }) {
|
|||||||
let alt = false;
|
let alt = false;
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
if (y > doc.page.height - 75) {
|
if (y > doc.page.height - 75) {
|
||||||
footer(doc, '—');
|
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
header(doc, '', '');
|
header(doc, '', '');
|
||||||
y = doc.y;
|
y = doc.y;
|
||||||
@@ -121,7 +134,7 @@ function table(doc, { title, headers, rows, colWidths }) {
|
|||||||
doc.fontSize(7).font('Helvetica');
|
doc.fontSize(7).font('Helvetica');
|
||||||
let x = 40;
|
let x = 40;
|
||||||
for (let i = 0; i < row.length; i++) {
|
for (let i = 0; i < row.length; i++) {
|
||||||
doc.text(String(row[i] ?? '—'), x + 3, y + 4, { width: widths[i] - 6, ellipsis: true });
|
doc.text(String(row[i] ?? '—'), x + 3, y + 4, { width: widths[i] - 6, height: rowH - 4, ellipsis: true });
|
||||||
x += widths[i];
|
x += widths[i];
|
||||||
}
|
}
|
||||||
stroke(doc, C.border);
|
stroke(doc, C.border);
|
||||||
@@ -137,7 +150,7 @@ const NL = n => ({ critique:'CRITIQUE', attention:'ATTENTION', élevé:'ÉLEVÉ'
|
|||||||
// ─── Vues ─────────────────────────────────────────────────────────────────────
|
// ─── Vues ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function generateSynthese(data) {
|
function generateSynthese(data) {
|
||||||
const doc = new PDFDocument({ margin:40, size:'A4' });
|
const doc = new PDFDocument({ margin:40, size:'A4', bufferPages:true });
|
||||||
return pdfToBuffer(doc, d => {
|
return pdfToBuffer(doc, d => {
|
||||||
header(d, 'Synthèse Globale — Marchés RLA', 'Tunisie Telecom Zone Sud');
|
header(d, 'Synthèse Globale — Marchés RLA', 'Tunisie Telecom Zone Sud');
|
||||||
const kpis = [
|
const kpis = [
|
||||||
@@ -147,15 +160,16 @@ function generateSynthese(data) {
|
|||||||
{ l:'Alertes', v: data.alertes_delais?.count||0, c: C.warning },
|
{ l:'Alertes', v: data.alertes_delais?.count||0, c: C.warning },
|
||||||
{ l:'Avt. Moy.(%)', v:`${data.taux_avancement_moyen||0}%`, c: C.accent },
|
{ l:'Avt. Moy.(%)', v:`${data.taux_avancement_moyen||0}%`, c: C.accent },
|
||||||
];
|
];
|
||||||
|
const kpiY = d.y;
|
||||||
let kx = 40;
|
let kx = 40;
|
||||||
for (const k of kpis) { kpiBox(d, kx, d.y, 95, 48, k.l, k.v, k.c); kx += 101; }
|
for (const k of kpis) { kpiBox(d, kx, kpiY, 95, 48, k.l, k.v, k.c); kx += 101; }
|
||||||
d.y += 58;
|
d.y = kpiY + 58;
|
||||||
|
|
||||||
if (data.budget) {
|
if (data.budget) {
|
||||||
fill(d, C.text); d.fontSize(10).font('Helvetica-Bold').text('Budget', 40, d.y); d.y += 12;
|
fill(d, C.text); d.fontSize(10).font('Helvetica-Bold').text('Budget', 40, d.y); d.y += 12;
|
||||||
for (const [l, v] of [['Total',data.budget.total],['Consommé',data.budget.consomme],['Restant',data.budget.restant]]) {
|
for (const [l, v] of [['Total',data.budget.total],['Consommé',data.budget.consomme],['Restant',data.budget.restant]]) {
|
||||||
fill(d, C.muted); d.fontSize(8.5).font('Helvetica').text(l+' :', 40, d.y, {width:110});
|
fill(d, C.muted); d.fontSize(8.5).font('Helvetica').text(l+' :', 40, d.y, {width:110});
|
||||||
fill(d, C.text); d.fontSize(8.5).font('Helvetica-Bold').text(v, 155, d.y, {width:250}); d.y += 13;
|
fill(d, C.text); d.fontSize(8.5).font('Helvetica-Bold').text(String(v ?? '—'), 155, d.y, {width:250}); d.y += 13;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.par_statut) {
|
if (data.par_statut) {
|
||||||
@@ -168,24 +182,22 @@ function generateSynthese(data) {
|
|||||||
table(d, { title:`Top ${items.length} Alertes Délais`, headers:['Réf.','Projet','Région','Entrepreneur','J. Rest.','Niveau'],
|
table(d, { title:`Top ${items.length} Alertes Délais`, headers:['Réf.','Projet','Région','Entrepreneur','J. Rest.','Niveau'],
|
||||||
colWidths:[70,140,65,120,55,55], rows: items.map(a=>[a.ref,a.projet,a.region,a.entrepreneur,a.delai_restant,NL(a.niveau)]) });
|
colWidths:[70,140,65,120,55,55], rows: items.map(a=>[a.ref,a.projet,a.region,a.entrepreneur,a.delai_restant,NL(a.niveau)]) });
|
||||||
}
|
}
|
||||||
footer(d, 1);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateAlertes(data) {
|
function generateAlertes(data) {
|
||||||
const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape' });
|
const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape', bufferPages:true });
|
||||||
return pdfToBuffer(doc, d => {
|
return pdfToBuffer(doc, d => {
|
||||||
header(d, 'Alertes Délais — Marchés RLA', `${data.count||0} alerte(s) — dont ${data.critique||0} critique(s)`);
|
header(d, 'Alertes Délais — Marchés RLA', `${data.count||0} alerte(s) — dont ${data.critique||0} critique(s)`);
|
||||||
if (data.items?.length) {
|
if (data.items?.length) {
|
||||||
table(d, { title:'Liste des Alertes', headers:['Réf.','Projet','Région','Entrepreneur','Taux Phy.','Date Fin','J. Rest.','Niveau'],
|
table(d, { title:'Liste des Alertes', headers:['Réf.','Projet','Région','Entrepreneur','Taux Phy.','Date Fin','J. Rest.','Niveau'],
|
||||||
colWidths:[70,155,65,125,55,65,55,55], rows: data.items.map(a=>[a.ref,a.projet,a.region,a.entrepreneur,a.taux_phy,a.date_fin,a.delai_restant,NL(a.niveau_alerte||a.niveau)]) });
|
colWidths:[70,155,65,125,55,65,55,55], rows: data.items.map(a=>[a.ref,a.projet,a.region,a.entrepreneur,a.taux_phy,a.date_fin,a.delai_restant,NL(a.niveau_alerte||a.niveau)]) });
|
||||||
} else { fill(d, C.success); d.fontSize(14).font('Helvetica-Bold').text('Aucune alerte active.', {align:'center'}); }
|
} else { fill(d, C.success); d.fontSize(14).font('Helvetica-Bold').text('Aucune alerte active.', {align:'center'}); }
|
||||||
footer(d, 1);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateEnService(data) {
|
function generateEnService(data) {
|
||||||
const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape' });
|
const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape', bufferPages:true });
|
||||||
return pdfToBuffer(doc, d => {
|
return pdfToBuffer(doc, d => {
|
||||||
header(d, 'Marchés en Service — RLA', `${data.count||0} marché(s) actif(s)`);
|
header(d, 'Marchés en Service — RLA', `${data.count||0} marché(s) actif(s)`);
|
||||||
table(d, { title:'Liste des Marchés en Service',
|
table(d, { title:'Liste des Marchés en Service',
|
||||||
@@ -193,12 +205,11 @@ function generateEnService(data) {
|
|||||||
colWidths:[65,140,65,110,90,90,55,55,60],
|
colWidths:[65,140,65,110,90,90,55,55,60],
|
||||||
rows: (data.items||[]).map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.montant,
|
rows: (data.items||[]).map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.montant,
|
||||||
`${r.date_debut||'—'} → ${r.date_fin||'—'}`,r.taux_phy,r.delai_restant??'—',NL(r.niveau_alerte)]) });
|
`${r.date_debut||'—'} → ${r.date_fin||'—'}`,r.taux_phy,r.delai_restant??'—',NL(r.niveau_alerte)]) });
|
||||||
footer(d, 1);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateEnCours(data) {
|
function generateEnCours(data) {
|
||||||
const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape' });
|
const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape', bufferPages:true });
|
||||||
return pdfToBuffer(doc, d => {
|
return pdfToBuffer(doc, d => {
|
||||||
header(d, 'Marchés en Cours — RLA', `${data.count||0} marché(s) en cours`);
|
header(d, 'Marchés en Cours — RLA', `${data.count||0} marché(s) en cours`);
|
||||||
table(d, { title:'Liste des Marchés en Cours',
|
table(d, { title:'Liste des Marchés en Cours',
|
||||||
@@ -206,16 +217,15 @@ function generateEnCours(data) {
|
|||||||
colWidths:[65,140,65,110,90,90,60,70],
|
colWidths:[65,140,65,110,90,90,60,70],
|
||||||
rows: (data.items||[]).map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.montant,
|
rows: (data.items||[]).map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.montant,
|
||||||
`${r.date_debut||'—'} → ${r.date_fin||'—'}`,r.taux_phy,r.niveau_avancement||'—']) });
|
`${r.date_debut||'—'} → ${r.date_fin||'—'}`,r.taux_phy,r.niveau_avancement||'—']) });
|
||||||
footer(d, 1);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateParRegion(data) {
|
function generateParRegion(data) {
|
||||||
const doc = new PDFDocument({ margin:40, size:'A4' });
|
const doc = new PDFDocument({ margin:40, size:'A4', bufferPages:true });
|
||||||
return pdfToBuffer(doc, d => {
|
return pdfToBuffer(doc, d => {
|
||||||
header(d, 'Vue par Région — Marchés RLA', `${data.count||0} région(s)`);
|
header(d, 'Vue par Région — Marchés RLA', `${data.count||0} région(s)`);
|
||||||
for (const reg of data.regions||[]) {
|
for (const reg of data.regions||[]) {
|
||||||
if (d.y > d.page.height - 120) { footer(d, '—'); d.addPage(); header(d,'',''); }
|
if (d.y > d.page.height - 120) { d.addPage(); header(d,'',''); }
|
||||||
fill(d, C.primary); d.rect(40, d.y, d.page.width-80, 22).fill();
|
fill(d, C.primary); d.rect(40, d.y, d.page.width-80, 22).fill();
|
||||||
fill(d, C.white); d.fontSize(11).font('Helvetica-Bold').text(reg.region, 52, d.y+5); d.y += 30;
|
fill(d, C.white); d.fontSize(11).font('Helvetica-Bold').text(reg.region, 52, d.y+5); d.y += 30;
|
||||||
const kpis = [
|
const kpis = [
|
||||||
@@ -223,27 +233,26 @@ function generateParRegion(data) {
|
|||||||
{l:'Alertes',v:reg.alertes_count,c:C.warning},{l:'Critiques',v:reg.alertes_critique,c:C.danger},
|
{l:'Alertes',v:reg.alertes_count,c:C.warning},{l:'Critiques',v:reg.alertes_critique,c:C.danger},
|
||||||
{l:'Taux moy.',v:`${reg.taux_moyen}%`},
|
{l:'Taux moy.',v:`${reg.taux_moyen}%`},
|
||||||
];
|
];
|
||||||
let kx=40; for(const k of kpis){kpiBox(d,kx,d.y,92,40,k.l,k.v,k.c||C.primary);kx+=98;}
|
const kpiY = d.y;
|
||||||
d.y+=50; d.moveDown(0.3);
|
let kx=40; for(const k of kpis){kpiBox(d,kx,kpiY,92,40,k.l,k.v,k.c||C.primary);kx+=98;}
|
||||||
|
d.y = kpiY + 50; d.moveDown(0.3);
|
||||||
}
|
}
|
||||||
footer(d, 1);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateClotures(data) {
|
function generateClotures(data) {
|
||||||
const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape' });
|
const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape', bufferPages:true });
|
||||||
return pdfToBuffer(doc, d => {
|
return pdfToBuffer(doc, d => {
|
||||||
header(d, 'Marchés Clôturés — RLA', `${data.count||0} marché(s) — Budget : ${data.budget_total||'—'}`);
|
header(d, 'Marchés Clôturés — RLA', `${data.count||0} marché(s) — Budget : ${data.budget_total||'—'}`);
|
||||||
table(d, { title:'Liste des Marchés Clôturés',
|
table(d, { title:'Liste des Marchés Clôturés',
|
||||||
headers:['Réf.','Projet','Région','Entrepreneur','Montant','Taux Phy.','Date Clôture'],
|
headers:['Réf.','Projet','Région','Entrepreneur','Montant','Taux Phy.','Date Clôture'],
|
||||||
colWidths:[70,155,65,130,100,60,75],
|
colWidths:[70,155,65,130,100,60,75],
|
||||||
rows: (data.items||[]).map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.montant,r.taux_phy,r.date_cloture]) });
|
rows: (data.items||[]).map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.montant,r.taux_phy,r.date_cloture]) });
|
||||||
footer(d, 1);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function generatePilotage(data) {
|
function generatePilotage(data) {
|
||||||
const doc = new PDFDocument({ margin:40, size:'A4' });
|
const doc = new PDFDocument({ margin:40, size:'A4', bufferPages:true });
|
||||||
return pdfToBuffer(doc, d => {
|
return pdfToBuffer(doc, d => {
|
||||||
const r = data.resume||{};
|
const r = data.resume||{};
|
||||||
header(d, 'Pilotage Proactif — Marchés RLA', `Total actifs : ${r.total||0}`);
|
header(d, 'Pilotage Proactif — Marchés RLA', `Total actifs : ${r.total||0}`);
|
||||||
@@ -252,19 +261,19 @@ function generatePilotage(data) {
|
|||||||
{l:'Sous avancement',v:r.sous_avancement||0,c:C.warning},
|
{l:'Sous avancement',v:r.sous_avancement||0,c:C.warning},
|
||||||
{l:'Dépassé',v:r.depasse||0,c:C.danger},
|
{l:'Dépassé',v:r.depasse||0,c:C.danger},
|
||||||
];
|
];
|
||||||
let kx=40; for(const k of kpis){kpiBox(d,kx,d.y,148,50,k.l,k.v,k.c);kx+=156;}
|
const kpiY = d.y;
|
||||||
d.y+=62;
|
let kx=40; for(const k of kpis){kpiBox(d,kx,kpiY,148,50,k.l,k.v,k.c);kx+=156;}
|
||||||
|
d.y = kpiY + 62;
|
||||||
const problematic = [...(data.depasse||[]),...(data.sous_avancement||[])];
|
const problematic = [...(data.depasse||[]),...(data.sous_avancement||[])];
|
||||||
if (problematic.length) {
|
if (problematic.length) {
|
||||||
table(d, { title:'Marchés à surveiller', headers:['Réf.','Projet','Région','Entrepreneur','Taux Phy.','Niveau'],
|
table(d, { title:'Marchés à surveiller', headers:['Réf.','Projet','Région','Entrepreneur','Taux Phy.','Niveau'],
|
||||||
colWidths:[70,155,65,130,60,75], rows: problematic.map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.taux_phy,r.niveau_avancement]) });
|
colWidths:[70,155,65,130,60,75], rows: problematic.map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.taux_phy,r.niveau_avancement]) });
|
||||||
}
|
}
|
||||||
footer(d, 1);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateMatriceRisque(data) {
|
function generateMatriceRisque(data) {
|
||||||
const doc = new PDFDocument({ margin:40, size:'A4' });
|
const doc = new PDFDocument({ margin:40, size:'A4', bufferPages:true });
|
||||||
return pdfToBuffer(doc, d => {
|
return pdfToBuffer(doc, d => {
|
||||||
const pn = data.par_niveau||{};
|
const pn = data.par_niveau||{};
|
||||||
header(d, 'Matrice de Risque — Marchés RLA', `${data.total||0} marchés analysés`);
|
header(d, 'Matrice de Risque — Marchés RLA', `${data.total||0} marchés analysés`);
|
||||||
@@ -274,19 +283,19 @@ function generateMatriceRisque(data) {
|
|||||||
{l:'Moyen',v:pn.moyen||0,c:'#6366f1'},
|
{l:'Moyen',v:pn.moyen||0,c:'#6366f1'},
|
||||||
{l:'Faible',v:pn.faible||0,c:C.success},
|
{l:'Faible',v:pn.faible||0,c:C.success},
|
||||||
];
|
];
|
||||||
let kx=40; for(const k of kpis){kpiBox(d,kx,d.y,110,50,k.l,k.v,k.c);kx+=118;}
|
const kpiY = d.y;
|
||||||
d.y+=62;
|
let kx=40; for(const k of kpis){kpiBox(d,kx,kpiY,110,50,k.l,k.v,k.c);kx+=118;}
|
||||||
|
d.y = kpiY + 62;
|
||||||
if (data.items?.length) {
|
if (data.items?.length) {
|
||||||
const sorted = [...data.items].sort((a,b)=>(b.score_delai+b.score_avancement)-(a.score_delai+a.score_avancement));
|
const sorted = [...data.items].sort((a,b)=>(b.score_delai+b.score_avancement)-(a.score_delai+a.score_avancement));
|
||||||
table(d, { title:'Marchés classés par niveau de risque', headers:['Réf.','Projet','Région','Entrepreneur','Taux Phy.','J. Rest.','Risque'],
|
table(d, { title:'Marchés classés par niveau de risque', headers:['Réf.','Projet','Région','Entrepreneur','Taux Phy.','J. Rest.','Risque'],
|
||||||
colWidths:[70,145,65,125,55,50,55], rows: sorted.map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.taux_phy,r.delai_restant??'—',NL(r.niveau_risque)]) });
|
colWidths:[70,145,65,125,55,50,55], rows: sorted.map(r=>[r.ref,r.projet,r.region,r.entrepreneur,r.taux_phy,r.delai_restant??'—',NL(r.niveau_risque)]) });
|
||||||
}
|
}
|
||||||
footer(d, 1);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateGeneric(title, data) {
|
function generateGeneric(title, data) {
|
||||||
const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape' });
|
const doc = new PDFDocument({ margin:40, size:'A4', layout:'landscape', bufferPages:true });
|
||||||
return pdfToBuffer(doc, d => {
|
return pdfToBuffer(doc, d => {
|
||||||
header(d, title, '');
|
header(d, title, '');
|
||||||
const items = data.items||[];
|
const items = data.items||[];
|
||||||
@@ -295,7 +304,6 @@ function generateGeneric(title, data) {
|
|||||||
table(d, { headers:keys, colWidths:keys.map(()=>Math.floor(700/keys.length)),
|
table(d, { headers:keys, colWidths:keys.map(()=>Math.floor(700/keys.length)),
|
||||||
rows: items.slice(0,100).map(r=>keys.map(k=>r[k])) });
|
rows: items.slice(0,100).map(r=>keys.map(k=>r[k])) });
|
||||||
}
|
}
|
||||||
footer(d, 1);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* Dernière : En cours / Hors service (4 cols)
|
* Dernière : En cours / Hors service (4 cols)
|
||||||
*/
|
*/
|
||||||
const PptxGenJS = require('pptxgenjs');
|
const PptxGenJS = require('pptxgenjs');
|
||||||
const { parseNum, selectVal, parseDateFR, formatDateFR } = require('./calc');
|
const { parseNum, selectVal, parseDateFR, formatDateFR, buildRef } = require('./calc');
|
||||||
|
|
||||||
const REGION_COLORS = {
|
const REGION_COLORS = {
|
||||||
Gabes:'0891B2', Gafsa:'059669', Kebili:'7C3AED',
|
Gabes:'0891B2', Gafsa:'059669', Kebili:'7C3AED',
|
||||||
@@ -113,7 +113,7 @@ async function generatePptx(actifs, _clotures, filtered) {
|
|||||||
const phy = parseNum(r.taux_phy || r.avt_phy);
|
const phy = parseNum(r.taux_phy || r.avt_phy);
|
||||||
return [
|
return [
|
||||||
txt(r.region || '—', { fontSize: 7.5, color: REGION_COLORS[r.region] || '1E293B', bold: true }),
|
txt(r.region || '—', { fontSize: 7.5, color: REGION_COLORS[r.region] || '1E293B', bold: true }),
|
||||||
txt(r.ref || '', { fontSize: 7, color: '1E293B' }),
|
txt(buildRef(r), { fontSize: 7, color: '1E293B' }),
|
||||||
txt(r.projet || '', { fontSize: 7.5, color: '1E293B' }),
|
txt(r.projet || '', { fontSize: 7.5, color: '1E293B' }),
|
||||||
txt(r.entrepreneur || '', { fontSize: 7.5, color: '1E293B' }),
|
txt(r.entrepreneur || '', { fontSize: 7.5, color: '1E293B' }),
|
||||||
txt(periode(r), { fontSize: 6.5, color: '475569' }),
|
txt(periode(r), { fontSize: 6.5, color: '475569' }),
|
||||||
@@ -156,7 +156,7 @@ async function generatePptx(actifs, _clotures, filtered) {
|
|||||||
const d = getDelai(r);
|
const d = getDelai(r);
|
||||||
const al = niveauAlerte(d);
|
const al = niveauAlerte(d);
|
||||||
return [
|
return [
|
||||||
txt(r.ref || '', { fontSize: 7, color: '1E293B' }),
|
txt(buildRef(r), { fontSize: 7, color: '1E293B' }),
|
||||||
txt(r.projet || '', { fontSize: 7.5, color: '1E293B' }),
|
txt(r.projet || '', { fontSize: 7.5, color: '1E293B' }),
|
||||||
txt(r.entrepreneur || '', { fontSize: 7.5, color: '1E293B' }),
|
txt(r.entrepreneur || '', { fontSize: 7.5, color: '1E293B' }),
|
||||||
txt(periode(r), { fontSize: 6.5, color: '475569' }),
|
txt(periode(r), { fontSize: 6.5, color: '475569' }),
|
||||||
@@ -189,7 +189,7 @@ async function generatePptx(actifs, _clotures, filtered) {
|
|||||||
txt(t, { bold: true, color: 'FFFFFF', fill: '1E40AF', fontSize: 9, valign: 'middle' }));
|
txt(t, { bold: true, color: 'FFFFFF', fill: '1E40AF', fontSize: 9, valign: 'middle' }));
|
||||||
|
|
||||||
const ecRows = enCours.slice(0, 20).map(r => [
|
const ecRows = enCours.slice(0, 20).map(r => [
|
||||||
txt(r.ref || r.id_marche || '', { fontSize: 8, color: '1E293B' }),
|
txt(buildRef(r), { fontSize: 8, color: '1E293B' }),
|
||||||
txt(r.projet || '', { fontSize: 8, color: '1E293B' }),
|
txt(r.projet || '', { fontSize: 8, color: '1E293B' }),
|
||||||
txt(r.entrepreneur || '', { fontSize: 8, color: '1E293B' }),
|
txt(r.entrepreneur || '', { fontSize: 8, color: '1E293B' }),
|
||||||
txt(selectVal(r.observation) || '—', { fontSize: 8, color: '475569' }),
|
txt(selectVal(r.observation) || '—', { fontSize: 8, color: '475569' }),
|
||||||
|
|||||||
Reference in New Issue
Block a user