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
105 lines
4.7 KiB
TypeScript
105 lines
4.7 KiB
TypeScript
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()
|
|
}
|