@studious-lms/server 1.23.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +0 -1
- package/data/curriculum/nepal-cdc-g4-g5/README.md +90 -0
- package/data/curriculum/nepal-cdc-g4-g5/manifest.json +97 -0
- package/data/curriculum/nepal-cdc-g4-g5/proficiency-scale.json +76 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/english.json +6936 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/health-physical-creative-arts.json +3401 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/mathematics.json +1511 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/nepali.json +7928 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/science-technology.json +2420 -0
- package/data/curriculum/nepal-cdc-g4-g5/subjects/social-studies.json +2177 -0
- package/dist/lib/assignmentTypeDefaults.d.ts +41 -0
- package/dist/lib/assignmentTypeDefaults.d.ts.map +1 -0
- package/dist/lib/assignmentTypeDefaults.js +167 -0
- package/dist/lib/assignmentTypeDefaults.js.map +1 -0
- package/dist/lib/config/env.d.ts +0 -8
- package/dist/lib/config/env.d.ts.map +1 -1
- package/dist/lib/config/env.js +2 -5
- package/dist/lib/config/env.js.map +1 -1
- package/dist/lib/feePdf.d.ts +19 -0
- package/dist/lib/feePdf.d.ts.map +1 -0
- package/dist/lib/feePdf.js +167 -0
- package/dist/lib/feePdf.js.map +1 -0
- package/dist/lib/feePdfFormat.d.ts +4 -0
- package/dist/lib/feePdfFormat.d.ts.map +1 -0
- package/dist/lib/feePdfFormat.js +16 -0
- package/dist/lib/feePdfFormat.js.map +1 -0
- package/dist/lib/idCardPdf.d.ts +26 -0
- package/dist/lib/idCardPdf.d.ts.map +1 -0
- package/dist/lib/idCardPdf.js +272 -0
- package/dist/lib/idCardPdf.js.map +1 -0
- package/dist/lib/pdfLetterhead.d.ts +41 -0
- package/dist/lib/pdfLetterhead.d.ts.map +1 -0
- package/dist/lib/pdfLetterhead.js +86 -0
- package/dist/lib/pdfLetterhead.js.map +1 -0
- package/dist/lib/transcriptPdf.d.ts +55 -0
- package/dist/lib/transcriptPdf.d.ts.map +1 -0
- package/dist/lib/transcriptPdf.js +303 -0
- package/dist/lib/transcriptPdf.js.map +1 -0
- package/dist/models/academicCalendar.d.ts +198 -0
- package/dist/models/academicCalendar.d.ts.map +1 -0
- package/dist/models/academicCalendar.js +106 -0
- package/dist/models/academicCalendar.js.map +1 -0
- package/dist/models/agenda.d.ts +8 -0
- package/dist/models/agenda.d.ts.map +1 -1
- package/dist/models/announcement.d.ts +2 -0
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +66 -0
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/attendance.d.ts +659 -21
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/attendance.js +547 -75
- package/dist/models/attendance.js.map +1 -1
- package/dist/models/auth.d.ts +2 -0
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/class.d.ts +12 -0
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +3 -2
- package/dist/models/class.js.map +1 -1
- package/dist/models/event.d.ts +16 -0
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/fees.d.ts +554 -0
- package/dist/models/fees.d.ts.map +1 -0
- package/dist/models/fees.js +278 -0
- package/dist/models/fees.js.map +1 -0
- package/dist/models/file.d.ts +29 -0
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/folder.d.ts +2 -0
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +2 -0
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/marketplace.d.ts +180 -0
- package/dist/models/marketplace.d.ts.map +1 -0
- package/dist/models/marketplace.js +116 -0
- package/dist/models/marketplace.js.map +1 -0
- package/dist/models/moduleItem.d.ts +308 -0
- package/dist/models/moduleItem.d.ts.map +1 -0
- package/dist/models/moduleItem.js +93 -0
- package/dist/models/moduleItem.js.map +1 -0
- package/dist/models/newtonChat.d.ts +1 -0
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/school.d.ts +18 -0
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/school.js +7 -2
- package/dist/models/school.js.map +1 -1
- package/dist/models/section.d.ts +4 -0
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/worksheet.d.ts +19 -0
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +2 -2
- package/dist/models/worksheet.js.map +1 -1
- package/dist/routers/_app.d.ts +12024 -7778
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +12 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/academicCalendar.d.ts +229 -0
- package/dist/routers/academicCalendar.d.ts.map +1 -0
- package/dist/routers/academicCalendar.js +75 -0
- package/dist/routers/academicCalendar.js.map +1 -0
- package/dist/routers/admin.d.ts +2 -0
- package/dist/routers/admin.d.ts.map +1 -1
- package/dist/routers/agenda.d.ts +8 -0
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +75 -0
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +10 -2
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/attendance.d.ts +103 -0
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +8 -5
- package/dist/routers/attendance.js.map +1 -1
- package/dist/routers/class.d.ts +27 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +6 -2
- package/dist/routers/class.js.map +1 -1
- package/dist/routers/event.d.ts +8 -0
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/fees.d.ts +513 -0
- package/dist/routers/fees.d.ts.map +1 -0
- package/dist/routers/fees.js +88 -0
- package/dist/routers/fees.js.map +1 -0
- package/dist/routers/file.d.ts +2 -0
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/gradebook.d.ts +4 -1
- package/dist/routers/gradebook.d.ts.map +1 -1
- package/dist/routers/marketplace.d.ts +371 -0
- package/dist/routers/marketplace.d.ts.map +1 -0
- package/dist/routers/marketplace.js +87 -0
- package/dist/routers/marketplace.js.map +1 -0
- package/dist/routers/moduleItem.d.ts +263 -0
- package/dist/routers/moduleItem.d.ts.map +1 -0
- package/dist/routers/moduleItem.js +61 -0
- package/dist/routers/moduleItem.js.map +1 -0
- package/dist/routers/parent.d.ts +170 -0
- package/dist/routers/parent.d.ts.map +1 -0
- package/dist/routers/parent.js +49 -0
- package/dist/routers/parent.js.map +1 -0
- package/dist/routers/school.d.ts +363 -7
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/school.js +110 -3
- package/dist/routers/school.js.map +1 -1
- package/dist/routers/worksheet.d.ts +27 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +10 -3
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/services/academicCalendar.d.ts +173 -0
- package/dist/services/academicCalendar.d.ts.map +1 -0
- package/dist/services/academicCalendar.js +242 -0
- package/dist/services/academicCalendar.js.map +1 -0
- package/dist/services/adminInstitution.d.ts +2 -0
- package/dist/services/adminInstitution.d.ts.map +1 -1
- package/dist/services/agenda.d.ts +8 -0
- package/dist/services/agenda.d.ts.map +1 -1
- package/dist/services/assignment.d.ts +75 -0
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +91 -12
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts +327 -10
- package/dist/services/attendance.d.ts.map +1 -1
- package/dist/services/attendance.js +389 -25
- package/dist/services/attendance.js.map +1 -1
- package/dist/services/attendanceMerge.d.ts +53 -0
- package/dist/services/attendanceMerge.d.ts.map +1 -0
- package/dist/services/attendanceMerge.js +87 -0
- package/dist/services/attendanceMerge.js.map +1 -0
- package/dist/services/attendanceRate.d.ts +24 -0
- package/dist/services/attendanceRate.d.ts.map +1 -0
- package/dist/services/attendanceRate.js +31 -0
- package/dist/services/attendanceRate.js.map +1 -0
- package/dist/services/attendanceStats.d.ts +22 -0
- package/dist/services/attendanceStats.d.ts.map +1 -0
- package/dist/services/attendanceStats.js +45 -0
- package/dist/services/attendanceStats.js.map +1 -0
- package/dist/services/class.d.ts +34 -2
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +27 -4
- package/dist/services/class.js.map +1 -1
- package/dist/services/event.d.ts +8 -0
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/fees.d.ts +422 -0
- package/dist/services/fees.d.ts.map +1 -0
- package/dist/services/fees.js +330 -0
- package/dist/services/fees.js.map +1 -0
- package/dist/services/feesAuthz.d.ts +46 -0
- package/dist/services/feesAuthz.d.ts.map +1 -0
- package/dist/services/feesAuthz.js +49 -0
- package/dist/services/feesAuthz.js.map +1 -0
- package/dist/services/file.d.ts +4 -0
- package/dist/services/file.d.ts.map +1 -1
- package/dist/services/gradebook.d.ts +18 -1
- package/dist/services/gradebook.d.ts.map +1 -1
- package/dist/services/gradebook.js +57 -2
- package/dist/services/gradebook.js.map +1 -1
- package/dist/services/idCard.d.ts +11 -0
- package/dist/services/idCard.d.ts.map +1 -0
- package/dist/services/idCard.js +122 -0
- package/dist/services/idCard.js.map +1 -0
- package/dist/services/marketplace.d.ts +301 -0
- package/dist/services/marketplace.d.ts.map +1 -0
- package/dist/services/marketplace.js +631 -0
- package/dist/services/marketplace.js.map +1 -0
- package/dist/services/moduleItem.d.ts +196 -0
- package/dist/services/moduleItem.d.ts.map +1 -0
- package/dist/services/moduleItem.js +204 -0
- package/dist/services/moduleItem.js.map +1 -0
- package/dist/services/notification.d.ts +58 -0
- package/dist/services/notification.d.ts.map +1 -1
- package/dist/services/notification.js +30 -2
- package/dist/services/notification.js.map +1 -1
- package/dist/services/parentPortal.d.ts +131 -0
- package/dist/services/parentPortal.d.ts.map +1 -0
- package/dist/services/parentPortal.js +314 -0
- package/dist/services/parentPortal.js.map +1 -0
- package/dist/services/school.d.ts +28 -3
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/school.js +98 -54
- package/dist/services/school.js.map +1 -1
- package/dist/services/studentImport.d.ts +34 -0
- package/dist/services/studentImport.d.ts.map +1 -0
- package/dist/services/studentImport.js +249 -0
- package/dist/services/studentImport.js.map +1 -0
- package/dist/services/transcript.d.ts +60 -0
- package/dist/services/transcript.d.ts.map +1 -0
- package/dist/services/transcript.js +231 -0
- package/dist/services/transcript.js.map +1 -0
- package/dist/services/worksheet.d.ts +22 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +94 -2
- package/dist/services/worksheet.js.map +1 -1
- package/dist/trpc.d.ts +1 -1
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +5 -5
- package/dist/trpc.js.map +1 -1
- package/dist/utils/adminPanelAccess.d.ts +4 -5
- package/dist/utils/adminPanelAccess.d.ts.map +1 -1
- package/dist/utils/adminPanelAccess.js +12 -34
- package/dist/utils/adminPanelAccess.js.map +1 -1
- package/package.json +2 -1
- package/prisma/migrations/20260804120000_add_attendance_records_and_excusals/migration.sql +114 -0
- package/prisma/migrations/20260804120100_add_fee_plans_invoices_payments/migration.sql +101 -0
- package/prisma/migrations/20260804120200_add_marketplace_listings/migration.sql +54 -0
- package/prisma/migrations/20260804120300_content_platform_b3_b9/migration.sql +63 -0
- package/prisma/migrations/20260804120400_c_academic_ops/migration.sql +77 -0
- package/prisma/schema.prisma +371 -11
- package/scripts/import-nepal-cdc-curriculum.ts +402 -0
- package/src/lib/assignmentTypeDefaults.ts +222 -0
- package/src/lib/config/env.ts +0 -3
- package/src/lib/feePdf.ts +199 -0
- package/src/lib/feePdfFormat.ts +14 -0
- package/src/lib/idCardPdf.ts +336 -0
- package/src/lib/pdfLetterhead.ts +124 -0
- package/src/lib/transcriptPdf.ts +502 -0
- package/src/models/academicCalendar.ts +139 -0
- package/src/models/attendance.ts +702 -73
- package/src/models/class.ts +1 -0
- package/src/models/fees.ts +349 -0
- package/src/models/marketplace.ts +161 -0
- package/src/models/moduleItem.ts +125 -0
- package/src/models/school.ts +6 -0
- package/src/models/worksheet.ts +2 -0
- package/src/routers/_app.ts +10 -0
- package/src/routers/academicCalendar.ts +103 -0
- package/src/routers/assignment.ts +8 -0
- package/src/routers/attendance.ts +7 -3
- package/src/routers/class.ts +4 -0
- package/src/routers/fees.ts +126 -0
- package/src/routers/marketplace.ts +134 -0
- package/src/routers/moduleItem.ts +91 -0
- package/src/routers/parent.ts +68 -0
- package/src/routers/school.ts +149 -1
- package/src/routers/worksheet.ts +11 -0
- package/src/services/academicCalendar.ts +327 -0
- package/src/services/assignment.ts +122 -12
- package/src/services/attendance.ts +552 -26
- package/src/services/attendanceMerge.ts +139 -0
- package/src/services/attendanceRate.ts +41 -0
- package/src/services/attendanceStats.ts +63 -0
- package/src/services/class.ts +26 -2
- package/src/services/fees.ts +447 -0
- package/src/services/feesAuthz.ts +73 -0
- package/src/services/gradebook.ts +70 -0
- package/src/services/idCard.ts +147 -0
- package/src/services/marketplace.ts +790 -0
- package/src/services/moduleItem.ts +298 -0
- package/src/services/notification.ts +43 -0
- package/src/services/parentPortal.ts +367 -0
- package/src/services/school.ts +100 -54
- package/src/services/studentImport.ts +285 -0
- package/src/services/transcript.ts +301 -0
- package/src/services/worksheet.ts +111 -0
- package/src/trpc.ts +3 -3
- package/src/utils/adminPanelAccess.ts +12 -35
- package/tests/unit/assignmentTypeDefaults.test.ts +52 -0
- package/tests/unit/lib/feePdf.test.ts +15 -0
- package/tests/unit/lib/idCardPdf.test.ts +40 -0
- package/tests/unit/lib/transcriptPdf.test.ts +111 -0
- package/tests/unit/services/attendanceMerge.test.ts +90 -0
- package/tests/unit/services/attendanceRate.test.ts +38 -0
- package/tests/unit/services/attendanceStats.test.ts +54 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type RGB } from "pdf-lib";
|
|
2
|
+
/** CR80 ID card size in PDF points (3.375" × 2.125"). */
|
|
3
|
+
export declare const ID_CARD_WIDTH = 243;
|
|
4
|
+
export declare const ID_CARD_HEIGHT = 153;
|
|
5
|
+
export declare const DEFAULT_BRAND_COLOR = "#1E3A5F";
|
|
6
|
+
export type IdCardStudent = {
|
|
7
|
+
displayName: string;
|
|
8
|
+
username: string;
|
|
9
|
+
/** Class / grade label, e.g. "Grade 5 · Section A". */
|
|
10
|
+
classLabel: string | null;
|
|
11
|
+
/** Hex accent from class/department branding when available. */
|
|
12
|
+
brandColor: string | null;
|
|
13
|
+
profilePictureUrl: string | null;
|
|
14
|
+
};
|
|
15
|
+
export type IdCardSchool = {
|
|
16
|
+
name: string;
|
|
17
|
+
logoPath: string | null;
|
|
18
|
+
};
|
|
19
|
+
export declare function parseHexColor(hex: string | null | undefined): RGB;
|
|
20
|
+
/** Convert arbitrary image bytes (incl. SVG/WebP) to PNG for pdf-lib. */
|
|
21
|
+
export declare function toPngBytes(input: Buffer): Promise<Uint8Array | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Build a multi-page PDF: one CR80 front-side ID card per student.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildIdCardsPdf(school: IdCardSchool, students: IdCardStudent[]): Promise<Uint8Array>;
|
|
26
|
+
//# sourceMappingURL=idCardPdf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idCardPdf.d.ts","sourceRoot":"/","sources":["lib/idCardPdf.ts"],"names":[],"mappings":"AAMA,OAAO,EAOL,KAAK,GAAG,EACT,MAAM,SAAS,CAAC;AAIjB,yDAAyD;AACzD,eAAO,MAAM,aAAa,MAAM,CAAC;AACjC,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C,MAAM,MAAM,aAAa,GAAG;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gEAAgE;IAChE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AASF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,GAAG,CAMjE;AAoDD,yEAAyE;AACzE,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAQ1E;AA8LD;;GAEG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,aAAa,EAAE,GACxB,OAAO,CAAC,UAAU,CAAC,CA0BrB"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Student ID card PDF generation (front-side, CR80) via pdf-lib.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="77a2e4ef-b728-5a9e-85eb-71bd3525a454")}catch(e){}}();
|
|
6
|
+
import fontkit from "@pdf-lib/fontkit";
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { PDFDocument, StandardFonts, rgb, } from "pdf-lib";
|
|
10
|
+
import sharp from "sharp";
|
|
11
|
+
import { logger } from "../utils/logger.js";
|
|
12
|
+
/** CR80 ID card size in PDF points (3.375" × 2.125"). */
|
|
13
|
+
export const ID_CARD_WIDTH = 243;
|
|
14
|
+
export const ID_CARD_HEIGHT = 153;
|
|
15
|
+
export const DEFAULT_BRAND_COLOR = "#1E3A5F";
|
|
16
|
+
function hexToRgb(cleaned) {
|
|
17
|
+
const r = parseInt(cleaned.slice(0, 2), 16) / 255;
|
|
18
|
+
const g = parseInt(cleaned.slice(2, 4), 16) / 255;
|
|
19
|
+
const b = parseInt(cleaned.slice(4, 6), 16) / 255;
|
|
20
|
+
return rgb(r, g, b);
|
|
21
|
+
}
|
|
22
|
+
export function parseHexColor(hex) {
|
|
23
|
+
const fallback = hexToRgb(DEFAULT_BRAND_COLOR.replace(/^#/, ""));
|
|
24
|
+
if (!hex)
|
|
25
|
+
return fallback;
|
|
26
|
+
const cleaned = hex.trim().replace(/^#/, "");
|
|
27
|
+
if (!/^[0-9a-fA-F]{6}$/.test(cleaned))
|
|
28
|
+
return fallback;
|
|
29
|
+
return hexToRgb(cleaned);
|
|
30
|
+
}
|
|
31
|
+
function contrastTextColor(bg) {
|
|
32
|
+
const luminance = 0.2126 * bg.red + 0.7152 * bg.green + 0.0722 * bg.blue;
|
|
33
|
+
return luminance > 0.55 ? rgb(0.1, 0.1, 0.1) : rgb(1, 1, 1);
|
|
34
|
+
}
|
|
35
|
+
async function loadFonts(pdfDoc) {
|
|
36
|
+
pdfDoc.registerFontkit(fontkit);
|
|
37
|
+
const fontDir = join(process.cwd(), "src", "lib");
|
|
38
|
+
try {
|
|
39
|
+
const regularBytes = readFileSync(join(fontDir, "NotoSans-Regular.ttf"));
|
|
40
|
+
const boldBytes = readFileSync(join(fontDir, "NotoSans-Bold.ttf"));
|
|
41
|
+
return {
|
|
42
|
+
regular: await pdfDoc.embedFont(regularBytes),
|
|
43
|
+
bold: await pdfDoc.embedFont(boldBytes),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
logger.warn(`idCardPdf: falling back to Helvetica fonts: ${error}`);
|
|
48
|
+
return {
|
|
49
|
+
regular: await pdfDoc.embedFont(StandardFonts.Helvetica),
|
|
50
|
+
bold: await pdfDoc.embedFont(StandardFonts.HelveticaBold),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function fetchBytesFromUrl(url) {
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(url);
|
|
57
|
+
if (!response.ok)
|
|
58
|
+
return null;
|
|
59
|
+
return Buffer.from(await response.arrayBuffer());
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
logger.warn(`idCardPdf: failed to fetch image URL: ${error}`);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function loadGcsBytes(path) {
|
|
67
|
+
try {
|
|
68
|
+
// Dynamic import keeps unit tests free of GCS/env bootstrap.
|
|
69
|
+
const { bucket } = await import("./googleCloudStorage.js");
|
|
70
|
+
const [bytes] = await bucket.file(path).download();
|
|
71
|
+
return Buffer.from(bytes);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
logger.warn(`idCardPdf: failed to download GCS object ${path}: ${error}`);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Convert arbitrary image bytes (incl. SVG/WebP) to PNG for pdf-lib. */
|
|
79
|
+
export async function toPngBytes(input) {
|
|
80
|
+
try {
|
|
81
|
+
const png = await sharp(input).rotate().png().toBuffer();
|
|
82
|
+
return new Uint8Array(png);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
logger.warn(`idCardPdf: sharp conversion failed: ${error}`);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function embedImage(pdfDoc, bytes) {
|
|
90
|
+
if (!bytes || bytes.length === 0)
|
|
91
|
+
return null;
|
|
92
|
+
const png = await toPngBytes(bytes);
|
|
93
|
+
if (!png)
|
|
94
|
+
return null;
|
|
95
|
+
try {
|
|
96
|
+
return await pdfDoc.embedPng(png);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
logger.warn(`idCardPdf: embedPng failed: ${error}`);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function truncateToWidth(text, font, size, maxWidth) {
|
|
104
|
+
if (font.widthOfTextAtSize(text, size) <= maxWidth)
|
|
105
|
+
return text;
|
|
106
|
+
let truncated = text;
|
|
107
|
+
const ellipsis = "…";
|
|
108
|
+
while (truncated.length > 1 &&
|
|
109
|
+
font.widthOfTextAtSize(truncated + ellipsis, size) > maxWidth) {
|
|
110
|
+
truncated = truncated.slice(0, -1);
|
|
111
|
+
}
|
|
112
|
+
return truncated + ellipsis;
|
|
113
|
+
}
|
|
114
|
+
function drawCard(page, fonts, school, student, logo, photo) {
|
|
115
|
+
const brand = parseHexColor(student.brandColor ?? DEFAULT_BRAND_COLOR);
|
|
116
|
+
const onBrand = contrastTextColor(brand);
|
|
117
|
+
const muted = rgb(0.4, 0.45, 0.5);
|
|
118
|
+
const ink = rgb(0.12, 0.14, 0.18);
|
|
119
|
+
const border = rgb(0.85, 0.87, 0.9);
|
|
120
|
+
page.drawRectangle({
|
|
121
|
+
x: 0,
|
|
122
|
+
y: 0,
|
|
123
|
+
width: ID_CARD_WIDTH,
|
|
124
|
+
height: ID_CARD_HEIGHT,
|
|
125
|
+
color: rgb(1, 1, 1),
|
|
126
|
+
});
|
|
127
|
+
const headerH = 36;
|
|
128
|
+
page.drawRectangle({
|
|
129
|
+
x: 0,
|
|
130
|
+
y: ID_CARD_HEIGHT - headerH,
|
|
131
|
+
width: ID_CARD_WIDTH,
|
|
132
|
+
height: headerH,
|
|
133
|
+
color: brand,
|
|
134
|
+
});
|
|
135
|
+
page.drawRectangle({
|
|
136
|
+
x: 0,
|
|
137
|
+
y: 0,
|
|
138
|
+
width: ID_CARD_WIDTH,
|
|
139
|
+
height: 6,
|
|
140
|
+
color: brand,
|
|
141
|
+
});
|
|
142
|
+
page.drawRectangle({
|
|
143
|
+
x: 0.5,
|
|
144
|
+
y: 0.5,
|
|
145
|
+
width: ID_CARD_WIDTH - 1,
|
|
146
|
+
height: ID_CARD_HEIGHT - 1,
|
|
147
|
+
borderColor: border,
|
|
148
|
+
borderWidth: 0.75,
|
|
149
|
+
});
|
|
150
|
+
const logoSize = 22;
|
|
151
|
+
let schoolNameX = 12;
|
|
152
|
+
if (logo) {
|
|
153
|
+
const scale = Math.min(logoSize / logo.width, logoSize / logo.height);
|
|
154
|
+
const w = logo.width * scale;
|
|
155
|
+
const h = logo.height * scale;
|
|
156
|
+
page.drawImage(logo, {
|
|
157
|
+
x: 10,
|
|
158
|
+
y: ID_CARD_HEIGHT - headerH + (headerH - h) / 2,
|
|
159
|
+
width: w,
|
|
160
|
+
height: h,
|
|
161
|
+
});
|
|
162
|
+
schoolNameX = 10 + w + 8;
|
|
163
|
+
}
|
|
164
|
+
const schoolName = truncateToWidth(school.name, fonts.bold, 11, ID_CARD_WIDTH - schoolNameX - 10);
|
|
165
|
+
page.drawText(schoolName, {
|
|
166
|
+
x: schoolNameX,
|
|
167
|
+
y: ID_CARD_HEIGHT - headerH + 13,
|
|
168
|
+
size: 11,
|
|
169
|
+
font: fonts.bold,
|
|
170
|
+
color: onBrand,
|
|
171
|
+
});
|
|
172
|
+
const photoX = 12;
|
|
173
|
+
const photoY = 22;
|
|
174
|
+
const photoW = 64;
|
|
175
|
+
const photoH = 80;
|
|
176
|
+
page.drawRectangle({
|
|
177
|
+
x: photoX,
|
|
178
|
+
y: photoY,
|
|
179
|
+
width: photoW,
|
|
180
|
+
height: photoH,
|
|
181
|
+
color: rgb(0.94, 0.95, 0.97),
|
|
182
|
+
borderColor: border,
|
|
183
|
+
borderWidth: 0.5,
|
|
184
|
+
});
|
|
185
|
+
if (photo) {
|
|
186
|
+
const scale = Math.min(photoW / photo.width, photoH / photo.height);
|
|
187
|
+
const w = photo.width * scale;
|
|
188
|
+
const h = photo.height * scale;
|
|
189
|
+
page.drawImage(photo, {
|
|
190
|
+
x: photoX + (photoW - w) / 2,
|
|
191
|
+
y: photoY + (photoH - h) / 2,
|
|
192
|
+
width: w,
|
|
193
|
+
height: h,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const initials = student.displayName
|
|
198
|
+
.split(/\s+/)
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
.slice(0, 2)
|
|
201
|
+
.map((p) => p[0]?.toUpperCase() ?? "")
|
|
202
|
+
.join("");
|
|
203
|
+
const initialsSize = 18;
|
|
204
|
+
const iw = fonts.bold.widthOfTextAtSize(initials || "?", initialsSize);
|
|
205
|
+
page.drawText(initials || "?", {
|
|
206
|
+
x: photoX + (photoW - iw) / 2,
|
|
207
|
+
y: photoY + photoH / 2 - initialsSize / 3,
|
|
208
|
+
size: initialsSize,
|
|
209
|
+
font: fonts.bold,
|
|
210
|
+
color: muted,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const textX = photoX + photoW + 12;
|
|
214
|
+
const maxTextW = ID_CARD_WIDTH - textX - 10;
|
|
215
|
+
let y = ID_CARD_HEIGHT - headerH - 22;
|
|
216
|
+
const drawField = (label, value, valueSize = 10) => {
|
|
217
|
+
page.drawText(label, {
|
|
218
|
+
x: textX,
|
|
219
|
+
y,
|
|
220
|
+
size: 7,
|
|
221
|
+
font: fonts.regular,
|
|
222
|
+
color: muted,
|
|
223
|
+
});
|
|
224
|
+
y -= 12;
|
|
225
|
+
const line = truncateToWidth(value, fonts.bold, valueSize, maxTextW);
|
|
226
|
+
page.drawText(line, {
|
|
227
|
+
x: textX,
|
|
228
|
+
y,
|
|
229
|
+
size: valueSize,
|
|
230
|
+
font: fonts.bold,
|
|
231
|
+
color: ink,
|
|
232
|
+
});
|
|
233
|
+
y -= 16;
|
|
234
|
+
};
|
|
235
|
+
drawField("Name", student.displayName || student.username, 11);
|
|
236
|
+
drawField("Student ID", student.username, 10);
|
|
237
|
+
drawField("Class / Grade", student.classLabel || "—", 9);
|
|
238
|
+
page.drawText("STUDENT", {
|
|
239
|
+
x: textX,
|
|
240
|
+
y: 14,
|
|
241
|
+
size: 7,
|
|
242
|
+
font: fonts.bold,
|
|
243
|
+
color: brand,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Build a multi-page PDF: one CR80 front-side ID card per student.
|
|
248
|
+
*/
|
|
249
|
+
export async function buildIdCardsPdf(school, students) {
|
|
250
|
+
if (students.length === 0) {
|
|
251
|
+
throw new Error("No students provided for ID card generation");
|
|
252
|
+
}
|
|
253
|
+
const pdfDoc = await PDFDocument.create();
|
|
254
|
+
const fonts = await loadFonts(pdfDoc);
|
|
255
|
+
let logoBytes = null;
|
|
256
|
+
if (school.logoPath) {
|
|
257
|
+
logoBytes = await loadGcsBytes(school.logoPath);
|
|
258
|
+
}
|
|
259
|
+
const logo = await embedImage(pdfDoc, logoBytes);
|
|
260
|
+
for (const student of students) {
|
|
261
|
+
let photoBytes = null;
|
|
262
|
+
if (student.profilePictureUrl) {
|
|
263
|
+
photoBytes = await fetchBytesFromUrl(student.profilePictureUrl);
|
|
264
|
+
}
|
|
265
|
+
const photo = await embedImage(pdfDoc, photoBytes);
|
|
266
|
+
const page = pdfDoc.addPage([ID_CARD_WIDTH, ID_CARD_HEIGHT]);
|
|
267
|
+
drawCard(page, fonts, school, student, logo, photo);
|
|
268
|
+
}
|
|
269
|
+
return pdfDoc.save();
|
|
270
|
+
}
|
|
271
|
+
//# sourceMappingURL=idCardPdf.js.map
|
|
272
|
+
//# debugId=77a2e4ef-b728-5a9e-85eb-71bd3525a454
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idCardPdf.js","sources":["lib/idCardPdf.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Student ID card PDF generation (front-side, CR80) via pdf-lib.\n */\nimport fontkit from \"@pdf-lib/fontkit\";\nimport { readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport {\n PDFDocument,\n PDFFont,\n PDFImage,\n PDFPage,\n StandardFonts,\n rgb,\n type RGB,\n} from \"pdf-lib\";\nimport sharp from \"sharp\";\nimport { logger } from \"../utils/logger.js\";\n\n/** CR80 ID card size in PDF points (3.375\" × 2.125\"). */\nexport const ID_CARD_WIDTH = 243;\nexport const ID_CARD_HEIGHT = 153;\n\nexport const DEFAULT_BRAND_COLOR = \"#1E3A5F\";\n\nexport type IdCardStudent = {\n displayName: string;\n username: string;\n /** Class / grade label, e.g. \"Grade 5 · Section A\". */\n classLabel: string | null;\n /** Hex accent from class/department branding when available. */\n brandColor: string | null;\n profilePictureUrl: string | null;\n};\n\nexport type IdCardSchool = {\n name: string;\n logoPath: string | null;\n};\n\nfunction hexToRgb(cleaned: string): RGB {\n const r = parseInt(cleaned.slice(0, 2), 16) / 255;\n const g = parseInt(cleaned.slice(2, 4), 16) / 255;\n const b = parseInt(cleaned.slice(4, 6), 16) / 255;\n return rgb(r, g, b);\n}\n\nexport function parseHexColor(hex: string | null | undefined): RGB {\n const fallback = hexToRgb(DEFAULT_BRAND_COLOR.replace(/^#/, \"\"));\n if (!hex) return fallback;\n const cleaned = hex.trim().replace(/^#/, \"\");\n if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) return fallback;\n return hexToRgb(cleaned);\n}\n\nfunction contrastTextColor(bg: RGB): RGB {\n const luminance = 0.2126 * bg.red + 0.7152 * bg.green + 0.0722 * bg.blue;\n return luminance > 0.55 ? rgb(0.1, 0.1, 0.1) : rgb(1, 1, 1);\n}\n\nasync function loadFonts(pdfDoc: PDFDocument): Promise<{\n regular: PDFFont;\n bold: PDFFont;\n}> {\n pdfDoc.registerFontkit(fontkit);\n const fontDir = join(process.cwd(), \"src\", \"lib\");\n try {\n const regularBytes = readFileSync(join(fontDir, \"NotoSans-Regular.ttf\"));\n const boldBytes = readFileSync(join(fontDir, \"NotoSans-Bold.ttf\"));\n return {\n regular: await pdfDoc.embedFont(regularBytes),\n bold: await pdfDoc.embedFont(boldBytes),\n };\n } catch (error) {\n logger.warn(`idCardPdf: falling back to Helvetica fonts: ${error}`);\n return {\n regular: await pdfDoc.embedFont(StandardFonts.Helvetica),\n bold: await pdfDoc.embedFont(StandardFonts.HelveticaBold),\n };\n }\n}\n\nasync function fetchBytesFromUrl(url: string): Promise<Buffer | null> {\n try {\n const response = await fetch(url);\n if (!response.ok) return null;\n return Buffer.from(await response.arrayBuffer());\n } catch (error) {\n logger.warn(`idCardPdf: failed to fetch image URL: ${error}`);\n return null;\n }\n}\n\nasync function loadGcsBytes(path: string): Promise<Buffer | null> {\n try {\n // Dynamic import keeps unit tests free of GCS/env bootstrap.\n const { bucket } = await import(\"./googleCloudStorage.js\");\n const [bytes] = await bucket.file(path).download();\n return Buffer.from(bytes);\n } catch (error) {\n logger.warn(`idCardPdf: failed to download GCS object ${path}: ${error}`);\n return null;\n }\n}\n\n/** Convert arbitrary image bytes (incl. SVG/WebP) to PNG for pdf-lib. */\nexport async function toPngBytes(input: Buffer): Promise<Uint8Array | null> {\n try {\n const png = await sharp(input).rotate().png().toBuffer();\n return new Uint8Array(png);\n } catch (error) {\n logger.warn(`idCardPdf: sharp conversion failed: ${error}`);\n return null;\n }\n}\n\nasync function embedImage(\n pdfDoc: PDFDocument,\n bytes: Buffer | null\n): Promise<PDFImage | null> {\n if (!bytes || bytes.length === 0) return null;\n const png = await toPngBytes(bytes);\n if (!png) return null;\n try {\n return await pdfDoc.embedPng(png);\n } catch (error) {\n logger.warn(`idCardPdf: embedPng failed: ${error}`);\n return null;\n }\n}\n\nfunction truncateToWidth(\n text: string,\n font: PDFFont,\n size: number,\n maxWidth: number\n): string {\n if (font.widthOfTextAtSize(text, size) <= maxWidth) return text;\n let truncated = text;\n const ellipsis = \"…\";\n while (\n truncated.length > 1 &&\n font.widthOfTextAtSize(truncated + ellipsis, size) > maxWidth\n ) {\n truncated = truncated.slice(0, -1);\n }\n return truncated + ellipsis;\n}\n\nfunction drawCard(\n page: PDFPage,\n fonts: { regular: PDFFont; bold: PDFFont },\n school: IdCardSchool,\n student: IdCardStudent,\n logo: PDFImage | null,\n photo: PDFImage | null\n) {\n const brand = parseHexColor(student.brandColor ?? DEFAULT_BRAND_COLOR);\n const onBrand = contrastTextColor(brand);\n const muted = rgb(0.4, 0.45, 0.5);\n const ink = rgb(0.12, 0.14, 0.18);\n const border = rgb(0.85, 0.87, 0.9);\n\n page.drawRectangle({\n x: 0,\n y: 0,\n width: ID_CARD_WIDTH,\n height: ID_CARD_HEIGHT,\n color: rgb(1, 1, 1),\n });\n\n const headerH = 36;\n page.drawRectangle({\n x: 0,\n y: ID_CARD_HEIGHT - headerH,\n width: ID_CARD_WIDTH,\n height: headerH,\n color: brand,\n });\n\n page.drawRectangle({\n x: 0,\n y: 0,\n width: ID_CARD_WIDTH,\n height: 6,\n color: brand,\n });\n\n page.drawRectangle({\n x: 0.5,\n y: 0.5,\n width: ID_CARD_WIDTH - 1,\n height: ID_CARD_HEIGHT - 1,\n borderColor: border,\n borderWidth: 0.75,\n });\n\n const logoSize = 22;\n let schoolNameX = 12;\n if (logo) {\n const scale = Math.min(logoSize / logo.width, logoSize / logo.height);\n const w = logo.width * scale;\n const h = logo.height * scale;\n page.drawImage(logo, {\n x: 10,\n y: ID_CARD_HEIGHT - headerH + (headerH - h) / 2,\n width: w,\n height: h,\n });\n schoolNameX = 10 + w + 8;\n }\n\n const schoolName = truncateToWidth(\n school.name,\n fonts.bold,\n 11,\n ID_CARD_WIDTH - schoolNameX - 10\n );\n page.drawText(schoolName, {\n x: schoolNameX,\n y: ID_CARD_HEIGHT - headerH + 13,\n size: 11,\n font: fonts.bold,\n color: onBrand,\n });\n\n const photoX = 12;\n const photoY = 22;\n const photoW = 64;\n const photoH = 80;\n page.drawRectangle({\n x: photoX,\n y: photoY,\n width: photoW,\n height: photoH,\n color: rgb(0.94, 0.95, 0.97),\n borderColor: border,\n borderWidth: 0.5,\n });\n if (photo) {\n const scale = Math.min(photoW / photo.width, photoH / photo.height);\n const w = photo.width * scale;\n const h = photo.height * scale;\n page.drawImage(photo, {\n x: photoX + (photoW - w) / 2,\n y: photoY + (photoH - h) / 2,\n width: w,\n height: h,\n });\n } else {\n const initials = student.displayName\n .split(/\\s+/)\n .filter(Boolean)\n .slice(0, 2)\n .map((p) => p[0]?.toUpperCase() ?? \"\")\n .join(\"\");\n const initialsSize = 18;\n const iw = fonts.bold.widthOfTextAtSize(initials || \"?\", initialsSize);\n page.drawText(initials || \"?\", {\n x: photoX + (photoW - iw) / 2,\n y: photoY + photoH / 2 - initialsSize / 3,\n size: initialsSize,\n font: fonts.bold,\n color: muted,\n });\n }\n\n const textX = photoX + photoW + 12;\n const maxTextW = ID_CARD_WIDTH - textX - 10;\n let y = ID_CARD_HEIGHT - headerH - 22;\n\n const drawField = (label: string, value: string, valueSize = 10) => {\n page.drawText(label, {\n x: textX,\n y,\n size: 7,\n font: fonts.regular,\n color: muted,\n });\n y -= 12;\n const line = truncateToWidth(value, fonts.bold, valueSize, maxTextW);\n page.drawText(line, {\n x: textX,\n y,\n size: valueSize,\n font: fonts.bold,\n color: ink,\n });\n y -= 16;\n };\n\n drawField(\"Name\", student.displayName || student.username, 11);\n drawField(\"Student ID\", student.username, 10);\n drawField(\"Class / Grade\", student.classLabel || \"—\", 9);\n\n page.drawText(\"STUDENT\", {\n x: textX,\n y: 14,\n size: 7,\n font: fonts.bold,\n color: brand,\n });\n}\n\n/**\n * Build a multi-page PDF: one CR80 front-side ID card per student.\n */\nexport async function buildIdCardsPdf(\n school: IdCardSchool,\n students: IdCardStudent[]\n): Promise<Uint8Array> {\n if (students.length === 0) {\n throw new Error(\"No students provided for ID card generation\");\n }\n\n const pdfDoc = await PDFDocument.create();\n const fonts = await loadFonts(pdfDoc);\n\n let logoBytes: Buffer | null = null;\n if (school.logoPath) {\n logoBytes = await loadGcsBytes(school.logoPath);\n }\n const logo = await embedImage(pdfDoc, logoBytes);\n\n for (const student of students) {\n let photoBytes: Buffer | null = null;\n if (student.profilePictureUrl) {\n photoBytes = await fetchBytesFromUrl(student.profilePictureUrl);\n }\n const photo = await embedImage(pdfDoc, photoBytes);\n\n const page = pdfDoc.addPage([ID_CARD_WIDTH, ID_CARD_HEIGHT]);\n drawCard(page, fonts, school, student, logo, photo);\n }\n\n return pdfDoc.save();\n}\n"],"names":[],"mappings":"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","debug_id":"77a2e4ef-b728-5a9e-85eb-71bd3525a454"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable PDF letterhead stamp helper (pdf-lib).
|
|
3
|
+
*
|
|
4
|
+
* Callers (transcripts, fee receipts, ID cards) load school letterhead bytes
|
|
5
|
+
* and pass them here to draw a header image on the first page.
|
|
6
|
+
*/
|
|
7
|
+
import { PDFDocument, PDFPage } from "pdf-lib";
|
|
8
|
+
export interface StampLetterheadOptions {
|
|
9
|
+
/** Full page width (points). Defaults to A4 width. */
|
|
10
|
+
pageWidth?: number;
|
|
11
|
+
/** Top margin inset. Defaults to 24. */
|
|
12
|
+
topMargin?: number;
|
|
13
|
+
/** Max letterhead height in points. Defaults to 72. */
|
|
14
|
+
maxHeight?: number;
|
|
15
|
+
/** Horizontal inset from page edges. Defaults to 48. */
|
|
16
|
+
sideMargin?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Draw letterhead on `page` and return the Y coordinate below the image
|
|
20
|
+
* (content should start at or below this value). Returns the original
|
|
21
|
+
* `contentTopY` when letterhead cannot be embedded.
|
|
22
|
+
*/
|
|
23
|
+
export declare function stampLetterheadOnPage(input: {
|
|
24
|
+
pdfDoc: PDFDocument;
|
|
25
|
+
page: PDFPage;
|
|
26
|
+
letterheadBytes?: Uint8Array | null;
|
|
27
|
+
letterheadContentType?: string | null;
|
|
28
|
+
/** Current top Y for content (page height - top margin). */
|
|
29
|
+
contentTopY: number;
|
|
30
|
+
options?: StampLetterheadOptions;
|
|
31
|
+
}): Promise<number>;
|
|
32
|
+
/**
|
|
33
|
+
* Convenience: load an existing PDF, stamp letterhead on page 0, return bytes.
|
|
34
|
+
*/
|
|
35
|
+
export declare function stampLetterheadOnPdfBytes(input: {
|
|
36
|
+
pdfBytes: Uint8Array;
|
|
37
|
+
letterheadBytes?: Uint8Array | null;
|
|
38
|
+
letterheadContentType?: string | null;
|
|
39
|
+
options?: StampLetterheadOptions;
|
|
40
|
+
}): Promise<Uint8Array>;
|
|
41
|
+
//# sourceMappingURL=pdfLetterhead.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdfLetterhead.d.ts","sourceRoot":"/","sources":["lib/pdfLetterhead.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAY,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzD,MAAM,WAAW,sBAAsB;IACrC,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAqCD;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,KAAK,EAAE;IACjD,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,eAAe,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,sBAAsB,CAAC;CAClC,GAAG,OAAO,CAAC,MAAM,CAAC,CA6BlB;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAAC,KAAK,EAAE;IACrD,QAAQ,EAAE,UAAU,CAAC;IACrB,eAAe,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,sBAAsB,CAAC;CAClC,GAAG,OAAO,CAAC,UAAU,CAAC,CAgBtB"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable PDF letterhead stamp helper (pdf-lib).
|
|
3
|
+
*
|
|
4
|
+
* Callers (transcripts, fee receipts, ID cards) load school letterhead bytes
|
|
5
|
+
* and pass them here to draw a header image on the first page.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="dffa9b67-991b-5a4d-a8be-1fe64b153bf1")}catch(e){}}();
|
|
9
|
+
import { PDFDocument } from "pdf-lib";
|
|
10
|
+
function looksLikePng(bytes) {
|
|
11
|
+
return (bytes.length >= 8 &&
|
|
12
|
+
bytes[0] === 0x89 &&
|
|
13
|
+
bytes[1] === 0x50 &&
|
|
14
|
+
bytes[2] === 0x4e &&
|
|
15
|
+
bytes[3] === 0x47);
|
|
16
|
+
}
|
|
17
|
+
async function embedImage(pdfDoc, bytes, contentType) {
|
|
18
|
+
if (!bytes.length)
|
|
19
|
+
return null;
|
|
20
|
+
const type = (contentType ?? "").toLowerCase();
|
|
21
|
+
try {
|
|
22
|
+
if (type.includes("png") || (!type && looksLikePng(bytes))) {
|
|
23
|
+
return await pdfDoc.embedPng(bytes);
|
|
24
|
+
}
|
|
25
|
+
return await pdfDoc.embedJpg(bytes);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
try {
|
|
29
|
+
return await pdfDoc.embedPng(bytes);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
try {
|
|
33
|
+
return await pdfDoc.embedJpg(bytes);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Draw letterhead on `page` and return the Y coordinate below the image
|
|
43
|
+
* (content should start at or below this value). Returns the original
|
|
44
|
+
* `contentTopY` when letterhead cannot be embedded.
|
|
45
|
+
*/
|
|
46
|
+
export async function stampLetterheadOnPage(input) {
|
|
47
|
+
const { pdfDoc, page, letterheadBytes, letterheadContentType, contentTopY, options, } = input;
|
|
48
|
+
if (!letterheadBytes?.length)
|
|
49
|
+
return contentTopY;
|
|
50
|
+
const pageWidth = options?.pageWidth ?? page.getWidth();
|
|
51
|
+
const sideMargin = options?.sideMargin ?? 48;
|
|
52
|
+
const maxHeight = options?.maxHeight ?? 72;
|
|
53
|
+
const image = await embedImage(pdfDoc, letterheadBytes, letterheadContentType);
|
|
54
|
+
if (!image)
|
|
55
|
+
return contentTopY;
|
|
56
|
+
const maxWidth = pageWidth - sideMargin * 2;
|
|
57
|
+
const scale = Math.min(maxWidth / image.width, maxHeight / image.height, 1);
|
|
58
|
+
const drawWidth = image.width * scale;
|
|
59
|
+
const drawHeight = image.height * scale;
|
|
60
|
+
const x = (pageWidth - drawWidth) / 2;
|
|
61
|
+
const y = contentTopY - drawHeight;
|
|
62
|
+
page.drawImage(image, { x, y, width: drawWidth, height: drawHeight });
|
|
63
|
+
// Leave a small gap under the letterhead.
|
|
64
|
+
return y - 16;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Convenience: load an existing PDF, stamp letterhead on page 0, return bytes.
|
|
68
|
+
*/
|
|
69
|
+
export async function stampLetterheadOnPdfBytes(input) {
|
|
70
|
+
const pdfDoc = await PDFDocument.load(input.pdfBytes);
|
|
71
|
+
const page = pdfDoc.getPages()[0];
|
|
72
|
+
if (!page)
|
|
73
|
+
return input.pdfBytes;
|
|
74
|
+
const topMargin = input.options?.topMargin ?? 24;
|
|
75
|
+
await stampLetterheadOnPage({
|
|
76
|
+
pdfDoc,
|
|
77
|
+
page,
|
|
78
|
+
letterheadBytes: input.letterheadBytes,
|
|
79
|
+
letterheadContentType: input.letterheadContentType,
|
|
80
|
+
contentTopY: page.getHeight() - topMargin,
|
|
81
|
+
options: input.options,
|
|
82
|
+
});
|
|
83
|
+
return pdfDoc.save();
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=pdfLetterhead.js.map
|
|
86
|
+
//# debugId=dffa9b67-991b-5a4d-a8be-1fe64b153bf1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pdfLetterhead.js","sources":["lib/pdfLetterhead.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Reusable PDF letterhead stamp helper (pdf-lib).\n *\n * Callers (transcripts, fee receipts, ID cards) load school letterhead bytes\n * and pass them here to draw a header image on the first page.\n */\n\nimport { PDFDocument, PDFImage, PDFPage } from \"pdf-lib\";\n\nexport interface StampLetterheadOptions {\n /** Full page width (points). Defaults to A4 width. */\n pageWidth?: number;\n /** Top margin inset. Defaults to 24. */\n topMargin?: number;\n /** Max letterhead height in points. Defaults to 72. */\n maxHeight?: number;\n /** Horizontal inset from page edges. Defaults to 48. */\n sideMargin?: number;\n}\n\nfunction looksLikePng(bytes: Uint8Array): boolean {\n return (\n bytes.length >= 8 &&\n bytes[0] === 0x89 &&\n bytes[1] === 0x50 &&\n bytes[2] === 0x4e &&\n bytes[3] === 0x47\n );\n}\n\nasync function embedImage(\n pdfDoc: PDFDocument,\n bytes: Uint8Array,\n contentType?: string | null\n): Promise<PDFImage | null> {\n if (!bytes.length) return null;\n const type = (contentType ?? \"\").toLowerCase();\n try {\n if (type.includes(\"png\") || (!type && looksLikePng(bytes))) {\n return await pdfDoc.embedPng(bytes);\n }\n return await pdfDoc.embedJpg(bytes);\n } catch {\n try {\n return await pdfDoc.embedPng(bytes);\n } catch {\n try {\n return await pdfDoc.embedJpg(bytes);\n } catch {\n return null;\n }\n }\n }\n}\n\n/**\n * Draw letterhead on `page` and return the Y coordinate below the image\n * (content should start at or below this value). Returns the original\n * `contentTopY` when letterhead cannot be embedded.\n */\nexport async function stampLetterheadOnPage(input: {\n pdfDoc: PDFDocument;\n page: PDFPage;\n letterheadBytes?: Uint8Array | null;\n letterheadContentType?: string | null;\n /** Current top Y for content (page height - top margin). */\n contentTopY: number;\n options?: StampLetterheadOptions;\n}): Promise<number> {\n const {\n pdfDoc,\n page,\n letterheadBytes,\n letterheadContentType,\n contentTopY,\n options,\n } = input;\n if (!letterheadBytes?.length) return contentTopY;\n\n const pageWidth = options?.pageWidth ?? page.getWidth();\n const sideMargin = options?.sideMargin ?? 48;\n const maxHeight = options?.maxHeight ?? 72;\n\n const image = await embedImage(pdfDoc, letterheadBytes, letterheadContentType);\n if (!image) return contentTopY;\n\n const maxWidth = pageWidth - sideMargin * 2;\n const scale = Math.min(maxWidth / image.width, maxHeight / image.height, 1);\n const drawWidth = image.width * scale;\n const drawHeight = image.height * scale;\n const x = (pageWidth - drawWidth) / 2;\n const y = contentTopY - drawHeight;\n\n page.drawImage(image, { x, y, width: drawWidth, height: drawHeight });\n\n // Leave a small gap under the letterhead.\n return y - 16;\n}\n\n/**\n * Convenience: load an existing PDF, stamp letterhead on page 0, return bytes.\n */\nexport async function stampLetterheadOnPdfBytes(input: {\n pdfBytes: Uint8Array;\n letterheadBytes?: Uint8Array | null;\n letterheadContentType?: string | null;\n options?: StampLetterheadOptions;\n}): Promise<Uint8Array> {\n const pdfDoc = await PDFDocument.load(input.pdfBytes);\n const page = pdfDoc.getPages()[0];\n if (!page) return input.pdfBytes;\n\n const topMargin = input.options?.topMargin ?? 24;\n await stampLetterheadOnPage({\n pdfDoc,\n page,\n letterheadBytes: input.letterheadBytes,\n letterheadContentType: input.letterheadContentType,\n contentTopY: page.getHeight() - topMargin,\n options: input.options,\n });\n\n return pdfDoc.save();\n}\n"],"names":[],"mappings":"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","debug_id":"dffa9b67-991b-5a4d-a8be-1fe64b153bf1"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded transcript / progress-report PDF builder (pdf-lib).
|
|
3
|
+
*
|
|
4
|
+
* Variants:
|
|
5
|
+
* - `letter` — percentage + letter-grade style transcript
|
|
6
|
+
* - `level` — CDC Annex-4 style progress report (Below Basic / Basic /
|
|
7
|
+
* Proficient / Advanced) when grading boundaries are level-based
|
|
8
|
+
*
|
|
9
|
+
* School branding uses name + optional logo. Letterhead is not in the School
|
|
10
|
+
* schema yet — when added, callers can pass `letterheadBytes` without blocking.
|
|
11
|
+
*/
|
|
12
|
+
export type TranscriptReportVariant = "letter" | "level";
|
|
13
|
+
export interface TranscriptClassRow {
|
|
14
|
+
className: string;
|
|
15
|
+
subject: string;
|
|
16
|
+
section: string;
|
|
17
|
+
percentage: number | null;
|
|
18
|
+
gradeLabel: string | null;
|
|
19
|
+
categorySummaries: {
|
|
20
|
+
name: string;
|
|
21
|
+
average: number | null;
|
|
22
|
+
}[];
|
|
23
|
+
}
|
|
24
|
+
export interface TranscriptAttendanceSummary {
|
|
25
|
+
present: number;
|
|
26
|
+
late: number;
|
|
27
|
+
absent: number;
|
|
28
|
+
total: number;
|
|
29
|
+
/** (present + late) / total * 100, or null when total is 0. */
|
|
30
|
+
percent: number | null;
|
|
31
|
+
}
|
|
32
|
+
export interface TranscriptPdfInput {
|
|
33
|
+
schoolName: string;
|
|
34
|
+
studentName: string;
|
|
35
|
+
studentUsername?: string | null;
|
|
36
|
+
/** Free-text school year (no AcademicTerm model yet — see C1). */
|
|
37
|
+
schoolYear?: string | null;
|
|
38
|
+
periodLabel?: string | null;
|
|
39
|
+
variant: TranscriptReportVariant;
|
|
40
|
+
classes: TranscriptClassRow[];
|
|
41
|
+
attendance: TranscriptAttendanceSummary;
|
|
42
|
+
logoBytes?: Uint8Array | null;
|
|
43
|
+
logoContentType?: string | null;
|
|
44
|
+
/** Optional future letterhead image; ignored when null/undefined. */
|
|
45
|
+
letterheadBytes?: Uint8Array | null;
|
|
46
|
+
letterheadContentType?: string | null;
|
|
47
|
+
generatedAt?: Date;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Detects CDC Annex-4 style level boundaries from a GradingBoundary
|
|
51
|
+
* `structured` JSON payload.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isLevelBasedBoundary(structured: string | null | undefined): boolean;
|
|
54
|
+
export declare function buildTranscriptPdf(input: TranscriptPdfInput): Promise<Uint8Array>;
|
|
55
|
+
//# sourceMappingURL=transcriptPdf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcriptPdf.d.ts","sourceRoot":"/","sources":["lib/transcriptPdf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,MAAM,MAAM,uBAAuB,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEzD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iBAAiB,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,EAAE,CAAC;CAC/D;AAED,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,uBAAuB,CAAC;IACjC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,UAAU,EAAE,2BAA2B,CAAC;IACxC,SAAS,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,qEAAqE;IACrE,eAAe,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,WAAW,CAAC,EAAE,IAAI,CAAC;CACpB;AA+GD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAoBnF;AAED,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,UAAU,CAAC,CA8JrB"}
|