From 50e5520b6dfa22a01afada0d8e30cd8f6a324914 Mon Sep 17 00:00:00 2001 From: Mattsson <111893710+mattssonn@users.noreply.github.com> Date: Sun, 24 May 2026 16:59:36 +0200 Subject: [PATCH] Add/preview invoice image (#565) * feat: enhance invoice preview functionality with mock customer support * feat: update invoice preview logic to handle mock customers and improve error handling --- app/api/invoices/preview-pdf/route.ts | 76 +++++++++++++++---- components/settings/InvoicePreviewCard.tsx | 18 +---- .../general/mcp-server/resources/attention.ts | 6 +- extensions/general/mcp-server/server.ts | 8 +- messages/en.json | 1 - messages/sv.json | 1 - next.config.ts | 32 +++++++- 7 files changed, 100 insertions(+), 42 deletions(-) diff --git a/app/api/invoices/preview-pdf/route.ts b/app/api/invoices/preview-pdf/route.ts index dff24cf6..ef2ff7d2 100644 --- a/app/api/invoices/preview-pdf/route.ts +++ b/app/api/invoices/preview-pdf/route.ts @@ -26,20 +26,64 @@ export async function POST(request: Request) { const body = await request.json() const { customer_id, invoice_date, due_date, delivery_date, currency, items, your_reference, our_reference, notes, document_type, invoice_number } = body - if (!customer_id || !items || items.length === 0) { - return NextResponse.json({ error: 'Kunduppgifter och rader krävs' }, { status: 400 }) + if (!items || items.length === 0) { + return NextResponse.json({ error: 'Rader krävs' }, { status: 400 }) } - // Fetch customer - const { data: customer, error: customerError } = await supabase - .from('customers') - .select('*') - .eq('id', customer_id) - .eq('company_id', companyId) - .single() + // When customer_id is omitted, only allow the synthetic preview if the + // company has no real customers — this is the settings-preview dead-end + // case. Derived server-side so a client can't bypass the ownership check + // by passing a flag. + const isMockCustomer = !customer_id - if (customerError || !customer) { - return NextResponse.json({ error: 'Kunden hittades inte' }, { status: 404 }) + let customer: Customer + if (isMockCustomer) { + const { count, error: countError } = await supabase + .from('customers') + .select('id', { count: 'exact', head: true }) + .eq('company_id', companyId) + + if (countError || (count ?? 0) > 0) { + return NextResponse.json({ error: 'Kunduppgifter krävs' }, { status: 400 }) + } + + const nowIso = new Date().toISOString() + customer = { + id: 'preview-customer', + user_id: 'preview-user', + company_id: 'preview-company', + name: 'Exempel AB', + customer_type: 'swedish_business', + email: 'kund@exempel.se', + phone: null, + address_line1: 'Storgatan 1', + address_line2: null, + postal_code: '111 22', + city: 'Stockholm', + country: 'SE', + org_number: '556677-8899', + vat_number: null, + vat_number_validated: false, + vat_number_validated_at: null, + personal_number: null, + language: 'sv', + default_payment_terms: 30, + notes: null, + created_at: nowIso, + updated_at: nowIso, + } + } else { + const { data, error: customerError } = await supabase + .from('customers') + .select('*') + .eq('id', customer_id) + .eq('company_id', companyId) + .single() + + if (customerError || !data) { + return NextResponse.json({ error: 'Kunden hittades inte' }, { status: 404 }) + } + customer = data as Customer } // Fetch company settings @@ -88,9 +132,11 @@ export async function POST(request: Request) { // Construct a temporary Invoice-like object const previewInvoice = { id: 'preview', - user_id: user.id, - customer_id, - invoice_number: typeof invoice_number === 'string' && invoice_number.trim() ? invoice_number : null, + user_id: isMockCustomer ? 'preview-user' : user.id, + customer_id: customer.id, + invoice_number: typeof invoice_number === 'string' && invoice_number.trim() + ? invoice_number + : isMockCustomer ? '1' : null, invoice_date: invoice_date || new Date().toISOString().split('T')[0], due_date: due_date || new Date().toISOString().split('T')[0], delivery_date: delivery_date || null, @@ -124,7 +170,7 @@ export async function POST(request: Request) { const pdfBuffer = await renderToBuffer( InvoicePDF({ invoice: previewInvoice, - customer: customer as Customer, + customer, items: invoiceItems, company: company as CompanySettings, isPreview: true, diff --git a/components/settings/InvoicePreviewCard.tsx b/components/settings/InvoicePreviewCard.tsx index 9d22cb24..40796e5f 100644 --- a/components/settings/InvoicePreviewCard.tsx +++ b/components/settings/InvoicePreviewCard.tsx @@ -29,7 +29,6 @@ export function InvoicePreviewCard({ settings }: InvoicePreviewCardProps) { const [blobUrl, setBlobUrl] = useState(null) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) - const [noCustomers, setNoCustomers] = useState(false) const currentUrlRef = useRef(null) const sampleItemDescription = t('sample_item_description') @@ -51,7 +50,6 @@ export function InvoicePreviewCard({ settings }: InvoicePreviewCardProps) { async function run() { setIsLoading(true) setError(null) - setNoCustomers(false) try { const supabase = createClient() @@ -65,18 +63,12 @@ export function InvoicePreviewCard({ settings }: InvoicePreviewCardProps) { if (customerError) throw customerError if (cancelled) return - if (!customer) { - setNoCustomers(true) - setIsLoading(false) - return - } - const response = await fetch('/api/invoices/preview-pdf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: controller.signal, body: JSON.stringify({ - customer_id: customer.id, + customer_id: customer?.id, currency: 'SEK', document_type: 'invoice', items: [ @@ -148,19 +140,13 @@ export function InvoicePreviewCard({ settings }: InvoicePreviewCardProps) { )} - {!isLoading && noCustomers && ( -
-

{t('no_customers')}

-
- )} - {!isLoading && error && (

{t('error')}: {error}

)} - {!isLoading && !error && !noCustomers && blobUrl && ( + {!isLoading && !error && blobUrl && (