Compare commits
19 Commits
e8979738ab
...
4221d6a0d6
| Author | SHA1 | Date | |
|---|---|---|---|
| 4221d6a0d6 | |||
| 918ddd14ff | |||
| acd4184ef0 | |||
| 857a1536ef | |||
| 535ef4d113 | |||
| 351659f8eb | |||
| 4739769297 | |||
| 567db50a66 | |||
| dbfa04c766 | |||
| 0692089e39 | |||
| 8189a7648b | |||
| dd4d1a6d4f | |||
| 4f32527732 | |||
| 0811771ae6 | |||
| c6b55ce567 | |||
| 1f9a7080e1 | |||
| 6f7f1c7815 | |||
| 065d1182d7 | |||
| b42e2a6a44 |
@@ -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%
|
||||||
File diff suppressed because one or more lines are too long
@@ -148,7 +148,9 @@
|
|||||||
next-button-content]}]
|
next-button-content]}]
|
||||||
[:div.flex.justify-end
|
[:div.flex.justify-end
|
||||||
[:div.flex.items-baseline.gap-x-4
|
[:div.flex.items-baseline.gap-x-4
|
||||||
(com/form-errors {:errors (:errors (:step-params fc/*form-errors*))})
|
(let [step-errors (:step-params fc/*form-errors*)]
|
||||||
|
(com/form-errors {:errors (or (:errors step-errors)
|
||||||
|
(when (sequential? step-errors) step-errors))}))
|
||||||
(when (not= (first (steps linear-wizard))
|
(when (not= (first (steps linear-wizard))
|
||||||
(step-key step))
|
(step-key step))
|
||||||
(when validation-route
|
(when validation-route
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
(:require [auto-ap.ssr.hiccup-helper :as hh]
|
(:require [auto-ap.ssr.hiccup-helper :as hh]
|
||||||
[auto-ap.ssr.hx :as hx]))
|
[auto-ap.ssr.hx :as hx]))
|
||||||
|
|
||||||
(defn radio-card- [{:keys [options name title size orientation width] :or {size :medium width "w-48"} selected-value :value}]
|
(defn radio-card- [{:keys [options name title size orientation width] :or {size :medium width "w-48"} selected-value :value :as attrs}]
|
||||||
|
(let [htmx-attrs (select-keys attrs [:hx-post :hx-target :hx-swap :hx-include :hx-trigger])]
|
||||||
[:h3 {:class "mb-4 font-semibold text-gray-900 dark:text-white"} title]
|
[:h3 {:class "mb-4 font-semibold text-gray-900 dark:text-white"} title]
|
||||||
[:ul {:class (cond-> " text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
[:ul {:class (cond-> " text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||||
(= orientation :horizontal) (-> (hh/add-class "flex gap-2 flex-wrap")
|
(= orientation :horizontal) (-> (hh/add-class "flex gap-2 flex-wrap")
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
(hh/add-class "w-auto shrink-0 block rounded-lg border border-gray-200 dark:border-gray-600 px-3")))}
|
(hh/add-class "w-auto shrink-0 block rounded-lg border border-gray-200 dark:border-gray-600 px-3")))}
|
||||||
[:div {:class (cond-> "flex items-center"
|
[:div {:class (cond-> "flex items-center"
|
||||||
(not= orientation :horizontal) (hh/add-class "pl-3"))}
|
(not= orientation :horizontal) (hh/add-class "pl-3"))}
|
||||||
[:input (cond-> {:id (str "list-" name "-" value)
|
[:input (cond-> (merge {:id (str "list-" name "-" value)
|
||||||
:type "radio",
|
:type "radio",
|
||||||
:value value
|
:value value
|
||||||
:name name
|
:name name
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
|
|
||||||
(= size :medium)
|
(= size :medium)
|
||||||
(str " " "text-sm"))}
|
(str " " "text-sm"))}
|
||||||
|
htmx-attrs)
|
||||||
(= (cond-> selected-value (keyword? selected-value) clojure.core/name) value) (assoc :checked true))]
|
(= (cond-> selected-value (keyword? selected-value) clojure.core/name) value) (assoc :checked true))]
|
||||||
[:label {:for (str "list-" name "-" value)
|
[:label {:for (str "list-" name "-" value)
|
||||||
:class
|
:class
|
||||||
@@ -36,7 +38,7 @@
|
|||||||
(str " " "text-sm py-3")
|
(str " " "text-sm py-3")
|
||||||
|
|
||||||
(= orientation :horizontal)
|
(= orientation :horizontal)
|
||||||
(hh/remove-class "w-full"))} content]]])])
|
(hh/remove-class "w-full"))} content]]])]))
|
||||||
|
|
||||||
(defn radio-list- [{:keys [options name x-model title size orientation] :or {size :medium} selected-value :value}]
|
(defn radio-list- [{:keys [options name x-model title size orientation] :or {size :medium} selected-value :value}]
|
||||||
[:h3 {:class "mb-4 font-semibold text-gray-900 dark:text-white"} title]
|
[:h3 {:class "mb-4 font-semibold text-gray-900 dark:text-white"} title]
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
(:require
|
(:require
|
||||||
[auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-4
|
[auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-4
|
||||||
conn merge-query observable-query pull-many]]
|
conn merge-query observable-query pull-many]]
|
||||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
[auto-ap.datomic.accounts :as d-accounts]
|
||||||
|
[auto-ap.graphql.utils :refer [extract-client-ids is-admin?]]
|
||||||
[auto-ap.routes.invoice :as invoice-routes]
|
[auto-ap.routes.invoice :as invoice-routes]
|
||||||
[auto-ap.routes.ledger :as ledger-routes]
|
[auto-ap.routes.ledger :as ledger-routes]
|
||||||
[auto-ap.routes.payments :as payment-routes]
|
[auto-ap.routes.payments :as payment-routes]
|
||||||
@@ -33,9 +34,24 @@
|
|||||||
[:amount-lte {:optional true} [:maybe :double]]
|
[:amount-lte {:optional true} [:maybe :double]]
|
||||||
[:client-id {:optional true} [:maybe entity-id]]
|
[:client-id {:optional true} [:maybe entity-id]]
|
||||||
[:import-batch-id {:optional true} [:maybe entity-id]]
|
[:import-batch-id {:optional true} [:maybe entity-id]]
|
||||||
|
[:unresolved {:optional true}
|
||||||
|
[:maybe [:boolean {:decode/string {:enter #(cond (= % "on") true
|
||||||
|
(= % "") false
|
||||||
|
:else
|
||||||
|
(boolean %))}}]]]
|
||||||
[:description {:optional true} [:maybe [:string {:decode/string strip}]]]
|
[:description {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||||
[:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]]
|
[:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]]
|
||||||
[:bank-account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :bank-account/numeric-code]}]]]
|
[:bank-account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :bank-account/numeric-code]}]]]
|
||||||
|
[:account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :account/name]}]]]
|
||||||
|
[:linked-to {:optional true}
|
||||||
|
[:maybe [:enum {:decode/string {:enter #(if (seq %) % nil)}}
|
||||||
|
"payment" "expected-deposit" "invoice" "none"]]]
|
||||||
|
[:location {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||||
|
[:potential-duplicates {:optional true}
|
||||||
|
[:maybe [:boolean {:decode/string {:enter #(cond (= % "on") true
|
||||||
|
(= % "") false
|
||||||
|
:else
|
||||||
|
(boolean %))}}]]]
|
||||||
#_[:status {:optional true} [:maybe (ref->enum-schema "transaction-status")]]
|
#_[:status {:optional true} [:maybe (ref->enum-schema "transaction-status")]]
|
||||||
[:exact-match-id {:optional true} [:maybe entity-id]]
|
[:exact-match-id {:optional true} [:maybe entity-id]]
|
||||||
[:all-selected {:optional true :default nil} [:maybe :boolean]]
|
[:all-selected {:optional true :default nil} [:maybe :boolean]]
|
||||||
@@ -149,11 +165,70 @@
|
|||||||
(merge-query {:query {:in ['?vendor-id]
|
(merge-query {:query {:in ['?vendor-id]
|
||||||
:where ['[?e :transaction/vendor ?vendor-id]]}
|
:where ['[?e :transaction/vendor ?vendor-id]]}
|
||||||
:args [(:db/id (:vendor args))]})
|
:args [(:db/id (:vendor args))]})
|
||||||
|
|
||||||
|
(:db/id (:account args))
|
||||||
|
(merge-query {:query {:in ['?account-id]
|
||||||
|
:where ['[?e :transaction/accounts ?tas]
|
||||||
|
'[?tas :transaction-account/account ?account-id]]}
|
||||||
|
:args [(:db/id (:account args))]})
|
||||||
(:import-batch-id args)
|
(:import-batch-id args)
|
||||||
(merge-query {:query {:in ['?import-batch-id]
|
(merge-query {:query {:in ['?import-batch-id]
|
||||||
:where ['[?import-batch-id :import-batch/entry ?e]]}
|
:where ['[?import-batch-id :import-batch/entry ?e]]}
|
||||||
:args [(:import-batch-id args)]})
|
:args [(:import-batch-id args)]})
|
||||||
|
|
||||||
|
(:unresolved args)
|
||||||
|
(merge-query {:query {:where ['[?e :transaction/date]
|
||||||
|
'(or-join [?e]
|
||||||
|
(not [?e :transaction/accounts])
|
||||||
|
(and [?e :transaction/accounts ?tas]
|
||||||
|
(not [?tas :transaction-account/account]))) ]}})
|
||||||
|
|
||||||
|
(seq (:location args))
|
||||||
|
(merge-query {:query {:in ['?location]
|
||||||
|
:where ['[?e :transaction/accounts ?tas]
|
||||||
|
'[?tas :transaction-account/location ?location]]}
|
||||||
|
:args [(:location args)]})
|
||||||
|
|
||||||
|
(= (:linked-to args) "payment")
|
||||||
|
(merge-query {:query {:where ['[?e :transaction/payment]]}})
|
||||||
|
|
||||||
|
(= (:linked-to args) "expected-deposit")
|
||||||
|
(merge-query {:query {:where ['[?e :transaction/expected-deposit]]}})
|
||||||
|
|
||||||
|
(= (:linked-to args) "invoice")
|
||||||
|
(merge-query {:query {:where ['[?e :transaction/payment ?p]
|
||||||
|
'[_ :invoice-payment/payment ?p]]}})
|
||||||
|
|
||||||
|
(= (:linked-to args) "none")
|
||||||
|
(merge-query {:query {:where ['(not [?e :transaction/payment])
|
||||||
|
'(not [?e :transaction/expected-deposit])]}})
|
||||||
|
|
||||||
|
(:potential-duplicates args)
|
||||||
|
(merge-query (let [bank-account-id (:db/id (:bank-account args))
|
||||||
|
_ (when-not bank-account-id
|
||||||
|
(throw (ex-info "In order to select potential duplicates, you must choose a bank account."
|
||||||
|
{:validation-error "In order to select potential duplicates, you must choose a bank account."})))
|
||||||
|
duplicate-ids (->> (dc/q '[:find ?tx ?amount ?date
|
||||||
|
:in $ ?ba
|
||||||
|
:where
|
||||||
|
[?tx :transaction/bank-account ?ba]
|
||||||
|
[?tx :transaction/amount ?amount]
|
||||||
|
[?tx :transaction/date ?date]
|
||||||
|
(not [?tx :transaction/approval-status :transaction-approval-status/suppressed])]
|
||||||
|
db
|
||||||
|
bank-account-id)
|
||||||
|
(group-by (fn [[_ amount date]]
|
||||||
|
[amount date]))
|
||||||
|
(filter (fn [[_ txes]]
|
||||||
|
(> (count txes) 1)))
|
||||||
|
(vals)
|
||||||
|
(mapcat identity)
|
||||||
|
(map first)
|
||||||
|
set)]
|
||||||
|
{:query {:in '[[?e ...]]
|
||||||
|
:where []}
|
||||||
|
:args [duplicate-ids]}))
|
||||||
|
|
||||||
(:status route-params)
|
(:status route-params)
|
||||||
(merge-query {:query {:in ['?status]
|
(merge-query {:query {:in ['?status]
|
||||||
:where ['[?e :transaction/approval-status ?status]]}
|
:where ['[?e :transaction/approval-status ?status]]}
|
||||||
@@ -203,6 +278,18 @@
|
|||||||
svg/x)]])]
|
svg/x)]])]
|
||||||
[:div {:id "exact-match-id-tag"}]))
|
[:div {:id "exact-match-id-tag"}]))
|
||||||
|
|
||||||
|
(defn import-batch-id* [request]
|
||||||
|
(when-let [import-batch-id (:import-batch-id (:query-params request))]
|
||||||
|
[:div {:x-data (hx/json {:import_batch_id import-batch-id}) :id "import-batch-id-tag"}
|
||||||
|
(com/hidden {:name "import-batch-id"
|
||||||
|
"x-model" "import_batch_id"})
|
||||||
|
(com/pill {:color :primary}
|
||||||
|
[:span.inline-flex.space-x-2.items-center
|
||||||
|
[:div (str "Batch " import-batch-id)]
|
||||||
|
[:div.w-3.h-3
|
||||||
|
(com/link {"@click" "import_batch_id=null; $nextTick(() => $dispatch('change'))"}
|
||||||
|
svg/x)]])]))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(defn bank-account-filter* [request]
|
(defn bank-account-filter* [request]
|
||||||
@@ -244,6 +331,14 @@
|
|||||||
:value (:vendor (:query-params request))
|
:value (:vendor (:query-params request))
|
||||||
:value-fn :db/id
|
:value-fn :db/id
|
||||||
:content-fn :vendor/name}))
|
:content-fn :vendor/name}))
|
||||||
|
(com/field {:label "Financial Account"}
|
||||||
|
(com/typeahead {:name "account"
|
||||||
|
:id "account"
|
||||||
|
:url (bidi/path-for ssr-routes/only-routes :account-search)
|
||||||
|
:value (:account (:query-params request))
|
||||||
|
:value-fn :db/id
|
||||||
|
:content-fn #(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read (:db/id %))
|
||||||
|
(:db/id (:client request))))}))
|
||||||
(bank-account-filter* request)
|
(bank-account-filter* request)
|
||||||
|
|
||||||
(date-range-field* request)
|
(date-range-field* request)
|
||||||
@@ -255,6 +350,14 @@
|
|||||||
:placeholder "e.g., Groceries"
|
:placeholder "e.g., Groceries"
|
||||||
:size :small}))
|
:size :small}))
|
||||||
|
|
||||||
|
(com/field {:label "Location"}
|
||||||
|
(com/text-input {:name "location"
|
||||||
|
:id "location"
|
||||||
|
:class "hot-filter"
|
||||||
|
:value (:location (:query-params request))
|
||||||
|
:placeholder "SC"
|
||||||
|
:size :small}))
|
||||||
|
|
||||||
(com/field {:label "Amount"}
|
(com/field {:label "Amount"}
|
||||||
[:div.flex.space-x-4.items-baseline
|
[:div.flex.space-x-4.items-baseline
|
||||||
(com/money-input {:name "amount-gte"
|
(com/money-input {:name "amount-gte"
|
||||||
@@ -273,6 +376,40 @@
|
|||||||
:value (:amount-lte (:query-params request))
|
:value (:amount-lte (:query-params request))
|
||||||
:placeholder "9999.34"
|
:placeholder "9999.34"
|
||||||
:size :small})])
|
:size :small})])
|
||||||
|
|
||||||
|
(com/field {:label "Linking"}
|
||||||
|
(com/radio-card {:size :small
|
||||||
|
:name "linked-to"
|
||||||
|
:value (or (:linked-to (:query-params request)) "")
|
||||||
|
:options [{:value ""
|
||||||
|
:content "All"}
|
||||||
|
{:value "none"
|
||||||
|
:content "None"}
|
||||||
|
{:value "invoice"
|
||||||
|
:content "Invoice"}
|
||||||
|
{:value "expected-deposit"
|
||||||
|
:content "Expected Deposit"}
|
||||||
|
{:value "payment"
|
||||||
|
:content "Payment"}]}))
|
||||||
|
|
||||||
|
(when (is-admin? (:identity request))
|
||||||
|
[:div.mt-4 {:x-data (hx/json {:unresolvedOnly (:unresolved (:query-params request))})}
|
||||||
|
(com/hidden {:name "unresolved"
|
||||||
|
":value" "unresolvedOnly ? 'on' : ''"})
|
||||||
|
(com/checkbox {:value (:unresolved (:query-params request))
|
||||||
|
:x-model "unresolvedOnly"}
|
||||||
|
"Unresolved only")])
|
||||||
|
|
||||||
|
(when (and (is-admin? (:identity request))
|
||||||
|
(:db/id (:bank-account (:query-params request))))
|
||||||
|
[:div.mt-4 {:x-data (hx/json {:potentialDuplicates (:potential-duplicates (:query-params request))})}
|
||||||
|
(com/hidden {:name "potential-duplicates"
|
||||||
|
":value" "potentialDuplicates ? 'on' : ''"})
|
||||||
|
(com/checkbox {:value (:potential-duplicates (:query-params request))
|
||||||
|
:x-model "potentialDuplicates"}
|
||||||
|
"Same Amount + Date")])
|
||||||
|
|
||||||
|
(import-batch-id* request)
|
||||||
(exact-match-id* request)]])
|
(exact-match-id* request)]])
|
||||||
|
|
||||||
|
|
||||||
@@ -286,7 +423,8 @@
|
|||||||
:oob-render
|
:oob-render
|
||||||
(fn [request]
|
(fn [request]
|
||||||
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)
|
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)
|
||||||
(assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)])
|
(assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)
|
||||||
|
(some-> (import-batch-id* request) (assoc-in [1 :hx-swap-oob] true))])
|
||||||
:action-buttons (fn [request]
|
:action-buttons (fn [request]
|
||||||
[
|
[
|
||||||
(com/button {:color :primary
|
(com/button {:color :primary
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
[auto-ap.ssr-routes :as ssr-routes]
|
[auto-ap.ssr-routes :as ssr-routes]
|
||||||
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
|
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
|
||||||
[auto-ap.ssr.components :as com]
|
[auto-ap.ssr.components :as com]
|
||||||
|
[auto-ap.ssr.grid-page-helper :as helper]
|
||||||
|
[auto-ap.ssr.transaction.common :refer [grid-page]]
|
||||||
[auto-ap.ssr.components.multi-modal :as mm]
|
[auto-ap.ssr.components.multi-modal :as mm]
|
||||||
[auto-ap.ssr.form-cursor :as fc]
|
[auto-ap.ssr.form-cursor :as fc]
|
||||||
[auto-ap.ssr.hx :as hx]
|
[auto-ap.ssr.hx :as hx]
|
||||||
@@ -44,6 +46,8 @@
|
|||||||
:transaction-approval-status/approved "Approved"
|
:transaction-approval-status/approved "Approved"
|
||||||
:transaction-approval-status/suppressed "Suppressed"})
|
:transaction-approval-status/suppressed "Suppressed"})
|
||||||
|
|
||||||
|
(def row* (partial helper/row* grid-page))
|
||||||
|
|
||||||
(defn get-vendor [vendor-id]
|
(defn get-vendor [vendor-id]
|
||||||
(dc/pull
|
(dc/pull
|
||||||
(dc/db conn)
|
(dc/db conn)
|
||||||
@@ -60,6 +64,13 @@
|
|||||||
(defn check-vendor-default-account [vendor-id]
|
(defn check-vendor-default-account [vendor-id]
|
||||||
(some? (:vendor/default-account (get-vendor vendor-id))))
|
(some? (:vendor/default-account (get-vendor vendor-id))))
|
||||||
|
|
||||||
|
(defn require-approval [s]
|
||||||
|
[:and s
|
||||||
|
[:fn {:error/message "Approved transactions must have accounts assigned."}
|
||||||
|
(fn [{:transaction/keys [accounts approval-status]}]
|
||||||
|
(or (not= approval-status :transaction-approval-status/approved)
|
||||||
|
(seq accounts)))]])
|
||||||
|
|
||||||
(def edit-form-schema
|
(def edit-form-schema
|
||||||
(mc/schema
|
(mc/schema
|
||||||
[:and
|
[:and
|
||||||
@@ -69,6 +80,7 @@
|
|||||||
[:transaction/memo {:optional true} [:maybe [:string {:decode/string strip}]]]
|
[:transaction/memo {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||||
[:transaction/vendor {:optional true} [:maybe entity-id]]
|
[:transaction/vendor {:optional true} [:maybe entity-id]]
|
||||||
[:transaction/approval-status {:optional true} [:maybe (ref->enum-schema "transaction-approval-status")]]
|
[:transaction/approval-status {:optional true} [:maybe (ref->enum-schema "transaction-approval-status")]]
|
||||||
|
[:amount-mode {:optional true} [:maybe [:enum "$" "%"]]]
|
||||||
[:transaction/accounts {:optional true}
|
[:transaction/accounts {:optional true}
|
||||||
[:maybe
|
[:maybe
|
||||||
[:vector {:coerce? true}
|
[:vector {:coerce? true}
|
||||||
@@ -91,17 +103,13 @@
|
|||||||
[:unlink-payment [:map
|
[:unlink-payment [:map
|
||||||
[:transaction-id entity-id]]]
|
[:transaction-id entity-id]]]
|
||||||
[:link-unpaid-invoices [:map
|
[:link-unpaid-invoices [:map
|
||||||
|
|
||||||
[:unpaid-invoice-ids {:decode/string (fn [x] (edn/read-string x))}
|
[:unpaid-invoice-ids {:decode/string (fn [x] (edn/read-string x))}
|
||||||
[:vector {:coerce? true} entity-id]]]]
|
[:vector {:coerce? true} entity-id]]]]
|
||||||
[:link-autopay-invoices [:map
|
[:link-autopay-invoices [:map
|
||||||
|
|
||||||
[:autopay-invoice-ids {:decode/string (fn [x] (edn/read-string x))} [:vector {:coerce? true} entity-id]]]]
|
[:autopay-invoice-ids {:decode/string (fn [x] (edn/read-string x))} [:vector {:coerce? true} entity-id]]]]
|
||||||
[:link-payment [:map
|
[:link-payment [:map
|
||||||
[:payment-id entity-id]]]
|
[:payment-id entity-id]]]
|
||||||
[:manual [:map
|
[:manual (require-approval [:map])]]]))
|
||||||
]]]]))
|
|
||||||
|
|
||||||
|
|
||||||
(defn clientize-vendor [{:vendor/keys [terms-overrides automatically-paid-when-due default-account account-overrides] :as vendor} client-id]
|
(defn clientize-vendor [{:vendor/keys [terms-overrides automatically-paid-when-due default-account account-overrides] :as vendor} client-id]
|
||||||
(if (nil? vendor)
|
(if (nil? vendor)
|
||||||
@@ -156,7 +164,7 @@
|
|||||||
(com/typeahead {:name name
|
(com/typeahead {:name name
|
||||||
:placeholder "Search..."
|
:placeholder "Search..."
|
||||||
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
|
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
|
||||||
(cond-> { :purpose "transaction"}
|
(cond-> {:purpose "transaction"}
|
||||||
client-id (assoc :client-id client-id)))
|
client-id (assoc :client-id client-id)))
|
||||||
:id name
|
:id name
|
||||||
:x-model x-model
|
:x-model x-model
|
||||||
@@ -165,7 +173,7 @@
|
|||||||
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
|
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
|
||||||
client-id)))})])
|
client-id)))})])
|
||||||
|
|
||||||
(defn transaction-account-row* [{:keys [value client-id]}]
|
(defn transaction-account-row* [{:keys [value client-id amount-mode total]}]
|
||||||
(com/data-grid-row
|
(com/data-grid-row
|
||||||
(-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? value))))
|
(-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? value))))
|
||||||
:accountId (fc/field-value (:transaction-account/account value))})
|
:accountId (fc/field-value (:transaction-account/account value))})
|
||||||
@@ -190,7 +198,7 @@
|
|||||||
(com/validated-field
|
(com/validated-field
|
||||||
{:errors (fc/field-errors)
|
{:errors (fc/field-errors)
|
||||||
:x-hx-val:account-id "accountId"
|
:x-hx-val:account-id "accountId"
|
||||||
:hx-vals (hx/json (cond-> {:name (fc/field-name) }
|
:hx-vals (hx/json (cond-> {:name (fc/field-name)}
|
||||||
client-id (assoc :client-id client-id)))
|
client-id (assoc :client-id client-id)))
|
||||||
:x-dispatch:changed "accountId"
|
:x-dispatch:changed "accountId"
|
||||||
:hx-trigger "changed"
|
:hx-trigger "changed"
|
||||||
@@ -208,9 +216,67 @@
|
|||||||
{}
|
{}
|
||||||
(com/validated-field
|
(com/validated-field
|
||||||
{:errors (fc/field-errors)}
|
{:errors (fc/field-errors)}
|
||||||
|
(if (= "%" amount-mode)
|
||||||
|
(com/text-input {:name (fc/field-name)
|
||||||
|
:class "w-16"
|
||||||
|
:value (fc/field-value)
|
||||||
|
:type "number"
|
||||||
|
:step "0.01"})
|
||||||
(com/money-input {:name (fc/field-name)
|
(com/money-input {:name (fc/field-name)
|
||||||
:class "w-16"
|
:class "w-16"
|
||||||
:value (fc/field-value)}))))
|
:value (fc/field-value)})))))
|
||||||
|
(com/data-grid-cell {:class "align-top"}
|
||||||
|
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
|
||||||
|
|
||||||
|
(defn- account-field-name [index field]
|
||||||
|
(str "step-params[transaction/accounts][" index "]["
|
||||||
|
(if (keyword? field)
|
||||||
|
(str (when (namespace field)
|
||||||
|
(str (namespace field) "/"))
|
||||||
|
(name field))
|
||||||
|
field)
|
||||||
|
"]"))
|
||||||
|
|
||||||
|
(defn transaction-account-row-no-cursor* [{:keys [account index client-id amount-mode total]}]
|
||||||
|
(com/data-grid-row
|
||||||
|
(-> {:x-data (hx/json {:show true
|
||||||
|
:accountId (:transaction-account/account account)})
|
||||||
|
:data-key "show"
|
||||||
|
:x-ref "p"}
|
||||||
|
hx/alpine-mount-then-appear)
|
||||||
|
(com/hidden {:name (account-field-name index :db/id)
|
||||||
|
:value (or (:db/id account) "")})
|
||||||
|
(com/data-grid-cell
|
||||||
|
{}
|
||||||
|
(com/validated-field
|
||||||
|
{}
|
||||||
|
(account-typeahead* {:value (:transaction-account/account account)
|
||||||
|
:client-id client-id
|
||||||
|
:name (account-field-name index :transaction-account/account)
|
||||||
|
:x-model "accountId"})))
|
||||||
|
(com/data-grid-cell
|
||||||
|
{}
|
||||||
|
(com/validated-field
|
||||||
|
{}
|
||||||
|
(location-select* {:name (account-field-name index :transaction-account/location)
|
||||||
|
:account-location (:account/location (cond->> (:transaction-account/account account)
|
||||||
|
(nat-int? (:transaction-account/account account)) (dc/pull (dc/db conn)
|
||||||
|
'[:account/location])))
|
||||||
|
:client-locations (pull-attr (dc/db conn) :client/locations client-id)
|
||||||
|
:value (:transaction-account/location account)})))
|
||||||
|
(com/data-grid-cell
|
||||||
|
{}
|
||||||
|
(com/validated-field
|
||||||
|
{}
|
||||||
|
(if (= "%" amount-mode)
|
||||||
|
(com/text-input {:name (account-field-name index :transaction-account/amount)
|
||||||
|
:class "w-16"
|
||||||
|
:value (:transaction-account/amount account)
|
||||||
|
:type "number"
|
||||||
|
:step "0.01"})
|
||||||
|
(com/money-input {:name (account-field-name index :transaction-account/amount)
|
||||||
|
:class "w-16"
|
||||||
|
:value (:transaction-account/amount account)}))))
|
||||||
(com/data-grid-cell {:class "align-top"}
|
(com/data-grid-cell {:class "align-top"}
|
||||||
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
|
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
|
||||||
|
|
||||||
@@ -253,11 +319,157 @@
|
|||||||
(defn account-balance [request]
|
(defn account-balance [request]
|
||||||
(html-response (account-balance* request)))
|
(html-response (account-balance* request)))
|
||||||
|
|
||||||
(defn require-approval [s]
|
(defn ->percentage [amount total]
|
||||||
[:and s
|
(when (and amount total (not= total 0))
|
||||||
[:fn (fn [{:transaction/keys [accounts approval-status]}]
|
(* 100.0 (/ amount total))))
|
||||||
(or (not= approval-status :approved)
|
|
||||||
(seq accounts)))]])
|
(defn percentages->dollars [percentages total]
|
||||||
|
(let [total-cents (int (* 100 (Math/abs total)))
|
||||||
|
pct-sum (reduce + 0 percentages)
|
||||||
|
normalized-pcts (if (zero? pct-sum)
|
||||||
|
(repeat (count percentages) 0)
|
||||||
|
(map #(* (/ % pct-sum) 100) percentages))
|
||||||
|
individual-cents (map #(int (* total-cents (/ % 100))) normalized-pcts)
|
||||||
|
short-by (- total-cents (reduce + 0 individual-cents))
|
||||||
|
adjustments (concat (take short-by (repeat 1)) (repeat 0))
|
||||||
|
final-cents (map + individual-cents adjustments)]
|
||||||
|
(map #(* 0.01 %) final-cents)))
|
||||||
|
|
||||||
|
(defn convert-accounts-mode [accounts old-mode new-mode total]
|
||||||
|
(if (= old-mode new-mode)
|
||||||
|
accounts
|
||||||
|
(let [amounts (map :transaction-account/amount accounts)]
|
||||||
|
(map #(assoc %1 :transaction-account/amount %2)
|
||||||
|
accounts
|
||||||
|
(case [old-mode new-mode]
|
||||||
|
["$" "%"] (map #(->percentage % total) amounts)
|
||||||
|
["%" "$"] (percentages->dollars amounts total)
|
||||||
|
amounts)))))
|
||||||
|
|
||||||
|
(defn account-grid-body* [request]
|
||||||
|
(let [snapshot (-> request :multi-form-state :snapshot)
|
||||||
|
amount-mode (or (:amount-mode snapshot) "$")
|
||||||
|
total (Math/abs (or (:transaction/amount snapshot) 0.0))]
|
||||||
|
(com/data-grid {:headers [(com/data-grid-header {} "Account")
|
||||||
|
(com/data-grid-header {:class "w-32"} "Location")
|
||||||
|
(com/data-grid-header {:class "w-16"}
|
||||||
|
(com/radio-card {:options [{:value "$" :content "$"}
|
||||||
|
{:value "%" :content "%"}]
|
||||||
|
:value amount-mode
|
||||||
|
:name "step-params[amount-mode]"
|
||||||
|
:orientation :horizontal
|
||||||
|
: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"}))
|
||||||
|
(com/data-grid-header {:class "w-16"})]}
|
||||||
|
(fc/cursor-map #(transaction-account-row* {:value %
|
||||||
|
:client-id (-> request :entity :transaction/client :db/id)
|
||||||
|
:amount-mode amount-mode
|
||||||
|
:total total}))
|
||||||
|
|
||||||
|
(com/data-grid-new-row {:colspan 4
|
||||||
|
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||||
|
::route/edit-wizard-new-account)
|
||||||
|
:row-offset 0
|
||||||
|
:index (count (:transaction/accounts snapshot))
|
||||||
|
:tr-params {:hx-vals (hx/json {:client-id (:transaction/client snapshot)})}}
|
||||||
|
"New account")
|
||||||
|
(com/data-grid-row {}
|
||||||
|
(com/data-grid-cell {})
|
||||||
|
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"])
|
||||||
|
(com/data-grid-cell {:id "total"
|
||||||
|
:class "text-right"
|
||||||
|
:hx-trigger "change from:closest form target:.amount-field"
|
||||||
|
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/account-total)
|
||||||
|
:hx-target "this"
|
||||||
|
:hx-swap "innerHTML"}
|
||||||
|
(account-total* request))
|
||||||
|
(com/data-grid-cell {}))
|
||||||
|
|
||||||
|
(com/data-grid-row {}
|
||||||
|
(com/data-grid-cell {})
|
||||||
|
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "BALANCE"])
|
||||||
|
(com/data-grid-cell {:id "total"
|
||||||
|
:class "text-right"
|
||||||
|
:hx-trigger "change from:closest form target:.amount-field"
|
||||||
|
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/account-balance)
|
||||||
|
:hx-target "this"
|
||||||
|
:hx-swap "innerHTML"}
|
||||||
|
(account-balance* request))
|
||||||
|
(com/data-grid-cell {}))
|
||||||
|
|
||||||
|
(com/data-grid-row {}
|
||||||
|
(com/data-grid-cell {})
|
||||||
|
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TRANSACTION TOTAL"])
|
||||||
|
(com/data-grid-cell {:class "text-right"}
|
||||||
|
(format "$%,.2f" total))
|
||||||
|
(com/data-grid-cell {})))))
|
||||||
|
|
||||||
|
(defn toggle-amount-mode [request]
|
||||||
|
(let [snapshot (-> request :multi-form-state :snapshot)
|
||||||
|
old-mode (or (:amount-mode snapshot) "$")
|
||||||
|
new-mode (or (get-in request [:multi-form-state :step-params :amount-mode]) "$")
|
||||||
|
total (Math/abs (or (:transaction/amount snapshot) 0.0))
|
||||||
|
accounts (convert-accounts-mode (:transaction/accounts snapshot) old-mode new-mode total)
|
||||||
|
updated-request (-> request
|
||||||
|
(assoc-in [:multi-form-state :snapshot :transaction/accounts] accounts)
|
||||||
|
(assoc-in [:multi-form-state :snapshot :amount-mode] new-mode))]
|
||||||
|
(html-response
|
||||||
|
[:div#account-grid-body
|
||||||
|
(com/data-grid {:headers [(com/data-grid-header {} "Account")
|
||||||
|
(com/data-grid-header {:class "w-32"} "Location")
|
||||||
|
(com/data-grid-header {:class "w-16"}
|
||||||
|
(com/radio-card {:options [{:value "$" :content "$"}
|
||||||
|
{:value "%" :content "%"}]
|
||||||
|
:value new-mode
|
||||||
|
:name "step-params[amount-mode]"
|
||||||
|
:orientation :horizontal
|
||||||
|
: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"}))
|
||||||
|
(com/data-grid-header {:class "w-16"})]}
|
||||||
|
(map-indexed (fn [idx account]
|
||||||
|
(transaction-account-row-no-cursor*
|
||||||
|
{:account account
|
||||||
|
:index idx
|
||||||
|
:client-id (-> updated-request :entity :transaction/client :db/id)
|
||||||
|
:amount-mode new-mode
|
||||||
|
:total total}))
|
||||||
|
accounts)
|
||||||
|
(com/data-grid-new-row {:colspan 4
|
||||||
|
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||||
|
::route/edit-wizard-new-account)
|
||||||
|
:row-offset 0
|
||||||
|
:index (count accounts)
|
||||||
|
:tr-params {:hx-vals (hx/json {:client-id (:transaction/client snapshot)})}}
|
||||||
|
"New account")
|
||||||
|
(com/data-grid-row {}
|
||||||
|
(com/data-grid-cell {})
|
||||||
|
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"])
|
||||||
|
(com/data-grid-cell {:id "total"
|
||||||
|
:class "text-right"}
|
||||||
|
(format "$%,.2f" (double (reduce + 0.0 (map :transaction-account/amount accounts)))))
|
||||||
|
(com/data-grid-cell {}))
|
||||||
|
(com/data-grid-row {}
|
||||||
|
(com/data-grid-cell {})
|
||||||
|
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "BALANCE"])
|
||||||
|
(com/data-grid-cell {:id "total"
|
||||||
|
:class "text-right"}
|
||||||
|
(let [account-total (double (reduce + 0.0 (map :transaction-account/amount accounts)))
|
||||||
|
balance (- total account-total)]
|
||||||
|
[:span {:class (when-not (dollars= 0.0 balance)
|
||||||
|
"text-red-300")}
|
||||||
|
(format "$%,.2f" (double balance))]))
|
||||||
|
(com/data-grid-cell {}))
|
||||||
|
(com/data-grid-row {}
|
||||||
|
(com/data-grid-cell {})
|
||||||
|
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TRANSACTION TOTAL"])
|
||||||
|
(com/data-grid-cell {:class "text-right"}
|
||||||
|
(format "$%,.2f" total))
|
||||||
|
(com/data-grid-cell {})))])))
|
||||||
|
|
||||||
(defrecord BasicDetailsStep [linear-wizard]
|
(defrecord BasicDetailsStep [linear-wizard]
|
||||||
mm/ModalWizardStep
|
mm/ModalWizardStep
|
||||||
(step-name [_]
|
(step-name [_]
|
||||||
@@ -283,7 +495,7 @@
|
|||||||
:body (mm/default-step-body
|
:body (mm/default-step-body
|
||||||
{}
|
{}
|
||||||
[:div {:x-data (hx/json {:clientId (or (fc/field-value (:transaction/client fc/*current*))
|
[:div {:x-data (hx/json {:clientId (or (fc/field-value (:transaction/client fc/*current*))
|
||||||
(:db/id (:client request))) })}
|
(:db/id (:client request)))})}
|
||||||
|
|
||||||
;; Read-only transaction details
|
;; Read-only transaction details
|
||||||
[:div.mb-6.border.rounded-lg.p-4.bg-gray-50
|
[:div.mb-6.border.rounded-lg.p-4.bg-gray-50
|
||||||
@@ -325,9 +537,7 @@
|
|||||||
[:div.text-sm.font-medium.text-gray-500 "Transaction Type"]
|
[:div.text-sm.font-medium.text-gray-500 "Transaction Type"]
|
||||||
[:div.text-base (or (some-> tx :transaction/type) "-")]]]]
|
[:div.text-base (or (some-> tx :transaction/type) "-")]]]]
|
||||||
|
|
||||||
|
;; Transaction Links Section
|
||||||
|
|
||||||
;; Transaction Links Section
|
|
||||||
#_[:div.mb-6.border.rounded-lg.p-4.bg-gray-50
|
#_[:div.mb-6.border.rounded-lg.p-4.bg-gray-50
|
||||||
[:h3.text-lg.font-semibold.mb-2 "Transaction Links"]
|
[:h3.text-lg.font-semibold.mb-2 "Transaction Links"]
|
||||||
(let [tx-id (mm/get-mfs-field multi-form-state :db/id)
|
(let [tx-id (mm/get-mfs-field multi-form-state :db/id)
|
||||||
@@ -362,7 +572,7 @@
|
|||||||
(for [{:keys [entity-id date op]} all-payments
|
(for [{:keys [entity-id date op]} all-payments
|
||||||
:let [payment (dc/pull (dc/db conn)
|
:let [payment (dc/pull (dc/db conn)
|
||||||
'[:db/id :payment/invoice-number
|
'[:db/id :payment/invoice-number
|
||||||
[ :payment/date :xform clj-time.coerce/from-date]
|
[:payment/date :xform clj-time.coerce/from-date]
|
||||||
{:payment/vendor [:vendor/name]}]
|
{:payment/vendor [:vendor/name]}]
|
||||||
entity-id)]]
|
entity-id)]]
|
||||||
[:li.text-sm.text-gray-600 {:class (when-not op "line-through")}
|
[:li.text-sm.text-gray-600 {:class (when-not op "line-through")}
|
||||||
@@ -373,14 +583,12 @@
|
|||||||
|
|
||||||
;; Invoices section
|
;; Invoices section
|
||||||
|
|
||||||
|
;; Hidden ID field
|
||||||
;; Hidden ID field
|
|
||||||
(fc/with-field :db/id
|
(fc/with-field :db/id
|
||||||
(com/hidden {:name (fc/field-name)
|
(com/hidden {:name (fc/field-name)
|
||||||
:value (fc/field-value)}))
|
:value (fc/field-value)}))
|
||||||
|
|
||||||
|
;; Editable fields section
|
||||||
;; Editable fields section
|
|
||||||
;; Vendor field
|
;; Vendor field
|
||||||
)
|
)
|
||||||
:footer
|
:footer
|
||||||
@@ -409,8 +617,6 @@
|
|||||||
client-id))]
|
client-id))]
|
||||||
(filter #(dollars= (Math/abs (:transaction/amount tx)) (:payment/amount %)) payments)))
|
(filter #(dollars= (Math/abs (:transaction/amount tx)) (:payment/amount %)) payments)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(defn get-available-autopay-invoices [request]
|
(defn get-available-autopay-invoices [request]
|
||||||
(let [tx-id (or (-> request :multi-form-state :snapshot :db/id)
|
(let [tx-id (or (-> request :multi-form-state :snapshot :db/id)
|
||||||
(get-in request [:route-params :db/id]))
|
(get-in request [:route-params :db/id]))
|
||||||
@@ -442,9 +648,8 @@
|
|||||||
[:span.block.text-sm.font-medium (:invoice/invoice-number invoice)]
|
[:span.block.text-sm.font-medium (:invoice/invoice-number invoice)]
|
||||||
[:span.block.text-sm.text-gray-500 (-> invoice :invoice/vendor :vendor/name)]
|
[:span.block.text-sm.text-gray-500 (-> invoice :invoice/vendor :vendor/name)]
|
||||||
[:span.block.text-sm.font-medium (format "$%.2f" (:invoice/outstanding-balance invoice))]]))})
|
[:span.block.text-sm.font-medium (format "$%.2f" (:invoice/outstanding-balance invoice))]]))})
|
||||||
:name (fc/with-field :autopay-invoice-ids (fc/field-name ))
|
:name (fc/with-field :autopay-invoice-ids (fc/field-name))
|
||||||
:width "w-full"})]
|
:width "w-full"})]]
|
||||||
]
|
|
||||||
[:div.text-center.py-4.text-gray-500 "No matching autopay invoices available for this transaction."])]))
|
[:div.text-center.py-4.text-gray-500 "No matching autopay invoices available for this transaction."])]))
|
||||||
|
|
||||||
(defn get-available-unpaid-invoices [request]
|
(defn get-available-unpaid-invoices [request]
|
||||||
@@ -482,16 +687,15 @@
|
|||||||
:form ""})
|
:form ""})
|
||||||
[:div.space-y-2
|
[:div.space-y-2
|
||||||
[:label.block.text-sm.font-medium.mb-1 "Select an unpaid invoice to apply:"]
|
[:label.block.text-sm.font-medium.mb-1 "Select an unpaid invoice to apply:"]
|
||||||
(com/radio-card {:options (for [match-group invoice-matches]
|
(com/radio-card {:options (for [match-group invoice-matches]
|
||||||
{:value (pr-str (map :db/id match-group))
|
{:value (pr-str (map :db/id match-group))
|
||||||
:content (doall (for [invoice match-group]
|
:content (doall (for [invoice match-group]
|
||||||
[:div.ml-3
|
[:div.ml-3
|
||||||
[:span.block.text-sm.font-medium (:invoice/invoice-number invoice)]
|
[:span.block.text-sm.font-medium (:invoice/invoice-number invoice)]
|
||||||
[:span.block.text-sm.text-gray-500 (-> invoice :invoice/vendor :vendor/name)]
|
[:span.block.text-sm.text-gray-500 (-> invoice :invoice/vendor :vendor/name)]
|
||||||
[:span.block.text-sm.font-medium (format "$%.2f" (:invoice/outstanding-balance invoice))]]))})
|
[:span.block.text-sm.font-medium (format "$%.2f" (:invoice/outstanding-balance invoice))]]))})
|
||||||
:name (fc/with-field :unpaid-invoice-ids (fc/field-name ))
|
:name (fc/with-field :unpaid-invoice-ids (fc/field-name))
|
||||||
:width "w-full"})
|
:width "w-full"})]
|
||||||
]
|
|
||||||
#_(com/a-button {:color :primary "@click" "$dispatch('linkUnpaidInvoices')"} "Link")]]
|
#_(com/a-button {:color :primary "@click" "$dispatch('linkUnpaidInvoices')"} "Link")]]
|
||||||
[:div.text-center.py-4.text-gray-500 "No matching unpaid invoices available for this transaction."])]))
|
[:div.text-center.py-4.text-gray-500 "No matching unpaid invoices available for this transaction."])]))
|
||||||
|
|
||||||
@@ -521,7 +725,6 @@
|
|||||||
(-> rule
|
(-> rule
|
||||||
(update :transaction-rule/description #(some-> % iol-ion.query/->pattern))))))))))
|
(update :transaction-rule/description #(some-> % iol-ion.query/->pattern))))))))))
|
||||||
|
|
||||||
|
|
||||||
(defn transaction-rules-view [request]
|
(defn transaction-rules-view [request]
|
||||||
(let [matching-rules (get-available-rules request)]
|
(let [matching-rules (get-available-rules request)]
|
||||||
[:div
|
[:div
|
||||||
@@ -539,8 +742,8 @@
|
|||||||
:content [:div.ml-3
|
:content [:div.ml-3
|
||||||
[:span.block.text-sm.font-medium note]
|
[:span.block.text-sm.font-medium note]
|
||||||
[:span.block.text-sm.text-gray-500 description]]})
|
[:span.block.text-sm.text-gray-500 description]]})
|
||||||
:name (fc/with-field :rule-id (fc/field-name ))
|
:name (fc/with-field :rule-id (fc/field-name))
|
||||||
:width "w-full"}) ]
|
:width "w-full"})]
|
||||||
#_(com/a-button {"@click" "$dispatch('applyRule')"} "Apply")]
|
#_(com/a-button {"@click" "$dispatch('applyRule')"} "Apply")]
|
||||||
[:div.text-center.py-4.text-gray-500 "No matching rules found for this transaction."])]))
|
[:div.text-center.py-4.text-gray-500 "No matching rules found for this transaction."])]))
|
||||||
|
|
||||||
@@ -554,7 +757,7 @@
|
|||||||
'[:payment/amount
|
'[:payment/amount
|
||||||
:db/id
|
:db/id
|
||||||
[:payment/date :xform clj-time.coerce/from-date]
|
[:payment/date :xform clj-time.coerce/from-date]
|
||||||
{ [ :payment/status :xform iol-ion.query/ident] [:db/ident]
|
{[:payment/status :xform iol-ion.query/ident] [:db/ident]
|
||||||
|
|
||||||
:payment/vendor [:vendor/name]}]
|
:payment/vendor [:vendor/name]}]
|
||||||
|
|
||||||
@@ -599,7 +802,7 @@
|
|||||||
[:div.space-y-2
|
[:div.space-y-2
|
||||||
[:label.block.text-sm.font-medium.mb-1 "Select a payment to match:"]
|
[:label.block.text-sm.font-medium.mb-1 "Select a payment to match:"]
|
||||||
(when payments
|
(when payments
|
||||||
(let [payment-id-field (fc/with-field :payment-id (fc/field-name ))]
|
(let [payment-id-field (fc/with-field :payment-id (fc/field-name))]
|
||||||
(com/radio-card {:options (for [payment payments]
|
(com/radio-card {:options (for [payment payments]
|
||||||
{:value (:db/id payment)
|
{:value (:db/id payment)
|
||||||
:content (str (:payment/invoice-number payment) " - "
|
:content (str (:payment/invoice-number payment) " - "
|
||||||
@@ -607,7 +810,7 @@
|
|||||||
" - Amount: $" (format "%.2f" (:payment/amount payment))
|
" - Amount: $" (format "%.2f" (:payment/amount payment))
|
||||||
" • Date: " (some-> payment :payment/date coerce/to-date-time (atime/unparse-local atime/normal-date)))})
|
" • Date: " (some-> payment :payment/date coerce/to-date-time (atime/unparse-local atime/normal-date)))})
|
||||||
:name payment-id-field
|
:name payment-id-field
|
||||||
:width "w-full"}))) ]]
|
:width "w-full"})))]]
|
||||||
[:div.text-center.py-4.text-gray-500 "No matching payments available for this transaction."]))]))
|
[:div.text-center.py-4.text-gray-500 "No matching payments available for this transaction."]))]))
|
||||||
|
|
||||||
(defn count-payment-matches [request]
|
(defn count-payment-matches [request]
|
||||||
@@ -633,7 +836,7 @@
|
|||||||
[])
|
[])
|
||||||
|
|
||||||
(step-schema [_]
|
(step-schema [_]
|
||||||
#_(require-approval (mut/select-keys (mm/form-schema linear-wizard) #{:transaction/client :transaction/vendor :transaction/memo :transaction/approval-status :db/id}))
|
#_(require-approval (mut/select-keys (mm/form-schema linear-wizard) #{:transaction/client :transaction/vendor :transaction/memo :transaction/approval-status :db/id}))
|
||||||
(mm/form-schema linear-wizard))
|
(mm/form-schema linear-wizard))
|
||||||
|
|
||||||
(render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}]
|
(render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}]
|
||||||
@@ -716,8 +919,7 @@
|
|||||||
|
|
||||||
;; Memo field
|
;; Memo field
|
||||||
|
|
||||||
|
;; Approval status field
|
||||||
;; Approval status field
|
|
||||||
(fc/with-field :transaction/approval-status
|
(fc/with-field :transaction/approval-status
|
||||||
(com/validated-field
|
(com/validated-field
|
||||||
{:label "Status"
|
{:label "Status"
|
||||||
@@ -729,64 +931,21 @@
|
|||||||
(fc/with-field :transaction/accounts
|
(fc/with-field :transaction/accounts
|
||||||
(com/validated-field
|
(com/validated-field
|
||||||
{:errors (fc/field-errors)}
|
{:errors (fc/field-errors)}
|
||||||
(com/data-grid {:headers [(com/data-grid-header {} "Account")
|
[:div#account-grid-body
|
||||||
(com/data-grid-header {:class "w-32"} "Location")
|
(account-grid-body* request)]))]]]])
|
||||||
(com/data-grid-header {:class "w-16"} "$")
|
|
||||||
(com/data-grid-header {:class "w-16"})]}
|
|
||||||
(fc/cursor-map #(transaction-account-row* {:value %
|
|
||||||
:client-id (-> request :entity :transaction/client :db/id)}))
|
|
||||||
|
|
||||||
(com/data-grid-new-row {:colspan 4
|
|
||||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
|
||||||
::route/edit-wizard-new-account)
|
|
||||||
:row-offset 0
|
|
||||||
:index (count (fc/field-value))
|
|
||||||
:tr-params {:hx-vals (hx/json {:client-id (:transaction/client snapshot)})}}
|
|
||||||
"New account")
|
|
||||||
(com/data-grid-row {}
|
|
||||||
(com/data-grid-cell {})
|
|
||||||
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"])
|
|
||||||
(com/data-grid-cell {:id "total"
|
|
||||||
:class "text-right"
|
|
||||||
:hx-trigger "change from:closest form target:.amount-field"
|
|
||||||
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/account-total)
|
|
||||||
:hx-target "this"
|
|
||||||
:hx-swap "innerHTML"}
|
|
||||||
(account-total* request))
|
|
||||||
(com/data-grid-cell {}))
|
|
||||||
|
|
||||||
(com/data-grid-row {}
|
|
||||||
(com/data-grid-cell {})
|
|
||||||
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "BALANCE"])
|
|
||||||
(com/data-grid-cell {:id "total"
|
|
||||||
:class "text-right"
|
|
||||||
:hx-trigger "change from:closest form target:.amount-field"
|
|
||||||
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/account-balance)
|
|
||||||
:hx-target "this"
|
|
||||||
:hx-swap "innerHTML"}
|
|
||||||
(account-balance* request))
|
|
||||||
(com/data-grid-cell {}))
|
|
||||||
|
|
||||||
(com/data-grid-row {}
|
|
||||||
|
|
||||||
(com/data-grid-cell {})
|
|
||||||
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TRANSACTION TOTAL"])
|
|
||||||
(com/data-grid-cell {:class "text-right"}
|
|
||||||
(format "$%,.2f" (Math/abs (:transaction/amount snapshot))))
|
|
||||||
(com/data-grid-cell {})))))]]]])
|
|
||||||
:footer
|
:footer
|
||||||
(mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate
|
(mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate
|
||||||
:next-button (com/button {:color :primary :x-ref "next" :class "w-32"} "Done"))
|
:next-button (com/button {:color :primary :x-ref "next" :class "w-32"} "Done"))
|
||||||
:validation-route ::route/edit-wizard-navigate)) )
|
:validation-route ::route/edit-wizard-navigate)))
|
||||||
|
|
||||||
(defmulti save-handler (fn [request]
|
(defmulti save-handler (fn [request]
|
||||||
(-> request :multi-form-state :snapshot :action)))
|
(-> request :multi-form-state :snapshot :action)))
|
||||||
|
|
||||||
(defn- default-update-tx [snapshot transaction]
|
(defn- default-update-tx [snapshot transaction]
|
||||||
(merge {:transaction/memo (:transaction/memo snapshot) }
|
(merge {:transaction/memo (:transaction/memo snapshot)}
|
||||||
transaction))
|
transaction))
|
||||||
|
|
||||||
(defn- save-linked-transaction [{{ snapshot :snapshot} :multi-form-state :as request transaction :entity} payment]
|
(defn- save-linked-transaction [{{snapshot :snapshot} :multi-form-state :as request transaction :entity} payment]
|
||||||
(exception->4xx #(assert-not-locked (-> transaction :transaction/client :db/id) (:transaction/date transaction)))
|
(exception->4xx #(assert-not-locked (-> transaction :transaction/client :db/id) (:transaction/date transaction)))
|
||||||
(audit-transact (into
|
(audit-transact (into
|
||||||
[{:db/id (:db/id payment)
|
[{:db/id (:db/id payment)
|
||||||
@@ -806,7 +965,7 @@
|
|||||||
:transaction-account/amount (Math/abs (:transaction/amount transaction))}]})]])
|
:transaction-account/amount (Math/abs (:transaction/amount transaction))}]})]])
|
||||||
(:identity request)))
|
(:identity request)))
|
||||||
|
|
||||||
(defn- save-memo-only [{{ snapshot :snapshot} :multi-form-state :as request}]
|
(defn- save-memo-only [{{snapshot :snapshot} :multi-form-state :as request}]
|
||||||
(audit-transact [[:upsert-transaction (default-update-tx snapshot {})]]
|
(audit-transact [[:upsert-transaction (default-update-tx snapshot {})]]
|
||||||
(:identity request)))
|
(:identity request)))
|
||||||
|
|
||||||
@@ -817,8 +976,8 @@
|
|||||||
payment-id))
|
payment-id))
|
||||||
|
|
||||||
(defmethod save-handler
|
(defmethod save-handler
|
||||||
:link-payment [{{ {:keys [transaction-id payment-id] :as snapshot} :snapshot} :multi-form-state :as request transaction :entity}]
|
:link-payment [{{{:keys [transaction-id payment-id] :as snapshot} :snapshot} :multi-form-state :as request transaction :entity}]
|
||||||
(let [ payment (d-checks/get-by-id payment-id)]
|
(let [payment (d-checks/get-by-id payment-id)]
|
||||||
|
|
||||||
(exception->4xx #(assert-can-see-client (:identity request) (-> transaction :transaction/client :db/id)))
|
(exception->4xx #(assert-can-see-client (:identity request) (-> transaction :transaction/client :db/id)))
|
||||||
(exception->4xx #(assert-can-see-client (:identity request) (-> payment :payment/client :db/id)))
|
(exception->4xx #(assert-can-see-client (:identity request) (-> payment :payment/client :db/id)))
|
||||||
@@ -850,7 +1009,7 @@
|
|||||||
:headers {"hx-trigger" "invalidated"})))
|
:headers {"hx-trigger" "invalidated"})))
|
||||||
|
|
||||||
(defmethod save-handler :link-autopay-invoices
|
(defmethod save-handler :link-autopay-invoices
|
||||||
[{{ {:keys [autopay-invoice-ids] :as snapshot} :snapshot} :multi-form-state :as request transaction :entity} ]
|
[{{{:keys [autopay-invoice-ids] :as snapshot} :snapshot} :multi-form-state :as request transaction :entity}]
|
||||||
(let [db (dc/db conn)
|
(let [db (dc/db conn)
|
||||||
invoice-clients (set (map #(pull-ref db :invoice/client %) autopay-invoice-ids))
|
invoice-clients (set (map #(pull-ref db :invoice/client %) autopay-invoice-ids))
|
||||||
invoice-amount (reduce + 0.0 (map #(pull-attr db :invoice/total %) autopay-invoice-ids))]
|
invoice-amount (reduce + 0.0 (map #(pull-attr db :invoice/total %) autopay-invoice-ids))]
|
||||||
@@ -893,8 +1052,8 @@
|
|||||||
:headers {"hx-trigger" "invalidated"})))
|
:headers {"hx-trigger" "invalidated"})))
|
||||||
|
|
||||||
(defmethod save-handler :link-unpaid-invoices
|
(defmethod save-handler :link-unpaid-invoices
|
||||||
[{{ {:keys [unpaid-invoice-ids] :as snapshot} :snapshot} :multi-form-state :as request transaction :entity} ]
|
[{{{:keys [unpaid-invoice-ids] :as snapshot} :snapshot} :multi-form-state :as request transaction :entity}]
|
||||||
(let [ db (dc/db conn)
|
(let [db (dc/db conn)
|
||||||
invoice-clients (set (map #(pull-ref db :invoice/client %) unpaid-invoice-ids))
|
invoice-clients (set (map #(pull-ref db :invoice/client %) unpaid-invoice-ids))
|
||||||
invoice-amount (reduce + 0.0 (map #(pull-attr db :invoice/outstanding-balance %) unpaid-invoice-ids))]
|
invoice-amount (reduce + 0.0 (map #(pull-attr db :invoice/outstanding-balance %) unpaid-invoice-ids))]
|
||||||
|
|
||||||
@@ -1001,6 +1160,9 @@
|
|||||||
(let [{:keys [base-amount remainder]} (calculate-spread (:transaction-account/amount account) (count locations))]
|
(let [{:keys [base-amount remainder]} (calculate-spread (:transaction-account/amount account) (count locations))]
|
||||||
(map-indexed (fn [idx _]
|
(map-indexed (fn [idx _]
|
||||||
(assoc account
|
(assoc account
|
||||||
|
:db/id (if (= idx 0)
|
||||||
|
(:db/id account)
|
||||||
|
(random-tempid))
|
||||||
:transaction-account/amount (+ base-amount (if (< idx remainder) 1 0))
|
:transaction-account/amount (+ base-amount (if (< idx remainder) 1 0))
|
||||||
:transaction-account/location (nth locations idx)))
|
:transaction-account/location (nth locations idx)))
|
||||||
locations))
|
locations))
|
||||||
@@ -1052,13 +1214,23 @@
|
|||||||
tx-id (:db/id tx-data)
|
tx-id (:db/id tx-data)
|
||||||
client-id (->db-id (:transaction/client tx-data))
|
client-id (->db-id (:transaction/client tx-data))
|
||||||
existing-tx (d-transactions/get-by-id tx-id)
|
existing-tx (d-transactions/get-by-id tx-id)
|
||||||
|
amount-mode (or (:amount-mode tx-data) "$")
|
||||||
|
total (Math/abs (or (:transaction/amount existing-tx) 0.0))
|
||||||
|
tx-data (if (= "%" amount-mode)
|
||||||
|
(update tx-data :transaction/accounts
|
||||||
|
#(map (fn [account dollar-amount]
|
||||||
|
(assoc account :transaction-account/amount dollar-amount))
|
||||||
|
%
|
||||||
|
(percentages->dollars (map :transaction-account/amount %) total)))
|
||||||
|
tx-data)
|
||||||
|
tx-data (dissoc tx-data :amount-mode)
|
||||||
transaction [:upsert-transaction (maybe-spread-locations (assoc tx-data :db/id tx-id))]]
|
transaction [:upsert-transaction (maybe-spread-locations (assoc tx-data :db/id tx-id))]]
|
||||||
|
|
||||||
(alog/info ::transaction transaction :entity transaction)
|
(alog/info ::transaction transaction :entity transaction)
|
||||||
(exception->4xx #(assert-can-see-client (:identity request) client-id))
|
(exception->4xx #(assert-can-see-client (:identity request) client-id))
|
||||||
(exception->4xx #(assert-not-locked client-id (:transaction/date existing-tx)))
|
(exception->4xx #(assert-not-locked client-id (:transaction/date existing-tx)))
|
||||||
|
|
||||||
(when (and (= :approved (keyword (:transaction/approval-status tx-data)))
|
(when (and (= :transaction-approval-status/approved (keyword (:transaction/approval-status tx-data)))
|
||||||
(not (seq (:transaction/accounts tx-data))))
|
(not (seq (:transaction/accounts tx-data))))
|
||||||
(throw (ex-info "Approved transactions must have accounts assigned."
|
(throw (ex-info "Approved transactions must have accounts assigned."
|
||||||
{:validation-error "Approved transactions must have accounts assigned."})))
|
{:validation-error "Approved transactions must have accounts assigned."})))
|
||||||
@@ -1077,8 +1249,10 @@
|
|||||||
(alog/error ::cant-save-solr :error e)))
|
(alog/error ::cant-save-solr :error e)))
|
||||||
|
|
||||||
(html-response
|
(html-response
|
||||||
[:div]
|
(row* (:identity request) (d-transactions/get-by-id tx-id) {:flash? true})
|
||||||
:headers {"hx-trigger" "modalclose"}))))
|
:headers {"hx-trigger" "modalclose"
|
||||||
|
"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" tx-id)
|
||||||
|
"hx-reswap" "outerHTML"}))))
|
||||||
|
|
||||||
(defn unlink-payment [{{{transaction-id :db/id} :snapshot} :multi-form-state :as request}]
|
(defn unlink-payment [{{{transaction-id :db/id} :snapshot} :multi-form-state :as request}]
|
||||||
|
|
||||||
@@ -1149,7 +1323,6 @@
|
|||||||
(html-response (fc/with-field :step-params (payment-matches-view request))
|
(html-response (fc/with-field :step-params (payment-matches-view request))
|
||||||
:headers {"hx-trigger" "unlinked"}))))
|
:headers {"hx-trigger" "unlinked"}))))
|
||||||
|
|
||||||
|
|
||||||
(defrecord EditWizard [_ current-step]
|
(defrecord EditWizard [_ current-step]
|
||||||
mm/LinearModalWizard
|
mm/LinearModalWizard
|
||||||
(hydrate-from-request
|
(hydrate-from-request
|
||||||
@@ -1163,7 +1336,7 @@
|
|||||||
(mm/get-step this :basic-details)))
|
(mm/get-step this :basic-details)))
|
||||||
(render-wizard [this {:keys [multi-form-state] :as request}]
|
(render-wizard [this {:keys [multi-form-state] :as request}]
|
||||||
(println "HERE XYZ" (:form-errors request))
|
(println "HERE XYZ" (:form-errors request))
|
||||||
(clojure.pprint/pprint (:snapshot multi-form-state) )
|
(clojure.pprint/pprint (:snapshot multi-form-state))
|
||||||
(mm/default-render-wizard
|
(mm/default-render-wizard
|
||||||
this request
|
this request
|
||||||
:form-params
|
:form-params
|
||||||
@@ -1173,7 +1346,7 @@
|
|||||||
:render-timeline? false))
|
:render-timeline? false))
|
||||||
(steps [_]
|
(steps [_]
|
||||||
[:basic-details
|
[:basic-details
|
||||||
:links ])
|
:links])
|
||||||
(get-step [this step-key]
|
(get-step [this step-key]
|
||||||
(let [step-key-result (mc/parse mm/step-key-schema step-key)
|
(let [step-key-result (mc/parse mm/step-key-schema step-key)
|
||||||
[step-key-type step-key] step-key-result]
|
[step-key-type step-key] step-key-result]
|
||||||
@@ -1183,8 +1356,7 @@
|
|||||||
(form-schema [_]
|
(form-schema [_]
|
||||||
edit-form-schema)
|
edit-form-schema)
|
||||||
(submit [this {:keys [multi-form-state request-method identity] :as request}]
|
(submit [this {:keys [multi-form-state request-method identity] :as request}]
|
||||||
(save-handler request)
|
(save-handler request)))
|
||||||
))
|
|
||||||
|
|
||||||
(def edit-wizard (->EditWizard nil nil))
|
(def edit-wizard (->EditWizard nil nil))
|
||||||
|
|
||||||
@@ -1198,7 +1370,7 @@
|
|||||||
:transaction/status
|
:transaction/status
|
||||||
:transaction/type
|
:transaction/type
|
||||||
:transaction/memo
|
:transaction/memo
|
||||||
{ [ :transaction/approval-status :xform iol-ion.query/ident] [:db/ident]}
|
{[:transaction/approval-status :xform iol-ion.query/ident] [:db/ident]}
|
||||||
:transaction/amount
|
:transaction/amount
|
||||||
:transaction/accounts]
|
:transaction/accounts]
|
||||||
tx-id)
|
tx-id)
|
||||||
@@ -1240,12 +1412,20 @@
|
|||||||
::route/account-balance (-> account-balance
|
::route/account-balance (-> account-balance
|
||||||
(mm/wrap-wizard edit-wizard)
|
(mm/wrap-wizard edit-wizard)
|
||||||
(mm/wrap-decode-multi-form-state))
|
(mm/wrap-decode-multi-form-state))
|
||||||
|
::route/toggle-amount-mode (-> toggle-amount-mode
|
||||||
|
(mm/wrap-wizard edit-wizard)
|
||||||
|
(mm/wrap-decode-multi-form-state))
|
||||||
::route/edit-wizard-new-account (->
|
::route/edit-wizard-new-account (->
|
||||||
(add-new-entity-handler [:step-params :transaction/accounts]
|
(add-new-entity-handler [:step-params :transaction/accounts]
|
||||||
(fn render [cursor request]
|
(fn render [cursor request]
|
||||||
|
(let [snapshot (-> request :multi-form-state :snapshot)
|
||||||
|
amount-mode (or (:amount-mode snapshot) "$")
|
||||||
|
total (Math/abs (or (:transaction/amount snapshot) 0.0))]
|
||||||
(transaction-account-row*
|
(transaction-account-row*
|
||||||
{:value cursor
|
{:value cursor
|
||||||
:client-id (:client-id (:query-params request))}))
|
:client-id (:client-id (:query-params request))
|
||||||
|
:amount-mode amount-mode
|
||||||
|
:total total})))
|
||||||
(fn build-new-row [base _]
|
(fn build-new-row [base _]
|
||||||
(assoc base :transaction-account/location "Shared")))
|
(assoc base :transaction-account/location "Shared")))
|
||||||
(wrap-schema-enforce :query-schema [:map
|
(wrap-schema-enforce :query-schema [:map
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"/location-select" ::location-select
|
"/location-select" ::location-select
|
||||||
"/account-total" ::account-total
|
"/account-total" ::account-total
|
||||||
"/account-balance" ::account-balance
|
"/account-balance" ::account-balance
|
||||||
|
"/toggle-amount-mode" ::toggle-amount-mode
|
||||||
"/edit-wizard-new-account" ::edit-wizard-new-account
|
"/edit-wizard-new-account" ::edit-wizard-new-account
|
||||||
"/match-payment" ::link-payment
|
"/match-payment" ::link-payment
|
||||||
"/match-autopay-invoices" ::link-autopay-invoices
|
"/match-autopay-invoices" ::link-autopay-invoices
|
||||||
|
|||||||
Reference in New Issue
Block a user