feat: JKKP 6/7 PDF generation via pdf-lib, download from incident detail
Also excludes node_modules.nosync from vitest test discovery to fix pre-existing bleed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
This commit is contained in:
+104
@@ -0,0 +1,104 @@
|
||||
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'
|
||||
|
||||
export type JkkpIncident = {
|
||||
reference_no: string | null
|
||||
incident_type: string
|
||||
description: string
|
||||
reported_at: string
|
||||
severity: number | null
|
||||
is_fatality: boolean
|
||||
is_serious_bodily_injury: boolean
|
||||
is_dangerous_occurrence: boolean
|
||||
lost_days: number | null
|
||||
site_name: string
|
||||
reporter_name: string
|
||||
}
|
||||
|
||||
type EmbeddedFont = Awaited<ReturnType<PDFDocument['embedFont']>>
|
||||
type Page = ReturnType<PDFDocument['addPage']>
|
||||
|
||||
async function createBaseDoc(title: string): Promise<{
|
||||
doc: PDFDocument
|
||||
page: Page
|
||||
font: EmbeddedFont
|
||||
boldFont: EmbeddedFont
|
||||
y: { value: number }
|
||||
}> {
|
||||
const doc = await PDFDocument.create()
|
||||
const page = doc.addPage([595, 842]) // A4
|
||||
const font = await doc.embedFont(StandardFonts.Helvetica)
|
||||
const boldFont = await doc.embedFont(StandardFonts.HelveticaBold)
|
||||
const y = { value: 800 }
|
||||
|
||||
page.drawText(title, { x: 50, y: y.value, size: 14, font: boldFont, color: rgb(0, 0, 0) })
|
||||
y.value -= 8
|
||||
page.drawLine({ start: { x: 50, y: y.value }, end: { x: 545, y: y.value }, thickness: 1, color: rgb(0, 0, 0) })
|
||||
y.value -= 20
|
||||
|
||||
return { doc, page, font, boldFont, y }
|
||||
}
|
||||
|
||||
function drawField(
|
||||
page: Page,
|
||||
label: string,
|
||||
value: string,
|
||||
font: EmbeddedFont,
|
||||
boldFont: EmbeddedFont,
|
||||
y: { value: number }
|
||||
) {
|
||||
page.drawText(label + ':', { x: 50, y: y.value, size: 9, font: boldFont, color: rgb(0.3, 0.3, 0.3) })
|
||||
const lines = value.length > 80 ? [value.slice(0, 80), value.slice(80, 160)] : [value]
|
||||
for (const line of lines) {
|
||||
y.value -= 14
|
||||
page.drawText(line || '—', { x: 50, y: y.value, size: 10, font, color: rgb(0, 0, 0) })
|
||||
}
|
||||
y.value -= 8
|
||||
}
|
||||
|
||||
export async function buildJkkp6Pdf(incident: JkkpIncident): Promise<Uint8Array> {
|
||||
const { doc, page, font, boldFont, y } = await createBaseDoc('BORANG JKKP 6 — Notis Kemalangan / Kejadian Berbahaya')
|
||||
|
||||
const reportedDate = new Date(incident.reported_at).toLocaleDateString('en-MY', { timeZone: 'Asia/Kuala_Lumpur' })
|
||||
|
||||
drawField(page, 'Rujukan / Reference', incident.reference_no ?? '—', font, boldFont, y)
|
||||
drawField(page, 'Tapak / Site', incident.site_name, font, boldFont, y)
|
||||
drawField(page, 'Tarikh Laporan / Date Reported', reportedDate, font, boldFont, y)
|
||||
drawField(page, 'Jenis Insiden / Incident Type', incident.incident_type.replace(/_/g, ' '), font, boldFont, y)
|
||||
drawField(page, 'Dilaporkan Oleh / Reported By', incident.reporter_name, font, boldFont, y)
|
||||
drawField(page, 'Penerangan / Description', incident.description, font, boldFont, y)
|
||||
drawField(page, 'Kematian / Fatality', incident.is_fatality ? 'Yes' : 'No', font, boldFont, y)
|
||||
drawField(page, 'Kecederaan Serius / Serious Bodily Injury', incident.is_serious_bodily_injury ? 'Yes' : 'No', font, boldFont, y)
|
||||
drawField(page, 'Kejadian Berbahaya / Dangerous Occurrence', incident.is_dangerous_occurrence ? 'Yes' : 'No', font, boldFont, y)
|
||||
drawField(page, 'Hari Hilang Kerja / Lost Days', incident.lost_days !== null ? String(incident.lost_days) : '—', font, boldFont, y)
|
||||
|
||||
y.value -= 20
|
||||
page.drawText('Nota: Borang ini adalah draf yang dijana secara automatik. Sila semak sebelum dikemukakan ke DOSH.',
|
||||
{ x: 50, y: y.value, size: 8, font, color: rgb(0.5, 0.5, 0.5) })
|
||||
|
||||
return doc.save()
|
||||
}
|
||||
|
||||
export async function buildJkkp7Pdf(incident: JkkpIncident): Promise<Uint8Array> {
|
||||
const { doc, page, font, boldFont, y } = await createBaseDoc('BORANG JKKP 7 — Laporan Siasatan Kemalangan')
|
||||
|
||||
const reportedDate = new Date(incident.reported_at).toLocaleDateString('en-MY', { timeZone: 'Asia/Kuala_Lumpur' })
|
||||
|
||||
drawField(page, 'Rujukan / Reference', incident.reference_no ?? '—', font, boldFont, y)
|
||||
drawField(page, 'Tapak / Site', incident.site_name, font, boldFont, y)
|
||||
drawField(page, 'Tarikh Kemalangan / Incident Date', reportedDate, font, boldFont, y)
|
||||
drawField(page, 'Jenis Insiden / Incident Type', incident.incident_type.replace(/_/g, ' '), font, boldFont, y)
|
||||
drawField(page, 'Penerangan Kemalangan / Incident Description', incident.description, font, boldFont, y)
|
||||
drawField(page, 'Keterukan / Severity', incident.severity !== null ? String(incident.severity) + ' / 5' : '—', font, boldFont, y)
|
||||
drawField(page, 'Hari Hilang Kerja / Lost Days', incident.lost_days !== null ? String(incident.lost_days) : '—', font, boldFont, y)
|
||||
|
||||
y.value -= 20
|
||||
page.drawText('Rumusan Punca Asas / Root Cause Summary:', { x: 50, y: y.value, size: 9, font: boldFont, color: rgb(0.3, 0.3, 0.3) })
|
||||
y.value -= 14
|
||||
page.drawText('[To be completed by HSE investigator]', { x: 50, y: y.value, size: 10, font, color: rgb(0.6, 0.6, 0.6) })
|
||||
|
||||
y.value -= 30
|
||||
page.drawText('Nota: Borang ini adalah draf yang dijana secara automatik. Sila semak sebelum dikemukakan ke DOSH.',
|
||||
{ x: 50, y: y.value, size: 8, font, color: rgb(0.5, 0.5, 0.5) })
|
||||
|
||||
return doc.save()
|
||||
}
|
||||
Reference in New Issue
Block a user