import { formatOrgNumber } from '@/lib/utils'
import { Document, Page, StyleSheet, Text, View } from '@react-pdf/renderer'
import {
BEHANDLINGSHISTORIK_CATEGORIES,
BEHANDLINGSHISTORIK_CATEGORY_LABELS,
type BehandlingshistorikEvent,
type BehandlingshistorikReport,
} from '@/lib/reports/behandlingshistorik-types'
import { formatStockholmTimestamp } from '@/lib/reports/behandlingshistorik'
/**
* Behandlingshistorik as a printable document (BFL 5 kap. 11 §, BFNAR 2013:2
* punkt 9.16). Two sections in the order the law lists them in reverse of how
* a reader scans: the system changes first (short, the context), then the
* bokföringsposter in registreringsordning (long, the body).
*
* Layout rules shared with the other report PDFs: Helvetica/Courier (bundled
* standard fonts, so no Font.register), header + footer + table header
* `fixed` so they repeat on every page, every row `wrap={false}` so a row is
* never split across pages, and no `break` props (they deadlock multi-page
* renders in @react-pdf/renderer 4). Landscape: the details column needs the
* width.
*/
const INK = '#1a1a1a'
const MUTED = '#666'
const HAIRLINE = '#d4d4d4'
const styles = StyleSheet.create({
page: {
paddingTop: 36,
paddingHorizontal: 36,
paddingBottom: 54,
fontSize: 8.5,
fontFamily: 'Helvetica',
color: INK,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 10,
paddingBottom: 10,
borderBottomWidth: 1,
borderBottomColor: HAIRLINE,
},
titleBlock: { flex: 1 },
title: { fontSize: 18, fontWeight: 'bold', marginBottom: 3 },
subtitle: { fontSize: 9.5, color: '#333', marginBottom: 2 },
legal: { fontSize: 8, color: MUTED },
companyInfo: { textAlign: 'right' },
companyName: { fontSize: 10, fontWeight: 'bold', marginBottom: 2 },
companyMeta: { fontSize: 8.5, color: MUTED },
meta: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: 10,
},
metaItem: { width: '25%', paddingRight: 10, marginBottom: 4 },
metaLabel: { fontSize: 7, color: MUTED, textTransform: 'uppercase', letterSpacing: 0.4 },
metaValue: { fontSize: 9 },
summary: { fontSize: 8.5, color: '#333', marginBottom: 10 },
tableHeader: {
flexDirection: 'row',
paddingVertical: 4,
borderBottomWidth: 0.5,
borderBottomColor: INK,
marginBottom: 2,
},
tableHeaderText: {
fontSize: 7.5,
fontWeight: 'bold',
color: '#444',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
sectionHeading: {
fontSize: 10.5,
fontWeight: 'bold',
marginTop: 12,
marginBottom: 4,
paddingBottom: 3,
borderBottomWidth: 0.5,
borderBottomColor: '#888',
},
sectionNote: { fontSize: 8, color: MUTED, marginBottom: 4 },
row: {
flexDirection: 'row',
paddingVertical: 2.5,
borderBottomWidth: 0.5,
borderBottomColor: '#ececec',
},
// Column widths live in one place so the header and the rows line up; the
// time column gets Courier only in the body (the header stays Helvetica).
hdrTime: { width: 100, paddingRight: 6 },
hdrEvent: { width: 190, paddingRight: 8 },
hdrActor: { width: 140, paddingRight: 8 },
hdrDetails: { flex: 1 },
colTime: { width: 100, fontFamily: 'Courier', fontSize: 7.5, color: MUTED, paddingRight: 6 },
colEvent: { width: 190, paddingRight: 8 },
colActor: { width: 140, paddingRight: 8, color: '#333' },
colDetails: { flex: 1 },
eventLabel: { fontSize: 8.5 },
objectLabel: { fontFamily: 'Courier', fontSize: 7.5, color: MUTED, marginTop: 1 },
detail: { fontSize: 7.5, color: '#444', marginBottom: 1 },
empty: { fontSize: 8.5, color: MUTED, fontStyle: 'italic', paddingVertical: 6 },
footer: {
position: 'absolute',
bottom: 22,
left: 36,
right: 36,
borderTopWidth: 0.5,
borderTopColor: HAIRLINE,
paddingTop: 5,
flexDirection: 'row',
justifyContent: 'space-between',
},
footerText: { fontSize: 7.5, color: '#888' },
})
/**
* The bundled Helvetica/Courier AFM fonts only carry WinAnsi glyphs: an arrow
* or a true minus would be dropped silently. Map them to ASCII for print.
*/
export function pdfText(value: string): string {
return value.replace(/→/g, '->').replace(/−/g, '-')
}
function EventRow({ event }: { event: BehandlingshistorikEvent }) {
return (
{formatStockholmTimestamp(event.occurred_at)}
{pdfText(event.event)}
{event.object ? {pdfText(event.object)} : null}
{pdfText(event.actor.label)}
{event.details.length > 0 ? (
// One wrapped paragraph rather than one Text per line: half the
// layout nodes (render time) and roughly twice the rows per page.
{pdfText(event.details.join(' · '))}
) : null}
)
}
function Section({ title, note, events }: { title: string; note: string; events: BehandlingshistorikEvent[] }) {
return (
{title}
{note}
{events.length === 0 ? (
Inga händelser i urvalet.
) : (
events.map((event) => )
)}
)
}
export interface BehandlingshistorikPDFProps {
report: BehandlingshistorikReport
}
export function BehandlingshistorikPDF({ report }: BehandlingshistorikPDFProps) {
const systemEvents = report.events.filter((e) => e.category !== 'verifikation')
const voucherEvents = report.events.filter((e) => e.category === 'verifikation')
const scope =
report.mode === 'fiscal_year'
? 'Hela räkenskapsåret'
: `${report.range.from} till ${report.range.to}`
const categoryFilter =
report.category_filter && report.category_filter.length > 0
? report.category_filter.map((c) => BEHANDLINGSHISTORIK_CATEGORY_LABELS[c]).join(', ')
: null
const summary = BEHANDLINGSHISTORIK_CATEGORIES.filter((c) => report.by_category[c] > 0)
.map((c) => `${BEHANDLINGSHISTORIK_CATEGORY_LABELS[c]} ${report.by_category[c]}`)
.join(' · ')
const generated = formatStockholmTimestamp(report.generated_at)
return (
Behandlingshistorik
{report.period.name} ({report.period.start} till {report.period.end}) · Urval: {scope}
{categoryFilter ? ` · Kategori: ${categoryFilter}` : ''}
BFL 5 kap. 11 § · BFNAR 2013:2 punkt 9.16 · Tider i Europe/Stockholm
{report.company.name ? {report.company.name} : null}
{report.company.org_number ? (
Org.nr: {formatOrgNumber(report.company.org_number)}
) : null}
Genererad
{generated}
Programversion
{report.app_version ?? 'okänd'}
Antal händelser
{String(report.total_events)}
Källor
Verifikationer, oföränderlig ändringslogg, rättelselogg, importer
{summary ? {summary} : null}
Tidpunkt
Händelse
Utförd av
Detaljer
{report.company.name}
{report.company.org_number ? ` · ${formatOrgNumber(report.company.org_number)}` : ''}
{' · Behandlingshistorik'}
`Genererad ${generated} · Sida ${pageNumber} av ${totalPages}`}
/>
)
}