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

24 KiB
Raw Blame History

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:

"/edit-wizard-toggle-mode" ::edit-wizard-toggle-mode

The file should look like:

"/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
clj-nrepl-eval -p 9000 "(require '[auto-ap.routes.transactions] :reload)"

Expected: no errors.

  • Step 3: Commit
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)
(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
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"

Expected: no errors.

  • Step 3: Commit
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*:

(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:

(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:

[: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
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"

Expected: no errors.

  • Step 5: Commit
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):
(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:

::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
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"

Expected: no errors.

  • Step 4: Commit
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:

(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
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"

Expected: no errors.

  • Step 3: Commit
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
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*:

(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:

(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
clj-nrepl-eval -p 9000 "(require '[auto-ap.ssr.transaction.edit] :reload)"
  • Step 4: Commit if any changes were made
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
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:

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:

(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
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

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
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.