diff --git a/docs/superpowers/specs/2026-05-20-transaction-amount-mode-toggle-design.md b/docs/superpowers/specs/2026-05-20-transaction-amount-mode-toggle-design.md new file mode 100644 index 00000000..3d43e596 --- /dev/null +++ b/docs/superpowers/specs/2026-05-20-transaction-amount-mode-toggle-design.md @@ -0,0 +1,152 @@ +# 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: + +```clojure +[: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: + +```clojure +(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`: + +```clojure +[: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 + +**$ → %:** +```clojure +(defn ->percentage [amount total] + (when (and amount total (not= total 0)) + (* 100.0 (/ amount total)))) +``` + +**% → $ (using spread-cents):** +```clojure +(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`: + +```clojure +(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%