import { Document, Page, Text, View, StyleSheet, } from '@react-pdf/renderer' import type { CompanySettings } from '@/types' const styles = StyleSheet.create({ page: { paddingTop: 40, paddingHorizontal: 40, paddingBottom: 60, fontSize: 9, fontFamily: 'Helvetica', }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 20, paddingBottom: 12, borderBottomWidth: 1, borderBottomColor: '#d4d4d4', }, titleBlock: { flex: 1, }, title: { fontSize: 20, fontWeight: 'bold', color: '#1a1a1a', marginBottom: 4, }, subtitle: { fontSize: 11, color: '#333', marginBottom: 2, }, asOf: { fontSize: 10, color: '#666', }, companyInfo: { textAlign: 'right', }, companyName: { fontSize: 11, fontWeight: 'bold', marginBottom: 2, }, companyMeta: { fontSize: 9, color: '#666', }, summaryRow: { flexDirection: 'row', gap: 24, marginBottom: 16, }, summaryItem: { flexDirection: 'column', }, summaryLabel: { fontSize: 8, color: '#666', marginBottom: 2, }, summaryValue: { fontSize: 12, fontWeight: 'bold', fontFamily: 'Courier', }, sectionHeading: { fontSize: 12, fontWeight: 'bold', color: '#1a1a1a', marginTop: 12, marginBottom: 6, paddingBottom: 4, borderBottomWidth: 1, borderBottomColor: '#1a1a1a', }, tableHeader: { flexDirection: 'row', paddingVertical: 3, borderBottomWidth: 0.8, borderBottomColor: '#999', }, headerCell: { fontSize: 7.5, fontWeight: 'bold', color: '#555', textTransform: 'uppercase', }, row: { flexDirection: 'row', paddingVertical: 3, borderBottomWidth: 0.4, borderBottomColor: '#e4e4e4', }, totalRow: { flexDirection: 'row', paddingVertical: 4, marginTop: 2, borderTopWidth: 1, borderTopColor: '#1a1a1a', }, colName: { flex: 1, paddingRight: 8, color: '#1a1a1a', }, colAmount: { width: 62, textAlign: 'right', fontFamily: 'Courier', color: '#1a1a1a', }, bold: { fontWeight: 'bold', }, // Invoice detail table columns colInvName: { flex: 1, paddingRight: 6, }, colInvNumber: { width: 60, paddingRight: 6, }, colInvDate: { width: 56, fontFamily: 'Courier', }, colInvAmount: { width: 62, textAlign: 'right', fontFamily: 'Courier', }, colInvDays: { width: 36, textAlign: 'right', fontFamily: 'Courier', }, colInvCurrency: { width: 28, textAlign: 'right', color: '#666', }, fxNote: { marginTop: 8, fontSize: 8, color: '#92400e', }, emptyNote: { fontSize: 9, color: '#888', fontStyle: 'italic', marginTop: 4, }, footer: { position: 'absolute', bottom: 24, left: 40, right: 40, borderTopWidth: 0.5, borderTopColor: '#d4d4d4', paddingTop: 6, flexDirection: 'row', justifyContent: 'space-between', }, footerText: { fontSize: 8, color: '#888', }, }) function formatAmount(amount: number): string { return new Intl.NumberFormat('sv-SE', { minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(amount) } function formatOrgNumber(orgNumber: string): string { const cleaned = orgNumber.replace(/\D/g, '') if (cleaned.length === 10) { return `${cleaned.slice(0, 6)}-${cleaned.slice(6)}` } return orgNumber } function formatDateSv(iso: string): string { if (!iso) return '' return new Date(iso).toLocaleDateString('sv-SE') } export interface ReskontraAgingRow { name: string current: number days_1_30: number days_31_60: number days_61_90: number days_90_plus: number total_outstanding: number } export interface ReskontraInvoiceRow { counterparty: string invoice_number: string invoice_date: string due_date: string outstanding: number currency: string days_overdue: number } interface ReskontraPDFProps { /** 'Kundreskontra' | 'Leverantörsreskontra' */ title: string /** 'Kund' | 'Leverantör' */ counterpartyLabel: string asOfDate: string aging: ReskontraAgingRow[] totals: ReskontraAgingRow unpaidCount: number unconvertedFxCount: number /** Per-invoice detail rows (kundreskontra only). */ invoices?: ReskontraInvoiceRow[] company: CompanySettings generatedAt: string } const AGING_COLUMNS: Array<{ key: keyof ReskontraAgingRow; label: string }> = [ { key: 'current', label: 'Ej förfallet' }, { key: 'days_1_30', label: '1-30 dgr' }, { key: 'days_31_60', label: '31-60 dgr' }, { key: 'days_61_90', label: '61-90 dgr' }, { key: 'days_90_plus', label: '90+ dgr' }, { key: 'total_outstanding', label: 'Totalt' }, ] export function ReskontraPDF({ title, counterpartyLabel, asOfDate, aging, totals, unpaidCount, unconvertedFxCount, invoices, company, generatedAt, }: ReskontraPDFProps) { const companyDisplayName = company.company_name || '' return ( {title} {companyDisplayName && {companyDisplayName}} Per datum: {formatDateSv(asOfDate)} {company.company_name && {company.company_name}} {company.org_number && ( Org.nr: {formatOrgNumber(company.org_number)} )} {company.vat_number && VAT: {company.vat_number}} TOTALT UTESTÅENDE {formatAmount(totals.total_outstanding)} kr EJ FÖRFALLET {formatAmount(totals.current)} kr FÖRFALLET {formatAmount(totals.total_outstanding - totals.current)} kr FAKTUROR {unpaidCount} Åldersfördelning per {counterpartyLabel.toLowerCase()} {aging.length === 0 ? ( Inga utestående fakturor per detta datum. ) : ( {counterpartyLabel} {AGING_COLUMNS.map((col) => ( {col.label} ))} {aging.map((row, i) => ( {row.name} {AGING_COLUMNS.map((col) => ( {formatAmount(row[col.key] as number)} ))} ))} Summa {AGING_COLUMNS.map((col) => ( {formatAmount(totals[col.key] as number)} ))} )} {invoices && invoices.length > 0 && ( {/* NOTE: no `break` here: react-pdf 4.x deadlocks in layout when a break element's section spills across pages (verified against this template with 40+ rows). The table flows inline instead. */} Fakturor {counterpartyLabel} Fakturanr Fakturadatum Förfaller Utestående Dgr Val. {invoices.map((inv, i) => ( {inv.counterparty} {inv.invoice_number} {inv.invoice_date} {inv.due_date} {formatAmount(inv.outstanding)} {inv.days_overdue > 0 ? inv.days_overdue : ''} {inv.currency} ))} )} {unconvertedFxCount > 0 && ( {unconvertedFxCount} faktura i utländsk valuta utan växelkurs ingår inte i beloppen ovan. )} {companyDisplayName} {company.org_number ? ` · ${formatOrgNumber(company.org_number)}` : ''} `Genererad ${formatDateSv(generatedAt)} · Sida ${pageNumber} av ${totalPages}` } /> ) }