WFDASH-WEBAPP/src
SJCoetz 5291a044ed feat(collection-confirm): in-app invoice viewer with zoom + mobile layout
The photo-strip chips on the collection-confirm page used to call
`window.open(url, "_blank")` for the supplier invoice, kicking the
operator out to a new tab to verify the invoice against the items
table. That tab switch broke the side-by-side comparison the
operator actually wants to do — look at a line on the invoice,
look at the same line in the table, look back.

Replace that flow with a floating, draggable invoice viewer
that lives on the page:

- New `InvoiceViewer` component renders the PDF in an `<iframe>`
  against the existing `/api/v1/files/order_items/<id>/` route,
  which already serves PDFs with `Content-Disposition: inline`.
- Desktop: floating box anchored to the bottom-right (720×560),
  header is draggable. Position is clamped to the viewport so
  the close button can't be dragged off-screen. State lives in
  the component; not persisted to localStorage.
- Mobile (≤ 640px): full-screen panel (`100vw × 100dvh`,
  border-radius 0). Drag affordance, grip icon, and "Viewing"
  badge are dropped — they're meaningless on touch or when the
  panel fills the screen. Layout flips live on viewport change
  via a small `useMediaQuery` hook.
- Zoom controls: + / − / Reset with discrete levels
  (50 / 75 / 100 / 125 / 150 / 200 / 300%). The % display is
  itself a click-to-reset button. Implemented as
  `transform: scale()` on the iframe with a pre-shrunk CSS box
  (`${100 / zoom}%`) and a `${zoom * 100}%` scrollable wrapper
  so scrollbars appear at higher zoom levels. CSS-resize-only
  does not work for PDF iframes — the browser's native PDF
  viewer caches the initial render size and ignores post-load
  CSS resizes, so the page content never actually re-renders.
  `transform: scale()` works around that across Chrome / Safari
  / Firefox.
- "Open in new tab" header button is preserved as an opt-in
  escape hatch (landscape invoices, full-page viewing).

The PhotoStrip no longer opens a new tab by default. Clicking a
chip calls a page-level `onOpen` handler that toggles the viewer
(open / swap / close); the matching chip is highlighted with
`aria-pressed="true"` + a primary variant so the operator knows
which invoice is open. Clicking the active chip closes the
viewer. The chip falls back to `window.open(...)` only when no
`onOpen` is passed, so the strip stays usable in isolation.

Tests: 126 pass across the collection-confirm suite. New tests
cover viewer open / close (X + Esc), drag math + drag-bail-out
on header buttons, the active-state highlight, swap-on-different
chip, the in-viewer new-tab escape hatch, every zoom level +
clamping + reset, the PDF wrapper / iframe transform math at
multiple zooms, and the desktop ↔ mobile layout flip on a
mocked viewport change.

Co-authored-by: Orca <help@stably.ai>
2026-08-04 01:38:25 +02:00
..
app feat(collection-confirm): in-app invoice viewer with zoom + mobile layout 2026-08-04 01:38:25 +02:00
components feat(collection-confirm): in-app invoice viewer with zoom + mobile layout 2026-08-04 01:38:25 +02:00
lib feat(order-items): below-22% margin approval workflow 2026-08-04 00:58:33 +02:00
styles feat(item-history): add getItemHistory service with 5 PB queries 2026-07-29 14:51:58 +02:00
test-setup.ts feat(buyer-tasks): group card with color-coded action button 2026-07-29 20:11:39 +02:00