progress on bulk activities

This commit is contained in:
2025-03-31 10:55:47 -07:00
parent 7f12b31fdf
commit 5a05c144ea
7 changed files with 922 additions and 435 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,457 +1,128 @@
(ns auto-ap.ssr.transaction
(:require
[auto-ap.datomic
:refer [add-sorter-fields apply-pagination apply-sort-4 conn
merge-query observable-query pull-many]]
[auto-ap.graphql.utils :refer [extract-client-ids]]
:refer [audit-transact audit-transact-batch conn pull-attr
pull-many]]
[auto-ap.logging :as alog]
[auto-ap.permissions :refer [wrap-must]]
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
[auto-ap.routes.invoice :as invoice-routes]
[auto-ap.routes.ledger :as ledger-routes]
[auto-ap.routes.payments :as payment-routes]
[auto-ap.routes.transactions :as route]
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated]]
[auto-ap.rule-matching :as rm]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.components.link-dropdown :refer [link-dropdown]]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.ledger :refer [wrap-ensure-bank-account-belongs]]
[auto-ap.ssr.pos.common :refer [date-range-field*]]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.transaction.edit :as edit]
[auto-ap.ssr.transaction.bulk-code :as bulk-code :refer [all-ids-not-locked]]
[auto-ap.ssr.transaction.common :refer [bank-account-filter* fetch-ids
grid-page query-schema
wrap-status-from-source]]
[auto-ap.ssr.transaction.edit :as edit :refer [transaction-account-row*]]
[auto-ap.ssr.utils
:refer [apply-middleware-to-all-handlers clj-date-schema
entity-id html-response strip wrap-implied-route-param
wrap-merge-prior-hx wrap-schema-enforce]]
[auto-ap.time :as atime]
:refer [apply-middleware-to-all-handlers entity-id html-response
many-entity modal-response percentage ref->enum-schema
wrap-implied-route-param wrap-merge-prior-hx
wrap-schema-enforce]]
[bidi.bidi :as bidi]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[clojure.string :as str]
[datomic.api :as dc]
[hiccup.util :as hu]
[iol-ion.tx :refer [random-tempid]]
[malli.core :as mc]))
(defn exact-match-id* [request]
(if (nat-int? (:exact-match-id (:query-params request)))
[:div {:x-data (hx/json {:exact_match (:exact-match-id (:query-params request))}) :id "exact-match-id-tag"}
(com/hidden {:name "exact-match-id"
"x-model" "exact_match"})
(com/pill {:color :primary}
[:span.inline-flex.space-x-2.items-center
[:div "exact match"]
[:div.w-3.h-3
(com/link {"@click" "exact_match=null; $nextTick(() => $dispatch('change'))"}
svg/x)]])]
[:div {:id "exact-match-id-tag"}]))
(defn bank-account-filter* [request]
[:div {:hx-trigger "clientSelected from:body"
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::route/bank-account-filter)
:hx-target "this"
:hx-swap "outerHTML"}
(when (:client request)
(let [bank-account-belongs-to-client? (get (set (map :db/id (:client/bank-accounts (:client request))))
(:db/id (:bank-account (:query-params request))))]
(com/field {:label "Bank Account"}
(com/radio-card {:size :small
:name "bank-account"
:value (or (when bank-account-belongs-to-client?
(:db/id (:bank-account (:query-params request))))
"")
:options
(into [{:value ""
:content "All"}]
(for [ba (:client/bank-accounts (:client request))]
{:value (:db/id ba)
:content (:bank-account/name ba)}))}))))])
(defn bank-account-filter [request]
(html-response (bank-account-filter* request)))
(defn filters [request]
[:form#transaction-filters {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
"hx-get" (bidi/path-for ssr-routes/only-routes
::route/table)
"hx-target" "#entity-table"
"hx-indicator" "#entity-table"}
(com/hidden {:name "status"
:value (some-> (:status (:query-params request)) name)})
[:fieldset.space-y-6
(com/field {:label "Vendor"}
(com/typeahead {:name "vendor"
:id "vendor"
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
:value (:vendor (:query-params request))
:value-fn :db/id
:content-fn :vendor/name}))
(bank-account-filter* request)
(date-range-field* request)
(com/field {:label "Description"}
(com/text-input {:name "description"
:id "description"
:class "hot-filter"
:value (:description (:query-params request))
:placeholder "e.g., Groceries"
:size :small}))
(com/field {:label "Amount"}
[:div.flex.space-x-4.items-baseline
(com/money-input {:name "amount-gte"
:id "amount-gte"
:hx-preserve "true"
:class "hot-filter w-20"
:value (:amount-gte (:query-params request))
:placeholder "0.01"
:size :small})
[:div.align-baseline
"to"]
(com/money-input {:name "amount-lte"
:hx-preserve "true"
:id "amount-lte"
:class "hot-filter w-20"
:value (:amount-lte (:query-params request))
:placeholder "9999.34"
:size :small})])
(exact-match-id* request)]])
(defn fetch-ids [db {:keys [query-params route-params] :as request}]
(alog/peek ::ROUTE_PARAMS route-params)
(let [valid-clients (extract-client-ids (:clients request)
(:client-id request)
(:client-id query-params)
(when (:client-code request)
[:client/code (:client-code request)]))
args query-params
query
(if (:exact-match-id args)
{:query {:find '[?e]
:in '[$ ?e [?c ...]]
:where '[[?e :transaction/client ?c]]}
:args [db
(:exact-match-id args)
valid-clients]}
(cond-> {:query {:find []
:in ['$ '[?clients ?start ?end]]
:where '[[(iol-ion.query/scan-transactions $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]]}
:args [db
[valid-clients
(some-> (:start-date query-params) coerce/to-date)
(some-> (:end-date query-params) coerce/to-date)]]}
(seq (:description args))
(merge-query {:query {:in ['?description]
:where ['[?e :transaction/description-original ?do]
'[(clojure.string/lower-case ?do) ?do2]
'[(.contains ?do2 ?description)]]}
:args [(str/lower-case (:description args))]})
(:amount-gte args)
(merge-query {:query {:in ['?amount-gte]
:where ['[?e :transaction/amount ?a]
'[(>= ?a ?amount-gte)]]}
:args [(:amount-gte args)]})
(:amount-lte args)
(merge-query {:query {:in ['?amount-lte]
:where ['[?e :transaction/amount ?a]
'[(<= ?a ?amount-lte)]]}
:args [(:amount-lte args)]})
(:db/id (:bank-account args))
(merge-query {:query {:in ['?ba]
:where ['[?e :transaction/bank-account ?ba]]}
:args [(:db/id (:bank-account args))]})
(:vendor args)
(merge-query {:query {:in ['?vendor-id]
:where ['[?e :transaction/vendor ?vendor-id]]}
:args [(:db/id (:vendor args))]})
(:import-batch-id args)
(merge-query {:query {:in ['?import-batch-id]
:where ['[?import-batch-id :import-batch/entry ?e]]}
:args [(:import-batch-id args)]})
(:status route-params)
(merge-query {:query {:in ['?status]
:where ['[?e :transaction/approval-status ?status]]}
:args [(:status route-params)]})
(:sort args) (add-sorter-fields {"client" ['[?e :transaction/client ?c]
'[?c :client/name ?sort-client]]
"vendor" '[(or-join [?e ?sort-vendor]
(and
[?e :transaction/vendor ?v]
[?v :vendor/name ?sort-vendor])
(and [(missing? $ ?e :transaction/vendor)]
[(ground "") ?sort-vendor]))]
"date" ['[?e :transaction/date ?sort-date]]
"amount" ['[?e :transaction/amount ?sort-amount]]
"description" ['[?e :transaction/description-original ?sort-description]]}
args)
true
(merge-query {:query {:find ['?sort-default '?e]}})))]
(->> (observable-query query)
(apply-sort-4 (assoc query-params :default-asc? true))
(apply-pagination query-params))))
(def default-read
'[:transaction/amount
:transaction/description-original
:transaction/description-simple
[ :transaction/date :xform clj-time.coerce/from-date]
[ :transaction/post-date :xform clj-time.coerce/from-date]
:transaction/type
:transaction/status
:transaction/client-overrides
:db/id
{:transaction/vendor [:vendor/name :db/id]
:transaction/client [:client/name :client/code :db/id [ :client/locked-until :xform clj-time.coerce/from-date]]
:transaction/bank-account [:bank-account/numeric-code :bank-account/name]
:transaction/accounts [{:transaction-account/account [:account/name :db/id]}
:transaction-account/location
:transaction-account/amount]
:transaction/matched-rule [:matched-rule/name]
:transaction/payment [:db/id [:payment/date :xform clj-time.coerce/from-date]]}])
(defn hydrate-results [ids db _]
(let [results (->> (pull-many db default-read ids)
(group-by :db/id))
results (->> ids
(map results)
(map first))]
results))
(defn sum-amount [ids]
(->>
(dc/q {:find ['?id '?a]
:in ['$ '[?id ...]]
:where ['[?id :transaction/amount ?a]]}
(dc/db conn)
ids)
(map last)
(reduce + 0.0)))
(defn fetch-page [request]
(let [db (dc/db conn)
{ids-to-retrieve :ids matching-count :count
all-ids :all-ids} (fetch-ids db request)]
[(->> (hydrate-results ids-to-retrieve db request))
matching-count
(sum-amount all-ids)]))
(def query-schema (mc/schema
[:maybe [:map {:date-range [:date-range :start-date :end-date]}
[:sort {:optional true} [:maybe [:any]]]
[:per-page {:optional true :default 25} [:maybe :int]]
[:start {:optional true :default 0} [:maybe :int]]
[:amount-gte {:optional true} [:maybe :double]]
[:amount-lte {:optional true} [:maybe :double]]
[:client-id {:optional true} [:maybe entity-id]]
[:import-batch-id {:optional true} [:maybe entity-id]]
[:description {:optional true} [:maybe [:string {:decode/string strip}]]]
[: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]}]]]
#_[:status {:optional true} [:maybe (ref->enum-schema "transaction-status")]]
[:exact-match-id {:optional true} [:maybe entity-id]]
[:all-selected {:optional true :default nil} [:maybe :boolean]]
[:selected {:optional true :default nil} [:maybe [:vector {:coerce? true}
entity-id]]]
[:start-date {:optional true}
[:maybe clj-date-schema]]
[:end-date {:optional true}
[:maybe clj-date-schema]]]]))
(def grid-page
(helper/build {:id "entity-table"
:nav com/main-aside-nav
:check-boxes? true
:page-specific-nav filters
:fetch-page fetch-page
:query-schema query-schema
:oob-render
(fn [request]
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)
(assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)])
:action-buttons (fn [request]
[#_(com/button {:color :primary
:hx-get (bidi/path-for ssr-routes/only-routes
::route/new)}
"Add Transaction")])
:row-buttons (fn [request entity]
(let [client (:transaction/client entity)
locked-until (:client/locked-until client)
tx-date (:transaction/date entity)
is-locked (and locked-until tx-date (time/before? tx-date locked-until))]
(if is-locked
[ [:div.p-3.rounded-full.bg-gray-50.text-gray-400.w-6.h-6.box-content
svg/lock]]
[(com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes
::route/edit-wizard
:db/id (:db/id entity))}
svg/pencil)])))
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
"Transactions"]]
:title (fn [r]
"Transaction")
:entity-name "register"
:route ::route/table
:csv-route ::route/csv
:table-attributes (fn [_]
{:hx-trigger "refreshTable from:body"
:hx-get (bidi/path-for ssr-routes/only-routes ::route/table)
:hx-target "#entity-table"})
:break-table (fn [request entity]
(cond
(= (-> request :query-params :sort first :name) "Vendor")
(or (-> entity :transaction/vendor :vendor/name)
"No vendor")
:else nil))
:page->csv-entities (fn [[transactions]]
transactions)
:headers [{:key "id"
:name "Id"
:render-csv :db/id
:render-for #{:csv}}
{:key "client"
:name "Client"
:sort-key "client"
:hide? (fn [args]
(and (= (count (:clients args)) 1)
(= 1 (count (:client/locations (:client args))))))
:render (fn [x] [:div.flex.items-center.gap-2 (-> x :transaction/client :client/name)])
:render-csv (fn [x] (-> x :transaction/client :client/name))}
{:key "vendor"
:name "Vendor"
:sort-key "vendor"
:render (fn [e]
#_(alog/peek :vend e)
(or
(-> e :transaction/vendor :vendor/name)
[:span.italic.text-gray-400 (-> e :transaction/description-simple)]))
:render-csv (fn [e] (or (-> e :transaction/vendor :vendor/name)
(-> e :transaction/description-simple)))}
{:key "description"
:name "Description"
:sort-key "description"
:render :transaction/description-original
:render-csv :transaction/description-original}
{:key "date"
:sort-key "date"
:name "Date"
:show-starting "lg"
:render (fn [{:transaction/keys [date]}]
(some-> date (atime/unparse-local atime/normal-date)))}
{:key "amount"
:name "Amount"
:sort-key "amount"
:class "text-right"
:render #(format "$%,.2f" (:transaction/amount %))
:render-csv :transaction/amount}
{:key "links"
:name "Links"
:show-starting "lg"
:class "w-8"
:render (fn [i]
(let [db (dc/db conn)
journal-entries (when (:db/id i)
(dc/q '[:find (pull ?je [:db/id :journal-entry/id])
:in $ ?t-id
:where
[?je :journal-entry/original-entity ?t-id]]
db
(:db/id i)))
linked-invoices (when (and (:db/id i) (:transaction/payment i))
(dc/q '[:find (pull ?inv [:db/id :invoice/invoice-number :invoice/total])
:in $ ?payment-id
:where
[?ip :invoice-payment/payment ?payment-id]
[?ip :invoice-payment/invoice ?inv]]
db
(:db/id (:transaction/payment i))))]
(link-dropdown
(cond-> []
;; Payment link
(:transaction/payment i)
(conj
{:link (hu/url (bidi/path-for ssr-routes/only-routes
::payment-routes/all-page)
{:exact-match-id (:db/id (:transaction/payment i))})
:color :primary
:content (format "Payment '%s'" (-> i :transaction/payment :payment/date (atime/unparse-local atime/normal-date)))})
;; Journal entry links
(seq journal-entries)
(concat
(for [[je] journal-entries]
{:link (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/all-page)
{:exact-match-id (:db/id je)})
:color :yellow
:content "Ledger entry"}))
;; Invoice links
(seq linked-invoices)
(concat
(for [[inv] linked-invoices]
{:link (hu/url (bidi/path-for ssr-routes/only-routes
::invoice-routes/all-page)
{:exact-match-id (:db/id inv)})
:color :secondary
:content (format "Invoice '%s'" (:invoice/invoice-number inv))}))
))))
:render-for #{:html}}]}))
(def row* (partial helper/row* grid-page))
;; Handlers
(def page (helper/page-route grid-page))
(defn wrap-status-from-source [handler]
(fn [{:keys [matched-current-page-route] :as request}]
(let [ request (cond-> request
(= ::route/unapproved-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/unapproved)
(= ::route/approved-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/approved)
(= ::route/requires-feedback-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/requires-feedback)
(= ::route/page matched-current-page-route) (assoc-in [:route-params :status] nil))]
(handler request))))
(def table (helper/table-route grid-page))
(def csv (helper/csv-route grid-page))
;; Bulk action handlers
(defn bulk-delete [request]
(let [all-selected (:all-selected (:form-params request))
suppress (:suppress (:form-params request))
selected (:selected (:form-params request))
_ (alog/info ::selected-and-suppress :qp (:form-params request))
ids (cond
all-selected
(:ids (fetch-ids (dc/db conn) (-> request
(assoc-in [:form-params :start] 0)
(assoc-in [:form-params :per-page] 250))))
:else
selected)
all-ids (all-ids-not-locked ids)
db (dc/db conn)]
(alog/info ::bulk-delete-transactions
:count (count all-ids)
:sample (take 3 all-ids))
;; First retract journal entries and handle payment relationships
(audit-transact
(mapcat (fn [i]
(let [transaction (dc/pull db [:transaction/payment
:transaction/expected-deposit
:db/id] i)
payment-id (-> transaction :transaction/payment :db/id)
expected-deposit-id (-> transaction :transaction/expected-deposit :db/id)]
(cond->> [[:db/retractEntity [:journal-entry/original-entity i]]]
payment-id (into [{:db/id payment-id
:payment/status :payment-status/pending}
[:db/retract (:db/id transaction) :transaction/payment payment-id]])
expected-deposit-id (into [{:db/id expected-deposit-id
:expected-deposit/status :expected-deposit-status/pending}
[:db/retract (:db/id transaction) :transaction/expected-deposit expected-deposit-id]]))))
all-ids)
(:identity request))
;; Then retract or suppress the transactions
(audit-transact
(mapcat (fn [i]
(let [transaction-tx (if suppress
{:db/id i
:transaction/approval-status :transaction-approval-status/suppressed}
[:db/retractEntity i])]
[transaction-tx
[:db/retractEntity [:journal-entry/original-entity i]]]))
all-ids)
(:identity request))
(html-response
(com/success-modal {:title "Transactions Updated"}
[:p (str "Successfully " (if suppress "suppressed" "deleted") " " (count all-ids) " transactions.")])
:headers {"hx-trigger" "invalidated"})))
(def key->handler
(merge edit/key->handler
(apply-middleware-to-all-handlers
{::route/page page
::route/approved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/approved))
::route/unapproved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/unapproved))
::route/requires-feedback-page (-> page (wrap-implied-route-param :status :transaction-approval-status/requires-feedback))
::route/table table
::route/csv csv
::route/bank-account-filter bank-account-filter}
(fn [h]
(-> h
(wrap-copy-qp-pqp)
(wrap-apply-sort grid-page)
(wrap-ensure-bank-account-belongs)
(wrap-status-from-source)
(wrap-merge-prior-hx)
(wrap-schema-enforce :query-schema query-schema)
(wrap-schema-enforce :hx-schema query-schema)
(wrap-must {:activity :view :subject :transaction})
(wrap-client-redirect-unauthenticated))))))
(merge edit/key->handler
bulk-code/key->handler
(apply-middleware-to-all-handlers
{::route/page page
::route/approved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/approved))
::route/unapproved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/unapproved))
::route/requires-feedback-page (-> page (wrap-implied-route-param :status :transaction-approval-status/requires-feedback))
::route/table table
::route/csv csv
::route/bank-account-filter bank-account-filter
::route/bulk-delete (-> bulk-delete
(wrap-schema-enforce :form-schema query-schema))}
(fn [h]
(-> h
(wrap-copy-qp-pqp)
(wrap-apply-sort grid-page)
(wrap-ensure-bank-account-belongs)
(wrap-status-from-source)
(wrap-merge-prior-hx)
(wrap-schema-enforce :query-schema query-schema)
(wrap-schema-enforce :hx-schema query-schema)
(wrap-must {:activity :view :subject :transaction})
(wrap-client-redirect-unauthenticated))))))

View File

@@ -0,0 +1,348 @@
(ns auto-ap.ssr.transaction.bulk-code
(:require
[auto-ap.datomic
:refer [audit-transact-batch conn pull-attr pull-many]]
[auto-ap.logging :as alog]
[auto-ap.permissions :refer [wrap-must]]
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
[auto-ap.routes.transactions :as route]
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated]]
[auto-ap.rule-matching :as rm]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.components.multi-modal :as mm :refer [wrap-wizard]]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.grid-page-helper :refer [wrap-apply-sort]]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.ledger :refer [wrap-ensure-bank-account-belongs]]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.transaction.common :refer [grid-page query-schema
selected->ids
wrap-status-from-source]]
[auto-ap.ssr.transaction.edit :as edit :refer [account-typeahead*
location-select*]]
[auto-ap.ssr.utils
:refer [apply-middleware-to-all-handlers entity-id
form-validation-error html-response percentage
ref->enum-schema wrap-merge-prior-hx wrap-schema-enforce]]
[bidi.bidi :as bidi]
[datomic.api :as dc]
[iol-ion.query :refer [dollars=]]
[iol-ion.tx :refer [random-tempid]]
[malli.core :as mc]))
(defn transaction-account-row* [{:keys [value client-id]}]
(com/data-grid-row
(-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? value))))
:accountId (fc/field-value (:account value))})
:data-key "show"
:x-ref "p"}
hx/alpine-mount-then-appear)
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
:value (fc/field-value)}))
(fc/with-field :account
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)}
(account-typeahead* {:value (fc/field-value)
:client-id client-id
:name (fc/field-name)
:x-model "accountId"}))))
(fc/with-field :location
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)
:x-hx-val:account-id "accountId"
:hx-vals (hx/json (cond-> {:name (fc/field-name) }
client-id (assoc :client-id client-id)))
:x-dispatch:changed "accountId"
: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 (cond->> (:account @value)
(nat-int? (:account @value)) (dc/pull (dc/db conn)
'[:account/location])))
:client-locations (pull-attr (dc/db conn) :client/locations client-id)
:value (fc/field-value)}))))
(fc/with-field :percentage
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)}
(com/money-input {:name (fc/field-name)
:class "w-16"
:value (some-> (fc/field-value)
(* 100)
(long))}))))
(com/data-grid-cell {:class "align-top"}
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
(defn initial-bulk-edit-state [request]
(mm/->MultiStepFormState {:search-params (:query-params request)
:accounts []}
[]
{:search-params (:query-params request)
:accounts []}))
(defn all-ids-not-locked
"Filters transaction IDs to only those that aren't locked (client locked date earlier than transaction date)"
[all-ids]
(->> all-ids
(dc/q '[:find ?t
:in $ [?t ...]
:where
[?t :transaction/client ?c]
[(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
[?t :transaction/date ?d]
[(>= ?d ?lu)]]
(dc/db conn))
(map first)))
(def bulk-code-schema
(mc/schema [:map
[:vendor {:optional true} [:maybe entity-id]]
[:approval-status {:optional true} [:maybe (ref->enum-schema "transaction-approval-status")] ]
[:accounts {:optional true}
[:maybe
[:vector {:coerce? true}
[:map [:account entity-id]
[:location [:string {:min 1 :error/message "required"}]]
[:percentage percentage]]]] ]]))
(defn maybe-code-accounts [transaction account-rules valid-locations]
(with-precision 2
(let [accounts (vec (mapcat
(fn [ar]
(let [cents-to-distribute (int (Math/round (Math/abs (* (:percentage ar)
(:transaction/amount transaction)
100))))]
(if (= "Shared" (:location ar))
(->> valid-locations
(map
(fn [cents location]
{:db/id (random-tempid)
:transaction-account/account (:account ar)
:transaction-account/amount (* 0.01 cents)
:transaction-account/location location})
(rm/spread-cents cents-to-distribute (count valid-locations))))
[(cond-> {:db/id (random-tempid)
:transaction-account/account (:account ar)
:transaction-account/amount (* 0.01 cents-to-distribute)}
(:location ar) (assoc :transaction-account/location (:location ar)))])))
account-rules))
accounts (mapv
(fn [a]
(update a :transaction-account/amount
#(with-precision 2
(double (.setScale (bigdec %) 2 java.math.RoundingMode/HALF_UP)))))
accounts)
leftover (with-precision 2 (.round (bigdec (- (Math/abs (:transaction/amount transaction))
(Math/abs (reduce + 0.0 (map #(:transaction-account/amount %) accounts)))))
*math-context*))
accounts (if (seq accounts)
(update-in accounts [(dec (count accounts)) :transaction-account/amount] #(+ % (double leftover)))
[])]
accounts)))
(defrecord AccountsStep [linear-wizard]
mm/ModalWizardStep
(step-name [_]
"Bulk Code")
(step-key [_]
:accounts)
(edit-path [_ _]
[])
(step-schema [_]
(mm/form-schema linear-wizard))
(render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}]
(let [_ (alog/peek ::SEARCH_PARAMS (:search-params snapshot))
selected-ids (selected->ids (assoc request :query-params (:search-params snapshot)) (:search-params snapshot))
all-ids (all-ids-not-locked selected-ids)]
(mm/default-render-step
linear-wizard this
:head [:div.p-2 "Bulk editing " (count all-ids) " transactions"]
:body (mm/default-step-body
{}
[:div
#_(com/hidden {:name "ids" :value (pr-str ids)})
[:div.space-y-4.p-4
[:div.grid.grid-cols-2.gap-4
;; Vendor field
[:div
(fc/with-field :vendor
(com/validated-field {:label "Vendor"
:errors (fc/field-errors)}
(com/typeahead {:name (fc/field-name)
:placeholder "Search for vendor..."
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})))]
;; Status field
[:div
(fc/with-field :approval-status
(com/validated-field {:label "Status"
:errors (fc/field-errors)}
(com/select {:name (fc/field-name)
:options [["" "No Change"]
["approved" "Approved"]
["unapproved" "Unapproved"]
["suppressed" "Suppressed"]
["requires_feedback" "Requires Feedback"]]})))]
;; Accounts section
[:div.col-span-2.pt-4
[:h3.text-lg.font-medium.mb-3 "Expense Accounts"]
[:div.space-y-3#account-entries
(fc/with-field :accounts
(com/validated-field
{:errors (fc/field-errors)}
(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/data-grid-header {:class "w-16"})]}
(fc/cursor-map #(transaction-account-row* {:value %}))
(com/data-grid-new-row {:colspan 4
:hx-get (bidi/path-for ssr-routes/only-routes
::route/bulk-code-new-account)
:row-offset 0
:index (count (fc/field-value))}
"New account")
)))
;; Button to add more accounts
]]]]])
:footer
(mm/default-step-footer linear-wizard this :validation-route ::route/new-wizard-navigate
:next-button (com/button {:color :primary :x-ref "next" :class "w-32"} "Save"))
:validation-route ::route/new-wizard-navigate))))
(defn assert-percentages-add-up [{:keys [accounts]}]
(let [account-total (reduce + 0 (map (fn [x] (:percentage x)) accounts))]
(when-not (dollars= 1.0 account-total)
(form-validation-error (str "Expense account total (" account-total ") does not equal 100%")))))
(defrecord BulkCodeWizard [_ current-step]
mm/LinearModalWizard
(hydrate-from-request
[this request]
this)
(navigate [this step-key]
(assoc this :current-step step-key))
(get-current-step [this]
(if current-step
(mm/get-step this current-step)
(mm/get-step this :accounts)))
(render-wizard [this {:keys [multi-form-state] :as request}]
(mm/default-render-wizard
this request
:form-params
(-> mm/default-form-props
(assoc :hx-put
(str (bidi/path-for ssr-routes/only-routes ::route/bulk-code-submit))))
:render-timeline? false))
(steps [_]
[:accounts])
(get-step [this step-key]
(let [step-key-result (mc/parse mm/step-key-schema step-key)
[step-key-type step-key] step-key-result]
(get {:accounts (->AccountsStep this)}
step-key)))
(form-schema [_]
bulk-code-schema)
(submit [this {:keys [multi-form-state request-method identity] :as request}]
(let [ ids (selected->ids (assoc request :query-params (:search-params (:snapshot multi-form-state))) (:search-params (:snapshot multi-form-state)))
all-ids (all-ids-not-locked ids)
vendor (-> request :multi-form-state :snapshot :vendor)
approval-status (-> request :multi-form-state :snapshot :approval-status)
accounts (-> request :multi-form-state :snapshot :accounts) ]
(when (seq accounts)
(assert-percentages-add-up (:snapshot multi-form-state)))
(alog/peek ::ACCOUNTS (-> request :multi-form-state :snapshot))
;; Get transactions and filter for locked ones
(let [db (dc/db conn)
transactions (pull-many db [:db/id :transaction/amount {:transaction/client [:db/id]}] (vec all-ids))
;; Get client locations
client->locations (->> (map (comp :db/id :transaction/client) transactions)
(distinct)
(dc/q '[:find (pull ?e [:db/id :client/locations])
:in $ [?e ...]]
db)
(map (fn [[client]]
[(:db/id client) (:client/locations client)]))
(into {}))]
(audit-transact-batch
(map (fn [t]
(let [locations (client->locations (-> t :transaction/client :db/id))]
[:upsert-transaction (cond-> t
approval-status
(assoc :transaction/approval-status approval-status)
vendor
(assoc :transaction/vendor vendor)
(seq accounts)
(assoc :transaction/accounts
(maybe-code-accounts t accounts locations)))]))
transactions)
(:identity request))
;; Return success modal
(html-response
(com/success-modal {:title "Transactions Coded"}
[:p (str "Successfully coded " (count all-ids) " transactions.")])
:headers {"hx-trigger" "refreshTable"})))))
(def bulk-code-wizard (->BulkCodeWizard nil nil))
(def key->handler
(apply-middleware-to-all-handlers
{::route/bulk-code (-> mm/open-wizard-handler
(mm/wrap-wizard bulk-code-wizard)
(mm/wrap-init-multi-form-state initial-bulk-edit-state))
::route/bulk-code-new-account (->
(add-new-entity-handler [:step-params :accounts]
(fn render [cursor request]
(transaction-account-row*
{:value cursor}))
(fn build-new-row [base _]
(assoc base :location "Shared")))
(wrap-schema-enforce :query-schema [:map
[:client-id {:optional true}
[:maybe entity-id]]]))
::route/bulk-code-submit (-> mm/submit-handler
(wrap-wizard bulk-code-wizard)
(mm/wrap-decode-multi-form-state))}
(fn [h]
(-> h
(wrap-copy-qp-pqp)
(wrap-ensure-bank-account-belongs)
(wrap-status-from-source)
(wrap-apply-sort grid-page)
(wrap-merge-prior-hx)
(wrap-schema-enforce :query-schema query-schema)
(wrap-schema-enforce :hx-schema query-schema)
(wrap-must {:activity :bulk-code :subject :transaction})
(wrap-client-redirect-unauthenticated)))))

View File

@@ -0,0 +1,466 @@
(ns auto-ap.ssr.transaction.common
(:require
[auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-4
conn merge-query observable-query pull-many]]
[auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.routes.invoice :as invoice-routes]
[auto-ap.routes.ledger :as ledger-routes]
[auto-ap.routes.payments :as payment-routes]
[auto-ap.routes.transactions :as route]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.components.link-dropdown :refer [link-dropdown]]
[auto-ap.ssr.grid-page-helper :as helper]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.pos.common :refer [date-range-field*]]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils :refer [clj-date-schema entity-id strip]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[clojure.string :as str]
[datomic.api :as dc]
[hiccup.util :as hu]
[malli.core :as mc]))
(def query-schema (mc/schema
[:maybe [:map {:date-range [:date-range :start-date :end-date]}
[:sort {:optional true} [:maybe [:any]]]
[:per-page {:optional true :default 25} [:maybe :int]]
[:start {:optional true :default 0} [:maybe :int]]
[:amount-gte {:optional true} [:maybe :double]]
[:amount-lte {:optional true} [:maybe :double]]
[:client-id {:optional true} [:maybe entity-id]]
[:import-batch-id {:optional true} [:maybe entity-id]]
[:description {:optional true} [:maybe [:string {:decode/string strip}]]]
[: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]}]]]
#_[:status {:optional true} [:maybe (ref->enum-schema "transaction-status")]]
[:exact-match-id {:optional true} [:maybe entity-id]]
[:all-selected {:optional true :default nil} [:maybe :boolean]]
[:selected {:optional true :default nil} [:maybe [:vector {:coerce? true}
entity-id]]]
[:start-date {:optional true}
[:maybe clj-date-schema]]
[:end-date {:optional true}
[:maybe clj-date-schema]]]]))
(def default-read
'[:transaction/amount
:transaction/description-original
:transaction/description-simple
[ :transaction/date :xform clj-time.coerce/from-date]
[ :transaction/post-date :xform clj-time.coerce/from-date]
:transaction/type
:transaction/status
:transaction/client-overrides
:db/id
{:transaction/vendor [:vendor/name :db/id]
:transaction/client [:client/name :client/code :db/id [ :client/locked-until :xform clj-time.coerce/from-date]]
:transaction/bank-account [:bank-account/numeric-code :bank-account/name]
:transaction/accounts [{:transaction-account/account [:account/name :db/id]}
:transaction-account/location
:transaction-account/amount]
:transaction/matched-rule [:matched-rule/name]
:transaction/payment [:db/id [:payment/date :xform clj-time.coerce/from-date]]}])
(defn hydrate-results [ids db _]
(let [results (->> (pull-many db default-read ids)
(group-by :db/id))
results (->> ids
(map results)
(map first))]
results))
(defn sum-amount [ids]
(->>
(dc/q {:find ['?id '?a]
:in ['$ '[?id ...]]
:where ['[?id :transaction/amount ?a]]}
(dc/db conn)
ids)
(map last)
(reduce + 0.0)))
(defn all-ids-not-locked
"Filters transaction IDs to only those that aren't locked (client locked date earlier than transaction date)"
[all-ids]
(->> all-ids
(dc/q '[:find ?t
:in $ [?t ...]
:where
[?t :transaction/client ?c]
[(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
[?t :transaction/date ?d]
[(>= ?d ?lu)]]
(dc/db conn))
(map first)))
(defn fetch-ids [db {:keys [query-params route-params] :as request}]
(let [valid-clients (extract-client-ids (:clients request)
(:client-id request)
(:client-id query-params)
(when (:client-code request)
[:client/code (:client-code request)]))
args query-params
query
(if (:exact-match-id args)
{:query {:find '[?e]
:in '[$ ?e [?c ...]]
:where '[[?e :transaction/client ?c]]}
:args [db
(:exact-match-id args)
valid-clients]}
(cond-> {:query {:find []
:in ['$ '[?clients ?start ?end]]
:where '[[(iol-ion.query/scan-transactions $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]]}
:args [db
[valid-clients
(some-> (:start-date query-params) coerce/to-date)
(some-> (:end-date query-params) coerce/to-date)]]}
(seq (:description args))
(merge-query {:query {:in ['?description]
:where ['[?e :transaction/description-original ?do]
'[(clojure.string/lower-case ?do) ?do2]
'[(.contains ?do2 ?description)]]}
:args [(str/lower-case (:description args))]})
(:amount-gte args)
(merge-query {:query {:in ['?amount-gte]
:where ['[?e :transaction/amount ?a]
'[(>= ?a ?amount-gte)]]}
:args [(:amount-gte args)]})
(:amount-lte args)
(merge-query {:query {:in ['?amount-lte]
:where ['[?e :transaction/amount ?a]
'[(<= ?a ?amount-lte)]]}
:args [(:amount-lte args)]})
(:db/id (:bank-account args))
(merge-query {:query {:in ['?ba]
:where ['[?e :transaction/bank-account ?ba]]}
:args [(:db/id (:bank-account args))]})
(:vendor args)
(merge-query {:query {:in ['?vendor-id]
:where ['[?e :transaction/vendor ?vendor-id]]}
:args [(:db/id (:vendor args))]})
(:import-batch-id args)
(merge-query {:query {:in ['?import-batch-id]
:where ['[?import-batch-id :import-batch/entry ?e]]}
:args [(:import-batch-id args)]})
(:status route-params)
(merge-query {:query {:in ['?status]
:where ['[?e :transaction/approval-status ?status]]}
:args [(:status route-params)]})
(:sort args) (add-sorter-fields {"client" ['[?e :transaction/client ?c]
'[?c :client/name ?sort-client]]
"vendor" '[(or-join [?e ?sort-vendor]
(and
[?e :transaction/vendor ?v]
[?v :vendor/name ?sort-vendor])
(and [(missing? $ ?e :transaction/vendor)]
[(ground "") ?sort-vendor]))]
"date" ['[?e :transaction/date ?sort-date]]
"amount" ['[?e :transaction/amount ?sort-amount]]
"description" ['[?e :transaction/description-original ?sort-description]]}
args)
true
(merge-query {:query {:find ['?sort-default '?e]}})))]
(->> (observable-query query)
(apply-sort-4 (assoc query-params :default-asc? true))
(apply-pagination query-params))))
(defn fetch-page [request]
(let [db (dc/db conn)
{ids-to-retrieve :ids matching-count :count
all-ids :all-ids} (fetch-ids db request)]
[(->> (hydrate-results ids-to-retrieve db request))
matching-count
(sum-amount all-ids)]))
(defn exact-match-id* [request]
(if (nat-int? (:exact-match-id (:query-params request)))
[:div {:x-data (hx/json {:exact_match (:exact-match-id (:query-params request))}) :id "exact-match-id-tag"}
(com/hidden {:name "exact-match-id"
"x-model" "exact_match"})
(com/pill {:color :primary}
[:span.inline-flex.space-x-2.items-center
[:div "exact match"]
[:div.w-3.h-3
(com/link {"@click" "exact_match=null; $nextTick(() => $dispatch('change'))"}
svg/x)]])]
[:div {:id "exact-match-id-tag"}]))
(defn bank-account-filter* [request]
[:div {:hx-trigger "clientSelected from:body"
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::route/bank-account-filter)
:hx-target "this"
:hx-swap "outerHTML"}
(when (:client request)
(let [bank-account-belongs-to-client? (get (set (map :db/id (:client/bank-accounts (:client request))))
(:db/id (:bank-account (:query-params request))))]
(com/field {:label "Bank Account"}
(com/radio-card {:size :small
:name "bank-account"
:value (or (when bank-account-belongs-to-client?
(:db/id (:bank-account (:query-params request))))
"")
:options
(into [{:value ""
:content "All"}]
(for [ba (:client/bank-accounts (:client request))]
{:value (:db/id ba)
:content (:bank-account/name ba)}))}))))])
(defn filters [request]
[:form#transaction-filters {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
"hx-get" (bidi/path-for ssr-routes/only-routes
::route/table)
"hx-target" "#entity-table"
"hx-indicator" "#entity-table"}
(com/hidden {:name "status"
:value (some-> (:status (:query-params request)) name)})
[:fieldset.space-y-6
(com/field {:label "Vendor"}
(com/typeahead {:name "vendor"
:id "vendor"
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
:value (:vendor (:query-params request))
:value-fn :db/id
:content-fn :vendor/name}))
(bank-account-filter* request)
(date-range-field* request)
(com/field {:label "Description"}
(com/text-input {:name "description"
:id "description"
:class "hot-filter"
:value (:description (:query-params request))
:placeholder "e.g., Groceries"
:size :small}))
(com/field {:label "Amount"}
[:div.flex.space-x-4.items-baseline
(com/money-input {:name "amount-gte"
:id "amount-gte"
:hx-preserve "true"
:class "hot-filter w-20"
:value (:amount-gte (:query-params request))
:placeholder "0.01"
:size :small})
[:div.align-baseline
"to"]
(com/money-input {:name "amount-lte"
:hx-preserve "true"
:id "amount-lte"
:class "hot-filter w-20"
:value (:amount-lte (:query-params request))
:placeholder "9999.34"
:size :small})])
(exact-match-id* request)]])
(def grid-page
(helper/build {:id "entity-table"
:nav com/main-aside-nav
:check-boxes? true
:page-specific-nav filters
:fetch-page fetch-page
:query-schema query-schema
:oob-render
(fn [request]
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)
(assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)])
:action-buttons (fn [request]
[
(com/button {:color :primary
:hx-get (bidi/path-for ssr-routes/only-routes ::route/bulk-code)
:hx-target "#modal-holder"
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
"hx-include" "#transaction-filters"
}
"Code")
(com/button {:color :primary
:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-delete)
:hx-target "#modal-holder"
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
"hx-include" "#transaction-filters"
:hx-confirm "Are you sure you want to delete these transactions?"}
"Delete")
(com/button {:color :primary
:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-delete)
:hx-target "#modal-holder"
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
"hx-include" "#transaction-filters"
:hx-confirm "Are you sure you want to suppress these transactions?"}
"Suppress")])
:row-buttons (fn [request entity]
(let [client (:transaction/client entity)
locked-until (:client/locked-until client)
tx-date (:transaction/date entity)
is-locked (and locked-until tx-date (time/before? tx-date locked-until))]
(if is-locked
[ [:div.p-3.rounded-full.bg-gray-50.text-gray-400.w-6.h-6.box-content
svg/lock]]
[(com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes
::route/edit-wizard
:db/id (:db/id entity))}
svg/pencil)])))
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
"Transactions"]]
:title (fn [r]
"Transaction")
:entity-name "register"
:route ::route/table
:csv-route ::route/csv
:table-attributes (fn [_]
{:hx-trigger "refreshTable from:body"
:hx-get (bidi/path-for ssr-routes/only-routes ::route/table)
:hx-target "#entity-table"})
:break-table (fn [request entity]
(cond
(= (-> request :query-params :sort first :name) "Vendor")
(or (-> entity :transaction/vendor :vendor/name)
"No vendor")
:else nil))
:page->csv-entities (fn [[transactions]]
transactions)
:headers [{:key "id"
:name "Id"
:render-csv :db/id
:render-for #{:csv}}
{:key "client"
:name "Client"
:sort-key "client"
:hide? (fn [args]
(and (= (count (:clients args)) 1)
(= 1 (count (:client/locations (:client args))))))
:render (fn [x] [:div.flex.items-center.gap-2 (-> x :transaction/client :client/name)])
:render-csv (fn [x] (-> x :transaction/client :client/name))}
{:key "vendor"
:name "Vendor"
:sort-key "vendor"
:render (fn [e]
#_(alog/peek :vend e)
(or
(-> e :transaction/vendor :vendor/name)
[:span.italic.text-gray-400 (-> e :transaction/description-simple)]))
:render-csv (fn [e] (or (-> e :transaction/vendor :vendor/name)
(-> e :transaction/description-simple)))}
{:key "description"
:name "Description"
:sort-key "description"
:render :transaction/description-original
:render-csv :transaction/description-original}
{:key "date"
:sort-key "date"
:name "Date"
:show-starting "lg"
:render (fn [{:transaction/keys [date]}]
(some-> date (atime/unparse-local atime/normal-date)))}
{:key "amount"
:name "Amount"
:sort-key "amount"
:class "text-right"
:render #(format "$%,.2f" (:transaction/amount %))
:render-csv :transaction/amount}
{:key "links"
:name "Links"
:show-starting "lg"
:class "w-8"
:render (fn [i]
(let [db (dc/db conn)
journal-entries (when (:db/id i)
(dc/q '[:find (pull ?je [:db/id :journal-entry/id])
:in $ ?t-id
:where
[?je :journal-entry/original-entity ?t-id]]
db
(:db/id i)))
linked-invoices (when (and (:db/id i) (:transaction/payment i))
(dc/q '[:find (pull ?inv [:db/id :invoice/invoice-number :invoice/total])
:in $ ?payment-id
:where
[?ip :invoice-payment/payment ?payment-id]
[?ip :invoice-payment/invoice ?inv]]
db
(:db/id (:transaction/payment i))))]
(link-dropdown
(cond-> []
;; Payment link
(:transaction/payment i)
(conj
{:link (hu/url (bidi/path-for ssr-routes/only-routes
::payment-routes/all-page)
{:exact-match-id (:db/id (:transaction/payment i))})
:color :primary
:content (format "Payment '%s'" (-> i :transaction/payment :payment/date (atime/unparse-local atime/normal-date)))})
;; Journal entry links
(seq journal-entries)
(concat
(for [[je] journal-entries]
{:link (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/all-page)
{:exact-match-id (:db/id je)})
:color :yellow
:content "Ledger entry"}))
;; Invoice links
(seq linked-invoices)
(concat
(for [[inv] linked-invoices]
{:link (hu/url (bidi/path-for ssr-routes/only-routes
::invoice-routes/all-page)
{:exact-match-id (:db/id inv)})
:color :secondary
:content (format "Invoice '%s'" (:invoice/invoice-number inv))}))
))))
:render-for #{:html}}]}))
(defn wrap-status-from-source [handler]
(fn [{:keys [matched-current-page-route] :as request}]
(let [ request (cond-> request
(= ::route/unapproved-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/unapproved)
(= ::route/approved-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/approved)
(= ::route/requires-feedback-page matched-current-page-route) (assoc-in [:route-params :status] :transaction-approval-status/requires-feedback)
(= ::route/page matched-current-page-route) (assoc-in [:route-params :status] nil))]
(handler request))))
(defn selected->ids [request params]
(let [all-selected (:all-selected params)
selected (:selected params)
ids (cond
all-selected
(:ids (fetch-ids (dc/db conn) (-> request
(assoc :query-params params)
(assoc-in [:query-params :start] 0)
(assoc-in [:query-params :per-page] 250))))
:else
selected)]
ids))

View File

@@ -156,8 +156,8 @@
(com/typeahead {:name name
:placeholder "Search..."
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
{:client-id client-id
:purpose "invoice"})
(cond-> { :purpose "transaction"}
client-id (assoc :client-id client-id)))
:id name
:x-model x-model
:value value
@@ -190,8 +190,8 @@
(com/validated-field
{:errors (fc/field-errors)
:x-hx-val:account-id "accountId"
:hx-vals (hx/json {:name (fc/field-name)
:client-id client-id})
:hx-vals (hx/json (cond-> {:name (fc/field-name) }
client-id (assoc :client-id client-id)))
:x-dispatch:changed "accountId"
:hx-trigger "changed"
:hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select)
@@ -209,7 +209,7 @@
(com/validated-field
{:errors (fc/field-errors)}
(com/money-input {:name (fc/field-name)
:class "w-16 amount-field"
:class "w-16"
: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))))
@@ -733,7 +733,6 @@
(com/data-grid-header {:class "w-32"} "Location")
(com/data-grid-header {:class "w-16"} "$")
(com/data-grid-header {:class "w-16"})]}
(println "WE ARE NOW HERE" (fc/field-value))
(fc/cursor-map #(transaction-account-row* {:value %
:client-id (-> request :entity :transaction/client :db/id)}))

View File

@@ -4,7 +4,12 @@
:put ::edit-wizard-navigate
"/unapproved" ::unapproved-page
"/requires-feedback" ::requires-feedback-page
"/approved" ::approved-page}
"/approved" ::approved-page
"/bulk-delete" ::bulk-delete
"/bulk-suppress" ::bulk-suppress
"/bulk-code" {:get ::bulk-code
:put ::bulk-code-submit
"/new-account" ::bulk-code-new-account}}
"/new" {:get ::new
:post ::new-submit
"/location-select" ::location-select

2
tasks
View File

@@ -1,7 +1,5 @@
* Add tests for edit transaction.
* Make it so you can create a new vendor again.
* Check permissions on ledger, transactions, reports
* make locked transactions clearer on the transaction table
* Make locked transactions not look butt ugly with errors
* Implement bulk actions
* Get rid of new transaction button