Files
integreat/docs/superpowers/specs/2026-05-20-transaction-amount-mode-toggle-design.md

5.9 KiB

Transaction Account Amount Mode Toggle - Design Spec

Date: 2026-05-20 Feature: Global $/% Toggle for Transaction Accounts (Manual Action)

Overview

In the transaction edit modal's "manual" action view, replace the static "$" column header with a sliding toggle that allows users to switch between viewing amounts as dollar values or percentages. When toggled, the entire account grid re-renders via HTMX with converted values. Percentages are multiplied by 100 (e.g., $200 on a $200 transaction → 100%). When switching back to dollars, use spread-cents to ensure accurate cent distribution.

Motivation

The cljs master version supports per-row $/% toggles. Users want this capability in the SSR version, but with a single global toggle in the table header for simplicity and consistency with the bulk coding interface.

Schema Changes

Form State

Add amount-mode to the edit form's step params:

[:amount-mode [:enum "$" "%"] {:default "$"}]

Stored in multi-form-state alongside existing transaction data. Not persisted to Datomic—purely a UI preference.

UI Design

Table Header

Replace the static "$" header cell (line ~739 in edit.clj) with a radio toggle:

(com/radio-card {:options [{:value "$" :content "$"}
                           {:value "%" :content "%"}]
                 :value (or amount-mode "$")
                 :name "step-params[amount-mode]"
                 :hx-post (bidi/path-for ssr-routes/only-routes ::route/toggle-amount-mode)
                 :hx-target "#account-grid-body"
                 :hx-swap "outerHTML"
                 :hx-include "closest form"})

Grid Body

Wrap the account grid rows in a container with id account-grid-body:

[:div#account-grid-body
 (fc/cursor-map #(transaction-account-row* ...))
 ...total/balance rows...]

When toggled, only this container re-renders. All other form fields (vendor, memo, approval status) are preserved.

Data Flow

Toggle Request (HTMX)

  1. User clicks toggle
  2. HTMX serializes entire form via hx-include "closest form"
  3. POST to ::route/toggle-amount-mode
  4. Server:
    • Merges form params into existing multi-form-state
    • Extracts old mode and new mode
    • Converts all :transaction-account/amount values:
      • If old="$" new="%": multiply by 100/total
      • If old="%" new="$": use percentages->dollars (see Conversion Logic)
    • Updates amount-mode in state
    • Re-renders #account-grid-body
  5. Client swaps grid body. Tab order preserved.

Conversion Logic

$ → %:

(defn ->percentage [amount total]
  (when (and amount total (not= total 0))
    (* 100.0 (/ amount total))))

% → $ (using spread-cents):

(defn percentages->dollars [percentages total]
  (let [total-cents (int (* 100 (Math/abs total)))
        pct-sum (reduce + 0 percentages)
        ;; Normalize percentages to sum to 100
        normalized-pcts (if (zero? pct-sum)
                          (repeat (count percentages) 0)
                          (map #(* (/ % pct-sum) 100) percentages))
        ;; Convert each pct to its share of cents
        individual-cents (map #(int (* total-cents (/ % 100))) normalized-pcts)
        short-by (- total-cents (reduce + 0 individual-cents))
        ;; Distribute remainder using spread-cents pattern
        adjustments (concat (take short-by (repeat 1)) (repeat 0))
        final-cents (map + individual-cents adjustments)]
    (map #(* 0.01 %) final-cents)))

Example: One account at 100% of $200.00 → total-cents=20000, individual-cents=[20000], result: [200.00]

Save Handling

Before validation in save-handler :manual:

(let [snapshot (:snapshot multi-form-state)
      accounts (:transaction/accounts snapshot)
      total (Math/abs (:transaction/amount existing-tx))
      mode (:amount-mode snapshot "$")
      ;; If in % mode, convert back to $ before saving
      accounts' (if (= "%" mode)
                  (let [percentages (map :transaction-account/amount accounts)
                        dollar-amounts (percentages->dollars percentages total)]
                    (map #(assoc %1 :transaction-account/amount %2) accounts dollar-amounts))
                  accounts)]
  ...)

Form Preservation

The HTMX toggle is designed to preserve:

  • Tab order: All inputs remain in DOM with same tabindex attributes
  • Other form fields: Vendor, memo, approval status are outside #account-grid-body
  • Alpine.js state: x-data on rows uses data-key="show" for animation—this is re-established on re-render
  • Field names: Account/location/amount field names follow step-params[transaction/accounts][N][...] pattern

Error Handling

  • Zero transaction amount: If total is $0, percentages are all 0%. Toggle is disabled or shows error.
  • Percentage sum ≠ 100: After editing in % mode, if percentages don't sum to 100, normalize proportionally before converting back to $.
  • Invalid input: If user types non-numeric in % mode, existing form validation catches it on submit.

Testing Strategy

  1. Toggle $→%: 200/200 transaction shows 100.0
  2. Toggle %→$: 100% on 200 transaction shows 200.00
  3. Multiple accounts: 50/50 split on 200 → 100.00/100.00 after conversion
  4. Cent distribution: 33.33/33.33/33.34% on $100 → uses spread-cents for accurate distribution
  5. Form preservation: Toggle doesn't lose vendor/memo data
  6. Save in % mode: Correctly converts back to $ before Datomic transaction

Files to Modify

  • src/clj/auto_ap/ssr/transaction/edit.clj — main implementation
  • src/clj/auto_ap/routes/transactions.clj — add ::route/toggle-amount-mode
  • src/clj/auto_ap/ssr/transaction/edit.clj routes map — register handler

Future Considerations

  • This pattern could be extracted for reuse in invoice expense accounts
  • Consider persisting user's last-used mode preference in localStorage
  • Could add visual indicator when percentages don't sum to 100%