fix(ui/export): centrage login, vitesse ticker, alignement KPI/budget/hauteur table PDF, désignation PPTX

This commit is contained in:
2026-08-21 22:23:15 +01:00
parent ff4effa663
commit dd0279ad09
5 changed files with 68 additions and 50 deletions
+46 -38
View File
@@ -35,7 +35,17 @@ function pdfToBuffer(doc, writeFn) {
doc.on('data', c => chunks.push(c));
doc.on('end', () => resolve(Buffer.concat(chunks)));
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;
}
function footer(doc, n) {
const y = doc.page.height - 38;
fill(doc, C.border);
doc.rect(0, y - 4, doc.page.width, 1).fill();
function footer(doc, n, totalPages) {
const oldBottom = doc.page.margins.bottom;
doc.page.margins.bottom = 0;
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);
const pageStr = totalPages && totalPages > 1 ? `Page ${n} / ${totalPages}` : `Page ${n}`;
doc.fontSize(7.5).font('Helvetica')
.text('RLA — Marchés Tunisie Telecom Zone Sud', 40, y)
.text(`Page ${n}`, 0, y, { align:'right', width: doc.page.width - 40 });
.text('RLA — Marchés Tunisie Telecom Zone Sud', 40, y, { lineBreak: false })
.text(pageStr, 0, y, { align:'right', width: doc.page.width - 40, lineBreak: false });
doc.page.margins.bottom = oldBottom;
}
// ─── KPI Box ─────────────────────────────────────────────────────────────────
@@ -97,7 +111,7 @@ function table(doc, { title, headers, rows, colWidths }) {
doc.fontSize(7.5).font('Helvetica-Bold');
let x = 40;
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];
}
y += 17;
@@ -107,7 +121,6 @@ function table(doc, { title, headers, rows, colWidths }) {
let alt = false;
for (const row of rows) {
if (y > doc.page.height - 75) {
footer(doc, '—');
doc.addPage();
header(doc, '', '');
y = doc.y;
@@ -121,7 +134,7 @@ function table(doc, { title, headers, rows, colWidths }) {
doc.fontSize(7).font('Helvetica');
let x = 40;
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];
}
stroke(doc, C.border);
@@ -137,7 +150,7 @@ const NL = n => ({ critique:'CRITIQUE', attention:'ATTENTION', élevé:'ÉLEVÉ'
// ─── Vues ─────────────────────────────────────────────────────────────────────
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 => {
header(d, 'Synthèse Globale — Marchés RLA', 'Tunisie Telecom Zone Sud');
const kpis = [
@@ -147,15 +160,16 @@ function generateSynthese(data) {
{ l:'Alertes', v: data.alertes_delais?.count||0, c: C.warning },
{ l:'Avt. Moy.(%)', v:`${data.taux_avancement_moyen||0}%`, c: C.accent },
];
const kpiY = d.y;
let kx = 40;
for (const k of kpis) { kpiBox(d, kx, d.y, 95, 48, k.l, k.v, k.c); kx += 101; }
d.y += 58;
for (const k of kpis) { kpiBox(d, kx, kpiY, 95, 48, k.l, k.v, k.c); kx += 101; }
d.y = kpiY + 58;
if (data.budget) {
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]]) {
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) {
@@ -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'],
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) {
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 => {
header(d, 'Alertes Délais — Marchés RLA', `${data.count||0} alerte(s) — dont ${data.critique||0} critique(s)`);
if (data.items?.length) {
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)]) });
} else { fill(d, C.success); d.fontSize(14).font('Helvetica-Bold').text('Aucune alerte active.', {align:'center'}); }
footer(d, 1);
});
}
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 => {
header(d, 'Marchés en Service — RLA', `${data.count||0} marché(s) actif(s)`);
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],
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)]) });
footer(d, 1);
});
}
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 => {
header(d, 'Marchés en Cours — RLA', `${data.count||0} 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],
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||'—']) });
footer(d, 1);
});
}
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 => {
header(d, 'Vue par Région — Marchés RLA', `${data.count||0} région(s)`);
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.white); d.fontSize(11).font('Helvetica-Bold').text(reg.region, 52, d.y+5); d.y += 30;
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:'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;}
d.y+=50; d.moveDown(0.3);
const kpiY = d.y;
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) {
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 => {
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',
headers:['Réf.','Projet','Région','Entrepreneur','Montant','Taux Phy.','Date Clôture'],
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]) });
footer(d, 1);
});
}
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 => {
const r = data.resume||{};
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:'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;}
d.y+=62;
const kpiY = d.y;
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||[])];
if (problematic.length) {
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]) });
}
footer(d, 1);
});
}
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 => {
const pn = data.par_niveau||{};
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:'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;}
d.y+=62;
const kpiY = d.y;
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) {
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'],
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) {
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 => {
header(d, title, '');
const items = data.items||[];
@@ -295,7 +304,6 @@ function generateGeneric(title, data) {
table(d, { headers:keys, colWidths:keys.map(()=>Math.floor(700/keys.length)),
rows: items.slice(0,100).map(r=>keys.map(k=>r[k])) });
}
footer(d, 1);
});
}