# 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 `` 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 2–5 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.