import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer' import { getBranding } from '@/lib/branding/service' /** * Pay slip PDF template (Lönespecifikation). * * Legally required per BFL as räkenskapsinformation/underlag. * Subject to 7-year retention per BFL 7 kap. * * Contents: * - Company + employee identification * - Line items (salary, absence, benefits, deductions) * - Gross → Tax → Net summary with tax table reference * - Employer cost breakdown (avgifter, vacation accrual): transparency feature * - YTD totals (cumulative year-to-date) * - Calculation breakdown (optional detail showing every formula step) */ const styles = StyleSheet.create({ page: { fontFamily: 'Helvetica', fontSize: 9, padding: 40, color: '#1a1a1a', }, header: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 20, }, title: { fontSize: 16, fontFamily: 'Helvetica-Bold', marginBottom: 4, }, subtitle: { fontSize: 10, color: '#666', }, companyName: { fontSize: 12, fontFamily: 'Helvetica-Bold', }, companyInfo: { fontSize: 8, color: '#666', marginTop: 2, }, section: { marginBottom: 14, }, sectionTitle: { fontSize: 10, fontFamily: 'Helvetica-Bold', marginBottom: 6, paddingBottom: 3, borderBottomWidth: 1, borderBottomColor: '#e0e0e0', }, row: { flexDirection: 'row', paddingVertical: 3, }, rowAlt: { flexDirection: 'row', paddingVertical: 3, backgroundColor: '#f8f8f8', }, headerRow: { flexDirection: 'row', paddingVertical: 4, borderBottomWidth: 1, borderBottomColor: '#ccc', marginBottom: 2, }, colDesc: { flex: 3.5 }, colQty: { flex: 1, textAlign: 'right' as const }, colRate: { flex: 1.5, textAlign: 'right' as const }, colAmount: { flex: 1.5, textAlign: 'right' as const }, headerText: { fontSize: 8, fontFamily: 'Helvetica-Bold', color: '#666', textTransform: 'uppercase' as const, }, summaryRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 3, }, summaryLabel: { fontSize: 9, color: '#444', }, summaryValue: { fontSize: 9, fontFamily: 'Helvetica-Bold', textAlign: 'right' as const, }, totalRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 6, borderTopWidth: 2, borderTopColor: '#1a1a1a', marginTop: 4, }, totalLabel: { fontSize: 11, fontFamily: 'Helvetica-Bold', }, totalValue: { fontSize: 11, fontFamily: 'Helvetica-Bold', textAlign: 'right' as const, }, infoGrid: { flexDirection: 'row', gap: 20, marginBottom: 14, }, infoColumn: { flex: 1, }, infoLabel: { fontSize: 7, color: '#999', textTransform: 'uppercase' as const, marginBottom: 1, }, infoValue: { fontSize: 9, marginBottom: 6, }, breakdownSection: { marginTop: 10, padding: 10, backgroundColor: '#f5f5f5', borderRadius: 3, }, breakdownTitle: { fontSize: 9, fontFamily: 'Helvetica-Bold', marginBottom: 6, }, breakdownRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 1.5, }, breakdownLabel: { fontSize: 7.5, color: '#555', flex: 3, }, breakdownFormula: { fontSize: 7, color: '#888', flex: 3, fontFamily: 'Courier', }, breakdownValue: { fontSize: 7.5, textAlign: 'right' as const, flex: 1.5, }, footer: { position: 'absolute' as const, bottom: 30, left: 40, right: 40, borderTopWidth: 1, borderTopColor: '#e0e0e0', paddingTop: 6, fontSize: 7, color: '#999', textAlign: 'center' as const, }, ytdSection: { marginTop: 10, }, ytdRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 2, }, ytdLabel: { fontSize: 8, color: '#666', }, ytdValue: { fontSize: 8, color: '#666', textAlign: 'right' as const, }, }) export interface PayslipData { // Company companyName: string companyOrgNumber: string companyAddress?: string // Employee employeeName: string personnummerMasked: string // YYYYMMDD-XXXX employmentType: string // Period periodYear: number periodMonth: number paymentDate: string // Line items lineItems: PayslipLineItem[] // Summary grossSalary: number taxWithheld: number netSalary: number taxReference: string // e.g. "Tabell 33, kolumn 1" // Employer cost (transparency feature) avgifterRate: number avgifterAmount: number vacationAccrual: number vacationAccrualAvgifter: number totalEmployerCost: number // YTD ytdGross: number ytdTax: number ytdNet: number // Bank bankAccount?: string // masked // Calculation breakdown (optional) breakdownSteps?: { label: string; formula: string; output: number }[] } export interface PayslipLineItem { description: string quantity?: number unitPrice?: number amount: number } function fmt(amount: number): string { return new Intl.NumberFormat('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount) } const MONTH_NAMES = [ 'januari', 'februari', 'mars', 'april', 'maj', 'juni', 'juli', 'augusti', 'september', 'oktober', 'november', 'december', ] export function PayslipPDF({ data }: { data: PayslipData }) { const periodLabel = `${MONTH_NAMES[data.periodMonth - 1]} ${data.periodYear}` return ( {/* Header */} Lönespecifikation {periodLabel} {data.companyName} Org.nr {data.companyOrgNumber} {data.companyAddress && {data.companyAddress}} {/* Employee + Period info */} Anställd {data.employeeName} Personnummer {data.personnummerMasked} Period {periodLabel} Utbetalningsdag {data.paymentDate} Skattetabell {data.taxReference} {data.bankAccount && ( <> Bankkonto {data.bankAccount} )} {/* Line items table */} Lönespecifikation Beskrivning Antal á-pris Belopp {data.lineItems.map((item, i) => ( {item.description} {item.quantity != null ? item.quantity : ''} {item.unitPrice != null ? fmt(item.unitPrice) : ''} {fmt(item.amount)} ))} {/* Summary: Gross → Tax → Net */} Bruttolön {fmt(data.grossSalary)} Preliminär skatt ({data.taxReference}) −{fmt(data.taxWithheld)} Nettolön (utbetalas) {fmt(data.netSalary)} {/* Employer cost (transparency feature: our differentiator) */} Arbetsgivarkostnad Arbetsgivaravgifter ({(data.avgifterRate * 100).toFixed(2)}%) {fmt(data.avgifterAmount)} Semesteravsättning {fmt(data.vacationAccrual)} Sociala avgifter på semester {fmt(data.vacationAccrualAvgifter)} Total arbetsgivarkostnad {fmt(data.totalEmployerCost)} {/* YTD */} Ackumulerat {data.periodYear} Brutto {fmt(data.ytdGross)} Skatt {fmt(data.ytdTax)} Netto {fmt(data.ytdNet)} {/* Calculation breakdown (optional detail page) */} {data.breakdownSteps && data.breakdownSteps.length > 0 && ( Beräkningsunderlag {data.breakdownSteps.map((step, i) => ( {step.label} {step.formula} {fmt(step.output)} ))} )} {/* Footer */} {data.companyName} · Org.nr {data.companyOrgNumber} · Lönespecifikation {periodLabel} · Genererad av {getBranding().appName.toLowerCase()} ) }