refactor(ssr): Phase 4 — full Selmer migration of POS Sales Summary; remove the wizard; fix add-item + totals

Migrates the POS Sales Summary edit modal off the wizard to a plain Selmer form,
building on the parity gate committed earlier. Largest migration so far and the
first with no prior test coverage.

What changed
- Wizard removed: deleted MainStep/EditWizard records, MultiStepFormState, the
  step-params[...] prefix, the EDN snapshot round-trip, and all mm/* middleware.
  Replaced with a plain handler + flat wrap-decode/wrap-derive-state. The 51 fc/
  cursor refs are de-cursored into explicit data + Selmer templates.
- db/id-keyed item merge: wrap-derive-state overlays posted items onto the
  persisted items by :db/id, so read-only fields the form doesn't post
  (ledger-side, amount) survive a re-render and the debit/credit split + totals
  stay correct. New manual rows (temp db/id) ride through as-is.
- Inline click-to-edit account cell preserved as three small targeted
  .account-cell-swap routes (edit/save/cancel-item-account), ported to Selmer
  with the new field-name scheme.
- 100% Selmer modal render path (the remaining Hiccup / hx-swap-oob / "hx-"
  strings are all grid-page code — grid render lambdas, the filters form, and the
  submit response-header map — not the modal).
- Routes: dropped edit-wizard-navigate + new-summary-item; added form-changed.

Fixes (two pre-existing bugs, per request)
- "New Summary Item" add button (was throwing `newRowIndex is not defined` and
  targeting a non-existent `.new-row`) is now a whole-form-swap op=new-item that
  adds an editable manual row (category + account typeahead + debit/credit money
  inputs + remove).
- The dead totals/balance display (malformed Hiccup that discarded its labels) is
  replaced by a proper #summary-totals block showing running Total +
  Balanced/Unbalanced, refreshed via a Rule-4 targeted swap on manual amount edits.

Scorecard delta (pos/sales_summaries.clj): LOC 790->732, mm coupling 20->0,
wizard records 4->0, fc/ cursor 51->0, step-params 27->0 (2 comments), modal
routes 8->6. (hx-swap-oob 1 and mixed-hx live in the grid page, not the modal.)

Verification: sales-summary spec 7/7 (incl. the two fixes); full Playwright suite
46/46; cljfmt clean. Skill fed: scorecard row + narrative; gotchas (parity-gate-
first, characterize-then-fix, keyup-trigger tests); cookbook (inline click-to-edit
cell, db/id-keyed item merge).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 22:13:19 -07:00
parent a289ff2557
commit 599b849e6f
8 changed files with 524 additions and 484 deletions

View File

@@ -45,11 +45,12 @@ test.describe('Sales Summary Edit (characterization)', () => {
expect(await modal.locator('[hx-get*="edit/item-account"]').count()).toBe(2);
});
test('seeded summary is balanced (no out-of-balance indicator)', async ({ page }) => {
test('seeded summary is balanced (shows Balanced totals, no out-of-balance)', async ({ page }) => {
await openEditModal(page);
const modal = page.locator('#wizardmodal');
// balanced: $500 debit == $500 credit, so no unbalanced warning text
await expect(modal).not.toContainText('Out of balance');
// balanced: $500 debit == $500 credit -> the (now-fixed) totals block shows Balanced
await expect(modal.locator('#summary-totals')).toContainText('Balanced');
await expect(modal.locator('#summary-totals')).toContainText('$500.00');
await expect(modal).not.toContainText('Unbalanced');
});
@@ -92,22 +93,36 @@ test.describe('Sales Summary Edit (characterization)', () => {
await expect(display.locator('[hx-get*="edit/item-account"]')).toBeVisible();
});
// NOTE: the "New Summary Item" button is currently BROKEN in this modal and is
// therefore intentionally not characterized as working. Its Alpine handler
// (`@click="$dispatch('newRow', {index: (newRowIndex++)})"`) throws
// "newRowIndex is not defined" (the modal's x-data is empty), and even if it
// fired, `hx-target="closest .new-row"` matches no ancestor in this div layout,
// so the `new-summary-item` route never fires. We assert the *current* behavior:
// the button is present but adds nothing. The migration may fix or preserve this.
test('New Summary Item button is present but currently inert (documents the break)', async ({ page }) => {
// The "New Summary Item" button was broken in the pre-migration wizard (its Alpine
// handler threw "newRowIndex is not defined"); the migration fixes it as a whole-form
// swap (op=new-item). This asserts the FIXED behavior: clicking adds an editable manual
// row (category + account typeahead + debit/credit money inputs).
test('New Summary Item adds an editable manual row (fixed)', async ({ page }) => {
await openEditModal(page);
const modal = page.locator('#wizardmodal');
const before = await modal.locator('input').count();
await modal.locator('[hx-get*="sales-summary-item"]').first().click();
await page.waitForTimeout(500);
// no manual row was added (no category input appeared, input count unchanged)
expect(await modal.locator('input[placeholder="Category/Explanation"]').count()).toBe(0);
expect(await modal.locator('input').count()).toBe(before);
expect(await modal.locator('.manual-item-row').count()).toBe(0);
await modal.locator('a[hx-vals*="new-item"]').click();
await expect(modal.locator('.manual-item-row').first()).toBeVisible();
expect(await modal.locator('.manual-item-row').count()).toBe(1);
const row = modal.locator('.manual-item-row').first();
await expect(row.locator('input[placeholder="Category/Explanation"]')).toBeVisible();
expect(await row.locator('input[name*="[debit]"]').count()).toBe(1);
expect(await row.locator('input[name*="[credit]"]').count()).toBe(1);
});
test('a manual debit amount recomputes the totals to Unbalanced (fixed)', async ({ page }) => {
await openEditModal(page);
const modal = page.locator('#wizardmodal');
await expect(modal.locator('#summary-totals')).toContainText('Balanced');
await modal.locator('a[hx-vals*="new-item"]').click();
await expect(modal.locator('.manual-item-row').first()).toBeVisible();
// adding a $500 debit -> $1000 debit vs $500 credit -> the totals block recomputes
const debit = modal.locator('.manual-item-row input[name*="[debit]"]').first();
await debit.click();
await debit.pressSequentially('500'); // fires keyup -> hx-trigger "keyup changed delay:300ms"
await expect(modal.locator('#summary-totals')).toContainText('Unbalanced', { timeout: 5000 });
});
test('Save closes the modal and the summary stays in the grid', async ({ page }) => {