diff --git a/.gitignore b/.gitignore index e3b16f7..4e1f5fd 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ next-env.d.ts # Superpowers brainstorming session (local-only mockups + state) /.superpowers/ +.superpowers/ diff --git a/docs/superpowers/specs/2026-09-04-pos-slip-layout-editor-design.md b/docs/superpowers/specs/2026-09-04-pos-slip-layout-editor-design.md new file mode 100644 index 0000000..78f6817 --- /dev/null +++ b/docs/superpowers/specs/2026-09-04-pos-slip-layout-editor-design.md @@ -0,0 +1,102 @@ +# POS Slip Layout Editor — Design + +## Problem + +The three thermal slips the till prints (sale receipt, delivery note, cash-up report) have their content and order hardcoded inside `public/pos/epos-print.js`'s three XML builders (`buildEposXml`, `buildEposDeliveryXml`, `buildEposCashupXml`). There's no way to see what a slip will actually look like beyond the existing "Slip preview" tabs in `settings.html` (which are already a faithful preview, just not editable), no way to reorder or hide sections, and the delivery note's "show pricing" choice is a per-print checkbox on the receipt page rather than a business-level default. + +## Goals + +- A reorderable, toggleable block-list editor per slip type (sale, delivery note, cash-up), reachable from Settings. +- The existing live preview becomes the editor's preview — it re-renders as blocks are reordered/toggled/edited. +- A persisted-per-letterhead "Show pricing" default for delivery notes (replacing the per-print-only checkbox as the source of truth for the default; the per-print checkbox remains as a one-off override). +- Printing (`receipt.html`, `cashup.html`) respects the saved layout. No saved layout yet → falls back to the current hardcoded order, so nothing regresses for existing tills. + +## Non-goals + +- Freeform/drag-to-position layout (thermal printers print top-to-bottom on a continuous strip — there's no page to position things on; see the block-list vs. freeform comparison discussed during brainstorming). +- Per-block text styling (alignment, bold, font size). Blocks keep the formatting they already have; only visibility, order, and — for a small number of blocks — their text content are editable. +- Adding brand-new content types beyond what the three slips already print today. This formalizes existing content into movable/toggleable units; it doesn't add new sections. +- True drag gestures. Reordering is up/down buttons per row (native HTML5 drag-and-drop doesn't work reliably on touchscreens, and every till here is a phone or tablet). + +## Architecture + +**Storage:** a new PocketBase collection, `pos_slip_layouts`, one row per letterhead: + +| field | type | notes | +|---|---|---| +| `letterhead` | relation → `letterhead_settings` | single, required, one row per letterhead | +| `sale_blocks` | JSON | ordered array of block objects, see schema below | +| `delivery_blocks` | JSON | ordered array of block objects | +| `cashup_blocks` | JSON | ordered array of block objects | + +This is a **new** collection rather than a field on `letterhead_settings` because that collection is shared with the main WFDASH PDF-quote flow — POS-only slip layout has no business being coupled to that schema. Per this repo's PocketBase convention, the collection itself must be created in the PB admin UI before any code lands (this is a UI-only, user-performed step — flagged clearly in the implementation plan). + +**Server:** `src/lib/server/pos-slip-layout-service.ts` — `getSlipLayout(letterheadId)` (returns the saved row, or the hardcoded defaults below if none exists yet) and `saveSlipLayout(letterheadId, { sale_blocks?, delivery_blocks?, cashup_blocks? })` (upserts — creates the row on first save). Exposed via `GET`/`PUT /api/v1/pos/slip-layout/[letterheadId]`, both behind `assertPosAuth`, matching every other `/api/v1/pos/*` route. + +**Client:** a new `public/pos/slip-layout.js` module (parallel to `letterhead-config.js`) caches the active letterhead's layout in `localStorage` (`pos.slipLayout.`) and exposes `getBlocks(slipType)` / `saveBlocks(slipType, blocks)` / the hardcoded defaults. `settings.html` uses it for the new editor; `receipt.html` and `cashup.html` use it (read-only) at print time. + +**Print integration:** `buildEposXml`, `buildEposDeliveryXml`, and `buildEposCashupXml` in `epos-print.js` change from a fixed sequence of `parts.push(...)` calls to a small dispatch table (`{ blockId: (ctx) => xmlString }`) that the caller iterates over the resolved block list, skipping disabled blocks. This is a mechanical refactor — the XML each block produces is unchanged, just extracted into a named function and made conditional/reorderable. While in there: `buildEposCashupXml`'s logo block (line 188) currently emits an `` tag with no actual image data and an invalid `height="auto"` — it's a no-op today. The refactor fixes this by rasterizing the logo the same way `buildEposXml`/`printTest` already do, since the block-based cash-up slip now genuinely supports a logo block. + +## Block schema + +```js +{ id: "footer", enabled: true, text: "Thank you for your business!" } +``` + +`id` is one of a fixed catalog per slip type (operators reorder/toggle/edit-text existing blocks; they can't invent new ones — no requirement calls for that). `text` is present only on the few blocks that carry editable text; it's `null`/absent to mean "use the default" (the letterhead's own `footer_text` for the footer block, `"WFSALES POS"` for the header-title block). + +Some blocks are **required** — reorderable, but not toggle-off-able, because a slip without them stops being a valid record of the transaction (e.g. a receipt with no total). This mirrors how real receipt-customization tools (Square, Shopify POS) scope their "hide this section" options to genuinely optional content. + +**Sale slip** (default order — all currently printed content, formalized into blocks): +1. `header_title` — optional, editable text, default `"WFSALES POS"` (the app's own brand line, separate from the letterhead's business info — this is the one most likely to get turned off once a shop's own letterhead is already showing their name) +2. `logo` — optional +3. `business_info` — optional (letterhead name/address/phone/VAT as one unit) +4. `date_slip` — optional (date + slip number) +5. `customer` — optional (only rendered if the sale has one anyway) +6. `items` — **required** +7. `totals` — **required** (subtotal/VAT/discount/TOTAL as one unit) +8. `payment` — optional (paid amount + change) +9. `footer` — optional, editable text + +**Delivery note** (default order): +1. `business_info` — optional +2. `customer` — optional (company/customer line) +3. `date` — optional +4. `items` — **required** +5. `totals` — **required**, but its visibility is driven by the "Show pricing" setting rather than its own toggle (see below) +6. `signature` — optional (received-by/driver line) + +**"Show pricing" for delivery notes:** one combined setting (not a block toggle) stored alongside `delivery_blocks` as `delivery_show_pricing: boolean`, defaulting to `true` (today's default). When `false`, `items` renders without the per-line price and `totals` is skipped entirely — showing one without the other reads as a mistake on a real slip. The existing on-screen checkbox in `receipt.html` (`renderDeliveryNote`) stays as a per-print override of this default, initialized from it instead of always defaulting to checked. + +**Cash-up report** (default order): +1. `business_info` — optional +2. `shift_date` — optional +3. `payment_breakdown` — **required** +4. `grand_total` — **required** +5. `cash_reconciliation` — optional +6. `denominations` — optional +7. `footer` — optional, editable text + +## Settings UI + +A new page, `public/pos/slip-layout.html`, linked from an "Edit slip layout" row in `settings.html` (the settings page is already 668 lines; this keeps each page focused, matching the existing one-page-per-concern pattern of `index.html`/`pos.html`/`receipt.html`/`history.html`/`cashup.html`/`settings.html`). The "Slip preview" section — tabs plus the `receipt-preview` pane — moves from `settings.html` into this new page rather than being duplicated in both places, since the preview and the editor are tightly coupled (every edit re-renders it). `settings.html` keeps only the letterhead picker and the "Edit slip layout" / "Test print" actions. The preview rendering functions (`previewSale`/`previewDelivery`/`previewCashup`) move along with it and become block-aware — walking the same resolved block list the editor is manipulating, instead of the fixed hardcoded sequence they render today. + +`slip-layout.html` reuses the moved `.preview-tabs` (Sale slip / Delivery note / Cash-up) to pick which layout is being edited. Below the live preview, an ordered list of rows, one per block: + +- A label (block's human name). +- An enabled/disabled toggle switch (hidden/disabled control for required blocks — shown as always-on, no switch). +- Up/down buttons to move the row (disabled at the top/bottom of the list). +- For blocks with editable text (`header_title`, `footer`): a text input, shown inline or via a small expand. +- For the delivery note's `items`/`totals` pair specifically: a single "Show pricing" toggle instead of individual block toggles. + +Every change re-renders the existing preview pane immediately (client-side, no network round-trip). A "Save" button persists via `PUT /api/v1/pos/slip-layout/[letterheadId]`. + +## Error handling + +- Layout fetch fails (offline, first run before the PB collection has a row) → fall back to the hardcoded defaults above. This is the same graceful-degradation pattern the existing preview already uses for a missing letterhead (`DEMO_LETTERHEAD`). +- Save fails → existing `showBanner('err', ...)` pattern in `settings.html`. +- At print time, if `slip-layout.js`'s cached layout is missing or fails to parse → fall back to the hardcoded defaults, so a settings-sync problem never blocks printing a real slip. + +## Testing + +No automated test harness exists for this static PWA today (confirmed: no `.test.ts`/`.test.js` files anywhere under `public/pos/`), so this plan follows the existing convention — verification is manual, against the real TM-T20II, covering: default layout (unchanged output), a block disabled, blocks reordered, `header_title`/`footer` text edited, and a delivery note printed with "Show pricing" off. Server-side, `pos-slip-layout-service.ts` gets a `.test.ts` following the shape of the existing `pos-service.test.ts`.