import { Document, Page, Text, View, Image, StyleSheet, } from '@react-pdf/renderer' import type { Invoice, InvoiceItem, Customer, CompanySettings, InvoiceDocumentType } from '@/types' import { generateOcrReference } from '@/lib/bankgiro/luhn' import { getDisplayTotal } from '@/lib/invoices/rounding' // Create styles const styles = StyleSheet.create({ page: { padding: 40, fontSize: 10, fontFamily: 'Helvetica', }, header: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 30, }, title: { fontSize: 24, fontWeight: 'bold', color: '#1a1a1a', }, companyInfo: { textAlign: 'left', }, companyName: { fontSize: 14, fontWeight: 'bold', marginBottom: 4, }, section: { marginBottom: 20, }, sectionTitle: { fontSize: 11, fontWeight: 'bold', marginBottom: 8, color: '#666', textTransform: 'uppercase', letterSpacing: 0.5, }, row: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4, }, label: { color: '#666', }, value: { fontWeight: 'bold', }, customerBox: { backgroundColor: '#f5f5f5', padding: 15, borderRadius: 4, marginBottom: 20, }, customerName: { fontSize: 12, fontWeight: 'bold', marginBottom: 4, }, table: { marginTop: 10, }, tableHeader: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#ddd', paddingBottom: 8, marginBottom: 8, }, tableRow: { flexDirection: 'row', paddingVertical: 6, borderBottomWidth: 1, borderBottomColor: '#eee', }, colDescription: { flex: 3.5, }, colQty: { flex: 1, textAlign: 'right', }, colUnit: { flex: 1, textAlign: 'center', }, colPrice: { flex: 1.5, textAlign: 'right', }, colVat: { flex: 1, textAlign: 'right', }, colTotal: { flex: 1.5, textAlign: 'right', }, tableHeaderText: { fontWeight: 'bold', color: '#666', fontSize: 9, textTransform: 'uppercase', }, totalsSection: { marginTop: 20, paddingTop: 15, borderTopWidth: 2, borderTopColor: '#ddd', }, totalRow: { flexDirection: 'row', justifyContent: 'flex-end', marginBottom: 4, }, totalLabel: { width: 120, textAlign: 'right', paddingRight: 15, color: '#666', }, totalValue: { width: 100, textAlign: 'right', }, grandTotal: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 10, paddingTop: 10, borderTopWidth: 1, borderTopColor: '#333', }, grandTotalLabel: { width: 120, textAlign: 'right', paddingRight: 15, fontSize: 14, fontWeight: 'bold', }, grandTotalValue: { width: 100, textAlign: 'right', fontSize: 14, fontWeight: 'bold', }, paymentSection: { marginTop: 30, padding: 15, backgroundColor: '#f8f9fa', borderRadius: 4, }, paymentTitle: { fontSize: 11, fontWeight: 'bold', marginBottom: 10, color: '#333', }, paymentRow: { flexDirection: 'row', marginBottom: 4, }, paymentLabel: { width: 100, color: '#666', }, paymentValue: { flex: 1, }, reverseChargeBox: { marginTop: 20, padding: 12, backgroundColor: '#fff3cd', borderRadius: 4, borderWidth: 1, borderColor: '#ffc107', }, reverseChargeText: { fontSize: 9, color: '#856404', }, notesBox: { marginTop: 20, padding: 12, backgroundColor: '#e8f4fd', borderRadius: 4, }, notesText: { fontSize: 9, color: '#0c5460', }, creditNoteBox: { marginBottom: 20, padding: 12, backgroundColor: '#f8d7da', borderRadius: 4, borderWidth: 1, borderColor: '#f5c6cb', }, creditNoteText: { fontSize: 10, color: '#721c24', }, creditNoteTitle: { color: '#721c24', }, draftBanner: { marginBottom: 16, padding: 10, backgroundColor: '#fff3cd', borderWidth: 2, borderColor: '#856404', borderRadius: 4, }, draftBannerTitle: { fontSize: 14, fontWeight: 'bold', color: '#856404', textAlign: 'center', marginBottom: 2, }, draftBannerText: { fontSize: 9, color: '#856404', textAlign: 'center', }, cancelledBanner: { marginBottom: 16, padding: 10, backgroundColor: '#f8d7da', borderWidth: 2, borderColor: '#721c24', borderRadius: 4, }, cancelledBannerTitle: { fontSize: 14, fontWeight: 'bold', color: '#721c24', textAlign: 'center', marginBottom: 2, }, cancelledBannerText: { fontSize: 9, color: '#721c24', textAlign: 'center', }, footer: { position: 'absolute', bottom: 30, left: 40, right: 40, borderTopWidth: 1, borderTopColor: '#ddd', paddingTop: 10, }, footerText: { fontSize: 8, color: '#999', textAlign: 'center', }, twoColumn: { flexDirection: 'row', justifyContent: 'space-between', }, column: { width: '48%', }, }) // Format currency function formatCurrency(amount: number, currency: string = 'SEK'): string { return new Intl.NumberFormat('sv-SE', { style: 'currency', currency, minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(amount) } // Format date function formatDate(date: string): string { return new Date(date).toLocaleDateString('sv-SE') } // Format org number 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 getDocumentTitle(invoice: Invoice): string { if (invoice.credited_invoice_id) return 'KREDITFAKTURA' const docType = (invoice as Invoice & { document_type?: InvoiceDocumentType }).document_type || 'invoice' if (docType === 'proforma') return 'PROFORMAFAKTURA' if (docType === 'delivery_note') return 'FÖLJESEDEL' return 'FAKTURA' } interface InvoicePDFProps { invoice: Invoice customer: Customer items: InvoiceItem[] company: CompanySettings originalInvoiceNumber?: string } export function InvoicePDF({ invoice, customer, items, company, originalInvoiceNumber }: InvoicePDFProps) { const isCreditNote = !!invoice.credited_invoice_id // Check if items have mixed VAT rates const hasPerLineVat = items.some((item) => item.vat_rate !== undefined && item.vat_rate !== null) const uniqueRates = hasPerLineVat ? new Set(items.map((item) => item.vat_rate)) : new Set() const showVatColumn = hasPerLineVat && uniqueRates.size > 1 // Calculate per-rate VAT breakdown for totals const vatByRate = new Map() if (hasPerLineVat) { for (const item of items) { const rate = item.vat_rate ?? 0 const group = vatByRate.get(rate) || { base: 0, vat: 0 } group.base += Math.abs(item.line_total) group.vat += Math.abs(item.vat_amount || 0) vatByRate.set(rate, group) } } const docType = (invoice as Invoice & { document_type?: InvoiceDocumentType }).document_type || 'invoice' const isDeliveryNote = docType === 'delivery_note' const isProforma = docType === 'proforma' return ( {/* Status banner — cancelled takes precedence over draft so a cancelled row that lacks a number (legacy un-numbered draft that was later cancelled) still surfaces as MAKULERAD rather than UTKAST. The draft banner only shows for genuine drafts and for the corrupt-state case of a non-cancelled invoice that somehow lacks a number. */} {invoice.status === 'cancelled' ? ( MAKULERAD – inte en giltig faktura {invoice.invoice_number ? `Faktura ${invoice.invoice_number} har makulerats. Numret behålls i serien för att hålla nummerföljden obruten enligt ML 17 kap 24§, men dokumentet är inte ett giltigt fakturaunderlag.` : 'Detta utkast har makulerats och är inte ett giltigt fakturaunderlag.'} ) : (invoice.status === 'draft' || !invoice.invoice_number) && ( UTKAST – inte en giltig faktura {invoice.invoice_number ? 'Detta är ett utkast. Markera fakturan som skickad eller skicka via systemet för att göra den giltig som fakturaunderlag.' : 'Denna faktura saknar löpnummer och kan inte användas som fakturaunderlag enligt ML 17 kap 24§. Skicka fakturan via systemet för att tilldela ett nummer.'} )} {/* Header */} {company.logo_url && (company.invoice_show_logo ?? true) && ( )} {(company.invoice_show_company_name ?? true) && (company.invoice_company_name_position ?? 'header') === 'header' && ( {company.company_name} )} {getDocumentTitle(invoice)} {invoice.invoice_number ?? 'FÖRHANDSGRANSKNING'} {/* Credit note reference */} {isCreditNote && originalInvoiceNumber && ( Denna kreditfaktura avser och krediterar faktura nr {originalInvoiceNumber} )} {/* Invoice details and Customer - two columns */} {/* Invoice details */} Fakturainformation Fakturadatum: {formatDate(invoice.invoice_date)} Förfallodatum: {formatDate(invoice.due_date)} {invoice.delivery_date && invoice.delivery_date !== invoice.invoice_date && ( Leveransdatum: {formatDate(invoice.delivery_date)} )} {invoice.your_reference && ( Er referens: {invoice.your_reference.split(',').map((ref, i) => ( {ref.trim()} ))} )} {invoice.our_reference && ( Vår referens: {invoice.our_reference.split(',').map((ref, i) => ( {ref.trim()} ))} )} {/* Customer */} Faktureras till {customer.name} {customer.address_line1 && {customer.address_line1}} {customer.address_line2 && {customer.address_line2}} {(customer.postal_code || customer.city) && ( {customer.postal_code} {customer.city} )} {customer.country && customer.country !== 'SE' && ( {customer.country} )} {customer.org_number && ( Org.nr: {customer.org_number} )} {customer.vat_number && VAT: {customer.vat_number}} {/* Items table */} Specifikation {/* Table header */} Beskrivning Antal Enhet {!isDeliveryNote && ( à-pris )} {!isDeliveryNote && showVatColumn && ( Moms )} {!isDeliveryNote && ( Summa )} {/* Table rows */} {items.map((item, index) => ( {item.description} {item.quantity} {item.unit} {!isDeliveryNote && ( {formatCurrency(item.unit_price, invoice.currency)} )} {!isDeliveryNote && showVatColumn && ( {item.vat_rate ?? 0}% )} {!isDeliveryNote && ( {formatCurrency(item.line_total, invoice.currency)} )} ))} {/* Totals - hidden for delivery notes */} {!isDeliveryNote && ( Delsumma: {formatCurrency(invoice.subtotal, invoice.currency)} {vatByRate.size > 1 ? ( Array.from(vatByRate.entries()) .sort(([a], [b]) => b - a) .map(([rate, group]) => ( Netto {rate}%: {formatCurrency(group.base, invoice.currency)} {group.vat > 0 && ( Moms {rate}%: {formatCurrency(group.vat, invoice.currency)} )} )) ) : ( Moms ({invoice.vat_rate ?? (vatByRate.size === 1 ? vatByRate.keys().next().value : 0)}%): {formatCurrency(invoice.vat_amount, invoice.currency)} )} {(() => { const rounding = getDisplayTotal(invoice, company) return ( <> {rounding.applies && ( Öresavrundning: {formatCurrency(rounding.roundingDelta, 'SEK')} )} {isCreditNote ? 'Att kreditera:' : 'Att betala:'} {formatCurrency(rounding.displayed, invoice.currency)} ) })()} {invoice.currency !== 'SEK' && invoice.total_sek && ( {invoice.vat_amount_sek != null && invoice.vat_amount_sek !== 0 && ( Moms i SEK (kurs {invoice.exchange_rate}): {formatCurrency(invoice.vat_amount_sek, 'SEK')} )} Totalt i SEK: {formatCurrency(invoice.total_sek, 'SEK')} )} )} {/* Proforma notice */} {isProforma && ( Detta är en proformafaktura och utgör ingen betalningsanmodan. )} {/* Payment information - not shown for credit notes, proformas, or delivery notes */} {!isCreditNote && !isProforma && !isDeliveryNote && ( Betalningsinformation {company.bank_name && ( Bank: {company.bank_name} )} {(company.clearing_number || company.account_number) && ( Kontonummer: {company.clearing_number}-{company.account_number} )} {company.bankgiro && (company.invoice_show_bankgiro ?? true) && ( Bankgiro: {company.bankgiro} )} {company.plusgiro && (company.invoice_show_plusgiro ?? true) && ( Plusgiro: {company.plusgiro} )} {company.iban && ( IBAN: {company.iban} )} {company.bic && ( BIC/SWIFT: {company.bic} )} Förfallodatum: {formatDate(invoice.due_date)} {(company.invoice_show_ocr ?? true) && ( OCR/Referens: {invoice.invoice_number ? generateOcrReference(invoice.invoice_number) : '—'} )} )} {/* Reverse charge / export / exempt notice */} {invoice.reverse_charge_text && ( {invoice.reverse_charge_text} )} {invoice.vat_treatment === 'exempt' && !invoice.reverse_charge_text && ( Undantag från skatteplikt, ML 3 kap. )} {/* Notes */} {invoice.notes && ( {invoice.notes} )} {/* Late fee & credit terms */} {(company.invoice_late_fee_text || company.invoice_credit_terms_text) && ( {company.invoice_late_fee_text && ( {company.invoice_late_fee_text} )} {company.invoice_credit_terms_text && ( {company.invoice_credit_terms_text} )} )} {/* Footer — collected legal info per ML 17 kap 24§ */} {[ (company.invoice_show_company_name ?? true) && (company.invoice_company_name_position ?? 'header') === 'footer' ? company.company_name : null, company.address_line1, (company.postal_code || company.city) ? `${company.postal_code ?? ''} ${company.city ?? ''}`.trim() : null, company.org_number ? `Org.nr: ${formatOrgNumber(company.org_number)}` : null, company.vat_number ? `Momsreg.nr: ${company.vat_number}` : null, company.f_skatt ? 'Godkänd för F-skatt' : null, ].filter(Boolean).join(' · ')} ) }