Add design spec for transaction account $/% toggle
This commit is contained in:
@@ -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%
|
||||||
Reference in New Issue
Block a user