Skip to content

Commit f458114

Browse files
committed
feat(tatakforms): implement dynamic tatakform pdf generation
1 parent 3815c82 commit f458114

File tree

18 files changed

+218
-10
lines changed

18 files changed

+218
-10
lines changed

assets/tatakform/colleges/cas.png

46.3 KB
Loading

assets/tatakform/colleges/cba.png

58.4 KB
Loading

assets/tatakform/colleges/cca.png

46.1 KB
Loading

assets/tatakform/colleges/ccj.png

64.6 KB
Loading

assets/tatakform/colleges/ccs.png

20.6 KB
Loading

assets/tatakform/colleges/chm.png

31.1 KB
Loading

assets/tatakform/colleges/coe.png

27.6 KB
Loading

assets/tatakform/colleges/csw.png

18.1 KB
Loading

assets/tatakform/colleges/cte.png

33.6 KB
Loading

assets/tatakform/ucdays2024.pdf

183 KB
Binary file not shown.

bun.lockb

1.48 KB
Binary file not shown.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"mariadb": "^3.3.0",
1818
"nodemailer": "^6.9.13",
1919
"p-queue": "^8.0.1",
20+
"pdf-lib": "^1.17.1",
2021
"pug": "^3.0.2",
2122
"xlsx-template": "^1.4.4"
2223
},

src/api/tatakforms/attendance.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { status501 } from "../../routes";
55
import TatakFormStudent from "../../db/models/tatakform/student";
66
import response from "../../utils/response";
77
import Strings from "../../config/strings";
8-
import Attendance from "../../db/models/tatakform/attendance";
8+
import TatakFormAttendance from "../../db/models/tatakform/attendance";
99
import Tatakform from "../../db/models/tatakform";
10+
import { setHeader } from "../../utils/security";
1011

1112
/**
1213
* Tatakform Attendance API
@@ -42,7 +43,7 @@ async function postAttendance(context: ElysiaContext) {
4243
const student = await TatakFormStudent.getByStudentId(context.body.student_id);
4344

4445
if (tatak_event) {
45-
await Attendance.attendStudent(context.body.student_id, tatak_event);
46+
await TatakFormAttendance.attendStudent(context.body.student_id, tatak_event);
4647
}
4748

4849
return response.success("Attended successfully", { student_id: context.body.student_id });
@@ -70,8 +71,9 @@ async function postAttendance(context: ElysiaContext) {
7071
*
7172
* GET /tatakforms/attendance/:slug
7273
* Fetches the attedance history of a student in an event
73-
* @param context
7474
*
75+
* GET /tatakforms/attendance/:slug/download
76+
* Download tatakform attendance
7577
*/
7678
async function getAttendance(context: ElysiaContext) {
7779
try {
@@ -80,17 +82,29 @@ async function getAttendance(context: ElysiaContext) {
8082

8183
// If slug is specified
8284
if (slug) {
85+
// If path ends with "download"
86+
if (context.path.endsWith("download")) {
87+
// Generate PDF
88+
const image = await Tatakform.generatePDF(context.user?.student_id, slug);
89+
// Set content disposition
90+
setHeader(context, 'Content-Disposition', `attachment; filename="${image.name}"`);
91+
// Set response content type
92+
setHeader(context, 'Content-Type', 'application/pdf');
93+
// Return image
94+
return image;
95+
}
96+
8397
// Get event by slug
8498
const tatakform = await Tatakform.getBySlug(slug);
8599
// Get attendance history by event
86-
const attendance = await Attendance.getAttendanceByEvent(context.user?.student_id, tatakform.id);
100+
const attendance = await TatakFormAttendance.getAttendanceByEvent(context.user?.student_id, tatakform.id);
87101

88102
// Return response
89103
return response.success("Fetch Successful", attendance);
90104
}
91105

92106
// Otherwise, get all attendance history
93-
const history = await Attendance.getAttendance(context.user?.student_id);
107+
const history = await TatakFormAttendance.getAttendance(context.user?.student_id);
94108
return response.success("Fetch Successful", history);
95109
}
96110

@@ -123,7 +137,7 @@ async function getAllStudentsAttended(context: ElysiaContext) {
123137
// If event id is specified
124138
if (eventId) {
125139
// Get all students attended by event and college
126-
const attendance = await Attendance.getStudentsAttendedByEventAndCollege(eventId, context.user.college_id);
140+
const attendance = await TatakFormAttendance.getStudentsAttendedByEventAndCollege(eventId, context.user.college_id);
127141
return response.success("Success", attendance)
128142
}
129143
}

src/db/models/college.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,49 @@ class College {
8686
}
8787
});
8888
}
89+
90+
/**
91+
* Get college by acronym
92+
* @param acronym
93+
*/
94+
public static getByCourseId(courseId: number): Promise<CollegeModel> {
95+
return new Promise(async (resolve, reject) => {
96+
// Get database instance
97+
const db = Database.getInstance();
98+
99+
try {
100+
101+
// Get course
102+
const courses = await db.query<CourseModel[]>(`SELECT * FROM colleges_courses WHERE id = ? LIMIT 1`, [ courseId ]);
103+
104+
// If no results
105+
if (courses.length === 0) {
106+
return reject(`Course with id #${courseId} not found`);
107+
}
108+
109+
// Set the courses to the college
110+
const college = await db.query<CollegeModel[]>(`SELECT * FROM colleges WHERE id = ? LIMIT 1`, [courses[0].college_id]);
111+
112+
// If no results
113+
if (college.length === 0) {
114+
Log.e(`College with course id #${courseId} not found`);
115+
return reject(`College with course id #${courseId} not found`);
116+
}
117+
118+
// Set the courses to the college
119+
college[0].courses = courses;
120+
121+
// Return the college
122+
resolve(college[0]);
123+
}
124+
125+
// Log error and reject promise
126+
catch (e) {
127+
Log.e(e);
128+
reject(ErrorTypes.DB_ERROR);
129+
}
130+
});
131+
}
89132
}
90133

91134
export default College;

src/db/models/tatakform.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { ErrorTypes } from "../../types/enums";
22
import { TatakformModel } from "../../types/models";
3+
import { PDFDocument, StandardFonts } from 'pdf-lib';
4+
import { getReadableDate } from "../../utils/date";
5+
import { join } from "path";
36

7+
import TatakFormAttendance from "./tatakform/attendance";
8+
import TatakFormStudent from "./tatakform/student";
49
import Log from "../../utils/log";
10+
import College from "../../db/models/college";
511
import Database from "../";
612

713
/**
@@ -72,6 +78,149 @@ class Tatakform {
7278
}
7379
});
7480
}
81+
82+
/**
83+
* Generate PDF file
84+
*/
85+
public static generatePDF(studentId: string, slug: string): Promise<File> {
86+
return new Promise(async (resolve, reject) => {
87+
// Get event by slug
88+
const tatakform = await Tatakform.getBySlug(slug);
89+
// Get student
90+
const student = await TatakFormStudent.getByStudentId(studentId);
91+
// Get attendance history by event
92+
const attendance = await TatakFormAttendance.getAttendanceByEvent(studentId, tatakform.id);
93+
// Get college
94+
const college = await College.getByCourseId(student.course_id);
95+
96+
try {
97+
// Get tatakform template pdf file
98+
const template = Bun.file(join(Bun.main, `../../assets/tatakform/${slug}.pdf`));
99+
// Get logo file
100+
const logoFile = Bun.file(join(Bun.main, `../../assets/tatakform/colleges/${college.acronym.toLowerCase()}.png`));
101+
// Load pdf
102+
const pdf = await PDFDocument.load(await template.arrayBuffer(), { updateMetadata: false });
103+
// Load logo
104+
const logo = await pdf.embedPng(await logoFile.arrayBuffer());
105+
// Font
106+
const font = await pdf.embedFont(StandardFonts.Courier);
107+
// Get first page
108+
const page = pdf.getPages()[0];
109+
110+
// Set metadata
111+
pdf.setTitle(`Tatakform - ${college.acronym} - ${student.first_name} ${student.last_name} - ${studentId}`);
112+
pdf.setAuthor("UC Main CSP-S");
113+
pdf.setCreationDate(new Date());
114+
pdf.setModificationDate(new Date());
115+
pdf.setSubject(slug);
116+
117+
// Full name
118+
page.drawText(`${student.first_name} ${student.last_name}`, {
119+
x: 205, y: page.getHeight() - 365, size: 28, font
120+
});
121+
122+
// Course and year
123+
page.drawText(`${college.courses?.[0].acronym} - ${student.year_level}`, {
124+
x: 1420, y: page.getHeight() - 365, size: 28, font
125+
});
126+
127+
// Date
128+
page.drawText(getReadableDate(new Date()), {
129+
x: 205, y: page.getHeight() - 416, size: 28, font
130+
});
131+
132+
// Department
133+
page.drawText(college.name, {
134+
x: 1305, y: page.getHeight() - 416, size: 28, font
135+
});
136+
137+
// Note
138+
page.drawText("This tatakform was generated at https://ucmncsps.org/tatakforms", {
139+
x: 170, y: 80, size: 20, font
140+
});
141+
142+
// TODO: Make this more dynamic
143+
if (attendance !== null) {
144+
// Day 1 AM
145+
if (attendance.day1_am) {
146+
page.drawImage(logo, {
147+
x: 250,
148+
y: page.getHeight() - 790,
149+
width: 250,
150+
height: 250,
151+
opacity: 0.7
152+
});
153+
}
154+
155+
// Day 1 PM
156+
if (attendance.day1_pm) {
157+
page.drawImage(logo, {
158+
x: 250,
159+
y: page.getHeight() - 1090,
160+
width: 250,
161+
height: 250,
162+
opacity: 0.7
163+
});
164+
}
165+
166+
// Day 2 AM
167+
if (attendance.day2_am) {
168+
page.drawImage(logo, {
169+
x: 875,
170+
y: page.getHeight() - 790,
171+
width: 250,
172+
height: 250,
173+
opacity: 0.7
174+
});
175+
}
176+
177+
// Day 2 PM
178+
if (attendance.day2_pm) {
179+
page.drawImage(logo, {
180+
x: 875,
181+
y: page.getHeight() - 1090,
182+
width: 250,
183+
height: 250,
184+
opacity: 0.7
185+
});
186+
}
187+
188+
// Day 3 AM
189+
if (attendance.day3_am) {
190+
page.drawImage(logo, {
191+
x: 1525,
192+
y: page.getHeight() - 790,
193+
width: 250,
194+
height: 250,
195+
opacity: 0.7
196+
});
197+
}
198+
199+
// Day 3 PM
200+
if (attendance.day3_pm) {
201+
page.drawImage(logo, {
202+
x: 1525,
203+
y: page.getHeight() - 1090,
204+
width: 250,
205+
height: 250,
206+
opacity: 0.7
207+
});
208+
}
209+
}
210+
211+
// Save pdf
212+
const buffer = await pdf.save();
213+
// Resolve promise
214+
resolve(new File([buffer], `tatakform_${college.acronym.toLowerCase()}_${studentId}.pdf`, { type: "application/pdf" }));
215+
}
216+
217+
// Log error
218+
catch (error) {
219+
Log.e(error);
220+
reject(error);
221+
}
222+
});
223+
}
75224
}
76225

77226
export default Tatakform;

src/db/models/tatakform/attendance.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class TatakFormAttendance {
105105
/**
106106
* Get attendance history of student
107107
*/
108-
public static getAttendanceByEvent(studentId: string, eventId: number) {
108+
public static getAttendanceByEvent(studentId: string, eventId: number): Promise<AttendanceModel | null> {
109109
return new Promise(async (resolve, reject) => {
110110
// Get database instance
111111
const db = Database.getInstance();

src/routes/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ const routes: AppRoutes[] = [
3939
{ path: "/tatakforms/admin/login", methods: ["POST", "OPTIONS"], handler: tatakformAdminLogin },
4040
{ path: "/tatakforms/:slug", methods: ["GET"], handler: tatakforms },
4141
{ path: "/tatakforms", methods: ["GET"], handler: tatakforms },
42-
{ path: "/tatakforms/attendance/:slug" , methods: ['POST','GET'], handler: tatakformAttendance, auth: {POST: AuthType.COLLEGE_ADMIN} },
4342
{ path: "/tatakforms/attendance/" , methods: ['GET'], handler: tatakformAttendance, auth: {GET: AuthType.UNIV_ACCOUNT} },
4443
{ path: "/tatakforms/attendance/event/:eventId" , methods: ['GET'], handler: tatakformAttendance, auth: {GET: AuthType.COLLEGE_ADMIN}},
44+
{ path: "/tatakforms/attendance/:slug" , methods: ['POST','GET'], handler: tatakformAttendance, auth: {POST: AuthType.COLLEGE_ADMIN} },
45+
{ path: "/tatakforms/attendance/:slug/download" , methods: ['GET'], handler: tatakformAttendance, auth: {GET: AuthType.UNIV_ACCOUNT} },
4546

4647
{ path: "/announcements/:id", methods: ["PUT", "DELETE", "OPTIONS"], handler: announcements, auth: { PUT: AuthType.ADMIN, DELETE: AuthType.ADMIN }},
4748
{ path: "/announcements", methods: ["GET","POST"], handler: announcements, auth: { POST: AuthType.ADMIN }},

src/types/models.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,10 @@ export type StudentModel = {
156156
export type UnivStudentModel = {
157157
id: number,
158158
student_id: string,
159-
course_id: string,
159+
course_id: number,
160160
last_name: string,
161161
first_name: string,
162-
year_level: string,
162+
year_level: number,
163163
email_address: string,
164164
password?: string,
165165
date_stamp: string,

0 commit comments

Comments
 (0)