feat: add header logo, fix teacher field, compact program, date range, total under UNITS

This commit is contained in:
adrianbonpin
2026-08-18 19:47:59 +08:00
parent 631ff4e9d2
commit 792974bd32
3 changed files with 92 additions and 16 deletions
+74 -15
View File
@@ -38,12 +38,58 @@
return out.length ? out : [''];
}
// Compact a degree name for display:
// "BACHELOR OF SCIENCE IN INFORMATION TECHNOLOGY" -> "BS IT".
// Multi-word fields become initials; single-word fields stay title-cased.
function compactProgram(program) {
const s = String(program || '').trim().toUpperCase();
let m = /^BACHELOR OF SCIENCE IN (.+)$/.exec(s);
let prefix = 'BS';
if (!m) {
m = /^BACHELOR OF ARTS IN (.+)$/.exec(s);
if (m) prefix = 'AB';
}
if (!m) return String(program || '').trim();
const field = m[1];
const words = field.split(/\s+/).filter((w) => !/^(AND|OF|THE|IN)$/.test(w));
if (words.length <= 1) {
const single = words[0] || field;
return prefix + ' ' + single.charAt(0).toUpperCase() + single.slice(1).toLowerCase();
}
return prefix + ' ' + words.map((w) => w[0]).join('');
}
// Map a semester to the official date range label.
function deriveDateRange(semester) {
const s = String(semester || '').toUpperCase();
if (/1ST|FIRST/.test(s)) return 'AUGUST - JANUARY';
if (/2ND|SECOND/.test(s)) return 'JANUARY - MAY';
if (/SUMMER/.test(s)) return 'JUNE - JULY';
return '';
}
// Load the bundled header logo (JPEG) in the extension background page.
// Returns null in Node (tests) or if the asset is unavailable.
async function loadLogo() {
try {
const url = (typeof browser !== 'undefined' && browser.runtime && browser.runtime.getURL)
? browser.runtime.getURL('assets/logo.jpeg')
: null;
if (!url) return null;
const resp = await fetch(url);
if (!resp.ok) return null;
return new Uint8Array(await resp.arrayBuffer());
} catch (e) {
return null;
}
}
// Builds a Letter-size PDF replicating the official ISMIS Study Load layout:
// title block (STUDENT'S STUDY LOAD, semester, date range, date enrolled),
// a student info row (ID, name, program, Year Level), the course table
// (GROUP, COURSE, COURSE DESCRIPTION, TEACHER, UNITS, NOTE), and a footer
// (total units, printed-by). NOTE is the status; UNITS/NOTE are right-aligned;
// schedule is stacked under the course description.
// header logo, title, semester + date range, date enrolled, a student info
// row (ID, name, program, Year Level), the course table (GROUP, COURSE,
// COURSE DESCRIPTION, TEACHER, UNITS, NOTE), and a footer (total under the
// UNITS column, printed-by). NOTE is the status; schedule is stacked under
// the course description.
async function buildStudyLoadPdf(data) {
const PDFLib = getPDFLib();
const { PDFDocument, StandardFonts, rgb } = PDFLib;
@@ -62,6 +108,12 @@
const black = rgb(0, 0, 0);
const gray = rgb(0.3, 0.3, 0.3);
const logoBytes = await loadLogo();
let logoImg = null;
if (logoBytes) {
try { logoImg = await doc.embedJpg(logoBytes); } catch (e) { logoImg = null; }
}
function center(text, size, f, dy) {
const w = f.widthOfTextAtSize(text, size);
page.drawText(text, { x: (612 - w) / 2, y, size, font: f, color: black });
@@ -76,16 +128,23 @@
}
// ---- Header ----
center("STUDENT'S STUDY LOAD", 9.5, bold, 18);
if (header.semester) center(header.semester, 9, font, 15);
if (header.dateRange) center(header.dateRange, 9, font, 15);
if (logoImg) {
const logoW = 292.5;
const logoH = logoW * (logoImg.height / logoImg.width);
page.drawImage(logoImg, { x: (612 - logoW) / 2, y: y - logoH, width: logoW, height: logoH });
y -= logoH + 14;
}
center("STUDENT'S STUDY LOAD", 9.5, bold, 16);
if (header.semester) center(String(header.semester).toUpperCase(), 9, font, 14);
const dateRange = header.dateRange || deriveDateRange(header.semester);
if (dateRange) center(dateRange, 9, font, 14);
if (header.dateEnrolled) left('Date Enrolled: ' + header.dateEnrolled, 8, italic, margin, 13);
// Student info row: ID (bold), name, program, "Year Level: N"
// Student info row: ID (bold), name, program (compacted), "Year Level: N"
const studentParts = [];
if (header.studentId) studentParts.push({ text: header.studentId, f: bold });
if (header.fullName) studentParts.push({ text: header.fullName, f: font });
if (header.program) studentParts.push({ text: header.program, f: font });
if (header.program) studentParts.push({ text: compactProgram(header.program), f: font });
if (header.yearLevel) studentParts.push({ text: 'Year Level: ' + header.yearLevel, f: font });
if (studentParts.length) {
let sx = margin;
@@ -137,7 +196,6 @@
}
if (isHeader) {
// top line above the header row
hline(margin, margin + contentWidth, y);
}
@@ -159,7 +217,6 @@
y -= rowHeight;
if (isHeader) {
// line below the header row
hline(margin, margin + contentWidth, y);
}
return true;
@@ -179,7 +236,7 @@
courseCode: course.courseCode,
description: course.description,
schedule: course.schedule,
teacher: course.teacher,
teacher: course.teacher || course.faculty,
units: course.units,
status: course.status
};
@@ -193,8 +250,10 @@
}
y -= 8;
if (data.totalUnits) {
// Total sits under the UNITS column, right-aligned within it.
const unitsCol = cols[4];
const w = bold.widthOfTextAtSize(data.totalUnits, 9);
page.drawText(data.totalUnits, { x: margin + contentWidth - w, y, size: 9, font: bold, color: black });
page.drawText(data.totalUnits, { x: unitsCol.x + unitsCol.w - 3 - w, y, size: 9, font: bold, color: black });
y -= 14;
}
hline(margin, margin + contentWidth, y);
@@ -206,5 +265,5 @@
return doc.save(); // returns Promise<Uint8Array>
}
return { buildStudyLoadPdf };
return { buildStudyLoadPdf, compactProgram, deriveDateRange };
});