feat(invoices): preview invoices and underlag in the browser instead of downloading (#1228)

Reviewing an invoice or a verifikat bilaga meant saving a file and opening it
from the Downloads folder (user request, christian@odinaero.se 2026-07-25).

- GET /api/invoices/[id]/pdf accepts ?disposition=inline and serves the PDF for
  in-browser review; anything else keeps the download behaviour every existing
  caller relies on. The filename still travels in the header, so the browser
  viewer's own save action produces the same name as the download button, and
  nosniff pins the content type.
- The invoice detail page gets a "Förhandsgranska" action next to "Ladda ner
  PDF". It resolves the document through the same resolveInvoicePdfSource path
  as the download, so preview cannot become the shortcut that presents a
  re-render as the invoice the customer received: the archived delivery wins,
  a re-render is shown with its caveat, and an unreadable delivery history
  still asks instead of guessing. The archive dialog now remembers whether the
  user asked to view or to save, and its fallback does that.
- DocumentViewButton (supplier-invoice underlag, staged agent previews) points
  at the existing /api/documents/:id/inline proxy, so bilagor render in the
  browser. Navigation now happens straight from the click, so the signed-URL
  fetch and its popup-blocker workaround are gone.
- The three re-render caveat strings and the two archive-dialog descriptions
  lose their "you downloaded" wording so they stay true for both actions;
  five new keys in sv + en.

Tests: route cases for the default, inline and unknown disposition values;
invoiceRerenderUrl cases for both modes and id encoding. npm test 11364
passed, lint 0 errors. Button row screenshotted against the design system
(pill outline, Eye icon) via a temporary sandbox route.

Closes #1190

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-27 14:59:33 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent df29817826
commit c62d00bcb3
9 changed files with 238 additions and 81 deletions
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
invoiceDocumentCaveat,
invoiceRerenderUrl,
resolveInvoicePdfSource,
type InvoicePdfDelivery,
} from '@/lib/invoices/invoice-pdf-source'
@@ -209,3 +210,24 @@ describe('invoiceDocumentCaveat', () => {
).toBeNull()
})
})
describe('invoiceRerenderUrl', () => {
it('defaults to the download endpoint', () => {
expect(invoiceRerenderUrl(INVOICE_ID)).toBe(`/api/invoices/${INVOICE_ID}/pdf`)
})
// #1190: previewing must not silently become a download, and the archived
// delivery URL above is already an inline proxy, so both source kinds can be
// opened in the browser the same way.
it('asks for an inline response when previewing', () => {
expect(invoiceRerenderUrl(INVOICE_ID, { inline: true })).toBe(
`/api/invoices/${INVOICE_ID}/pdf?disposition=inline`,
)
})
it('encodes the invoice id', () => {
expect(invoiceRerenderUrl('a/b', { inline: true })).toBe(
'/api/invoices/a%2Fb/pdf?disposition=inline',
)
})
})
+8 -2
View File
@@ -86,8 +86,14 @@ export type InvoicePdfSource =
reason: 'delivery_history_unreadable'
}
export function invoiceRerenderUrl(invoiceId: string): string {
return `/api/invoices/${encodeURIComponent(invoiceId)}/pdf`
/**
* The re-render endpoint. `inline` asks it to serve the PDF for in-browser
* review instead of a download (#1190); the archived-delivery URL below is
* already an inline proxy, so both source kinds can be previewed the same way.
*/
export function invoiceRerenderUrl(invoiceId: string, options?: { inline?: boolean }): string {
const base = `/api/invoices/${encodeURIComponent(invoiceId)}/pdf`
return options?.inline ? `${base}?disposition=inline` : base
}
/**