Files
integreat/docs/superpowers/plans/2026-05-27-transaction-edit-simple-advanced-mode.md

602 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Transaction Edit Modal: Simple / Advanced Mode Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the always-visible account split table in the transaction edit modal with a simple mode (single account + location fields) that is the default for uncoded or single-account transactions, with a toggle to the full advanced split table.
**Architecture:** HTMX-driven server-side swap. A new `edit-wizard-toggle-mode` GET endpoint re-renders the manual coding section in the requested mode. Mode is carried via a hidden `<input name="mode">` field included in all HTMX requests. `edit-vendor-changed` is updated to branch on mode. The `LinksStep` render function selects initial mode based on account row count.
**Tech Stack:** Clojure/Hiccup server-side rendering, HTMX, Alpine.js, Bidi routing, Datomic
**Spec:** `docs/superpowers/specs/2026-05-27-transaction-edit-simple-advanced-mode-design.md`
---
## File Map
| File | What changes |
|------|-------------|
| `src/cljc/auto_ap/routes/transactions.cljc` | Add `::edit-wizard-toggle-mode` route |
| `src/clj/auto_ap/ssr/transaction/edit.clj` | Add `simple-mode-fields*`, `manual-coding-section*`, `edit-wizard-toggle-mode-handler`; update `LinksStep` render; update `edit-vendor-changed-handler`; register new route handler |
---
## Task 1: Add the toggle-mode route
**Files:**
- Modify: `src/cljc/auto_ap/routes/transactions.cljc`
- [ ] **Step 1: Add the route entry**
Open `src/cljc/auto_ap/routes/transactions.cljc`. After the `"/edit-wizard-new-account"` line, add:
```clojure
"/edit-wizard-toggle-mode" ::edit-wizard-toggle-mode
```
The file should look like:
```clojure
"/edit-wizard-new-account" ::edit-wizard-new-account
"/edit-wizard-toggle-mode" ::edit-wizard-toggle-mode
"/match-payment" ::link-payment
```
- [ ] **Step 2: Verify the file compiles**
```bash
clj-nrepl-eval -p 9000 "(require '[auto-ap.routes.transactions] :reload)"
```
Expected: no errors.
- [ ] **Step 3: Commit**
```bash
git add src/cljc/auto_ap/routes/transactions.cljc
git commit -m "feat: add edit-wizard-toggle-mode route"
```
---
## Task 2: Add `simple-mode-fields*` — the simple-mode account/location UI
**Files:**
- Modify: `src/clj/auto_ap/ssr/transaction/edit.clj`
This function renders the account typeahead + location select + toggle link for simple mode. It goes near the existing `account-typeahead*` and `location-select*` helpers (around line 180).
- [ ] **Step 1: Add `simple-mode-fields*` after `account-typeahead*` (around line 180)**
```clojure
(defn simple-mode-fields*
"Renders the simple-mode account + location row and the toggle link.
request must have :multi-form-state and :entity bound."
[request]
(let [snapshot (-> request :multi-form-state :snapshot)
client-id (or (-> request :entity :transaction/client :db/id)
(:transaction/client snapshot))
existing-row (first (:transaction/accounts snapshot))
account-val (:transaction-account/account existing-row)
location-val (or (:transaction-account/location existing-row) "Shared")
account-id (when (nat-int? account-val)
(dc/pull (dc/db conn) '[:account/location] account-val))
row-id (or (:db/id existing-row) (str (java.util.UUID/randomUUID)))]
[:div
;; hidden inputs to encode the single row as transaction/accounts[0]
(fc/with-field :transaction/accounts
(fc/with-cursor-index 0
[:span
(fc/with-field :db/id
(com/hidden {:name (fc/field-name) :value row-id}))
[:div.flex.gap-2.mt-2
(fc/with-field :transaction-account/account
(com/validated-field
{:label "Account" :errors (fc/field-errors)}
[:div.w-72
(account-typeahead* {:value account-val
:client-id client-id
:name (fc/field-name)
:x-model "simpleAccountId"})]))
(fc/with-field :transaction-account/location
(com/validated-field
{:label "Location"
:errors (fc/field-errors)
:x-hx-val:account-id "simpleAccountId"
:hx-vals (hx/json (cond-> {:name (fc/field-name)}
client-id (assoc :client-id client-id)))
:x-dispatch:changed "simpleAccountId"
:hx-trigger "changed"
:hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select)
:hx-target "find *"
:hx-swap "outerHTML"}
(location-select*
{:name (fc/field-name)
:account-location (:account/location account-id)
:client-locations (pull-attr (dc/db conn) :client/locations client-id)
:value location-val})))
;; hidden amount — full transaction total
(fc/with-field :transaction-account/amount
(let [total (Math/abs (or (-> request :entity :transaction/amount)
(:transaction/amount snapshot)
0.0))]
(com/hidden {:name (fc/field-name) :value total})))]))
;; toggle link
[:div.mt-1
[:a.text-sm.text-blue-600.hover:underline.cursor-pointer
{:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-toggle-mode)
:hx-include "closest form"
:hx-target "#manual-coding-section"
:hx-swap "outerHTML"}
"Switch to advanced mode"]]]))
```
- [ ] **Step 2: Verify the file has no parse errors**
```bash
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
```
Expected: no errors.
- [ ] **Step 3: Commit**
```bash
git add src/clj/auto_ap/ssr/transaction/edit.clj
git commit -m "feat: add simple-mode-fields* for transaction edit modal"
```
---
## Task 3: Extract `manual-coding-section*` and update `LinksStep`
**Files:**
- Modify: `src/clj/auto_ap/ssr/transaction/edit.clj`
Currently the manual coding block (vendor field + account grid) is inlined inside `LinksStep/render-step`. Extract it into `manual-coding-section*` which selects mode and renders accordingly. This also adds the `mode` hidden input and wraps the section in `#manual-coding-section`.
- [ ] **Step 1: Add `manual-mode-initial` helper** (determines initial mode from snapshot)
Add this function after `simple-mode-fields*`:
```clojure
(defn- manual-mode-initial
"Returns :simple or :advanced based on existing account row count."
[snapshot]
(let [rows (seq (:transaction/accounts snapshot))]
(if (and rows (> (count rows) 1))
:advanced
:simple)))
```
- [ ] **Step 2: Add `manual-coding-section*`**
Add after `manual-mode-initial`:
```clojure
(defn manual-coding-section*
"Renders the vendor field + account/location section for the manual tab.
mode is :simple or :advanced."
[mode request]
(let [snapshot (-> request :multi-form-state :snapshot)
row-count (count (:transaction/accounts snapshot))]
[:div#manual-coding-section
;; hidden mode input — carried by all hx-include=\"closest form\" calls
(com/hidden {:name "mode" :value (name mode)})
;; vendor field
[:div {:hx-trigger "change"
:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-vendor-changed)
:hx-target "#manual-coding-section"
:hx-swap "outerHTML"
:hx-include "closest form"}
(fc/with-field :transaction/vendor
(com/validated-field
{:label "Vendor" :errors (fc/field-errors)}
[:div.w-96
(com/typeahead {:name (fc/field-name)
:error? (fc/error?)
:class "w-96"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
:value (fc/field-value)
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})]))]
;; account/location section
(if (= mode :simple)
[:div {:x-data (hx/json {:simpleAccountId
(-> snapshot :transaction/accounts first
:transaction-account/account)})}
(fc/start-form (:multi-form-state request) nil
(fc/with-field :step-params
(simple-mode-fields* request)))]
;; advanced mode
[:div
(when (<= row-count 1)
[:div.mb-2
[:a.text-sm.text-blue-600.hover:underline.cursor-pointer
{:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-toggle-mode)
:hx-include "closest form"
:hx-target "#manual-coding-section"
:hx-swap "outerHTML"}
"Switch to simple mode"]])
(fc/start-form (:multi-form-state request) nil
(fc/with-field :step-params
(fc/with-field :transaction/accounts
[:div#account-grid-body
(account-grid-body* request)])))])]))
```
- [ ] **Step 3: Update `LinksStep/render-step` to use `manual-coding-section*`**
In `LinksStep/render-step` (around line 826), replace the entire `[:div {}` block inside `[:div {:x-show "activeForm === 'manual'" ...}]` (which currently contains the vendor typeahead + approval status + `account-grid-body*`) with:
```clojure
[:div {:x-show "activeForm === 'manual'", :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 {}
(manual-coding-section* (manual-mode-initial snapshot) request)
;; Approval status field
(fc/with-field :transaction/approval-status
(com/validated-field
{:label "Status"
:errors (fc/field-errors)}
(let [current-value (name (or (fc/field-value) :transaction-approval-status/unapproved))]
[:div {:x-data (hx/json {:approvalStatus current-value})}
(com/hidden {:name (fc/field-name)
:value current-value
":value" "approvalStatus"})
[:div {:class "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")]]]))]]]
```
Also remove the now-redundant `(fc/with-field :transaction/accounts ...)` wrapper that previously wrapped `account-grid-body*` (it is now handled inside `manual-coding-section*`).
- [ ] **Step 4: Verify the file compiles**
```bash
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
```
Expected: no errors.
- [ ] **Step 5: Commit**
```bash
git add src/clj/auto_ap/ssr/transaction/edit.clj
git commit -m "feat: extract manual-coding-section* with simple/advanced mode selection"
```
---
## Task 4: Add `edit-wizard-toggle-mode-handler`
**Files:**
- Modify: `src/clj/auto_ap/ssr/transaction/edit.clj`
This handler re-renders `#manual-coding-section` in the opposite mode. It reads `mode` from the form params (via `step-params` in the decoded multi-form-state) and flips it.
- [ ] **Step 1: Add the handler function** (add after `edit-vendor-changed-handler`):
```clojure
(defn edit-wizard-toggle-mode-handler [request]
(let [step-params (-> request :multi-form-state :step-params)
current-mode (keyword (or (:mode step-params) "simple"))
target-mode (if (= current-mode :simple) :advanced :simple)
snapshot (-> request :multi-form-state :snapshot)
;; When switching simple→advanced, promote simple-mode values into accounts
render-request
(if (and (= target-mode :advanced)
(= current-mode :simple))
;; carry the simple-mode single row into snapshot so the table shows it
(let [accounts (or (seq (:transaction/accounts step-params))
(seq (:transaction/accounts snapshot)))]
(-> request
(assoc-in [:multi-form-state :snapshot :transaction/accounts]
(vec accounts))
(assoc-in [:multi-form-state :step-params :transaction/accounts]
(vec accounts))))
;; advanced→simple: take first row only
(let [first-row (first (or (seq (:transaction/accounts step-params))
(seq (:transaction/accounts snapshot))))]
(-> request
(assoc-in [:multi-form-state :snapshot :transaction/accounts]
(if first-row [first-row] []))
(assoc-in [:multi-form-state :step-params :transaction/accounts]
(if first-row [first-row] [])))))]
(html-response
(fc/start-form (:multi-form-state render-request) nil
(fc/with-field :step-params
(manual-coding-section* target-mode render-request))))))
```
- [ ] **Step 2: Register the handler in `key->handler`**
In the `key->handler` map (around line 1357), add after the `::route/edit-wizard-new-account` entry:
```clojure
::route/edit-wizard-toggle-mode (-> edit-wizard-toggle-mode-handler
(mm/wrap-wizard edit-wizard)
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
(mm/wrap-decode-multi-form-state))
```
- [ ] **Step 3: Verify the file compiles**
```bash
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
```
Expected: no errors.
- [ ] **Step 4: Commit**
```bash
git add src/clj/auto_ap/ssr/transaction/edit.clj
git commit -m "feat: add edit-wizard-toggle-mode-handler"
```
---
## Task 5: Update `edit-vendor-changed-handler` to support both modes
**Files:**
- Modify: `src/clj/auto_ap/ssr/transaction/edit.clj`
Currently this handler always returns `[:div#account-grid-body ...]`. It must now return `#manual-coding-section` in the correct mode.
- [ ] **Step 1: Replace `edit-vendor-changed-handler`**
Replace the entire `edit-vendor-changed-handler` function body with:
```clojure
(defn edit-vendor-changed-handler [request]
(let [multi-form-state (:multi-form-state request)
snapshot (:snapshot multi-form-state)
step-params (:step-params multi-form-state)
mode (keyword (or (:mode step-params) "simple"))
client-id (or (:transaction/client snapshot)
(-> request :entity :transaction/client :db/id))
vendor-id (or (:transaction/vendor step-params)
(:transaction/vendor snapshot))
total (Math/abs (or (-> request :entity :transaction/amount)
(:transaction/amount snapshot)
0.0))
amount-mode (or (:amount-mode snapshot) "$")
existing-accounts (or (seq (:transaction/accounts step-params))
(seq (:transaction/accounts snapshot)))
default-account (when (and (empty? existing-accounts) vendor-id client-id)
(vendor-default-account vendor-id client-id))
render-request
(if (and (empty? existing-accounts) vendor-id client-id)
(let [new-account (cond-> {:db/id (str (java.util.UUID/randomUUID))
:transaction-account/location (or (:account/location default-account) "Shared")
:transaction-account/amount (if (= amount-mode "%") 100.0 total)}
default-account (assoc :transaction-account/account (:db/id default-account)))]
(-> request
(assoc-in [:multi-form-state :snapshot :transaction/accounts] [new-account])
(assoc-in [:multi-form-state :step-params :transaction/accounts] [new-account])))
request)]
(html-response
(fc/start-form (:multi-form-state render-request) nil
(fc/with-field :step-params
(manual-coding-section* mode render-request))))))
```
Note: the `hx-target` on the vendor field in `manual-coding-section*` must point to `#manual-coding-section` (not `#account-grid-body`) — this was set correctly in Task 3.
- [ ] **Step 2: Verify the file compiles**
```bash
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
```
Expected: no errors.
- [ ] **Step 3: Commit**
```bash
git add src/clj/auto_ap/ssr/transaction/edit.clj
git commit -m "feat: update edit-vendor-changed-handler to support simple/advanced mode"
```
---
## Task 6: Fix `fc/with-cursor-index` usage in `simple-mode-fields*`
**Context:** The simple-mode fields need to emit form names like `step-params[transaction/accounts][0][transaction-account/account]`. The existing `fc/cursor-map` used in `account-grid-body*` handles this automatically. For the single-row simple mode we need to manually set index 0.
Look up how `fc/with-cursor-index` (or equivalent) works in `src/clj/auto_ap/ssr/form_cursor.clj` before writing the code in Task 2. If no such helper exists, use `fc/cursor-nth` or replicate the index manually via the form cursor API.
- [ ] **Step 1: Inspect the form cursor API**
```bash
clj-nrepl-eval -p 9000 "(clj-mcp.repl-tools/list-vars 'auto-ap.ssr.form-cursor)"
```
Note the available functions.
- [ ] **Step 2: Update `simple-mode-fields*` if needed**
If `fc/with-cursor-index` does not exist, replace the `fc/with-cursor-index 0` call in Task 2 with the correct form-cursor idiom. The key requirement is that the hidden `db/id`, `transaction-account/account`, `transaction-account/location`, and `transaction-account/amount` fields emit names matching index 0 of `transaction/accounts`.
A known working pattern from `account-grid-body*`:
```clojure
(fc/cursor-map #(transaction-account-row* {:value % ...}))
```
For simple mode with a single synthetic row, build a one-element vector in the snapshot and let `fc/cursor-map` iterate it — but render a flat div instead of a table. Or pass the cursor manually:
```clojure
(fc/with-field :transaction/accounts
(let [row-cursor (fc/cursor-nth 0)] ; adjust to actual API
(fc/with-cursor row-cursor
...field rendering...)))
```
Verify field names are correct in a browser after implementation.
- [ ] **Step 3: Verify the file compiles**
```bash
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
```
- [ ] **Step 4: Commit if any changes were made**
```bash
git add src/clj/auto_ap/ssr/transaction/edit.clj
git commit -m "fix: correct form-cursor indexing for simple-mode account field"
```
---
## Task 7: Manual smoke test
Before writing automated tests, verify the UI works end-to-end in a browser.
- [ ] **Step 1: Start the application**
```bash
INTEGREAT_JOB="" lein run
```
- [ ] **Step 2: Open a transaction with no accounts**
Navigate to a transaction with no coded accounts. Open the edit modal. Verify it opens in simple mode with blank account and location fields.
- [ ] **Step 3: Test vendor selection in simple mode**
Select a vendor. Verify the account field is populated with the vendor's default account and the location is set appropriately.
- [ ] **Step 4: Test toggle to advanced**
Click "Switch to advanced mode". Verify the full split table appears with one pre-populated row.
- [ ] **Step 5: Test toggle back to simple**
With 1 row, click "Switch to simple mode". Verify the single account/location fields appear with that row's values.
- [ ] **Step 6: Test with a split transaction**
Open a transaction that already has 2+ accounts. Verify it opens in advanced mode. Verify the "Switch to simple mode" link is absent.
- [ ] **Step 7: Test save round-trip**
In simple mode, set a vendor, account, and location. Save. Re-open. Verify the same values are pre-populated in simple mode.
---
## Task 8: Write e2e tests
**Files:**
- Create: `test/clj/auto_ap/ssr/transaction/edit_simple_advanced_mode_test.clj`
Check how existing e2e tests are structured first:
```bash
clj-nrepl-eval -p 9000 "(clj-mcp.repl-tools/list-ns)"
```
Look for test namespaces matching `auto-ap.ssr.transaction.*` or `auto-ap.e2e.*`. Follow the same fixture/helper patterns.
The test file should cover all 20 acceptance criteria from the spec. Group them with `testing` blocks:
```clojure
(ns auto-ap.ssr.transaction.edit-simple-advanced-mode-test
(:require
[clojure.test :refer [deftest is testing use-fixtures]]
;; ... project-specific test helpers ...
))
(deftest simple-advanced-mode-initial-state
(testing "AC1: uncoded transaction opens in simple mode"
;; create a transaction with no accounts
;; open edit modal
;; verify #manual-coding-section has mode=simple hidden input
;; verify no #account-grid-body present
(is ...))
(testing "AC2: single-account transaction opens in simple mode with values pre-populated"
...)
(testing "AC3: multi-account transaction opens in advanced mode"
...))
(deftest simple-mode-vendor-selection
(testing "AC4: selecting vendor populates account and location"
...)
(testing "AC5: selecting vendor does not overwrite manually chosen account"
...))
(deftest mode-toggle
(testing "AC9: switching to advanced carries account/location into first row"
...)
(testing "AC10: switching to advanced from blank simple gives empty table"
...)
(testing "AC11: switch-to-simple link visible with 0 or 1 rows"
...)
(testing "AC12: switch-to-simple link absent with 2+ rows"
...)
(testing "AC13: switching to simple pre-populates from first row"
...))
(deftest save-round-trip
(testing "AC6: save in simple mode persists vendor/account/location"
...)
(testing "AC18: switching modes mid-edit then saving produces valid transaction"
...)
(testing "AC19: split transaction re-opens in advanced mode with splits intact"
...)
(testing "AC20: single-account transaction re-opens in simple mode"
...))
```
Fill in actual test bodies using the project's test infrastructure (browser automation or ring mock depending on what exists).
- [ ] **Step 1: Check existing test conventions**
```bash
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.testing-conventions] :reload)"
```
Also load the testing-conventions skill for guidance:
```
Load skill: testing-conventions
```
- [ ] **Step 2: Write the test file** following project conventions
- [ ] **Step 3: Run the tests**
```bash
clj-nrepl-eval -p 9000 "(clojure.test/run-tests 'auto-ap.ssr.transaction.edit-simple-advanced-mode-test)"
```
Expected: all tests pass.
- [ ] **Step 4: Commit**
```bash
git add test/clj/auto_ap/ssr/transaction/edit_simple_advanced_mode_test.clj
git commit -m "test: add e2e acceptance tests for simple/advanced mode"
```
---
## Self-Review Checklist (completed inline)
- **Spec coverage:** All 20 ACs addressed — Tasks 25 implement the behaviour; Task 8 tests all 20.
- **Placeholder scan:** Task 6 and Task 8 contain some "fill in" guidance — this is intentional because they depend on runtime API discovery. The instructions tell the engineer exactly where to look and what to verify.
- **Type consistency:** `manual-coding-section*` is used consistently by `LinksStep/render-step`, `edit-vendor-changed-handler`, and `edit-wizard-toggle-mode-handler`. `#manual-coding-section` is the swap target throughout. `mode` hidden input uses `(name mode)` for string serialization and `(keyword ...)` for deserialization.