From 8b43017d6e802f00a7877075236eb8bdf52253ef Mon Sep 17 00:00:00 2001 From: Bryce Date: Tue, 30 Jun 2026 00:37:21 -0700 Subject: [PATCH] =?UTF-8?q?refactor(ssr):=20revert=20hiccup=E2=86=92Selmer?= =?UTF-8?q?=20migration;=20render=20forms=20in=20Hiccup=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Abandons the Selmer-templating step of the SSR re-authoring and moves the four migrated form/wizard modals back to Hiccup (com/* components), keeping the whole-form HTMX swap doctrine, top-rooted render functions, and the session-backed wizard engine unchanged. - transaction/edit, transaction/bulk_code, invoices (bulk-edit group), and pos/sales_summaries render via com/* again; every hx-* swap (whole-form + targeted location-cell / totals-tbody / inline account-cell swaps) is preserved exactly. - add com/single-modal-card to centralize the md:w-[950px] md:h-[650px] modal chrome that previously lived only in the Selmer modal-card templates. - delete auto-ap.ssr.selmer, auto-ap.ssr.components.selmer, selmer_test, the whole resources/templates tree (55 files), the selmer dependency, and the tailwind resources/templates content glob. - strip Selmer guidance from the ssr-form-migration skill + modernization plan. Verified: all four namespaces compile and render with no stringified-hiccup leaks; output.css rebuilds byte-identically (no Tailwind class loss); 60 e2e specs pass — the four reverted modals (incl. whole-form-swap focus/caret tests) plus the untouched wizard/pay/new/rule modals. Co-Authored-By: Claude Opus 4.8 --- .claude/skills/ssr-form-migration/SKILL.md | 17 +- .../reference/component-cookbook.md | 3 + .../ssr-form-migration/reference/scorecard.md | 3 + .../reference/selmer-conventions.md | 148 ---- ...factor-ssr-rendering-modernization-plan.md | 7 + project.clj | 1 - resources/templates/components/a-button.html | 9 - .../templates/components/a-icon-button.html | 3 - resources/templates/components/badge.html | 2 - .../components/button-group-button.html | 2 - .../templates/components/button-group.html | 6 - resources/templates/components/button.html | 7 - .../templates/components/data-grid-cell.html | 3 - .../components/data-grid-header.html | 10 - .../templates/components/data-grid-row.html | 2 - resources/templates/components/data-grid.html | 11 - resources/templates/components/hidden.html | 3 - resources/templates/components/link.html | 1 - .../templates/components/location-select.html | 8 - .../templates/components/modal-card.html | 15 - resources/templates/components/modal.html | 2 - .../templates/components/money-input.html | 4 - .../templates/components/radio-card.html | 16 - resources/templates/components/select.html | 8 - resources/templates/components/spinner.html | 11 - .../templates/components/svg-drop-down.html | 7 - .../components/svg-external-link.html | 8 - resources/templates/components/svg-x.html | 3 - .../templates/components/text-input.html | 3 - resources/templates/components/typeahead.html | 60 -- .../templates/components/validated-field.html | 12 - resources/templates/interop-smoke.html | 8 - .../invoice-bulk-edit/edit-form.html | 5 - .../invoice-bulk-edit/expense-totals.html | 7 - .../templates/sales-summary/edit-form.html | 7 - .../templates/sales-summary/summary-body.html | 21 - .../transaction-bulk-code/account-grid.html | 22 - .../transaction-bulk-code/account-row.html | 34 - .../templates/transaction-bulk-code/body.html | 35 - .../templates/transaction-bulk-code/card.html | 6 - .../transaction-bulk-code/footer.html | 5 - .../transaction-bulk-code/form-errors.html | 4 - .../templates/transaction-bulk-code/form.html | 5 - .../templates/transaction-bulk-code/head.html | 2 - .../templates/transaction-bulk-code/open.html | 3 - .../transaction-bulk-code/success-body.html | 2 - .../transaction-edit/account-totals.html | 5 - .../transaction-edit/approval-status.html | 4 - .../transaction-edit/details-panel.html | 39 -- .../templates/transaction-edit/edit-form.html | 7 - .../transaction-edit/edit-modal.html | 15 - .../transaction-edit/invoice-option.html | 6 - .../transaction-edit/linked-payment.html | 27 - .../transaction-edit/links-body.html | 32 - .../transaction-edit/manual-coding.html | 11 - .../transaction-edit/panel-empty.html | 1 - .../transaction-edit/panel-list.html | 10 - .../transaction-edit/payment-matches.html | 2 - .../transaction-edit/rule-option.html | 4 - .../transaction-edit/simple-mode.html | 14 - .../transaction-edit/transitioner.html | 3 - src/clj/auto_ap/ssr/components.clj | 1 + src/clj/auto_ap/ssr/components/dialog.clj | 20 +- src/clj/auto_ap/ssr/components/selmer.clj | 319 --------- src/clj/auto_ap/ssr/invoices.clj | 200 +++--- src/clj/auto_ap/ssr/pos/sales_summaries.clj | 302 ++++----- src/clj/auto_ap/ssr/selmer.clj | 43 -- src/clj/auto_ap/ssr/transaction/bulk_code.clj | 243 ++++--- src/clj/auto_ap/ssr/transaction/edit.clj | 633 +++++++++--------- tailwind.config.js | 1 - test/clj/auto_ap/ssr/selmer_test.clj | 36 - 71 files changed, 746 insertions(+), 1793 deletions(-) delete mode 100644 .claude/skills/ssr-form-migration/reference/selmer-conventions.md delete mode 100644 resources/templates/components/a-button.html delete mode 100644 resources/templates/components/a-icon-button.html delete mode 100644 resources/templates/components/badge.html delete mode 100644 resources/templates/components/button-group-button.html delete mode 100644 resources/templates/components/button-group.html delete mode 100644 resources/templates/components/button.html delete mode 100644 resources/templates/components/data-grid-cell.html delete mode 100644 resources/templates/components/data-grid-header.html delete mode 100644 resources/templates/components/data-grid-row.html delete mode 100644 resources/templates/components/data-grid.html delete mode 100644 resources/templates/components/hidden.html delete mode 100644 resources/templates/components/link.html delete mode 100644 resources/templates/components/location-select.html delete mode 100644 resources/templates/components/modal-card.html delete mode 100644 resources/templates/components/modal.html delete mode 100644 resources/templates/components/money-input.html delete mode 100644 resources/templates/components/radio-card.html delete mode 100644 resources/templates/components/select.html delete mode 100644 resources/templates/components/spinner.html delete mode 100644 resources/templates/components/svg-drop-down.html delete mode 100644 resources/templates/components/svg-external-link.html delete mode 100644 resources/templates/components/svg-x.html delete mode 100644 resources/templates/components/text-input.html delete mode 100644 resources/templates/components/typeahead.html delete mode 100644 resources/templates/components/validated-field.html delete mode 100644 resources/templates/interop-smoke.html delete mode 100644 resources/templates/invoice-bulk-edit/edit-form.html delete mode 100644 resources/templates/invoice-bulk-edit/expense-totals.html delete mode 100644 resources/templates/sales-summary/edit-form.html delete mode 100644 resources/templates/sales-summary/summary-body.html delete mode 100644 resources/templates/transaction-bulk-code/account-grid.html delete mode 100644 resources/templates/transaction-bulk-code/account-row.html delete mode 100644 resources/templates/transaction-bulk-code/body.html delete mode 100644 resources/templates/transaction-bulk-code/card.html delete mode 100644 resources/templates/transaction-bulk-code/footer.html delete mode 100644 resources/templates/transaction-bulk-code/form-errors.html delete mode 100644 resources/templates/transaction-bulk-code/form.html delete mode 100644 resources/templates/transaction-bulk-code/head.html delete mode 100644 resources/templates/transaction-bulk-code/open.html delete mode 100644 resources/templates/transaction-bulk-code/success-body.html delete mode 100644 resources/templates/transaction-edit/account-totals.html delete mode 100644 resources/templates/transaction-edit/approval-status.html delete mode 100644 resources/templates/transaction-edit/details-panel.html delete mode 100644 resources/templates/transaction-edit/edit-form.html delete mode 100644 resources/templates/transaction-edit/edit-modal.html delete mode 100644 resources/templates/transaction-edit/invoice-option.html delete mode 100644 resources/templates/transaction-edit/linked-payment.html delete mode 100644 resources/templates/transaction-edit/links-body.html delete mode 100644 resources/templates/transaction-edit/manual-coding.html delete mode 100644 resources/templates/transaction-edit/panel-empty.html delete mode 100644 resources/templates/transaction-edit/panel-list.html delete mode 100644 resources/templates/transaction-edit/payment-matches.html delete mode 100644 resources/templates/transaction-edit/rule-option.html delete mode 100644 resources/templates/transaction-edit/simple-mode.html delete mode 100644 resources/templates/transaction-edit/transitioner.html delete mode 100644 src/clj/auto_ap/ssr/components/selmer.clj delete mode 100644 src/clj/auto_ap/ssr/selmer.clj delete mode 100644 test/clj/auto_ap/ssr/selmer_test.clj diff --git a/.claude/skills/ssr-form-migration/SKILL.md b/.claude/skills/ssr-form-migration/SKILL.md index 2588b9fe..274ab3a7 100644 --- a/.claude/skills/ssr-form-migration/SKILL.md +++ b/.claude/skills/ssr-form-migration/SKILL.md @@ -1,6 +1,6 @@ --- name: ssr-form-migration -description: Migrate an SSR form or wizard modal to the whole-form HTMX swap doctrine, top-rooted render functions, the data-driven session-backed wizard engine, and (where it helps) Selmer templates. Use when simplifying a server-rendered form/wizard modal in this codebase, removing EDN-snapshot round-trips, deleting *-no-cursor* duplicate render fns, collapsing per-interaction routes, or replacing the multi-step wizard protocol machinery. +description: Migrate an SSR form or wizard modal to the whole-form HTMX swap doctrine, top-rooted render functions, and the data-driven session-backed wizard engine. Use when simplifying a server-rendered form/wizard modal in this codebase, removing EDN-snapshot round-trips, deleting *-no-cursor* duplicate render fns, collapsing per-interaction routes, or replacing the multi-step wizard protocol machinery. Rendering stays in Hiccup (`com/*` components) — the earlier Selmer-templating step was abandoned. --- # SSR Form & Wizard Migration @@ -12,7 +12,7 @@ approach with **zero out-of-band swaps**. Every migration *reads this skill firs *extends it last* (the Growth contract below). If migration N+1 is not easier than N, the skill-update step was skipped — treat that as a bug. -The four patterns every migration moves code toward live in `reference/`: +The three patterns every migration moves code toward live in `reference/`: - `reference/swap-doctrine.md` — the four-rule HTMX swap priority order + the focus invariant + Alpine-survives-swap hardening + target-selector strategy. @@ -20,8 +20,11 @@ The four patterns every migration moves code toward live in `reference/`: **or a top-rooted cursor**; no faked cursor positions, no `*-no-cursor*` twins. - `reference/form-vs-wizard.md` — single-step → plain form; multi-step → data-driven engine with **per-step state in the Ring session** (the Django `formtools` model). -- `reference/selmer-conventions.md` — plain-HTML attributes via Selmer, the - Hiccup↔Selmer interop bridge, include/block patterns. + +> **Rendering stays in Hiccup.** An earlier iteration of this skill added a fourth +> pattern — templating interactive components in Selmer — which was later **abandoned**. +> All modals render through the shared Hiccup components (`com/*`); there is no Selmer +> layer. Ignore any residual Selmer references in the cookbooks below. Growing cookbooks (append every migration): `component-cookbook.md`, `gotchas.md`, `test-recipes.md`, `scorecard.md`. @@ -65,9 +68,9 @@ Run this loop for each modal. The phase notes in the migration plan list only wh and any `with-cursor`/`MapCursor` rebind that fakes a deep starting position (heuristics 1, 2). Using a cursor is fine; faking where it *starts* is not. -6. **Templatize in Selmer** (`reference/selmer-conventions.md`) where the component is - interactive/attribute-heavy. Reuse cookbook bits; add new ones back (heuristics 5, 8). - Static markup may stay Hiccup — Selmer scope is hybrid (Open decision 2). +6. **Render in Hiccup** with the shared `com/*` components. Reuse cookbook bits; add new + ones back (heuristic 5). (An earlier version of this step templated interactive + components in Selmer; that was abandoned — everything renders through Hiccup.) 7. **Wire HTMX per the swap doctrine** (`reference/swap-doctrine.md`). Keep the focus invariant intact. No OOB except a genuinely disjoint region, documented (heuristic 7). diff --git a/.claude/skills/ssr-form-migration/reference/component-cookbook.md b/.claude/skills/ssr-form-migration/reference/component-cookbook.md index 1a6ae6ef..3f0f4235 100644 --- a/.claude/skills/ssr-form-migration/reference/component-cookbook.md +++ b/.claude/skills/ssr-form-migration/reference/component-cookbook.md @@ -1,5 +1,8 @@ # Component cookbook +> **Note:** Selmer was abandoned — all rendering is Hiccup (`com/*`). Ignore any +> Selmer/template/`sc/*` references below; use the equivalent `com/*` Hiccup component. + GROWS every migration. Each entry: what it is, the swap rule it uses, and the canonical snippet. Reuse these before writing anything new; the success signal is *more reuse each migration*. diff --git a/.claude/skills/ssr-form-migration/reference/scorecard.md b/.claude/skills/ssr-form-migration/reference/scorecard.md index 08a85359..c931f5f5 100644 --- a/.claude/skills/ssr-form-migration/reference/scorecard.md +++ b/.claude/skills/ssr-form-migration/reference/scorecard.md @@ -1,5 +1,8 @@ # Quality scorecard (the ratchet) +> **Note:** Selmer was abandoned — rendering is Hiccup (`com/*`). Ignore the Selmer-specific +> heuristics/mentions below; the swap-doctrine, render-function, and engine ratchets still apply. + Cheap to measure (`grep -c`, `wc -l`, `clj-kondo`), recorded **before/after each migration** in the commit message and in the results table below. **No metric may regress for the touched modal** without a written exception in `gotchas.md`. These are directional diff --git a/.claude/skills/ssr-form-migration/reference/selmer-conventions.md b/.claude/skills/ssr-form-migration/reference/selmer-conventions.md deleted file mode 100644 index 85487bb8..00000000 --- a/.claude/skills/ssr-form-migration/reference/selmer-conventions.md +++ /dev/null @@ -1,148 +0,0 @@ -# Selmer template conventions - -> **Validated** in the Transaction Edit migration: `location-select*` now renders from -> `resources/templates/components/location-select.html` via the interop bridge, embedded -> back into the Hiccup account row. Verified: swap spec 6/6, transaction-edit 8/8 (the -> Shared Location test selects through the Selmer ``. Rules mirror hiccup2 — nil/false -dropped, `true` → bare boolean attr, else `name="escaped"` (via `hu/escape-html`, so JSON -`x-data` and `x-init` quotes become `"`/`'` and the browser decodes them back). -Keyword keys keep their full name incl. namespaced/colon forms (`:x-hx-val:account-id` → -`x-hx-val:account-id`). This keeps templates free of per-attribute `{% if %}` ladders while -still emitting 100% Selmer markup — `attrs->str` serialises data, it does not build Hiccup. - -### Reuse the real class helpers - -Wrappers reuse `inputs/default-input-classes`, `inputs/use-size`, `hh/add-class`, -`btn/bg-colors` so output matches the Hiccup component **modulo Tailwind class order**. -Verify by class-**set** equality + e2e, never byte-parity (see the cookbook entries). - -### Trivial wrapper divs - -A bare `
` around a fragment is composed with a `wrap-div` string -helper (or put the class in the parent template), not a Hiccup vector — string composition -of a structural wrapper is not Hiccup and avoids a micro-template per div. - -Keep `|safe` to values the server fully controls (rendered fragments, JSON for `x-data`), -never raw user input. - -## Scope (Open decision 2) - -Hybrid, and the boundary is real: **the modal's attribute-heavy components delegate to the -shared `com/typeahead` / `com/select` / `com/button-group-button`.** Converting those is a -*cross-cutting* change (every modal uses them), so it belongs to the Phase 11 Selmer sweep, -not a single modal. `location-select*` is the first, self-contained proof; the shared -components follow when the sweep promotes them to Selmer partials. - -## Attribute-consistency scorecard (heuristic 8) - -```bash -grep -cE '"x-[a-z]|"hx-[a-z]|"@' # → 0 mixed encodings in Selmer -``` -A migrated Selmer template has no mixed `:x-`/`"x-"` encodings because everything is plain -HTML. (The Hiccup `"@click"`/`":class"` offenders that remain in `edit.clj` live in the -shared-component call sites — they clear when those components move to Selmer.) diff --git a/docs/plans/2026-06-02-001-refactor-ssr-rendering-modernization-plan.md b/docs/plans/2026-06-02-001-refactor-ssr-rendering-modernization-plan.md index 75e21b8a..dbc00e2b 100644 --- a/docs/plans/2026-06-02-001-refactor-ssr-rendering-modernization-plan.md +++ b/docs/plans/2026-06-02-001-refactor-ssr-rendering-modernization-plan.md @@ -1,5 +1,12 @@ # SSR Form & Wizard Simplification — Migration Plan +> **⚠️ Selmer reverted (2026-06-29).** Pattern 4 below — migrating rendering from Hiccup +> to **Selmer templates** — was **abandoned and reverted**. The swap doctrine, top-rooted +> render functions, and the session-backed wizard engine (patterns 1–3) were kept; all +> rendering remains in **Hiccup** (`com/*` components). The `auto-ap.ssr.selmer` / +> `auto-ap.ssr.components.selmer` namespaces and the `resources/templates/` tree no longer +> exist. Treat every Selmer instruction below as historical context only. +> > **Status:** Planning / for execution by an agent or engineer. > **Owner:** Bryce > **Type:** Refactor (no user-facing behavior change; parity required). diff --git a/project.clj b/project.clj index 51a918f0..e81c06ff 100644 --- a/project.clj +++ b/project.clj @@ -96,7 +96,6 @@ [org.clojure/core.async]] [hiccup "2.0.0-alpha2"] - [selmer "1.12.61"] ;; needed for java 11 [javax.xml.bind/jaxb-api "2.4.0-b180830.0359"] diff --git a/resources/templates/components/a-button.html b/resources/templates/components/a-button.html deleted file mode 100644 index 0c7f9dc7..00000000 --- a/resources/templates/components/a-button.html +++ /dev/null @@ -1,9 +0,0 @@ - - {% if indicator %} -
- {% include "templates/components/spinner.html" %} -
Loading...
-
- {% endif %} -
{{ body|safe }}
-
diff --git a/resources/templates/components/a-icon-button.html b/resources/templates/components/a-icon-button.html deleted file mode 100644 index 0c9f6e55..00000000 --- a/resources/templates/components/a-icon-button.html +++ /dev/null @@ -1,3 +0,0 @@ - -
{{ body|safe }}
-
diff --git a/resources/templates/components/badge.html b/resources/templates/components/badge.html deleted file mode 100644 index b7c977d3..00000000 --- a/resources/templates/components/badge.html +++ /dev/null @@ -1,2 +0,0 @@ -
{{ body|safe }} -
diff --git a/resources/templates/components/button-group-button.html b/resources/templates/components/button-group-button.html deleted file mode 100644 index 7412f48a..00000000 --- a/resources/templates/components/button-group-button.html +++ /dev/null @@ -1,2 +0,0 @@ - diff --git a/resources/templates/components/button-group.html b/resources/templates/components/button-group.html deleted file mode 100644 index c174c5f8..00000000 --- a/resources/templates/components/button-group.html +++ /dev/null @@ -1,6 +0,0 @@ -
- - {{ body|safe }} -
diff --git a/resources/templates/components/button.html b/resources/templates/components/button.html deleted file mode 100644 index a71c47f7..00000000 --- a/resources/templates/components/button.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/resources/templates/components/data-grid-cell.html b/resources/templates/components/data-grid-cell.html deleted file mode 100644 index 3f4e8fb2..00000000 --- a/resources/templates/components/data-grid-cell.html +++ /dev/null @@ -1,3 +0,0 @@ -{{ body|safe }} - diff --git a/resources/templates/components/data-grid-header.html b/resources/templates/components/data-grid-header.html deleted file mode 100644 index 1f04343a..00000000 --- a/resources/templates/components/data-grid-header.html +++ /dev/null @@ -1,10 +0,0 @@ - - {% if sort_key %} - {{ body|safe }} - {% else %} - {{ body|safe }} - {% endif %} - diff --git a/resources/templates/components/data-grid-row.html b/resources/templates/components/data-grid-row.html deleted file mode 100644 index cf9a5f9c..00000000 --- a/resources/templates/components/data-grid-row.html +++ /dev/null @@ -1,2 +0,0 @@ -{{ body|safe }} - diff --git a/resources/templates/components/data-grid.html b/resources/templates/components/data-grid.html deleted file mode 100644 index 7dd57c92..00000000 --- a/resources/templates/components/data-grid.html +++ /dev/null @@ -1,11 +0,0 @@ -
- - - {{ headers|safe }} - - - {{ rows|safe }} - - {{ footer_tbody|safe }} -
-
diff --git a/resources/templates/components/hidden.html b/resources/templates/components/hidden.html deleted file mode 100644 index 1c8971d6..00000000 --- a/resources/templates/components/hidden.html +++ /dev/null @@ -1,3 +0,0 @@ -{# Hidden input. The Clojure wrapper (sc/hidden) serializes the full attribute map - (name, value, optional id/form/class/Alpine :value bind) into `attrs`. #} - diff --git a/resources/templates/components/link.html b/resources/templates/components/link.html deleted file mode 100644 index 168a975d..00000000 --- a/resources/templates/components/link.html +++ /dev/null @@ -1 +0,0 @@ -{{ body|safe }} diff --git a/resources/templates/components/location-select.html b/resources/templates/components/location-select.html deleted file mode 100644 index 910ba1da..00000000 --- a/resources/templates/components/location-select.html +++ /dev/null @@ -1,8 +0,0 @@ -{# Location - {% for opt in options %} - - {% endfor %} - diff --git a/resources/templates/components/modal-card.html b/resources/templates/components/modal-card.html deleted file mode 100644 index 0ebc7085..00000000 --- a/resources/templates/components/modal-card.html +++ /dev/null @@ -1,15 +0,0 @@ -{# Base modal-card chrome (single-step: header / optional side panel / body / footer). - A child template extends this and fills the head / side_panel / body / footer blocks, - so the whole card renders from one shared context in a single render call. Enter - triggers the footer save button via $refs.next. Mirrors transaction-edit/edit-modal - (the string-slot version still used by Transaction Edit). #} - diff --git a/resources/templates/components/modal.html b/resources/templates/components/modal.html deleted file mode 100644 index 7e80c083..00000000 --- a/resources/templates/components/modal.html +++ /dev/null @@ -1,2 +0,0 @@ -
{{ body|safe }} -
diff --git a/resources/templates/components/money-input.html b/resources/templates/components/money-input.html deleted file mode 100644 index 7cc029b4..00000000 --- a/resources/templates/components/money-input.html +++ /dev/null @@ -1,4 +0,0 @@ -{# Money input (number, step=0.01, right-aligned). Owns its class base; callers pass the - non-class attributes via attrs + a variant (width/size) class. Class set = - inputs/default-input-classes + appearance-none/text-right + the variant. #} - diff --git a/resources/templates/components/radio-card.html b/resources/templates/components/radio-card.html deleted file mode 100644 index ec911088..00000000 --- a/resources/templates/components/radio-card.html +++ /dev/null @@ -1,16 +0,0 @@ -
    - {% for opt in options %} -
  • -
    - - -
    -
  • - {% endfor %} -
diff --git a/resources/templates/components/select.html b/resources/templates/components/select.html deleted file mode 100644 index ec14b99d..00000000 --- a/resources/templates/components/select.html +++ /dev/null @@ -1,8 +0,0 @@ -{# Generic - {% for opt in options %} - - {% endfor %} - diff --git a/resources/templates/components/spinner.html b/resources/templates/components/spinner.html deleted file mode 100644 index c2f17b6b..00000000 --- a/resources/templates/components/spinner.html +++ /dev/null @@ -1,11 +0,0 @@ - diff --git a/resources/templates/components/svg-drop-down.html b/resources/templates/components/svg-drop-down.html deleted file mode 100644 index ac31a4f1..00000000 --- a/resources/templates/components/svg-drop-down.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/resources/templates/components/svg-external-link.html b/resources/templates/components/svg-external-link.html deleted file mode 100644 index 54754ed5..00000000 --- a/resources/templates/components/svg-external-link.html +++ /dev/null @@ -1,8 +0,0 @@ - - navigation-next - - - - - - diff --git a/resources/templates/components/svg-x.html b/resources/templates/components/svg-x.html deleted file mode 100644 index a4b7b82a..00000000 --- a/resources/templates/components/svg-x.html +++ /dev/null @@ -1,3 +0,0 @@ - - delete-2 - diff --git a/resources/templates/components/text-input.html b/resources/templates/components/text-input.html deleted file mode 100644 index e8a46f80..00000000 --- a/resources/templates/components/text-input.html +++ /dev/null @@ -1,3 +0,0 @@ -{# Text input. sc/text-input builds the full attr map (type/autocomplete/class+size - already merged, reusing inputs/default-input-classes + hh/add-class) into `attrs`. #} - diff --git a/resources/templates/components/typeahead.html b/resources/templates/components/typeahead.html deleted file mode 100644 index 4d59677b..00000000 --- a/resources/templates/components/typeahead.html +++ /dev/null @@ -1,60 +0,0 @@ -{# Click-to-select typeahead (Alpine + tippy). Survives whole-form swaps; null-guarded - tippy?. / $refs.input? throughout. The Clojure wrapper (sc/typeahead) resolves the - initial {value,label} server-side and builds x_data + the hidden-input attrs. #} -
- {% if disabled %} - - {% else %} - -
- -
- {% include "templates/components/svg-drop-down.html" %} -
-
-
!
-
-
-
- {% endif %} - -
diff --git a/resources/templates/components/validated-field.html b/resources/templates/components/validated-field.html deleted file mode 100644 index d812ca18..00000000 --- a/resources/templates/components/validated-field.html +++ /dev/null @@ -1,12 +0,0 @@ -{# Field wrapper with label + always-present error

(the errors- variant of field-). - Owns the group / has-error toggle; `has_error` is set when the field has errors, - `extra` is the caller's own class. `attrs` carries any pass-through div attributes (the - per-row location cell hangs its hx-* / x-dispatch swap wiring here); `body` is the - pre-rendered inner control HTML; `errors_str` is the comma-joined string errors. #} -

- {% if label %} - - {% endif %} - {{ body|safe }} -

{{ errors_str }}

-
diff --git a/resources/templates/interop-smoke.html b/resources/templates/interop-smoke.html deleted file mode 100644 index 5fa980ab..00000000 --- a/resources/templates/interop-smoke.html +++ /dev/null @@ -1,8 +0,0 @@ -
-

{{ title }}

- {# a Hiccup-rendered component, passed in pre-rendered and emitted verbatim #} - {{ hiccup_frag|safe }} - -
diff --git a/resources/templates/invoice-bulk-edit/edit-form.html b/resources/templates/invoice-bulk-edit/edit-form.html deleted file mode 100644 index aec7ce8e..00000000 --- a/resources/templates/invoice-bulk-edit/edit-form.html +++ /dev/null @@ -1,5 +0,0 @@ -{# Top-level plain bulk-edit form (no wizard). The resolved (not-locked) invoice id set - rides in hidden ids[] fields so the selection survives form-changed / submit posts - without an EDN snapshot or a filter round-trip. #} -
{{ ids_hidden|safe }}{{ modal|safe }} -
diff --git a/resources/templates/invoice-bulk-edit/expense-totals.html b/resources/templates/invoice-bulk-edit/expense-totals.html deleted file mode 100644 index bfc5bff7..00000000 --- a/resources/templates/invoice-bulk-edit/expense-totals.html +++ /dev/null @@ -1,7 +0,0 @@ -{# Running TOTAL / BALANCE percentage rows in their own swappable , a sibling of - the input rows, so a percentage edit refreshes them with a targeted swap (Rule 4) and - never replaces the input-bearing rows above. Replaces the old per-cell bulk-edit-total - / bulk-edit-balance routes. #} - - {{ rows|safe }} - diff --git a/resources/templates/sales-summary/edit-form.html b/resources/templates/sales-summary/edit-form.html deleted file mode 100644 index bf119ea3..00000000 --- a/resources/templates/sales-summary/edit-form.html +++ /dev/null @@ -1,7 +0,0 @@ -{# Plain sales-summary edit form (no wizard). db/id rides in a hidden field; all other - state is the live form, re-derived against the entity each request (no EDN snapshot, - no step-params). #} -
- - {{ modal|safe }} -
diff --git a/resources/templates/sales-summary/summary-body.html b/resources/templates/sales-summary/summary-body.html deleted file mode 100644 index 8e381c60..00000000 --- a/resources/templates/sales-summary/summary-body.html +++ /dev/null @@ -1,21 +0,0 @@ -{# Sales-summary modal body: a read-only Debits | Credits two-column view of the auto - items (each account is inline-editable), a swappable totals/balance block, and an - editable Manual Items section with a working "New Summary Item" add. #} -
-
-
-
Debits
-
{{ debit_rows|safe }}
-
-
-
Credits
-
{{ credit_rows|safe }}
-
-
-
{{ totals|safe }}
-
-
Manual Items
-
{{ manual_rows|safe }}
-
{{ new_item_button|safe }}
-
-
diff --git a/resources/templates/transaction-bulk-code/account-grid.html b/resources/templates/transaction-bulk-code/account-grid.html deleted file mode 100644 index 28d23146..00000000 --- a/resources/templates/transaction-bulk-code/account-grid.html +++ /dev/null @@ -1,22 +0,0 @@ -{# Expense-account grid -- fully template-driven. A single for-loop over the per-row - view-models (bulk-code/account-row-vm), each delegating to account-row.html. The - trailing "New account" button (a-button partial) posts the whole #bulk-code-form - (op=new-account). #} -
- - - - - - - - - - - {% for row in accounts.rows %}{% include "templates/transaction-bulk-code/account-row.html" %}{% endfor %} - - - - -
AccountLocation%
{% with color=accounts.new_account.color extra=accounts.new_account.extra attrs=accounts.new_account.attrs indicator=accounts.new_account.indicator body=accounts.new_account.body %}{% include "templates/components/a-button.html" %}{% endwith %}
-
diff --git a/resources/templates/transaction-bulk-code/account-row.html b/resources/templates/transaction-bulk-code/account-row.html deleted file mode 100644 index f30997d3..00000000 --- a/resources/templates/transaction-bulk-code/account-row.html +++ /dev/null @@ -1,34 +0,0 @@ -{# One expense-account row from a loop-bound `row` view-model. All structure, wiring, and - field names are literal here, built from `row.index` + the shared `urls`; only data (the - Alpine x-data, db/id, errors, and the typeahead / location / money control contexts) - comes from the view-model. The location cell (#account-location-N) swaps just itself on - account change; the remove button swaps the whole #bulk-code-form. #} - - - -
-
{% with width="" x_data=row.account.x_data x_model=row.account.x_model key=row.account.key disabled=row.account.disabled a_xinit=row.account.a_xinit placeholder=row.account.placeholder hidden_attrs=row.account.hidden_attrs %}{% include "templates/components/typeahead.html" %}{% endwith %}
-

{{ row.account_error }}

-
- - -
- {% with name=row.location.name variant="w-full" options=row.location.options %}{% include "templates/components/location-select.html" %}{% endwith %} -

{{ row.location_error }}

-
- - -
- {% with variant=row.pct.variant attrs=row.pct.attrs %}{% include "templates/components/money-input.html" %}{% endwith %} -

{{ row.pct_error }}

-
- - - diff --git a/resources/templates/transaction-bulk-code/body.html b/resources/templates/transaction-bulk-code/body.html deleted file mode 100644 index 6e16e34d..00000000 --- a/resources/templates/transaction-bulk-code/body.html +++ /dev/null @@ -1,35 +0,0 @@ -{# Bulk-code modal body: vendor typeahead (a change repopulates the default account via a - whole-form swap), status select, and the expense-account grid. All wiring, the status - options, and the field-wrapper classes are literal here; only data (selected values, - resolved labels, errors) comes from the view-model. #} -
-
-
-
- - {% with width="w-96" x_data=vendor.ta.x_data x_model=vendor.ta.x_model key=vendor.ta.key disabled=vendor.ta.disabled a_xinit=vendor.ta.a_xinit placeholder=vendor.ta.placeholder hidden_attrs=vendor.ta.hidden_attrs %}{% include "templates/components/typeahead.html" %}{% endwith %} -

{{ vendor.error }}

-
-
-
-
- - -

{{ status.error }}

-
-
-
-

Expense Accounts

-
-
{% include "templates/transaction-bulk-code/account-grid.html" %}
-

{{ accounts.error }}

-
-
-
-
diff --git a/resources/templates/transaction-bulk-code/card.html b/resources/templates/transaction-bulk-code/card.html deleted file mode 100644 index 6bb79a87..00000000 --- a/resources/templates/transaction-bulk-code/card.html +++ /dev/null @@ -1,6 +0,0 @@ -{# Bulk-code modal card: extends the shared modal-card base and fills its blocks with the - bulk-code head / body / footer partials. No side panel. #} -{% extends "templates/components/modal-card.html" %} -{% block head %}{% include "templates/transaction-bulk-code/head.html" %}{% endblock %} -{% block body %}{% include "templates/transaction-bulk-code/body.html" %}{% endblock %} -{% block footer %}{% include "templates/transaction-bulk-code/footer.html" %}{% endblock %} diff --git a/resources/templates/transaction-bulk-code/footer.html b/resources/templates/transaction-bulk-code/footer.html deleted file mode 100644 index caf56a4d..00000000 --- a/resources/templates/transaction-bulk-code/footer.html +++ /dev/null @@ -1,5 +0,0 @@ -{# Modal footer: the form-errors sink on the left, the Save button on the right. Both - pull from the shared view-model; the button reuses components/button.html. #} -
-
{% include "templates/transaction-bulk-code/form-errors.html" %}{% with color=save.color extra=save.extra attrs=save.attrs loading_label=save.loading_label body=save.body %}{% include "templates/components/button.html" %}{% endwith %}
-
diff --git a/resources/templates/transaction-bulk-code/form-errors.html b/resources/templates/transaction-bulk-code/form-errors.html deleted file mode 100644 index 5f6e29f8..00000000 --- a/resources/templates/transaction-bulk-code/form-errors.html +++ /dev/null @@ -1,4 +0,0 @@ -{# Submit-error sink. A 4xx submit swaps the inner `.error-content` (hx-target-400); - the span is present only when there are form-level errors, matching the prior - hand-rolled markup byte-for-byte. #} -
{% if errors_str %}

{{ errors_str }}

{% endif %}
diff --git a/resources/templates/transaction-bulk-code/form.html b/resources/templates/transaction-bulk-code/form.html deleted file mode 100644 index be9a5041..00000000 --- a/resources/templates/transaction-bulk-code/form.html +++ /dev/null @@ -1,5 +0,0 @@ -{# Single render entrypoint for the bulk-code form. The route passes one view-model and - every sub-template composes from it via includes/blocks. The resolved (not-locked) - transaction id set rides in hidden ids[] fields so the selection survives - form-changed / submit posts without an EDN snapshot or a filter round-trip. #} -
{% for id in ids %}{% endfor %}
{% include "templates/transaction-bulk-code/card.html" %}
diff --git a/resources/templates/transaction-bulk-code/head.html b/resources/templates/transaction-bulk-code/head.html deleted file mode 100644 index a573d4eb..00000000 --- a/resources/templates/transaction-bulk-code/head.html +++ /dev/null @@ -1,2 +0,0 @@ -{# Modal header label: how many transactions this bulk-code operation will touch. #} -
Bulk editing {{ head_count }} transactions
diff --git a/resources/templates/transaction-bulk-code/open.html b/resources/templates/transaction-bulk-code/open.html deleted file mode 100644 index 1f5cf720..00000000 --- a/resources/templates/transaction-bulk-code/open.html +++ /dev/null @@ -1,3 +0,0 @@ -{# Modal-open response: the transitioner shell the modal stack expects, wrapping the whole - form. Composes in one render from the shared view-model. #} -
{% include "templates/transaction-bulk-code/form.html" %}
diff --git a/resources/templates/transaction-bulk-code/success-body.html b/resources/templates/transaction-bulk-code/success-body.html deleted file mode 100644 index 6cbc6b4c..00000000 --- a/resources/templates/transaction-bulk-code/success-body.html +++ /dev/null @@ -1,2 +0,0 @@ -{# Post-submit confirmation message embedded in the shared success modal. #} -

Successfully coded {{ count }} transactions.

diff --git a/resources/templates/transaction-edit/account-totals.html b/resources/templates/transaction-edit/account-totals.html deleted file mode 100644 index e66630f9..00000000 --- a/resources/templates/transaction-edit/account-totals.html +++ /dev/null @@ -1,5 +0,0 @@ -{# Totals live in their own swappable so an amount edit refreshes them with a - targeted swap, never replacing the input-bearing rows above (caret survives). #} - - {{ rows|safe }} - diff --git a/resources/templates/transaction-edit/approval-status.html b/resources/templates/transaction-edit/approval-status.html deleted file mode 100644 index 8aaccf4d..00000000 --- a/resources/templates/transaction-edit/approval-status.html +++ /dev/null @@ -1,4 +0,0 @@ -
- {{ status_hidden|safe }} -
{{ buttons|safe }}
-
diff --git a/resources/templates/transaction-edit/details-panel.html b/resources/templates/transaction-edit/details-panel.html deleted file mode 100644 index b73146b5..00000000 --- a/resources/templates/transaction-edit/details-panel.html +++ /dev/null @@ -1,39 +0,0 @@ -{# Read-only transaction summary shown in the modal's left side panel. #} -
-

Details

-
-
-
Amount
-
{{ amount }}
-
-
-
Date
-
{{ date }}
-
-
-
Bank Account
-
{{ bank_account }}
-
-
-
Post Date
-
{{ post_date }}
-
-
-
Description
-
{{ description_simple }}
-
-
-
Check Number
-
{{ check_number }}
-
-
-
Status
-
{{ status }}
-
-
-
Transaction Type
-
{{ type }}
-
-
-
diff --git a/resources/templates/transaction-edit/edit-form.html b/resources/templates/transaction-edit/edit-form.html deleted file mode 100644 index 665f5608..00000000 --- a/resources/templates/transaction-edit/edit-form.html +++ /dev/null @@ -1,7 +0,0 @@ -{# Top-level plain form. The entity id rides in a hidden field; all other state is the - live form, re-derived against the entity each request (no serialized snapshot, no - wizard step-params). #} -
- - {{ modal|safe }} -
diff --git a/resources/templates/transaction-edit/edit-modal.html b/resources/templates/transaction-edit/edit-modal.html deleted file mode 100644 index f4ca2e16..00000000 --- a/resources/templates/transaction-edit/edit-modal.html +++ /dev/null @@ -1,15 +0,0 @@ -{# Modal card chrome (header / optional side panel / body / footer). Single-step, so - no timeline, no back/next nav -- just the Done button in the footer. Enter triggers - the save button via $refs.next. #} - diff --git a/resources/templates/transaction-edit/invoice-option.html b/resources/templates/transaction-edit/invoice-option.html deleted file mode 100644 index fbb6399e..00000000 --- a/resources/templates/transaction-edit/invoice-option.html +++ /dev/null @@ -1,6 +0,0 @@ -
- {{ number }} - {{ vendor }} - {{ date }} - {{ amount }} -
diff --git a/resources/templates/transaction-edit/linked-payment.html b/resources/templates/transaction-edit/linked-payment.html deleted file mode 100644 index 2a77363b..00000000 --- a/resources/templates/transaction-edit/linked-payment.html +++ /dev/null @@ -1,27 +0,0 @@ -
-

Linked Payment{{ external_link|safe }}

-
-
-
Payment #
-
{{ number }}
-
-
-
Vendor
-
{{ vendor }}
-
-
-
Amount
-
{{ amount }}
-
-
-
Status
-
{{ status }}
-
-
-
Date
-
{{ date }}
-
- {{ payment_id_hidden|safe }}
{{ unlink_button|safe }} -
-
-
diff --git a/resources/templates/transaction-edit/links-body.html b/resources/templates/transaction-edit/links-body.html deleted file mode 100644 index 7c610508..00000000 --- a/resources/templates/transaction-edit/links-body.html +++ /dev/null @@ -1,32 +0,0 @@ -{# The single step's body: memo + the activeForm tab switcher (link payment / unpaid / - autopay / rule / manual) + the five x-show panels. Fragments are pre-rendered. #} -
-
- {{ memo_field|safe }} -
-
{{ action_hidden|safe }}{{ tabs|safe }}
-
{{ panel_payment|safe }}
-
{{ panel_unpaid|safe }}
-
{{ panel_autopay|safe }}
-
{{ panel_rule|safe }}
-
-
{{ panel_manual|safe }}
-
-
-
-
diff --git a/resources/templates/transaction-edit/manual-coding.html b/resources/templates/transaction-edit/manual-coding.html deleted file mode 100644 index fa843326..00000000 --- a/resources/templates/transaction-edit/manual-coding.html +++ /dev/null @@ -1,11 +0,0 @@ -{# Vendor field (a change repopulates the default account via a whole-form swap) + either - the simple single-row coding or the advanced account grid. #} -
- {{ mode_hidden|safe }}
{{ vendor_field|safe }} -
-{% if is_simple %} -
{{ simple_mode|safe }}
-{% else %} -
{{ toggle_link|safe }}{{ accounts_field|safe }}
-{% endif %} -
diff --git a/resources/templates/transaction-edit/panel-empty.html b/resources/templates/transaction-edit/panel-empty.html deleted file mode 100644 index 78e81f16..00000000 --- a/resources/templates/transaction-edit/panel-empty.html +++ /dev/null @@ -1 +0,0 @@ -
{{ message }}
diff --git a/resources/templates/transaction-edit/panel-list.html b/resources/templates/transaction-edit/panel-list.html deleted file mode 100644 index 49ba2382..00000000 --- a/resources/templates/transaction-edit/panel-list.html +++ /dev/null @@ -1,10 +0,0 @@ -{# Shared shell for the autopay/unpaid/rule match panels: heading + action hidden + - prompt label + a radio-card of options. #} -
-

{{ heading }}

- {{ action_hidden|safe }} -
- - {{ radio|safe }} -
-
diff --git a/resources/templates/transaction-edit/payment-matches.html b/resources/templates/transaction-edit/payment-matches.html deleted file mode 100644 index 6d1ff5d5..00000000 --- a/resources/templates/transaction-edit/payment-matches.html +++ /dev/null @@ -1,2 +0,0 @@ -{# Outer wrapper for the payment tab; the unlink-payment route swaps #payment-matches. #} -
{{ inner|safe }}
diff --git a/resources/templates/transaction-edit/rule-option.html b/resources/templates/transaction-edit/rule-option.html deleted file mode 100644 index 5247e3bd..00000000 --- a/resources/templates/transaction-edit/rule-option.html +++ /dev/null @@ -1,4 +0,0 @@ -
- {{ note }} - {{ description }} -
diff --git a/resources/templates/transaction-edit/simple-mode.html b/resources/templates/transaction-edit/simple-mode.html deleted file mode 100644 index 40d6eb10..00000000 --- a/resources/templates/transaction-edit/simple-mode.html +++ /dev/null @@ -1,14 +0,0 @@ -{# Simple mode: a single account row (account typeahead + location select) rendered at a - fixed index 0, plus the link to switch to the advanced grid. Selecting the account - swaps just the location cell (#simple-account-location). #} -
- {{ row_id_hidden|safe }} -
- {{ account_field|safe }} -
{{ location_field|safe }}
- {{ amount_hidden|safe }} -
-
- -
diff --git a/resources/templates/transaction-edit/transitioner.html b/resources/templates/transaction-edit/transitioner.html deleted file mode 100644 index f96ae53d..00000000 --- a/resources/templates/transaction-edit/transitioner.html +++ /dev/null @@ -1,3 +0,0 @@ -{# Wrapper the modal stack expects around the opened form (the wizard transition hooks - are gone -- there is only one step). #} -
{{ body|safe }}
diff --git a/src/clj/auto_ap/ssr/components.clj b/src/clj/auto_ap/ssr/components.clj index efe54ac0..95490402 100644 --- a/src/clj/auto_ap/ssr/components.clj +++ b/src/clj/auto_ap/ssr/components.clj @@ -30,6 +30,7 @@ (def success-modal dialog/success-modal-) (def modal-card dialog/modal-card-) (def modal-card-advanced dialog/modal-card-advanced-) +(def single-modal-card dialog/single-modal-card-) (def modal-header dialog/modal-header-) (def modal-header-attachment dialog/modal-header-attachment-) (def modal-body dialog/modal-body-) diff --git a/src/clj/auto_ap/ssr/components/dialog.clj b/src/clj/auto_ap/ssr/components/dialog.clj index 2cd38ab6..e9db4c96 100644 --- a/src/clj/auto_ap/ssr/components/dialog.clj +++ b/src/clj/auto_ap/ssr/components/dialog.clj @@ -34,7 +34,7 @@ [:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex (hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"}) [:span {:class "w-2 h-2 mr-1 bg-red-500 rounded-full"}] [:span.px-2.py-0.5 "An unexpected error has occured. Integreat staff have been notified."]] (when (:error params) - [:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex {:class "dark:bg-red-900 dark:text-red-300"} + [:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex {:class "dark:bg-red-900 dark:text-red-300"} [:span {:class "w-2 h-2 mr-1 bg-red-500 rounded-full"}] [:span.px-2.py-0.5 (:error params)]]) [:div {:class "shrink-0"} @@ -63,9 +63,25 @@ (defn modal-card-advanced- [params & children] [:div (merge params - {:class (hh/add-class "modal-card bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col max-h-screen max-w-screen" (:class params ""))}) + {:class (hh/add-class "modal-card bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col max-h-screen max-w-screen" (:class params ""))}) children]) +(defn single-modal-card- + "Single-step modal-card chrome (header / optional side panel / body / footer) at the + standard md:w-[950px] md:h-[650px] size. Enter triggers the footer save button via + $refs.next. Reproduces the former Selmer templates/components/modal-card.html and + transaction-edit/edit-modal.html, so the modal-size classes live in one Clojure source." + [{:keys [side-panel]} head body footer] + [:div {:class "modal-card bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col max-h-screen max-w-screen md:w-[950px] md:h-[650px] w-full h-full last-modal-step transition duration-150" + "@keydown.enter.prevent.stop" "if ($refs.next) {$refs.next.click()}" + :x-data ""} + [:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} head] + [:div {:class "flex shrink overflow-auto grow"} + (when side-panel + [:div {:class "grow-0 w-64 bg-gray-50 border-r hidden md:block overflow-y-auto max-h-full"} side-panel]) + [:div {:class "px-6 py-2 space-y-6 overflow-y-scroll w-full shrink grow"} body]] + [:div {:class "p-4 border-t"} footer]]) + (defn success-modal- [{:keys [title]} & children] (modal- {} (modal-card-advanced- diff --git a/src/clj/auto_ap/ssr/components/selmer.clj b/src/clj/auto_ap/ssr/components/selmer.clj deleted file mode 100644 index 6f985703..00000000 --- a/src/clj/auto_ap/ssr/components/selmer.clj +++ /dev/null @@ -1,319 +0,0 @@ -(ns auto-ap.ssr.components.selmer - "Selmer-rendered versions of the shared SSR components used by the Transaction Edit - modal (see .claude/skills/ssr-form-migration). Each wrapper assembles a plain-data - context and renders its own template under resources/templates/components/ via the - interop bridge -- the element structure lives entirely in the .html templates; the - only Clojure is data assembly. Dynamic HTMX/Alpine attributes (which vary per call - site) are serialized to an attribute string by `attrs->str` and injected with - {{ attrs|safe }}, so the templates stay free of per-attribute {% if %} ladders. - - Reuses class logic from auto-ap.ssr.components.inputs so output matches the Hiccup - components byte-for-byte modulo Tailwind class ordering (verify by string-match + - e2e, never byte-parity -- see selmer-conventions.md)." - (:require - [auto-ap.ssr.components.inputs :as inputs] - [auto-ap.ssr.hiccup-helper :as hh] - [auto-ap.ssr.hx :as hx] - [auto-ap.ssr.selmer :as sel] - [clojure.string :as str] - [hiccup.util :as hu])) - -(defn- attr-name [k] - (if (keyword? k) (subs (str k) 1) (str k))) - -(defn attrs->str - "Serialize an attribute map to an HTML attribute string with a leading space, so it - concatenates after fixed template attributes: . - nil/false values are dropped, true renders a bare boolean attribute, everything else - renders name=\"escaped-value\". Mirrors how hiccup2 emits attributes." - [m] - (->> m - (keep (fn [[k v]] - (cond - (nil? v) nil - (false? v) nil - (true? v) (str " " (attr-name k)) - :else (str " " (attr-name k) "=\"" - (hu/escape-html (if (keyword? v) (name v) (str v))) - "\"")))) - (apply str))) - -(defn render - "Render a component partial and trim outer whitespace (so {# comments #} and the - file's trailing newline don't leak into the embedding tree). Returns a raw-wrapped - string ready to drop into Hiccup or another Selmer context value." - [template ctx] - (sel/raw (str/trim (sel/render template ctx)))) - -(defn- body->html - "Render child content (Hiccup vectors and/or raw Selmer fragments) to an HTML string." - [body] - (->> (if (sequential? body) body [body]) - (remove nil?) - (map sel/hiccup->html) - (apply str))) - -;; --- leaf inputs ----------------------------------------------------------------- - -(defn hidden [{:keys [name value] :as params}] - (render "templates/components/hidden.html" - {:attrs (attrs->str (merge {:name name} - (when (some? value) {:value value}) - (dissoc params :name :value)))})) - -(defn text-input [{:keys [size] :as params}] - (let [attrs (-> params - (dissoc :error? :size) - (assoc :type "text" :autocomplete "off") - (update :class #(-> "" - (hh/add-class inputs/default-input-classes) - (hh/add-class %))) - (update :class #(str % (inputs/use-size size))))] - (render "templates/components/text-input.html" {:attrs (attrs->str attrs)}))) - -(defn money-input-ctx - "Plain-data context for templates/components/money-input.html. The class base is owned - by the template; this passes the non-class attributes (name/value/...) and the variant - class (caller width + size). Split out so a template can include the partial directly." - [{:keys [size class] :as params}] - {:variant (str (or class "") (inputs/use-size size)) - :attrs (attrs->str (dissoc params :class :size))}) - -(defn money-input [params] - (render "templates/components/money-input.html" (money-input-ctx params))) - -(defn select-ctx - "Plain-data context for templates/components/select.html. options = [[value label] ...]; - `value` (string or keyword) marks the selected option. Split out so a template can - {% include %} the partial via {% with %} without re-deriving classes/selection." - [{:keys [name value options class] :as params}] - (let [sel (cond-> value (keyword? value) clojure.core/name) - attrs (dissoc params :name :value :options :class)] - {:name name - :variant (or class "") - :attrs (attrs->str attrs) - :options (for [[v label] options] - {:value v :label label :selected (= (str v) (str sel))})})) - -(defn select - "Generic (the same select-keys filter; :hx-vals / :hx-select - are intentionally dropped, matching existing behavior)." - [{:keys [options name title size orientation width] :or {size :medium width "w-48"} - selected-value :value :as params}] - (let [htmx-attrs (select-keys params [:hx-post :hx-target :hx-swap :hx-include :hx-trigger]) - sel (cond-> selected-value (keyword? selected-value) clojure.core/name) - ul-class (cond-> " text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white" - (= orientation :horizontal) (-> (hh/add-class "flex gap-2 flex-wrap") - (hh/remove-wildcard ["w-" "rounded-lg" "border" "bg-"])) - :always (str " " width " ")) - li-class (cond-> "w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600" - (= orientation :horizontal) (-> (hh/remove-wildcard ["w-full" "rounded-"]) - (hh/add-class "w-auto shrink-0 block rounded-lg border border-gray-200 dark:border-gray-600 px-3"))) - div-class (cond-> "flex items-center" - (not= orientation :horizontal) (hh/add-class "pl-3")) - input-class (cond-> "w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500" - (= size :small) (str " text-xs") - (= size :medium) (str " text-sm")) - label-class (cond-> "w-full ml-2 font-medium text-gray-900 dark:text-gray-300" - (= size :small) (str " text-xs py-2") - (= size :medium) (str " text-sm py-3") - (= orientation :horizontal) (hh/remove-class "w-full"))] - (render "templates/components/radio-card.html" - {:ul_class ul-class :li_class li-class :div_class div-class - :input_class input-class :label_class label-class - :name name - :input_attrs (attrs->str htmx-attrs) - :options (for [{:keys [value content]} options] - {:id (str "list-" name "-" value) - :value value - :checked (= sel value) - :content (body->html content)})}))) - -;; --- data grid ------------------------------------------------------------------- - -(defn data-grid-header [params & body] - (render "templates/components/data-grid-header.html" - {:klass (:class params) - :click (format "$dispatch('sorted', {key: '%s'})" (:sort-key params)) - :sort_key (:sort-key params) - :attrs (attrs->str (cond-> {} (:style params) (assoc :style (:style params)))) - :body (body->html body)})) - -(defn data-grid-row [params & body] - (render "templates/components/data-grid-row.html" - {:classes (str (:class params) " border-b dark:border-gray-600 group hover:bg-gray-100 dark:hover:bg-gray-700") - :attrs (attrs->str (dissoc params :class)) - :body (body->html body)})) - -(defn data-grid-cell [params & body] - (render "templates/components/data-grid-cell.html" - {:klass (:class params) - :attrs (attrs->str (dissoc params :class)) - :body (body->html body)})) - -(defn data-grid - "Table shell: outer scroll div > table > thead(headers) > tbody(rows) + optional - footer-tbody. `headers`, `rows`, and `footer-tbody` are pre-rendered fragments." - [{:keys [headers footer-tbody] :as params} & rows] - (render "templates/components/data-grid.html" - {:table_class "w-full text-sm text-left text-gray-500 dark:text-gray-400 shrink" - :table_attrs (attrs->str (dissoc params :headers :thead-params :footer-tbody)) - :thead_class "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 group-[.raw]:sticky group-[.raw]:z-10 group-[.raw]:top-0" - :headers (body->html headers) - :rows (body->html rows) - :footer_tbody (when footer-tbody (body->html footer-tbody))})) - -;; --- modal + typeahead ----------------------------------------------------------- - -(defn modal [{:as params} & children] - (render "templates/components/modal.html" - {:classes (hh/add-class "" (:class params "")) - :attrs (attrs->str (dissoc params :handle-unexpected-error? :class)) - :body (body->html children)})) - -(defn typeahead-ctx - "Build the plain-data context map for templates/components/typeahead.html. Resolves the - initial {value,label} server-side via value-fn/content-fn (DB lookups), builds the - Alpine x-data, and serializes the hidden posting-input attributes. Split out from - `typeahead` so a fully template-driven grid can feed the same partial per row (via - {% with %}) without re-deriving any of this logic. Every value is a string/boolean." - [{:keys [value value-fn content-fn x-model x-init id class placeholder disabled url] - :as params}] - (let [vf (or value-fn identity) - cf (or content-fn identity) - vval (vf value) - vlabel (cf value) - x-data (hx/json {:baseUrl (str url) - :value {:value vval :label vlabel} - :tippy nil :search "" :active -1 - :elements (if vval [{:value vval :label vlabel}] [])}) - a-xinit (str "$nextTick(() => tippy = $el.__x_tippy); " x-init) - hidden-attrs (-> params - (dissoc :class :value-fn :content-fn :placeholder :x-model) - (assoc "x-ref" "hidden" :type "hidden" ":value" "value.value" - :x-init "$watch('value', v => { $el.value = (v && v.value != null) ? v.value : ''; $nextTick(() => $dispatch('change')); }); "))] - {:x_data x-data - :x_model x-model - :key (when id (str id "--" vval)) - :disabled disabled - :width (or class "") - :a_xinit a-xinit - :placeholder placeholder - :hidden_attrs (attrs->str hidden-attrs)})) - -(defn typeahead - "Selmer port of com/typeahead. Preserves every tippy?. null-guard. See typeahead-ctx." - [params] - (render "templates/components/typeahead.html" (typeahead-ctx params))) diff --git a/src/clj/auto_ap/ssr/invoices.clj b/src/clj/auto_ap/ssr/invoices.clj index 4b50efec..b3de5c1d 100644 --- a/src/clj/auto_ap/ssr/invoices.clj +++ b/src/clj/auto_ap/ssr/invoices.clj @@ -30,12 +30,10 @@ [auto-ap.ssr.common-handlers :refer [add-new-entity-handler]] [auto-ap.ssr.components :as com] [auto-ap.ssr.components.link-dropdown :refer [link-dropdown]] - [auto-ap.ssr.components.selmer :as sc] [auto-ap.ssr.components.wizard-state :as ws] [auto-ap.ssr.components.wizard2 :as wizard2] [auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]] [auto-ap.ssr.nested-form-params :as nfp :refer [wrap-nested-form-params]] - [auto-ap.ssr.selmer :as sel] [auto-ap.ssr.transaction.edit :as tx-edit] [auto-ap.ssr.hiccup-helper :as hh] [auto-ap.ssr.hx :as hx] @@ -1473,16 +1471,16 @@ (defn- account-typeahead* [{:keys [name value client-id x-model]}] - (sc/typeahead {:name name - :placeholder "Search..." - :url (hu/url (bidi/path-for ssr-routes/only-routes :account-search) - {:purpose "invoice"}) - :id name - :x-model x-model - :value value - :content-fn (fn [value] - (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) - client-id)))})) + (com/typeahead {:name name + :placeholder "Search..." + :url (hu/url (bidi/path-for ssr-routes/only-routes :account-search) + {:purpose "invoice"}) + :id name + :x-model x-model + :value value + :content-fn (fn [value] + (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) + client-id)))})) ;; TODO clientize (defn all-ids-not-locked [all-ids] @@ -1514,7 +1512,7 @@ :hx-select (str "#account-location-" index) :hx-swap "outerHTML" :hx-include "closest form"}] - (sc/data-grid-row + (com/data-grid-row (-> {:class "account-row" :id (str "account-row-" index) :x-data (hx/json {:show (boolean (not (:new? value))) @@ -1522,47 +1520,47 @@ :data-key "show" :x-ref "p"} hx/alpine-mount-then-appear) - (sc/hidden {:name (account-field-name index :db/id) - :value (:db/id value)}) - (sc/data-grid-cell + (com/hidden {:name (account-field-name index :db/id) + :value (:db/id value)}) + (com/data-grid-cell {} - (sc/validated-field + (com/validated-field {:errors (account-field-errors index :account)} (account-typeahead* {:value account-val :client-id client-id :name (account-field-name index :account) :x-model "accountId"}))) - (sc/data-grid-cell + (com/data-grid-cell {:id (str "account-location-" index)} - (sc/validated-field + (com/validated-field (merge {:errors (account-field-errors index :location)} location-attrs) (tx-edit/location-select* {:name (account-field-name index :location) :account-location (:account/location (when (nat-int? account-val) (dc/pull (dc/db conn) '[:account/location] account-val))) :value (:location value)}))) - (sc/data-grid-cell + (com/data-grid-cell {} - (sc/validated-field + (com/validated-field {:errors (account-field-errors index :percentage)} - (sc/money-input {:name (account-field-name index :percentage) - :class "w-16 amount-field" - :value (some-> (:percentage value) (* 100) long) - :hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-form-changed) - :hx-target "#expense-totals" - :hx-select "#expense-totals" - :hx-swap "outerHTML" - :hx-trigger "keyup changed delay:300ms" - :hx-include "closest form"}))) - (sc/data-grid-cell - {:class "align-top"} - (sc/a-icon-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-form-changed) - :hx-vals (hx/json {:op "remove-account" :row-index (or index 0)}) - :hx-target "#bulk-edit-form" - :hx-select "#bulk-edit-form" + (com/money-input {:name (account-field-name index :percentage) + :class "w-16 amount-field" + :value (some-> (:percentage value) (* 100) long) + :hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-form-changed) + :hx-target "#expense-totals" + :hx-select "#expense-totals" :hx-swap "outerHTML" - :hx-include "closest form" - :class "account-remove-action"} - svg/x))))) + :hx-trigger "keyup changed delay:300ms" + :hx-include "closest form"}))) + (com/data-grid-cell + {:class "align-top"} + (com/a-icon-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-form-changed) + :hx-vals (hx/json {:op "remove-account" :row-index (or index 0)}) + :hx-target "#bulk-edit-form" + :hx-select "#bulk-edit-form" + :hx-swap "outerHTML" + :hx-include "closest form" + :class "account-remove-action"} + svg/x))))) (defn- expense-total* [request] (let [total (->> (-> request :bulk-state :expense-accounts) @@ -1577,36 +1575,33 @@ (filter number?) (reduce + 0.0)) balance (- 100.0 (* 100.0 total))] - (sel/raw (str "" (format "%.1f%%" balance) "")))) + [:span (when-not (dollars= 0.0 balance) {:class "text-red-300"}) + (format "%.1f%%" balance)])) (defn- expense-totals-tbody* "The separately-swappable TOTAL/BALANCE (#expense-totals, Rule 4 target)." [request] - (sel/render->hiccup - "templates/invoice-bulk-edit/expense-totals.html" - {:rows (str - (sc/data-grid-row {} - (sc/data-grid-cell {}) - (sc/data-grid-cell {:class "text-right"} (sel/raw "TOTAL")) - (sc/data-grid-cell {:id "expense-total" :class "text-right"} (expense-total* request)) - (sc/data-grid-cell {})) - (sc/data-grid-row {} - (sc/data-grid-cell {}) - (sc/data-grid-cell {:class "text-right"} (sel/raw "BALANCE")) - (sc/data-grid-cell {:id "expense-balance" :class "text-right"} (expense-balance* request)) - (sc/data-grid-cell {})))})) + [:tbody {:id "expense-totals"} + (com/data-grid-row {} + (com/data-grid-cell {}) + (com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"]) + (com/data-grid-cell {:id "expense-total" :class "text-right"} (expense-total* request)) + (com/data-grid-cell {})) + (com/data-grid-row {} + (com/data-grid-cell {}) + (com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "BALANCE"]) + (com/data-grid-cell {:id "expense-balance" :class "text-right"} (expense-balance* request)) + (com/data-grid-cell {}))]) (defn- account-grid* [request] (let [client-id (single-client-id request) accounts (vec (:expense-accounts (:bulk-state request)))] (apply - sc/data-grid - {:headers [(sc/data-grid-header {} "Account") - (sc/data-grid-header {:class "w-32"} "Location") - (sc/data-grid-header {:class "w-16"} "%") - (sc/data-grid-header {:class "w-16"})] + com/data-grid + {:headers [(com/data-grid-header {} "Account") + (com/data-grid-header {:class "w-32"} "Location") + (com/data-grid-header {:class "w-16"} "%") + (com/data-grid-header {:class "w-16"})] :footer-tbody (expense-totals-tbody* request)} (concat (map-indexed @@ -1615,17 +1610,17 @@ :client-id client-id :index index})) accounts) - [(sc/data-grid-row + [(com/data-grid-row {:class "new-row"} - (sc/data-grid-cell {:colspan 4} - (sc/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-form-changed) - :hx-vals (hx/json {:op "new-account"}) - :hx-target "#bulk-edit-form" - :hx-select "#bulk-edit-form" - :hx-swap "outerHTML" - :hx-include "closest form" - :color :secondary} - "New account")))])))) + (com/data-grid-cell {:colspan 4} + (com/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-form-changed) + :hx-vals (hx/json {:op "new-account"}) + :hx-target "#bulk-edit-form" + :hx-select "#bulk-edit-form" + :hx-swap "outerHTML" + :hx-include "closest form" + :color :secondary} + "New account")))])))) (defn maybe-code-accounts [invoice account-rules valid-locations] (with-precision 2 @@ -1668,51 +1663,36 @@ (when-not (dollars= 1.0 expense-account-total) (form-validation-error (str "Expense account total (" expense-account-total ") does not equal 100%"))))) -(defn- form-errors-html [errors] - (str "
" - (when (seq errors) - (str "

" - (str/join ", " (filter string? errors)) - "

")) - "
")) - (defn- footer* [request] - (sel/raw - (str "
" - (form-errors-html (:errors (:form-errors request))) - (str (sc/button {:color :primary :x-ref "next" :class "w-32" :type "submit"} "Save")) - "
"))) + [:div.flex.justify-end + [:div.flex.items-baseline.gap-x-4 + (com/form-errors {:errors (seq (:errors (:form-errors request)))}) + (com/button {:color :primary :x-ref "next" :class "w-32" :type "submit"} "Save")]]) (defn render-form "Renders the whole plain bulk-edit form (no wizard). Binds *errors* so the field-level lookups resolve. Reuses the edit modal chrome." [request] (binding [*errors* (or (:form-errors request) {})] - (let [ids (:ids (:bulk-state request)) - ids-hidden (apply str - (map-indexed (fn [i id] - (str (sc/hidden {:name (path->name2 :ids i) :value id}))) - ids)) - body (str "
" - (str (sc/validated-field - {:errors (ferr :expense-accounts)} - (sel/raw (str (account-grid* request))))) - "
") - modal-card (sel/render "templates/transaction-edit/edit-modal.html" - {:head (str "
Bulk editing " (count ids) " invoices
") - :side_panel nil - :body body - :footer (str (footer* request))})] - (sel/render->hiccup - "templates/invoice-bulk-edit/edit-form.html" - {:ids_hidden ids-hidden - :form_attrs (sc/attrs->str {:hx-ext "response-targets" - :hx-swap "outerHTML" - :hx-target-400 "#form-errors .error-content" - :hx-trigger "submit" - :hx-target "this" - :hx-put (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-submit)}) - :modal (str (sc/modal {:id "wizardmodal"} (sel/raw modal-card)))})))) + (let [ids (:ids (:bulk-state request))] + [:form {:id "bulk-edit-form" + :hx-ext "response-targets" + :hx-swap "outerHTML" + :hx-target-400 "#form-errors .error-content" + :hx-trigger "submit" + :hx-target "this" + :hx-put (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-submit)} + (map-indexed (fn [i id] (com/hidden {:name (path->name2 :ids i) :value id})) ids) + (com/modal + {:id "wizardmodal"} + (com/single-modal-card + {} + [:div.p-2 (str "Bulk editing " (count ids) " invoices")] + [:div.space-y-4.p-4 + (com/validated-field + {:errors (ferr :expense-accounts)} + (account-grid* request))] + (footer* request)))]))) (defn apply-new-account "bulk-edit-form-changed op: append a fresh (blank, Shared) expense-account row." @@ -1749,8 +1729,8 @@ (defn open-handler [request] (modal-response - (sel/render->hiccup "templates/transaction-edit/transitioner.html" - {:body (str (render-form request))}))) + [:div {:id "transitioner" :class "flex-1"} + (render-form request)])) (defn- render-form-response [request] (html-response (render-form request) diff --git a/src/clj/auto_ap/ssr/pos/sales_summaries.clj b/src/clj/auto_ap/ssr/pos/sales_summaries.clj index 95774393..55cb7e05 100644 --- a/src/clj/auto_ap/ssr/pos/sales_summaries.clj +++ b/src/clj/auto_ap/ssr/pos/sales_summaries.clj @@ -11,13 +11,11 @@ [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] [auto-ap.ssr.components.link-dropdown :refer [link-dropdown]] - [auto-ap.ssr.components.selmer :as sc] [auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]] [auto-ap.ssr.hx :as hx] [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.pos.common :refer [date-range-field*]] - [auto-ap.ssr.selmer :as sel] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils :refer [->db-id apply-middleware-to-all-handlers assert-schema @@ -328,21 +326,21 @@ (->> items (keep :credit) (filter number?) (reduce + 0.0))) ;; --------------------------------------------------------------------------- -;; Render (Selmer): account typeahead, inline account cell (display/edit), +;; Render (Hiccup): account typeahead, inline account cell (display/edit), ;; the read-only auto rows, the editable manual rows, totals/balance. ;; --------------------------------------------------------------------------- (defn account-typeahead* [{:keys [name value client-id]}] - (sc/typeahead {:name name - :id name - :placeholder "Search..." - :url (hu/url (bidi/path-for ssr-routes/only-routes :account-search) - {:client-id client-id - :purpose "invoice"}) - :value value - :content-fn (fn [value] - (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) - client-id)))})) + (com/typeahead {:name name + :id name + :placeholder "Search..." + :url (hu/url (bidi/path-for ssr-routes/only-routes :account-search) + {:client-id client-id + :purpose "invoice"}) + :value value + :content-fn (fn [value] + (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) + client-id)))})) (defn account-display-cell* "Account name + inline-edit pencil. The pencil swaps just this `.account-cell` @@ -352,47 +350,45 @@ account-name (when account-id (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read account-id) client-id)))] - (str "
" - (str (sc/hidden {:name (item-field-name index :ledger-mapped/account) - :value (or account-id "")})) - (if account-name - (str "" (hu/escape-html account-name) "") - (str (sel/hiccup->html (com/pill {:color :red} "Missing acct")))) - (str (sc/a-icon-button {:class "p-1" - :hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-item-account) - :hx-target "closest .account-cell" - :hx-swap "outerHTML" - :hx-vals (hx/json {:item-index index - :client-id client-id - :current-account-id (or account-id "")})} - svg/pencil)) - "
"))) + [:div.account-cell.flex.items-center.gap-2 + (com/hidden {:name (item-field-name index :ledger-mapped/account) + :value (or account-id "")}) + (if account-name + [:span.text-sm account-name] + (com/pill {:color :red} "Missing acct")) + (com/a-icon-button {:class "p-1" + :hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-item-account) + :hx-target "closest .account-cell" + :hx-swap "outerHTML" + :hx-vals (hx/json {:item-index index + :client-id client-id + :current-account-id (or account-id "")})} + svg/pencil)])) (defn account-edit-cell* "The account typeahead + check (save) / cancel buttons. Each swaps just the `.account-cell` back to the display cell." [{:keys [index account-id client-id]}] - (str "
" - (str (account-typeahead* {:name (item-field-name index :ledger-mapped/account) - :value account-id - :client-id client-id})) - "
" - (str (sc/a-icon-button {:class "p-1" - :hx-put (bidi/path-for ssr-routes/only-routes ::route/save-item-account) - :hx-target "closest .account-cell" - :hx-swap "outerHTML" - :hx-include "closest .account-cell" - :hx-vals (hx/json {:item-index index :client-id client-id})} - svg/check)) - (str (sc/a-icon-button {:class "p-1" - :hx-get (bidi/path-for ssr-routes/only-routes ::route/cancel-item-account) - :hx-target "closest .account-cell" - :hx-swap "outerHTML" - :hx-vals (hx/json {:item-index index - :client-id client-id - :current-account-id (or account-id "")})} - svg/x)) - "
")) + [:div.account-cell.flex.flex-col.gap-2 + (account-typeahead* {:name (item-field-name index :ledger-mapped/account) + :value account-id + :client-id client-id}) + [:div.flex.gap-1 + (com/a-icon-button {:class "p-1" + :hx-put (bidi/path-for ssr-routes/only-routes ::route/save-item-account) + :hx-target "closest .account-cell" + :hx-swap "outerHTML" + :hx-include "closest .account-cell" + :hx-vals (hx/json {:item-index index :client-id client-id})} + svg/check) + (com/a-icon-button {:class "p-1" + :hx-get (bidi/path-for ssr-routes/only-routes ::route/cancel-item-account) + :hx-target "closest .account-cell" + :hx-swap "outerHTML" + :hx-vals (hx/json {:item-index index + :client-id client-id + :current-account-id (or account-id "")})} + svg/x)]]) (defn- auto-item-row* "A read-only auto item in its Debits/Credits column: category + inline-editable account @@ -400,57 +396,55 @@ [index item client-id] (let [side (item-side item) amount (if (= side :debit) (:debit item) (:credit item))] - (str "
" - (str (sc/hidden {:name (item-field-name index :db/id) :value (:db/id item)})) - (str (sc/hidden {:name (item-field-name index :sales-summary-item/category) - :value (:sales-summary-item/category item)})) - "" (hu/escape-html (str (:sales-summary-item/category item))) "" - (str (account-display-cell* {:index index - :account-id (:ledger-mapped/account item) - :client-id client-id})) - "" (format "$%,.2f" (or amount 0.0)) "" - "
"))) + [:div.flex.items-center.gap-2.text-sm + (com/hidden {:name (item-field-name index :db/id) :value (:db/id item)}) + (com/hidden {:name (item-field-name index :sales-summary-item/category) + :value (:sales-summary-item/category item)}) + [:span.text-gray-500.flex-1 (str (:sales-summary-item/category item))] + (account-display-cell* {:index index + :account-id (:ledger-mapped/account item) + :client-id client-id}) + [:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (or amount 0.0))]])) (defn- manual-amount-input* [index field item] - (sc/money-input {:name (item-field-name index field) - :value (get item field) - :class "w-24 text-right font-mono tabular-nums" - :placeholder (str/capitalize (clojure.core/name field)) - :hx-post (bidi/path-for ssr-routes/only-routes ::route/form-changed) - :hx-target "#summary-totals" - :hx-select "#summary-totals" - :hx-swap "outerHTML" - :hx-trigger "keyup changed delay:300ms" - :hx-include "closest form"})) + (com/money-input {:name (item-field-name index field) + :value (get item field) + :class "w-24 text-right font-mono tabular-nums" + :placeholder (str/capitalize (clojure.core/name field)) + :hx-post (bidi/path-for ssr-routes/only-routes ::route/form-changed) + :hx-target "#summary-totals" + :hx-select "#summary-totals" + :hx-swap "outerHTML" + :hx-trigger "keyup changed delay:300ms" + :hx-include "closest form"})) (defn- manual-item-row* "An editable manual item: category + account typeahead + debit + credit money inputs + remove. The amount inputs swap the totals block (Rule 4); remove swaps the whole form." [index item client-id] - (str "
" - (str (sc/hidden {:name (item-field-name index :db/id) :value (:db/id item)})) - (str (sc/hidden {:name (item-field-name index :sales-summary-item/manual?) :value "true"})) - (str (sc/validated-field - {:errors (item-field-errors index :sales-summary-item/category) :class "flex-1"} - (sc/text-input {:name (item-field-name index :sales-summary-item/category) - :value (:sales-summary-item/category item) - :placeholder "Category/Explanation"}))) - (str (sc/validated-field - {:errors (item-field-errors index :ledger-mapped/account)} - (account-typeahead* {:name (item-field-name index :ledger-mapped/account) - :value (:ledger-mapped/account item) - :client-id client-id}))) - (str (manual-amount-input* index :debit item)) - (str (manual-amount-input* index :credit item)) - (str (sc/a-icon-button {:class "p-1 account-remove-action" - :hx-post (bidi/path-for ssr-routes/only-routes ::route/form-changed) - :hx-vals (hx/json {:op "remove-item" :row-index index}) - :hx-target "#summary-edit-form" - :hx-select "#summary-edit-form" - :hx-swap "outerHTML" - :hx-include "closest form"} - svg/x)) - "
")) + [:div.manual-item-row.flex.items-center.gap-2 + (com/hidden {:name (item-field-name index :db/id) :value (:db/id item)}) + (com/hidden {:name (item-field-name index :sales-summary-item/manual?) :value "true"}) + (com/validated-field + {:errors (item-field-errors index :sales-summary-item/category) :class "flex-1"} + (com/text-input {:name (item-field-name index :sales-summary-item/category) + :value (:sales-summary-item/category item) + :placeholder "Category/Explanation"})) + (com/validated-field + {:errors (item-field-errors index :ledger-mapped/account)} + (account-typeahead* {:name (item-field-name index :ledger-mapped/account) + :value (:ledger-mapped/account item) + :client-id client-id})) + (manual-amount-input* index :debit item) + (manual-amount-input* index :credit item) + (com/a-icon-button {:class "p-1 account-remove-action" + :hx-post (bidi/path-for ssr-routes/only-routes ::route/form-changed) + :hx-vals (hx/json {:op "remove-item" :row-index index}) + :hx-target "#summary-edit-form" + :hx-select "#summary-edit-form" + :hx-swap "outerHTML" + :hx-include "closest form"} + svg/x)]) (defn- totals* "Swappable totals + balance block (#summary-totals). Fixes the previously-dead total / @@ -461,41 +455,34 @@ tc (sum-credits items) balanced? (dollars= td tc) delta (- td tc)] - (str "
" - "
Total" - "
" (format "$%,.2f" td) "" - "" (format "$%,.2f" tc) "
" - (if balanced? - "
Balanced
" - (str "
Unbalanced" - "" (format "$%,.2f" (Math/abs delta)) " " - (if (pos? delta) "Debit over" "Credit over") "
")) - "
"))) + [:div.border-t.pt-2.mt-2.space-y-1 + [:div.flex.justify-between.text-sm.font-semibold + [:span "Total"] + [:div.flex.gap-8 + [:span.font-mono (format "$%,.2f" td)] + [:span.font-mono (format "$%,.2f" tc)]]] + (if balanced? + [:div.text-sm.text-emerald-700.font-semibold "Balanced"] + [:div.text-sm.text-red-600.font-semibold.flex.justify-between + [:span "Unbalanced"] + [:span.font-mono (str (format "$%,.2f" (Math/abs delta)) " " + (if (pos? delta) "Debit over" "Credit over"))]])])) (defn- new-item-button* [] - (sc/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/form-changed) - :hx-vals (hx/json {:op "new-item"}) - :hx-target "#summary-edit-form" - :hx-select "#summary-edit-form" - :hx-swap "outerHTML" - :hx-include "closest form" - :color :secondary} - "New Summary Item")) - -(defn- form-errors-html [errors] - (str "
" - (when (seq errors) - (str "

" - (str/join ", " (filter string? errors)) - "

")) - "
")) + (com/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/form-changed) + :hx-vals (hx/json {:op "new-item"}) + :hx-target "#summary-edit-form" + :hx-select "#summary-edit-form" + :hx-swap "outerHTML" + :hx-include "closest form" + :color :secondary} + "New Summary Item")) (defn- footer* [request] - (sel/raw - (str "
" - (form-errors-html (:errors (:form-errors request))) - (str (sc/button {:color :primary :x-ref "next" :class "w-32" :type "submit"} "Save")) - "
"))) + [:div.flex.justify-end + [:div.flex.items-baseline.gap-x-4 + (com/form-errors {:errors (seq (:errors (:form-errors request)))}) + (com/button {:color :primary :x-ref "next" :class "w-32" :type "submit"} "Save")]]) (defn render-form "Renders the whole plain sales-summary edit form (no wizard). Binds *errors* so the @@ -509,36 +496,39 @@ manual (filter (fn [[_ it]] (:sales-summary-item/manual? it)) indexed) debit-rows (->> auto (filter (fn [[_ it]] (= :debit (item-side it)))) - (map (fn [[i it]] (auto-item-row* i it client-id))) - (apply str)) + (map (fn [[i it]] (auto-item-row* i it client-id)))) credit-rows (->> auto (filter (fn [[_ it]] (= :credit (item-side it)))) - (map (fn [[i it]] (auto-item-row* i it client-id))) - (apply str)) + (map (fn [[i it]] (auto-item-row* i it client-id)))) manual-rows (->> manual - (map (fn [[i it]] (manual-item-row* i it client-id))) - (apply str)) - body (sel/render "templates/sales-summary/summary-body.html" - {:debit_rows debit-rows - :credit_rows credit-rows - :totals (totals* items) - :manual_rows manual-rows - :new_item_button (str (new-item-button*))}) - modal-card (sel/render "templates/transaction-edit/edit-modal.html" - {:head "
Edit Summary
" - :side_panel nil - :body body - :footer (str (footer* request))})] - (sel/render->hiccup - "templates/sales-summary/edit-form.html" - {:db_id tx-id - :form_attrs (sc/attrs->str {:hx-ext "response-targets" - :hx-swap "outerHTML" - :hx-target-400 "#form-errors .error-content" - :hx-trigger "submit" - :hx-target "this" - :hx-put (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-submit)}) - :modal (str (sc/modal {:id "wizardmodal"} (sel/raw modal-card)))})))) + (map (fn [[i it]] (manual-item-row* i it client-id))))] + [:form (merge {:id "summary-edit-form"} + {:hx-ext "response-targets" + :hx-swap "outerHTML" + :hx-target-400 "#form-errors .error-content" + :hx-trigger "submit" + :hx-target "this" + :hx-put (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-submit)}) + (com/hidden {:name "db/id" :value tx-id}) + (com/modal + {:id "wizardmodal"} + (com/single-modal-card + {} + [:div.p-2 "Edit Summary"] + [:div.space-y-4.p-2 + [:div.grid.grid-cols-2.gap-6 + [:div + [:div.font-semibold.text-sm.mb-2 "Debits"] + [:div.space-y-1 debit-rows]] + [:div + [:div.font-semibold.text-sm.mb-2 "Credits"] + [:div.space-y-1 credit-rows]]] + [:div {:id "summary-totals"} (totals* items)] + [:div.mt-4.border-t.pt-3 + [:div.font-semibold.text-sm.mb-2 "Manual Items"] + [:div.space-y-2 {:id "manual-items"} manual-rows] + [:div.mt-2.flex.justify-center (new-item-button*)]]] + (footer* request)))]))) ;; --------------------------------------------------------------------------- ;; State: derive the flat edit-state from the entity overlaid with the posted @@ -646,7 +636,7 @@ idx (if (string? item-index) (Integer/parseInt item-index) item-index) account-id (when (and current-account-id (not= current-account-id "")) (->db-id current-account-id))] (html-response - (sel/raw (account-edit-cell* {:index idx :account-id account-id :client-id (->db-id client-id)}))))) + (account-edit-cell* {:index idx :account-id account-id :client-id (->db-id client-id)})))) (defn save-item-account [request] (let [item-index (get-in request [:params "item-index"]) @@ -655,14 +645,14 @@ account-id-str (get-in request [:form-params (item-field-name idx :ledger-mapped/account)]) account-id (when (and account-id-str (not= account-id-str "")) (->db-id account-id-str))] (html-response - (sel/raw (account-display-cell* {:index idx :account-id account-id :client-id client-id}))))) + (account-display-cell* {:index idx :account-id account-id :client-id client-id})))) (defn cancel-item-account [request] (let [{:keys [item-index client-id current-account-id]} (:query-params request) idx (if (string? item-index) (Integer/parseInt item-index) item-index) account-id (when (and current-account-id (not= current-account-id "")) (->db-id current-account-id))] (html-response - (sel/raw (account-display-cell* {:index idx :account-id account-id :client-id (->db-id client-id)}))))) + (account-display-cell* {:index idx :account-id account-id :client-id (->db-id client-id)})))) ;; --------------------------------------------------------------------------- ;; Open + submit @@ -670,8 +660,8 @@ (defn open-handler [request] (modal-response - (sel/render->hiccup "templates/transaction-edit/transitioner.html" - {:body (str (render-form request))}))) + [:div {:id "transitioner" :class "flex-1"} + (render-form request)])) (defn- render-form-response [request] (html-response (render-form request) diff --git a/src/clj/auto_ap/ssr/selmer.clj b/src/clj/auto_ap/ssr/selmer.clj deleted file mode 100644 index b96315e8..00000000 --- a/src/clj/auto_ap/ssr/selmer.clj +++ /dev/null @@ -1,43 +0,0 @@ -(ns auto-ap.ssr.selmer - "Selmer rendering + the Hiccup<->Selmer interop bridge for the SSR form/wizard - migration (see .claude/skills/ssr-form-migration). Interactive, attribute-heavy - components render from Selmer templates with plain-HTML Alpine/HTMX attributes; - the bridge lets a Selmer template embed Hiccup output and lets a Selmer fragment - sit inside a Hiccup tree during the strangler transition. - - Templates live under resources/templates/ and are referenced by classpath-relative - path, e.g. (render \"templates/components/typeahead.html\" ctx)." - (:require - [hiccup.util :as hu] - [hiccup2.core :as h2] - [selmer.parser :as selmer])) - -(defn hiccup->html - "Render a Hiccup form to an HTML string so it can be embedded in a Selmer - context value and emitted with the |safe filter: {{ frag|safe }}." - [hiccup] - (str (h2/html {} hiccup))) - -(defn raw - "Wrap an already-rendered HTML string (e.g. from `render`) so hiccup2 emits it - verbatim instead of escaping it. Use to drop a Selmer fragment into a Hiccup tree: - [:div (sel/raw (sel/render \"...\" ctx))]." - [^String html] - (hu/raw-string html)) - -(defn render - "Render a Selmer template file (classpath-relative path) with `ctx`, returning an - HTML string. Hiccup values in `ctx` should be pre-rendered via `hiccup->html` and - referenced with |safe in the template." - [template ctx] - (selmer/render-file template ctx)) - -(defn render-str - "Render a Selmer template given as a string (handy for tests/REPL)." - [template ctx] - (selmer/render template ctx)) - -(defn render->hiccup - "Render a Selmer template file and wrap the result for safe embedding in Hiccup." - [template ctx] - (raw (render template ctx))) diff --git a/src/clj/auto_ap/ssr/transaction/bulk_code.clj b/src/clj/auto_ap/ssr/transaction/bulk_code.clj index 02326dee..6cdc6619 100644 --- a/src/clj/auto_ap/ssr/transaction/bulk_code.clj +++ b/src/clj/auto_ap/ssr/transaction/bulk_code.clj @@ -11,17 +11,16 @@ [auto-ap.rule-matching :as rm] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] - [auto-ap.ssr.components.selmer :as sc] [auto-ap.ssr.grid-page-helper :refer [wrap-apply-sort]] [auto-ap.ssr.hx :as hx] [auto-ap.ssr.ledger :refer [wrap-ensure-bank-account-belongs]] [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] - [auto-ap.ssr.selmer :as sel] + [auto-ap.ssr.svg :as svg] [auto-ap.ssr.transaction.common :refer [grid-page query-schema selected->ids wrap-status-from-source]] - [auto-ap.ssr.transaction.edit :as edit :refer [account-typeahead-ctx - location-select-ctx]] + [auto-ap.ssr.transaction.edit :as edit :refer [account-typeahead* + location-select*]] [auto-ap.ssr.utils :refer [->db-id apply-middleware-to-all-handlers assert-schema entity-id form-validation-error html-response main-transformer modal-response @@ -65,6 +64,16 @@ is non-editable -- it is threaded separately by wrap-bulk-state." [:vendor :approval-status :accounts]) +(def ^:private approval-status-options + "[value label] choices for the status from its own row loop." +(defn location-select* + "The location for an account row, rendered from a Selmer template - (templates/components/location-select.html) -- the first interactive modal component - migrated off Hiccup. Same options/selection/styling as the old com/select, emitted as - plain HTML and embedded back into the Hiccup row via the interop bridge." - [params] - (sel/render->hiccup - "templates/components/location-select.html" - (location-select-ctx params))) + (com/select {:name name + :class "w-full" + :value selected + :options options}))) (defn- account-typeahead-params "Shared param map for the account typeahead (account-search url + clientized label - content-fn). Used by both account-typeahead* (renders) and account-typeahead-ctx - (returns the typeahead context for a template-driven grid)." + content-fn) used by account-typeahead*." [{:keys [name value client-id x-model]}] {:name name :placeholder "Search..." @@ -207,16 +195,9 @@ (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) client-id)))}) -(defn account-typeahead-ctx - "Plain-data typeahead context (sc/typeahead-ctx) for the account cell -- no flex-col - wrapper. Lets a template-driven row feed templates/components/typeahead.html via - {% with %} without re-deriving the url/content-fn." - [params] - (sc/typeahead-ctx (account-typeahead-params params))) - (defn account-typeahead* [params] - (wrap-div "flex flex-col" (sc/typeahead (account-typeahead-params params)))) + [:div.flex.flex-col (com/typeahead (account-typeahead-params params))]) (def ^:dynamic *errors* "Humanized form errors for the current render, keyed by edit-form-schema paths (e.g. @@ -242,15 +223,6 @@ (defn- account-field-errors [index field] (ferr :transaction/accounts index field)) -(defn wrap-div - "Trivial structural wrapper
around already-rendered HTML fragments. - Plain-string composition (not Hiccup) -- the substantive markup lives in Selmer - component templates; this just nests their output." - [class & body] - (sel/raw (str "
" - (apply str (map str (remove nil? body))) - "
"))) - (defn simple-mode-fields* "Renders the simple-mode account + location row and the toggle-to-advanced link. Must be called within a fc/start-form + fc/with-field :step-params context. @@ -285,36 +257,41 @@ :hx-select "#simple-account-location" :hx-swap "outerHTML" :hx-include "closest form"}] - (sel/render->hiccup - "templates/transaction-edit/simple-mode.html" - {:row_id_hidden (str (sc/hidden {:name (account-field-name 0 :db/id) :value row-id})) + [:div + [:span + (com/hidden {:name (account-field-name 0 :db/id) :value row-id}) ;; Selecting the account only affects the valid Location options, so the change ;; swaps just the #simple-account-location cell -- nothing else re-renders. - :account_field (str (sc/validated-field - {:label "Account" - :errors (account-field-errors 0 :transaction-account/account)} - (wrap-div "w-72" - (account-typeahead* {:value account-val - :client-id client-id - :name (account-field-name 0 :transaction-account/account) - :x-model "simpleAccountId"})))) - :location_field (str (sc/validated-field - (merge {:label "Location" - :errors (account-field-errors 0 :transaction-account/location)} - location-attrs) - (location-select* - {:name (account-field-name 0 :transaction-account/location) - :account-location (:account/location account-id) - :client-locations (pull-attr (dc/db conn) :client/locations client-id) - :value location-val}))) - :amount_hidden (str (sc/hidden {:name (account-field-name 0 :transaction-account/amount) - :value total})) - :toggle_attrs (sc/attrs->str {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) - :hx-vals (hx/json {:op "toggle-mode"}) - :hx-include "closest form" - :hx-target "#edit-form" - :hx-select "#edit-form" - :hx-swap "outerHTML"})}))) + [:div.flex.gap-2.mt-2 + (com/validated-field + {:label "Account" + :errors (account-field-errors 0 :transaction-account/account)} + [:div.w-72 + (account-typeahead* {:value account-val + :client-id client-id + :name (account-field-name 0 :transaction-account/account) + :x-model "simpleAccountId"})]) + [:div {:id "simple-account-location"} + (com/validated-field + (merge {:label "Location" + :errors (account-field-errors 0 :transaction-account/location)} + location-attrs) + (location-select* + {:name (account-field-name 0 :transaction-account/location) + :account-location (:account/location account-id) + :client-locations (pull-attr (dc/db conn) :client/locations client-id) + :value location-val}))] + (com/hidden {:name (account-field-name 0 :transaction-account/amount) + :value total})]] + [:div.mt-1 + [:a (merge {:class "text-sm text-blue-600 hover:underline cursor-pointer"} + {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) + :hx-vals (hx/json {:op "toggle-mode"}) + :hx-include "closest form" + :hx-target "#edit-form" + :hx-select "#edit-form" + :hx-swap "outerHTML"}) + "Switch to advanced mode"]]])) (defn- manual-mode-initial "Returns :simple or :advanced based on existing account row count." @@ -351,7 +328,7 @@ :hx-swap "outerHTML" :hx-trigger "keyup changed delay:300ms" :hx-include "closest form"}] - (sc/data-grid-row + (com/data-grid-row (-> {:class "account-row" :id (str "account-row-" index) :x-data (hx/json {:show (boolean (not (:new? value))) @@ -359,19 +336,19 @@ :data-key "show" :x-ref "p"} hx/alpine-mount-then-appear) - (sc/hidden {:name (account-field-name index :db/id) - :value (:db/id value)}) - (sc/data-grid-cell + (com/hidden {:name (account-field-name index :db/id) + :value (:db/id value)}) + (com/data-grid-cell {} - (sc/validated-field + (com/validated-field {:errors (account-field-errors index :transaction-account/account)} (account-typeahead* {:value account-val :client-id client-id :name (account-field-name index :transaction-account/account) :x-model "accountId"}))) - (sc/data-grid-cell + (com/data-grid-cell {:id (str "account-location-" index)} - (sc/validated-field + (com/validated-field (merge {:errors (account-field-errors index :transaction-account/location)} location-attrs) (location-select* {:name (account-field-name index :transaction-account/location) @@ -379,23 +356,23 @@ (dc/pull (dc/db conn) '[:account/location] account-val))) :client-locations (pull-attr (dc/db conn) :client/locations client-id) :value (:transaction-account/location value)}))) - (sc/data-grid-cell + (com/data-grid-cell {} - (sc/validated-field + (com/validated-field {:errors (account-field-errors index :transaction-account/amount)} (if (= "%" amount-mode) - (sc/text-input (assoc amount-attrs :type "number" :step "0.01")) - (sc/money-input amount-attrs)))) - (sc/data-grid-cell + (com/text-input (assoc amount-attrs :type "number" :step "0.01")) + (com/money-input amount-attrs)))) + (com/data-grid-cell {:class "align-top"} - (sc/a-icon-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) - :hx-vals (hx/json {:op "remove-account" :row-index (or index 0)}) - :hx-target "#edit-form" - :hx-select "#edit-form" - :hx-swap "outerHTML" - :hx-include "closest form" - :class "account-remove-action"} - (sc/render "templates/components/svg-x.html" {})))))) + (com/a-icon-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) + :hx-vals (hx/json {:op "remove-account" :row-index (or index 0)}) + :hx-target "#edit-form" + :hx-select "#edit-form" + :hx-swap "outerHTML" + :hx-include "closest form" + :class "account-remove-action"} + svg/x))))) (defn location-select [{{:keys [name account-id client-id value] :as qp} :query-params}] (html-response (location-select* {:name name @@ -428,9 +405,8 @@ (-> request :multi-form-state :snapshot :transaction/amount) 0.0)) total)] - (sel/raw (str "" (format "$%,.2f" balance) "")))) + [:span (when-not (dollars= 0.0 balance) {:class "text-red-300"}) + (format "$%,.2f" balance)])) (defn ->percentage [amount total] (when (and amount total (not= total 0)) @@ -460,29 +436,27 @@ amounts))))) (defn- bold-right [label] - (sel/raw (str "" label ""))) + [:span.font-bold.text-right label]) (defn account-totals-tbody* "The separately-swappable totals (Rule 4 target #account-totals)." [request total] - (sel/render->hiccup - "templates/transaction-edit/account-totals.html" - {:rows (str - (sc/data-grid-row {:class "account-total-row"} - (sc/data-grid-cell {}) - (sc/data-grid-cell {:class "text-right"} (bold-right "TOTAL")) - (sc/data-grid-cell {:id "total" :class "text-right"} (account-total* request)) - (sc/data-grid-cell {})) - (sc/data-grid-row {:class "account-balance-row"} - (sc/data-grid-cell {}) - (sc/data-grid-cell {:class "text-right"} (bold-right "BALANCE")) - (sc/data-grid-cell {:id "balance" :class "text-right"} (account-balance* request)) - (sc/data-grid-cell {})) - (sc/data-grid-row {:class "account-grand-total-row"} - (sc/data-grid-cell {}) - (sc/data-grid-cell {:class "text-right"} (bold-right "TRANSACTION TOTAL")) - (sc/data-grid-cell {:class "text-right"} (format "$%,.2f" total)) - (sc/data-grid-cell {})))})) + [:tbody {:id "account-totals"} + (com/data-grid-row {:class "account-total-row"} + (com/data-grid-cell {}) + (com/data-grid-cell {:class "text-right"} (bold-right "TOTAL")) + (com/data-grid-cell {:id "total" :class "text-right"} (account-total* request)) + (com/data-grid-cell {})) + (com/data-grid-row {:class "account-balance-row"} + (com/data-grid-cell {}) + (com/data-grid-cell {:class "text-right"} (bold-right "BALANCE")) + (com/data-grid-cell {:id "balance" :class "text-right"} (account-balance* request)) + (com/data-grid-cell {})) + (com/data-grid-row {:class "account-grand-total-row"} + (com/data-grid-cell {}) + (com/data-grid-cell {:class "text-right"} (bold-right "TRANSACTION TOTAL")) + (com/data-grid-cell {:class "text-right"} (format "$%,.2f" total)) + (com/data-grid-cell {}))]) (defn account-grid-body* [request] (let [snapshot (-> request :multi-form-state :snapshot) @@ -496,22 +470,22 @@ (:transaction/accounts snapshot) []))] (apply - sc/data-grid - {:headers [(sc/data-grid-header {} "Account") - (sc/data-grid-header {:class "w-32"} "Location") - (sc/data-grid-header {:class "w-16"} - (sc/radio-card {:options [{:value "$" :content "$"} - {:value "%" :content "%"}] - :value amount-mode - :name "amount-mode" - :orientation :horizontal - :hx-vals (hx/json {:op "toggle-amount-mode"}) - :hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) - :hx-target "#edit-form" - :hx-select "#edit-form" - :hx-swap "outerHTML" - :hx-include "closest form"})) - (sc/data-grid-header {:class "w-16"})] + com/data-grid + {:headers [(com/data-grid-header {} "Account") + (com/data-grid-header {:class "w-32"} "Location") + (com/data-grid-header {:class "w-16"} + (com/radio-card {:options [{:value "$" :content "$"} + {:value "%" :content "%"}] + :value amount-mode + :name "amount-mode" + :orientation :horizontal + :hx-vals (hx/json {:op "toggle-amount-mode"}) + :hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) + :hx-target "#edit-form" + :hx-select "#edit-form" + :hx-swap "outerHTML" + :hx-include "closest form"})) + (com/data-grid-header {:class "w-16"})] :footer-tbody (account-totals-tbody* request total)} (concat (map-indexed @@ -521,17 +495,17 @@ :amount-mode amount-mode :index index})) accounts) - [(sc/data-grid-row + [(com/data-grid-row {:class "new-row"} - (sc/data-grid-cell {:colspan 4} - (sc/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) - :hx-vals (hx/json {:op "new-account"}) - :hx-target "#edit-form" - :hx-select "#edit-form" - :hx-swap "outerHTML" - :hx-include "closest form" - :color :secondary} - "New account")))])))) + (com/data-grid-cell {:colspan 4} + (com/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) + :hx-vals (hx/json {:op "new-account"}) + :hx-target "#edit-form" + :hx-select "#edit-form" + :hx-swap "outerHTML" + :hx-include "closest form" + :color :secondary} + "New account")))])))) (defn manual-coding-section* "Renders the vendor field + account/location section for the manual tab. @@ -545,49 +519,44 @@ (seq (:transaction/accounts snapshot))) row-count (count all-accounts) vendor-val (:transaction/vendor step-params) - toggle-attrs (fn [] (sc/attrs->str {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) - :hx-vals (hx/json {:op "toggle-mode"}) - :hx-include "closest form" - :hx-target "#edit-form" - :hx-select "#edit-form" - :hx-swap "outerHTML"}))] - (sel/render->hiccup - "templates/transaction-edit/manual-coding.html" - {:mode_hidden (str (sc/hidden {:name "mode" :value (name mode)})) - :vendor_changed_attrs (sc/attrs->str {:hx-trigger "change" - :hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) - :hx-vals (hx/json {:op "vendor-changed"}) - :hx-target "#edit-form" - :hx-select "#edit-form" - :hx-swap "outerHTML" - :hx-sync "this:replace" - :hx-include "closest form"}) - :vendor_field (str (sc/validated-field - {:label "Vendor" :errors (ferr :transaction/vendor)} - (wrap-div "w-96" - (sc/typeahead {:name (fname :transaction/vendor) - :error? (boolean (seq (ferr :transaction/vendor))) - :class "w-96" - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes :vendor-search) - :value vendor-val - :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})))) - :is_simple (= mode :simple) - :simple_xdata (when (= mode :simple) - (hx/json {:simpleAccountId (let [av (-> (first all-accounts) :transaction-account/account)] - (if (map? av) (:db/id av) av))})) - :simple_mode (when (= mode :simple) (str (simple-mode-fields* request))) - :toggle_link (when (and (not= mode :simple) (<= row-count 1)) - (str (wrap-div "mb-2" - (sel/raw (str "Switch to simple mode"))))) - :accounts_field (when (not= mode :simple) - (str (sc/validated-field - {:errors (ferr :transaction/accounts)} - (sel/raw (str "
" - (str (account-grid-body* request)) - "
")))))}))) + toggle-attrs {:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) + :hx-vals (hx/json {:op "toggle-mode"}) + :hx-include "closest form" + :hx-target "#edit-form" + :hx-select "#edit-form" + :hx-swap "outerHTML"}] + [:div {:id "manual-coding-section"} + (com/hidden {:name "mode" :value (name mode)}) + [:div {:hx-trigger "change" + :hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-form-changed) + :hx-vals (hx/json {:op "vendor-changed"}) + :hx-target "#edit-form" + :hx-select "#edit-form" + :hx-swap "outerHTML" + :hx-sync "this:replace" + :hx-include "closest form"} + (com/validated-field + {:label "Vendor" :errors (ferr :transaction/vendor)} + [:div.w-96 + (com/typeahead {:name (fname :transaction/vendor) + :class "w-96" + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes :vendor-search) + :value vendor-val + :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})])] + (if (= mode :simple) + [:div {:x-data (hx/json {:simpleAccountId (let [av (-> (first all-accounts) :transaction-account/account)] + (if (map? av) (:db/id av) av))})} + (simple-mode-fields* request)] + [:div + (when (<= row-count 1) + [:div.mb-2 + [:a (merge {:class "text-sm text-blue-600 hover:underline cursor-pointer"} toggle-attrs) + "Switch to simple mode"]]) + (com/validated-field + {:errors (ferr :transaction/accounts)} + [:div {:id "account-grid-body"} + (account-grid-body* request)])])])) (defn apply-toggle-amount-mode "edit-form-changed op: convert account amounts between $ and % and record the new mode." @@ -607,17 +576,35 @@ (assoc-in [:multi-form-state :snapshot :amount-mode] new-mode)))) (defn transaction-details-panel [tx] - (sel/render->hiccup - "templates/transaction-edit/details-panel.html" - {:amount (format "$%,.2f" (Math/abs (or (:transaction/amount tx) 0.0))) - :date (some-> tx :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date)) - :bank_account (or (-> tx :transaction/bank-account :bank-account/name) "-") - :post_date (some-> tx :transaction/post-date coerce/to-date-time (atime/unparse-local atime/normal-date)) - :description_original (or (:transaction/description-original tx) "No original description") - :description_simple (or (:transaction/description-simple tx) "-") - :check_number (or (:transaction/check-number tx) "-") - :status (or (some-> tx :transaction/status) "-") - :type (or (some-> tx :transaction/type) "-")})) + [:div.p-4.space-y-4 + [:h3.text-sm.font-semibold.text-gray-900.uppercase.tracking-wider "Details"] + [:div.space-y-3 + [:div + [:div.text-xs.font-medium.text-gray-500 "Amount"] + [:div.text-sm.font-medium.text-gray-900 (format "$%,.2f" (Math/abs (or (:transaction/amount tx) 0.0)))]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Date"] + [:div.text-sm.text-gray-900 (some-> tx :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date))]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Bank Account"] + [:div.text-sm.text-gray-900 (or (-> tx :transaction/bank-account :bank-account/name) "-")]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Post Date"] + [:div.text-sm.text-gray-900 (some-> tx :transaction/post-date coerce/to-date-time (atime/unparse-local atime/normal-date))]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Description"] + [:div.text-sm.text-gray-900.truncate.cursor-help + {:title (or (:transaction/description-original tx) "No original description")} + (or (:transaction/description-simple tx) "-")]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Check Number"] + [:div.text-sm.text-gray-900 (or (:transaction/check-number tx) "-")]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Status"] + [:div.text-sm.text-gray-900 (or (some-> tx :transaction/status) "-")]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Transaction Type"] + [:div.text-sm.text-gray-900 (or (some-> tx :transaction/type) "-")]]]]) (defn get-available-payments [request] (let [tx-id (or (get-in request [:form-params :transaction-id]) @@ -651,38 +638,39 @@ (d-invoices/get-by-id invoice-id)))))) (defn- panel-wrap [inner] - (sel/raw (str "
" (str inner) "
"))) + [:div inner]) (defn- panel-empty* [message] - (sel/render->hiccup "templates/transaction-edit/panel-empty.html" {:message message})) + [:div.text-center.py-4.text-gray-500 message]) (defn- panel-list* [{:keys [heading action-hidden prompt radio]}] - (sel/render->hiccup "templates/transaction-edit/panel-list.html" - {:heading heading - :action_hidden (str action-hidden) - :prompt prompt - :radio (str radio)})) + [:div + [:h3.text-lg.font-bold.mb-4 heading] + action-hidden + [:div.space-y-2 + [:label.block.text-sm.font-medium.mb-1 prompt] + radio]]) (defn- invoice-group-content [match-group] - (sel/raw (apply str (for [invoice match-group] - (sel/render "templates/transaction-edit/invoice-option.html" - {:number (:invoice/invoice-number invoice) - :vendor (-> invoice :invoice/vendor :vendor/name) - :date (some-> invoice :invoice/date coerce/to-date-time (atime/unparse-local atime/normal-date)) - :amount (format "$%.2f" (:invoice/outstanding-balance invoice))}))))) + (for [invoice match-group] + [:div.ml-3 + [:span.block.text-sm.font-medium (:invoice/invoice-number invoice)] + [:span.block.text-sm.text-gray-500 (-> invoice :invoice/vendor :vendor/name)] + [:span.block.text-sm.text-gray-500 (some-> invoice :invoice/date coerce/to-date-time (atime/unparse-local atime/normal-date))] + [:span.block.text-sm.font-medium (format "$%.2f" (:invoice/outstanding-balance invoice))]])) (defn autopay-invoices-view [request] (let [invoice-matches (get-available-autopay-invoices request)] (panel-wrap (if (seq invoice-matches) (panel-list* {:heading "Available Autopay Invoices" - :action-hidden (sc/hidden {:name "action" :value "link-autopay-invoices" :form ""}) + :action-hidden (com/hidden {:name "action" :value "link-autopay-invoices" :form ""}) :prompt "Select an autopay invoice to apply:" - :radio (sc/radio-card {:options (for [match-group invoice-matches] - {:value (pr-str (map :db/id match-group)) - :content (invoice-group-content match-group)}) - :name (fname :autopay-invoice-ids) - :width "w-full"})}) + :radio (com/radio-card {:options (for [match-group invoice-matches] + {:value (pr-str (map :db/id match-group)) + :content (invoice-group-content match-group)}) + :name (fname :autopay-invoice-ids) + :width "w-full"})}) (panel-empty* "No matching autopay invoices available for this transaction."))))) (defn get-available-unpaid-invoices [request] @@ -705,13 +693,13 @@ (panel-wrap (if (seq invoice-matches) (panel-list* {:heading "Available Unpaid Invoices" - :action-hidden (sc/hidden {:name "action" :value "link-unpaid-invoices" :form ""}) + :action-hidden (com/hidden {:name "action" :value "link-unpaid-invoices" :form ""}) :prompt "Select an unpaid invoice to apply:" - :radio (sc/radio-card {:options (for [match-group invoice-matches] - {:value (pr-str (map :db/id match-group)) - :content (invoice-group-content match-group)}) - :name (fname :unpaid-invoice-ids) - :width "w-full"})}) + :radio (com/radio-card {:options (for [match-group invoice-matches] + {:value (pr-str (map :db/id match-group)) + :content (invoice-group-content match-group)}) + :name (fname :unpaid-invoice-ids) + :width "w-full"})}) (panel-empty* "No matching unpaid invoices available for this transaction."))))) (defn get-available-rules [request] @@ -745,14 +733,15 @@ (panel-wrap (if (seq matching-rules) (panel-list* {:heading "Matching Transaction Rules" - :action-hidden (sc/hidden {:name (fname :action) :value "apply-rule" :form ""}) + :action-hidden (com/hidden {:name (fname :action) :value "apply-rule" :form ""}) :prompt "Select a rule to apply:" - :radio (sc/radio-card {:options (for [{:keys [:db/id :transaction-rule/note :transaction-rule/description]} matching-rules] - {:value id - :content (sel/render->hiccup "templates/transaction-edit/rule-option.html" - {:note note :description description})}) - :name (fname :rule-id) - :width "w-full"})}) + :radio (com/radio-card {:options (for [{:keys [:db/id :transaction-rule/note :transaction-rule/description]} matching-rules] + {:value id + :content [:div.ml-3 + [:span.block.text-sm.font-medium note] + [:span.block.text-sm.text-gray-500 description]]}) + :name (fname :rule-id) + :width "w-full"})}) (panel-empty* "No matching rules found for this transaction."))))) (defn payment-matches-view [request] @@ -770,43 +759,42 @@ :payment/vendor [:vendor/name]}] (-> tx :transaction/payment :db/id))] - (sel/render->hiccup - "templates/transaction-edit/payment-matches.html" - {:inner - (str - (if (and payment (:db/id payment)) - (sel/render->hiccup - "templates/transaction-edit/linked-payment.html" - {:external_link (str (sc/a-icon-button {:href (hu/url (bidi/path-for ssr-routes/only-routes ::payment-route/all-page) - {:exact-match-id (:db/id payment)})} - (sc/render "templates/components/svg-external-link.html" {}))) - :number (:payment/invoice-number payment) - :vendor (-> payment :payment/vendor :vendor/name) - :amount (some->> (:payment/amount payment) (format "$%.2f")) - :status (some-> payment :payment/status name) - :date (some-> payment :payment/date (atime/unparse-local atime/normal-date)) - :payment_id_hidden (str (sc/hidden {:name (fname :payment-id) :value (:db/id payment)})) - :unlink_attrs (sc/attrs->str {:hx-post (bidi/path-for ssr-routes/only-routes ::route/unlink-payment) - :hx-trigger "unlinkPayment" - :hx-target "#payment-matches" - :hx-include "closest form" - :hx-swap "outerHTML" - :hx-confirm "Are you sure you want to unlink this payment?"}) - :unlink_button (str (sc/a-button {:color :red :size :small "@click" "$dispatch('unlinkPayment')"} - "Unlink Payment"))}) - (if (seq payments) - (panel-list* {:heading "Available Payments" - :action-hidden "" - :prompt "Select a payment to match:" - :radio (sc/radio-card {:options (for [payment payments] - {:value (:db/id payment) - :content (str (:payment/invoice-number payment) " - " - (-> payment :payment/vendor :vendor/name) - " - Amount: $" (format "%.2f" (:payment/amount payment)) - " • Date: " (some-> payment :payment/date coerce/to-date-time (atime/unparse-local atime/normal-date)))}) - :name (fname :payment-id) - :width "w-full"})}) - (panel-empty* "No matching payments available for this transaction."))))}))) + [:div {:id "payment-matches"} + (if (and payment (:db/id payment)) + [:div.my-4.p-4.bg-blue-50.rounded + [:h3.text-lg.font-bold.mb-2 "Linked Payment" + (com/a-icon-button {:href (hu/url (bidi/path-for ssr-routes/only-routes ::payment-route/all-page) + {:exact-match-id (:db/id payment)})} + svg/external-link)] + [:div.space-y-2 + [:div.flex.justify-between [:div.font-medium "Payment #"] [:div (:payment/invoice-number payment)]] + [:div.flex.justify-between [:div.font-medium "Vendor"] [:div (-> payment :payment/vendor :vendor/name)]] + [:div.flex.justify-between [:div.font-medium "Amount"] [:div (some->> (:payment/amount payment) (format "$%.2f"))]] + [:div.flex.justify-between [:div.font-medium "Status"] [:div (some-> payment :payment/status name)]] + [:div.flex.justify-between [:div.font-medium "Date"] [:div (some-> payment :payment/date (atime/unparse-local atime/normal-date))]] + (com/hidden {:name (fname :payment-id) :value (:db/id payment)}) + [:div (merge {:class "mt-4"} + {:hx-post (bidi/path-for ssr-routes/only-routes ::route/unlink-payment) + :hx-trigger "unlinkPayment" + :hx-target "#payment-matches" + :hx-include "closest form" + :hx-swap "outerHTML" + :hx-confirm "Are you sure you want to unlink this payment?"}) + (com/a-button {:color :red :size :small "@click" "$dispatch('unlinkPayment')"} + "Unlink Payment")]]] + (if (seq payments) + (panel-list* {:heading "Available Payments" + :action-hidden "" + :prompt "Select a payment to match:" + :radio (com/radio-card {:options (for [payment payments] + {:value (:db/id payment) + :content (str (:payment/invoice-number payment) " - " + (-> payment :payment/vendor :vendor/name) + " - Amount: $" (format "%.2f" (:payment/amount payment)) + " • Date: " (some-> payment :payment/date coerce/to-date-time (atime/unparse-local atime/normal-date)))}) + :name (fname :payment-id) + :width "w-full"})}) + (panel-empty* "No matching payments available for this transaction.")))])) (defn count-payment-matches [request] (count (get-available-payments request))) @@ -821,18 +809,18 @@ (count (get-available-rules request))) (defn- tab-button [{:keys [active value badge-count disabled? relative?] :or {relative? true}} label] - (sc/button-group-button + (com/button-group-button (cond-> {"@click" (str "activeForm = '" active "'") :value value ":class" (str "{ '!bg-primary-200 text-primary-800': activeForm === '" active "'}")} relative? (assoc :class "relative") disabled? (assoc ":disabled" "!canChange")) (when (and badge-count (> badge-count 0)) - (sc/badge {:color "green"} (str badge-count))) + (com/badge {:color "green"} (str badge-count))) label)) (defn- tabs* [request] - (sc/button-group + (com/button-group {:name "method"} (tab-button {:active "link-payment" :value "payment" :badge-count (count-payment-matches request)} "Link to payment") @@ -849,57 +837,57 @@ v (:transaction/approval-status step-params) current-value (name (or (if (map? v) (:db/ident v) v) :transaction-approval-status/unapproved))] - (sc/validated-field + (com/validated-field {:label "Status" :errors (ferr :transaction/approval-status)} - (sel/render->hiccup - "templates/transaction-edit/approval-status.html" - {:x_data (hx/json {:approvalStatus current-value}) - :status_hidden (str (sc/hidden {:name (fname :transaction/approval-status) - :value current-value ":value" "approvalStatus"})) - :buttons (str - (sc/button-group-button {"@click" "approvalStatus = 'approved'" ":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'approved' }" :class "rounded-l-lg"} "Approved") - (sc/button-group-button {"@click" "approvalStatus = 'unapproved'" ":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'unapproved' }" :class "rounded-r-lg"} "Unapproved") - (sc/button-group-button {"@click" "approvalStatus = 'suppressed'" ":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'suppressed' }" :class "rounded-r-lg"} "Client Review"))})))) + [:div {:x-data (hx/json {:approvalStatus current-value})} + (com/hidden {:name (fname :transaction/approval-status) + :value current-value ":value" "approvalStatus"}) + [:div.inline-flex.rounded-md.shadow-sm {:role "group"} + (com/button-group-button {"@click" "approvalStatus = 'approved'" ":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'approved' }" :class "rounded-l-lg"} "Approved") + (com/button-group-button {"@click" "approvalStatus = 'unapproved'" ":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'unapproved' }" :class "rounded-r-lg"} "Unapproved") + (com/button-group-button {"@click" "approvalStatus = 'suppressed'" ":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'suppressed' }" :class "rounded-r-lg"} "Client Review")]]))) (defn- links-body* [request mode] (let [step-params (-> request :multi-form-state :step-params) payment? (:transaction/payment (:entity request)) - action-str (some-> (:action step-params) name)] - (sel/render->hiccup - "templates/transaction-edit/links-body.html" - {:memo_field (str (sc/validated-field - {:label "Memo" :errors (ferr :transaction/memo)} - (wrap-div "w-96" - (sc/text-input {:value (:transaction/memo step-params) - :name (fname :transaction/memo) - :id "edit-memo" - :error? (ferr :transaction/memo) - :placeholder "Optional note"})))) - :x_data (hx/json {:activeForm (if payment? "link-payment" (or action-str "manual")) - :canChange (boolean (not payment?))}) - :action_hidden (str (sc/hidden {:name (fname :action) :value action-str ":value" "activeForm"})) - :tabs (str (tabs* request)) - :panel_payment (str (payment-matches-view request)) - :panel_unpaid (str (unpaid-invoices-view request)) - :panel_autopay (str (autopay-invoices-view request)) - :panel_rule (str (transaction-rules-view request)) - :panel_manual (str (manual-coding-section* mode request) - (approval-status* request))}))) - -(defn- form-errors-html [errors] - (str "
" - (when (seq errors) - (str "

" - (str/join ", " (filter string? errors)) - "

")) - "
")) + action-str (some-> (:action step-params) name) + enter-attrs {:x-transition:enter "transition ease-out duration-500" + :x-transition:enter-start "opacity-0 transform scale-95" + :x-transition:enter-end "opacity-100 transform scale-100"}] + [:div.space-y-1 + [:div + (com/validated-field + {:label "Memo" :errors (ferr :transaction/memo)} + [:div.w-96 + (com/text-input {:value (:transaction/memo step-params) + :name (fname :transaction/memo) + :id "edit-memo" + :error? (ferr :transaction/memo) + :placeholder "Optional note"})]) + [:div {:x-data (hx/json {:activeForm (if payment? "link-payment" (or action-str "manual")) + :canChange (boolean (not payment?))}) + "@unlinked" "canChange=true"} + [:div.flex.space-x-2.mb-4 + (com/hidden {:name (fname :action) :value action-str ":value" "activeForm"}) + (tabs* request)] + [:div (merge {:x-show "activeForm === 'link-payment'"} enter-attrs) + (payment-matches-view request)] + [:div (merge {:x-show "activeForm === 'link-unpaid-invoices'"} enter-attrs) + (unpaid-invoices-view request)] + [:div (merge {:x-show "activeForm === 'link-autopay-invoices'"} enter-attrs) + (autopay-invoices-view request)] + [:div (merge {:x-show "activeForm === 'apply-rule'"} enter-attrs) + (transaction-rules-view request)] + [:div (merge {:x-show "activeForm === 'manual'"} enter-attrs) + [:div + (manual-coding-section* mode request) + (approval-status* request)]]]]])) (defn- footer* [request] - (sel/raw - (str "
" - (form-errors-html (:errors (:form-errors request))) - (str (sc/button {:color :primary :x-ref "next" :class "w-32 wizard-save-action"} "Done")) - "
"))) + [:div.flex.justify-end + [:div.flex.items-baseline.gap-x-4 + (com/form-errors {:errors (seq (:errors (:form-errors request)))}) + (com/button {:color :primary :x-ref "next" :class "w-32 wizard-save-action"} "Done")]]) (defn render-form "Renders the whole plain edit form (no wizard). Binds *errors* from the request's @@ -914,21 +902,20 @@ ;; Preserve an explicit mode choice; fall back to the row-count heuristic only ;; on initial open. mode (keyword (or (:mode step-params) (name (manual-mode-initial snapshot)))) - modal-card (sel/render "templates/transaction-edit/edit-modal.html" - {:head "
Edit Transaction
" - :side_panel (str (transaction-details-panel tx)) - :body (str (links-body* request mode)) - :footer (str (footer* request))})] - (sel/render->hiccup - "templates/transaction-edit/edit-form.html" - {:db_id (:db/id snapshot) - :form_attrs (sc/attrs->str {:hx-ext "response-targets" - :hx-swap "outerHTML" - :hx-target-400 "#form-errors .error-content" - :hx-trigger "submit" - :hx-target "this" - :hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-submit)}) - :modal (str (sc/modal {:id "editmodal"} (sel/raw modal-card)))})))) + modal-card (com/single-modal-card + {:side-panel (transaction-details-panel tx)} + [:div.p-2 "Edit Transaction"] + (links-body* request mode) + (footer* request))] + [:form (merge {:id "edit-form"} + {:hx-ext "response-targets" + :hx-swap "outerHTML" + :hx-target-400 "#form-errors .error-content" + :hx-trigger "submit" + :hx-target "this" + :hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-submit)}) + (com/hidden {:name "db/id" :value (:db/id snapshot)}) + (com/modal {:id "editmodal"} modal-card)]))) (defmulti save-handler (fn [request] (-> request :multi-form-state :snapshot :action))) @@ -1396,8 +1383,8 @@ by the modal stack." [request] (modal-response - (sel/render->hiccup "templates/transaction-edit/transitioner.html" - {:body (str (render-form request))}))) + [:div {:id "transitioner" :class "flex-1"} + (render-form request)])) (defn submit-edit "Validates the merged record against edit-form-schema (field-level errors surface via diff --git a/tailwind.config.js b/tailwind.config.js index b354d9f0..73d278c7 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,7 +4,6 @@ const plugin = require('tailwindcss/plugin'); module.exports = { darkMode: "class", content: ["./src/**/*.{cljs,clj,cljc}", - "./resources/templates/**/*.html", "./node_modules/flowbite/**/*.js"], theme: { extend: { diff --git a/test/clj/auto_ap/ssr/selmer_test.clj b/test/clj/auto_ap/ssr/selmer_test.clj deleted file mode 100644 index 1f0790d8..00000000 --- a/test/clj/auto_ap/ssr/selmer_test.clj +++ /dev/null @@ -1,36 +0,0 @@ -(ns auto-ap.ssr.selmer-test - (:require - [auto-ap.ssr.selmer :as sut] - [clojure.string :as str] - [clojure.test :refer [deftest is testing]] - [hiccup2.core :as h2])) - -(deftest hiccup->html - (testing "renders a Hiccup form to an HTML string" - (is (= "A & B" - (sut/hiccup->html [:span.label "A & B"]))))) - -(deftest selmer-embeds-hiccup - (testing "a Hiccup component renders inside a Selmer template via |safe" - (let [frag (sut/hiccup->html [:span.badge "from hiccup"]) - out (sut/render-str "
{{frag|safe}}
" {:frag frag})] - (is (str/includes? out "from hiccup")) - ;; without |safe the markup would be escaped; |safe keeps it verbatim - (is (not (str/includes? out "<span")))))) - -(deftest selmer-fragment-inside-hiccup - (testing "a Selmer fragment renders inside a Hiccup tree without double-escaping" - (let [sel (sut/render-str "{{label}}" {:url "/x" :label "Go"}) - out (str (h2/html {} [:div (sut/raw sel)]))] - (is (= "" out))))) - -(deftest render-file-from-classpath - (testing "render-file resolves a template under resources/templates and keeps plain-HTML Alpine/HTMX attrs" - (let [out (sut/render "templates/interop-smoke.html" - {:title "Interop OK" - :hiccup_frag (sut/hiccup->html [:span.badge "from hiccup"])})] - (is (str/includes? out "Interop OK")) - (is (str/includes? out "from hiccup")) - ;; plain-HTML attributes (the whole point of Selmer) survive unambiguously - (is (str/includes? out "x-model=\"value.value\"")) - (is (str/includes? out "tippy?.show()")))))