# 10 — Full-Page Product Editor — Report

**Date:** 2026-11-04 · **Branch:** `techtitans/main` · **Base:** backend `cfc7245`, frontend `f1a0649` / `d9a6a07`

**Scope:** product creation and editing move from the right-side drawer to dedicated full pages. The form,
its validation, the resumable create flow and every API call are **reused unchanged**. There are **no backend changes**.

## 1. Routes

| Route | Page | Notes |
|---|---|---|
| `/menu/products/new` | Create product | Quick-add presets arrive as query params: `?kind=phone&sell=multiple&stock=1` |
| `/menu/products/:productId/edit` | Edit product | A `#website`, `#pricing`, `#inventory` or `#details` hash scrolls to that section |

- Both routes sit inside the existing `products.manage` route guard, next to `/menu/products`.
- Path helpers in `routes/paths.ts`: `productNewPath(preset?)`, `productEditPath(id, section?)`, and `ROUTE_PATTERNS.productNew` / `ROUTE_PATTERNS.productEdit`.
- The static `new` segment outranks `:productId` in React Router, so the existing detail route is unaffected.

**Entry points updated:**

| Entry point | Before | Now |
|---|---|---|
| Add Product (header and empty state) | Drawer | `/menu/products/new` |
| Row action **Edit** (table and mobile cards) | Drawer | `/menu/products/:id/edit` |
| Quick add accessory (create & receive) | Drawer + preset | `/new?kind=general&sell=one&stock=1` |
| Product with variants | Drawer + preset | `/new?sell=multiple` |
| Phone / device model | Drawer + preset | `/new?kind=phone&sell=multiple&stock=1` |
| Service (no stock) | Drawer + preset | `/new?kind=service&sell=one&stock=0` |
| Product detail **Edit Product** | Drawer | `/:id/edit` |
| Product detail **Edit online details** | Drawer | `/:id/edit#website` |
| **Duplicate** (list and detail) | Dialog, then stayed on list / went to detail | The dialog (name + what to copy) is unchanged; on success it now **opens the copy in the editor** with "… was duplicated. Review and adjust the copy below." |

**After saving**, the editor goes to the product's detail page with "“Name” was created / updated."

**Cancel** returns to where staff came from. The list URL, including filters and page, is passed as `from`; edit returns to the product. Each visit mounts a fresh form, so no state is left over.

Success messages travel in navigation state and are removed from history once shown, so a reload doesn't repeat them.

## 2. Components reused (not rebuilt)

- **`ProductFormDrawer`.** The same component gains `presentation="page"`, plus `onDirtyChange` and `initialSection`, and exports `ProductEditor`.
  - Reused unchanged: all fields; the six product types; brand, model and specs; image upload; the variant option editor; structure preview; SKU generation; tax class; stock setup with opening stock and barcode; online price/sale; validation; 422 mapping; and the resumable create steps (structure, SKUs, tax, stock, online price, image) with their Retry/Continue footers.
  - The drawer presentation still exists and is still covered by its ~260 tests.
- **`VariantOptionsEditor`, `CatalogueImageUploader`, `CategoryFormDrawer`** (inline category create), **`DuplicateProductDrawer`, `ConfirmDialog`, `Breadcrumb`, `PageContainer`, `LoadingState`, `ErrorState`, `Alert`**: all unchanged.
- **New, page-only pieces:**
  - **`ProductEditorPage`:** loads data (categories, the current-location product list for duplicate-name checks and auto-SKU, the product being edited), handles navigation, and shows the discard confirmation.
  - **`useUnsavedChangesGuard`:** the leave-page warning.
  - **`product-editor.css`:** the page layout.

## 3. Layout

**Desktop (≥ 1025 px):**
- Page header with breadcrumb (Products › Product › Edit), title (`Add product` / `Edit product — {name}`) and a short explanation.
- A two-column grid capped at 1,360 px: the main form column (about 72–75%), and a 260–320 px sidebar that stays pinned while scrolling.
- The **section navigator** (Details · Variants & pricing · Inventory · Website) is a pinned bar under the app header. Each section is a **card**.
- Advanced settings are under a collapsed **More options** (status, Sold as, Available by default).
- The generated-variant preview uses a multi-column grid that fills the width (a 9-combination phone fits on screen).

**Sidebar** (derived from form state only, so there's no duplicate business logic). It updates live and shows:
- the image (pending upload preview on create, product image on edit), name, and brand · model;
- product type, category, variant count (1, the generated combinations, or the product's active variants);
- SKU, in-store price, stock tracking (quantity + opening stock / not tracked / service / managed on the Inventory tab), status, website availability and online-name override.

**Required-items checklist** in the sidebar: lists what's still missing (product name, category, required brand/model/specs, price, variant options). Each item scrolls to its section; when nothing is missing it shows **Ready to save**.

**Action bar:** pinned to the bottom of the viewport (no fixed heights). It shows the state (Not saved yet / Unsaved changes / Saving…) plus **Cancel** and **Create product** / **Save changes**. Create's partial-failure **Retry / Continue** buttons keep appearing here, and `busy` still prevents double submission.

**Tablet and mobile (≤ 1024 px):**
- one column, with the summary after the form;
- 2-column type cards on phones;
- the section navigator scrolls sideways inside itself;
- the action bar becomes a compact full-width two-button row.
- Measured: **no horizontal overflow** at 1440, 820 and 390 px (`scrollWidth == clientWidth`).

**Scrolling:** the window scrolls; there are no nested scroll areas. Pinned elements sit below the app header (`--header-h`).

## 4. Unsaved changes

The page tracks unsaved changes: typing, selects and toggles, product-type cards, category, variant options and a picked photo. It warns before discarding them when:
- **Cancel** is pressed (confirmation dialog);
- an **in-app link** is clicked (sidebar, breadcrumbs, header). The click is intercepted in the capture phase, and the dialog offers **Discard changes** or **Cancel**;
- the page is **reloaded or closed**, via the browser's native `beforeunload` prompt.

The guard is switched off for the page's own navigation after a successful save.

No draft-saving was added; there is no backend support for it.

## 5. Files modified (frontend only)

| File | Change |
|---|---|
| `src/pages/menu/ProductEditorPage.tsx` | **New**: create/edit page |
| `src/lib/hooks/useUnsavedChangesGuard.ts` | **New** |
| `src/components/menu/product-editor.css` | **New**: page layout, cards, sidebar, action bar, responsive rules |
| `src/components/menu/ProductFormDrawer.tsx` | `presentation` (`drawer` \| `page`), `onDirtyChange`, `initialSection`; live summary and checklist; `ProductEditor` export. The form body is shared by both presentations |
| `src/routes/paths.ts`, `src/App.tsx` | New routes and path helpers |
| `src/pages/menu/ProductsListPage.tsx` | Create/edit/presets/duplicate go to the editor pages; the drawer was removed from the list; shows a success message passed in from the editor |
| `src/pages/menu/ProductDetailPage.tsx` | Edit / Edit online details / duplicate go to the editor pages; the drawer was removed; shows a success message passed in from the editor |
| `ProductsListPage.test.tsx`, `ProductDetailPage.test.tsx`, `ProductFormDrawer.retail.test.tsx` | Drawer-based tests rewritten as navigation tests, plus new tests (§7) |

Docs (backend repo): this report plus `docs/techtitans/screenshots/product-editor/*.png`.

## 6. Screenshots

These were captured in Microsoft Edge (headless, using the project's Playwright) against the local TechTitans dev API/DB, logged in through the real login form. They are in `docs/techtitans/screenshots/product-editor/`.

| File | Shows |
|---|---|
| `desktop-01-new-phone.png` | `/new` from "Phone / device model": type cards, sidebar, 6-item checklist, action bar |
| `desktop-02-variants-9-combos.png` | Storage × Colour (3 × 3) with pinned navigator; sidebar shows **9 variants · Ready to save** |
| `desktop-03-inventory.png` | Inventory section with the honest unit-tracking notice |
| `desktop-04-unsaved-guard.png` | Leaving via a link with unsaved changes → "Discard unsaved changes?" |
| `desktop-05-after-create.png` | Detail page after create, with "… was created." and 9 active variants |
| `desktop-06-edit.png` | `/:id/edit`, with breadcrumb to the product, summary and **Save changes** |
| `tablet-01-top.png`, `tablet-02-inventory.png` | 820 px single column |
| `mobile-01-top.png`, `mobile-02-inventory.png` | 390 px: 2-column type cards, compact action bar |

**Data created during browser checks** (synthetic, `techtitans_pos_dev` only): several "Apple iPhone 16 NNNNN" phones with 9 variants each. They can be deleted.

## 7. Tests

| Check | Result |
|---|---|
| `tsc -b` | 0 errors |
| eslint | 0 errors (3 pre-existing warnings) |
| Products list (navigation): Add Product opens the page (no dialog) and creates with integer minor units, then shows the detail success message; 422 stays on the page; quick-add presets preselect the type; Cancel without changes; **unsaved-changes guard** (Cancel → keep editing; breadcrumb link → discard); duplicate-name warning on the page; **Edit** opens `/edit` with values loaded | **67/67** |
| Product detail: Edit online details → editor at Website; **edit round-trip** (PATCH, back to detail, success message, reloaded values); **duplicate → copy opens in the editor** | **106/106** |
| Product form (retail + page presentation): live summary (name, brand/model, type, category, variant count), checklist count, Ready to save, dirty signal, action bar Cancel | **19/19** |
| **Full frontend suite** | **1,713/1,713** (166 files) |
| Browser (Edge): create via preset, 9 combinations, guard dialog, save → detail, `/edit` loads; overflow at 1440/820/390 = none; page errors = 0 | ✅ |

Backend: no backend files changed in this task, so the backend suite was not re-run. It passed at `cfc7245` (Menu 787, Storefront/Orders/Inventory 606).

Correction to report 09: it listed 18 retail form tests; the actual count was 17. It is 19 now, with the two page tests.

## 8. Known limitations

1. **Browser Back is not intercepted.** The app uses `<BrowserRouter>`, not a data router, so React Router's `useBlocker` isn't available. Cancel, in-app links, reload and close are guarded; the Back button leaves without a prompt. Moving to `createBrowserRouter` would allow full blocking.
2. **Editing variants, stock and per-variant online prices stays on the product's tabs** (Variants / Inventory / Availability & Pricing). The edit page covers the product-level form, as the drawer did. Variant images are on the Images & Gallery tab. Nothing was removed.
3. **The create flow's variant pricing** is still "one in-store price applied to every generated variant", then adjusted per variant on the Variants tab. This is the existing logic, preserved as required.
4. **The section navigator scrolls but doesn't highlight the current section** while scrolling.
5. **On a slow API, product type cards appear only once the templates request returns.** Until then the sidebar shows "—" (seen in one screenshot on the single-threaded dev server).
6. **The drawer presentation still exists** (component tests and possible future embedding), but no screen opens it any more.
7. **Branding** (sidebar "Menu"/"Kitchen", the Cacart logo) is outside this task.
8. No IMEI or serialized stock, as instructed; see report 09 §7.

---

## Addendum (2026-11-05): two-step Add Product, and the late type-card fix

**Root cause of the late cards.**
- The six product-type cards were rendered **only after the backend templates response arrived** (`templates.length > 0`).
- That request started only once the editor mounted, which itself waited for categories. On the single-threaded dev API it then queued behind categories, the product list, presets and tax classes.
- When the cards finally appeared they were inserted **above** the form and pushed it down.
- Two related problems: create silently defaulted the type to Accessory (`'general'`), and the editor's React key was the URL query, so any URL update would remount it and lose typed data.

**Fix.** Add Product is now two steps, driven by the URL so it survives refresh and Back/Forward.

- **Step 1** (`/menu/products/new`, no `kind`): six type cards from **static UI options** (`lib/menu/productKinds.ts`, which mirrors `config/product_templates.php`).
  - Rendered instantly, with no API dependency and nothing preselected.
  - The page starts loading templates, categories and products here, in parallel.
- **Step 2** (`?kind=…`): the existing form.
  - The type is shown in the header, with **Change product type**.
  - The form receives the prefetched templates.
  - Any field rules still pending show **skeleton placeholders exactly where the brand/model and specification fields render**, and the category field shows "Loading categories…" in place. The whole page is never swapped out or shifted.
  - The form's key is stable, so type changes don't remount it.
- **Change product type** opens an in-form panel. If entered, type-specific data would be removed (specs that don't apply, brand/model for Service, stock settings for Service), a confirmation lists exactly what will be removed; everything else is kept. Stock tracking and selling model are never silently overwritten.
- **Shortcuts:**
  - Add Product → Step 1.
  - Quick add accessory / Phone / Service → Step 2 directly.
  - Product with variants → Step 1, then the form with multiple versions on.
  - Edit and Duplicate → the form directly, with the stored or copied type.

**Files:**
- `src/lib/menu/productKinds.ts` (new)
- `src/pages/menu/ProductEditorPage.tsx`
- `src/components/menu/ProductFormDrawer.tsx` (page-mode type bar and change panel, prefetched templates, in-place skeletons, switch confirmation; the drawer presentation is unchanged)
- `src/components/menu/product-editor.css`
- Tests: `ProductEditorPage.test.tsx` (new, 15), `ProductsListPage.test.tsx`, `ProductFormDrawer.retail.test.tsx`

**Verification:**
- Frontend:
  - full suite **1,728/1,728** before the skeleton reposition;
  - menu and product suites **514/514** after it;
  - `tsc` 0; eslint 0 errors (3 pre-existing warnings).
- Edge against TechTitans dev (`screenshots/product-editor/two-step/`): every check passed.

| Check | Result |
|---|---|
| Fresh Add Product | Chooser rendered **59 ms** after the click; 6 cards, no form, nothing selected |
| Each of the 6 types | Opens the right form, with the type in the header |
| Layout shift when the type rules arrive | Product name **0 px**, Category **1 px**; no cards inside the form |
| Refresh | Keeps the type |
| Back / Forward | Back → chooser, Forward → form |
| Shortcuts | Quick accessory, phone, service open directly; Product with variants → chooser → multiple versions |
| Change type | Warning lists the specification to be removed; name kept |
| Validation and save | Required errors shown, then create → detail page |
| Edit (phone) | Opens directly with its type |
| Duplicate | Keeps type and brand |
| Mobile (390 px), both steps | No horizontal overflow |
| Page errors | 0 |
