'use client' import { useTranslations } from 'next-intl' import { CalendarClock } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Separator } from '@/components/ui/separator' import { formatCurrency } from '@/lib/utils' import { getDisplayTotal } from '@/lib/invoices/rounding' import { itemHasAccrual } from '@/lib/bookkeeping/accruals/account-suggestions' import type { Customer, Currency } from '@/types' interface ReviewItem { description: string quantity: number unit: string unit_price: number vat_rate?: number /** 'text' rows are free-text/blank lines — description only, no amounts. */ line_type?: 'product' | 'text' // Periodisering: when both dates are set, the revenue books to the 29xx // interim account and dissolves monthly over the period. accrual_period_start?: string | null accrual_period_end?: string | null accrual_balance_account?: string | null } const accrualMonth = (date: string): string => date.slice(0, 7) interface InvoiceReviewContentProps { customer: Customer invoiceDate: string dueDate: string currency: Currency items: ReviewItem[] subtotal: number vatAmount: number total: number yourReference?: string ourReference?: string notes?: string /** The invoice number that will be assigned on confirm. Null when unknown * (e.g. delivery notes use a different sequence) or unfetched. */ numberPreview?: string | null /** Mirrors `company_settings.ore_rounding`. Defaults to true to match `getDisplayTotal`. */ oreRounding?: boolean /** Mirrors `company_settings.vat_registered`. When false and the invoice carries * no VAT, the moms row is suppressed to match the PDF (pdf-template.tsx:876). */ vatRegistered?: boolean } export function InvoiceReviewContent({ customer, invoiceDate, dueDate, currency, items, subtotal, vatAmount, total, yourReference, ourReference, notes, numberPreview, oreRounding, vatRegistered, }: InvoiceReviewContentProps) { const t = useTranslations('invoice_review') const rounding = getDisplayTotal({ total, currency }, { ore_rounding: oreRounding ?? true }) const customerTypeLabel: Record = { individual: t('customer_type_individual'), swedish_business: t('customer_type_swedish_business'), eu_business: t('customer_type_eu_business'), non_eu_business: t('customer_type_non_eu_business'), } // Calculate per-rate VAT breakdown (free-text rows carry no amounts). const vatByRate = new Map() for (const item of items) { if (item.line_type === 'text') continue const rate = item.vat_rate ?? 0 const lineTotal = item.quantity * item.unit_price const lineVat = Math.round(lineTotal * rate / 100 * 100) / 100 vatByRate.set(rate, (vatByRate.get(rate) || 0) + lineVat) } const showVatColumn = vatByRate.size > 1 return (
{numberPreview && (
{t('assigned_number_prefix')}{' '} {numberPreview}
)} {/* Customer info */}

{customer.name}

{customer.email}

{customerTypeLabel[customer.customer_type] || customer.customer_type}
{/* Dates */}
{t('invoice_date')}

{invoiceDate}

{t('due_date')}

{dueDate}

{/* Line items — table on desktop, cards on mobile */}
{showVatColumn && } {items.map((item, index) => item.line_type === 'text' ? ( ) : ( {showVatColumn && ( )} ) )}
{t('th_description')} {t('th_quantity')} {t('th_unit')} {t('th_unit_price')}{t('th_vat')}{t('th_amount')}
{item.description || ' '}
{item.description} {itemHasAccrual(item) && (

{t('accrual_line_info', { from: accrualMonth(item.accrual_period_start!), to: accrualMonth(item.accrual_period_end!), })}

)}
{item.quantity} {item.unit} {formatCurrency(item.unit_price, currency)}{item.vat_rate ?? 0}% {formatCurrency(item.quantity * item.unit_price, currency)}
{items.map((item, index) => item.line_type === 'text' ? (

{item.description || ' '}

) : (

{item.description}

{itemHasAccrual(item) && (

{t('accrual_line_info', { from: accrualMonth(item.accrual_period_start!), to: accrualMonth(item.accrual_period_end!), })}

)}
{item.quantity} {item.unit} × {formatCurrency(item.unit_price, currency)} {showVatColumn && {t('mobile_vat_suffix', { rate: item.vat_rate ?? 0 })}}

{formatCurrency(item.quantity * item.unit_price, currency)}

) )}
{/* Totals */}
{t('subtotal')} {formatCurrency(subtotal, currency)}
{Array.from(vatByRate.entries()) .filter(([, vat]) => vat > 0) .sort(([a], [b]) => b - a) .map(([rate, vat]) => (
{t('vat_at_rate', { rate })} {formatCurrency(vat, currency)}
))} {Array.from(vatByRate.values()).every((vat) => vat === 0) && !(vatRegistered === false && vatAmount === 0) && (
{t('vat_label')} {formatCurrency(0, currency)}
)} {rounding.applies && (
{t('ore_rounding')} {formatCurrency(rounding.roundingDelta, currency)}
)}
{t('total')} {formatCurrency(rounding.displayed, currency)}
{/* References/notes */} {(yourReference || ourReference || notes) && (
{yourReference && (
{t('your_reference')}
{yourReference.split(',').map((ref, i) => ( {ref.trim()} ))}
)} {ourReference && (
{t('our_reference')}
{ourReference.split(',').map((ref, i) => ( {ref.trim()} ))}
)} {notes &&

{t('notes_prefix', { notes })}

}
)}
) }