bulk edit is now possible
This commit is contained in:
@@ -106,6 +106,8 @@
|
||||
[])))
|
||||
|
||||
(defn rebuild-search-index []
|
||||
(solr/delete solr/impl "accounts")
|
||||
(Thread/sleep 10000)
|
||||
(solr/index-documents-raw
|
||||
solr/impl
|
||||
"accounts"
|
||||
@@ -141,4 +143,4 @@
|
||||
"name" (:account/search-terms result)
|
||||
"numeric_code" (:account/numeric-code result)
|
||||
"location" (:account/location result)
|
||||
"applicability" (name (:db/ident (:account/applicability result)))})))
|
||||
"applicability" (name (:db/ident (:account/applicability result)))})))
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
{:account_id (first account_id)
|
||||
:name (first name)})))
|
||||
|
||||
|
||||
(defn account-search [{{:keys [q client-id purpose vendor-id] :as qp} :query-params id :identity :as request}]
|
||||
(let [client-id (or client-id (:db/id (:client request)))]
|
||||
(when client-id
|
||||
|
||||
@@ -25,43 +25,41 @@
|
||||
"w-4 h-4 bg-gray-100 indeterminate:bg-gray-300 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600")
|
||||
|
||||
(defn select- [params & children]
|
||||
(into
|
||||
[:select (-> params
|
||||
(dissoc :allow-blank? :value :options)
|
||||
(update
|
||||
:class (fnil hh/add-class "") default-input-classes))
|
||||
(cond->>
|
||||
(map (fn [[k v]]
|
||||
[:option {:value k :selected (= k (:value params))} v])
|
||||
(:options params))
|
||||
(:allow-blank? params) (conj [:option {:value "" :selected (not (:value params))} ""]))]
|
||||
children))
|
||||
(into
|
||||
[:select (-> params
|
||||
(dissoc :allow-blank? :value :options)
|
||||
(update
|
||||
:class (fnil hh/add-class "") default-input-classes))
|
||||
(cond->>
|
||||
(map (fn [[k v]]
|
||||
[:option {:value k :selected (= k (:value params))} v])
|
||||
(:options params))
|
||||
(:allow-blank? params) (conj [:option {:value "" :selected (not (:value params))} ""]))]
|
||||
children))
|
||||
|
||||
(defn checkbox- [params & rest]
|
||||
(if (seq rest)
|
||||
[:label {:class "text-sm text-gray-800 dark:text-gray-300 "}
|
||||
[:input (merge (dissoc params :indeterminate?)
|
||||
[:input (merge (dissoc params :indeterminate?)
|
||||
{:type "checkbox" :class (hh/add-class default-checkbox-classes (:class params ""))}
|
||||
(when (:indeterminate? params)
|
||||
{:x-init "$el.indeterminate = true"}))]
|
||||
[:span.ml-2
|
||||
[:span.ml-2
|
||||
rest]]
|
||||
[:input (merge (dissoc params :indeterminate params)
|
||||
[:input (merge (dissoc params :indeterminate params)
|
||||
{:type "checkbox" :class (hh/add-class default-checkbox-classes (:class params ""))}
|
||||
(when (:indeterminate? params)
|
||||
{:x-init "$el.indeterminate = true"}))
|
||||
]))
|
||||
{:x-init "$el.indeterminate = true"}))]))
|
||||
|
||||
(defn typeahead- [params]
|
||||
[:div.relative {:x-data (hx/json { :baseUrl (str (:url params))
|
||||
[:div.relative {:x-data (hx/json {:baseUrl (str (:url params))
|
||||
:value {:value ((:value-fn params identity) (:value params)) :label ((:content-fn params identity) (:value params))}
|
||||
:tippy nil
|
||||
:search ""
|
||||
:active -1
|
||||
:elements (if ((:value-fn params identity) (:value params))
|
||||
[{:value ((:value-fn params identity) (:value params)) :label ((:content-fn params identity) (:value params))}]
|
||||
[])
|
||||
})
|
||||
[])})
|
||||
:x-modelable "value.value"
|
||||
:x-model (:x-model params)}
|
||||
(if (:disabled params)
|
||||
@@ -72,7 +70,7 @@
|
||||
"@keydown.down.prevent.stop" "tippy.show();"
|
||||
"@keydown.backspace" "tippy.hide(); value = {value: '', label: '' }"
|
||||
:tabindex 0
|
||||
:x-init (str "$nextTick(() => tippy = $el.__x_tippy); " (:x-init params))
|
||||
:x-init (str "$nextTick(() => tippy = $el.__x_tippy); " (:x-init params))
|
||||
:x-ref "input"}
|
||||
[:input (-> params
|
||||
(dissoc :class)
|
||||
@@ -91,9 +89,9 @@
|
||||
[:span.flex-grow.text-left {"x-text" "value.label"}]
|
||||
[:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"}
|
||||
svg/drop-down]
|
||||
[:div {:x-show "value.warning" }
|
||||
[:div {:x-show "value.warning"}
|
||||
(tags/badge- {:class "peer"
|
||||
:x-tooltip "value.warning"} "!") ]]])
|
||||
:x-tooltip "value.warning"} "!")]]])
|
||||
|
||||
[:template {:x-ref "dropdown"}
|
||||
[:ul.dropdown-contents {:class "bg-gray-100 dark:bg-gray-600 ring-1"
|
||||
@@ -148,22 +146,19 @@
|
||||
"x-init" " $el.focus(); $watch('search', s => { if($el.value.length > 2) {fetch(addQueryParam(baseUrl, 'q', s)).then(data=>data.json()).then(data => reset_elements(data)) }})"}]]
|
||||
[:div.dropdown-options {:class "overflow-hidden divide-y divide-gray-200 "}
|
||||
[:template {:x-for "(element, index) in elements"}
|
||||
[:li {":style" "index == 0 && 'border: 0 !important;'"}
|
||||
[:li {":style" "index == 0 && 'border: 0 !important;'"}
|
||||
[:label {:class "p-3 group rounded flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-300 [&.active]:dark:bg-primary-700 [&.implied]:text-gray-500 text-gray-800 dark:text-gray-100 cursor-pointer"
|
||||
|
||||
|
||||
|
||||
:href "#"
|
||||
":class" (hx/json {"active" (hx/js-fn "active==index")
|
||||
"implied" (hx/js-fn "all_selected && index != 0")
|
||||
} )
|
||||
"implied" (hx/js-fn "all_selected && index != 0")})
|
||||
"@mouseover" "active = index"
|
||||
"@mouseout" "active = -1"
|
||||
"@click.prevent" "toggle(element)"}
|
||||
(checkbox- {":checked" "value.has(element.value) || all_selected"
|
||||
:class "group-[&.implied]:bg-green-200"
|
||||
|
||||
})
|
||||
#_[:input {:type "checkbox" }]
|
||||
(checkbox- {":checked" "value.has(element.value) || all_selected"
|
||||
:class "group-[&.implied]:bg-green-200"})
|
||||
#_[:input {:type "checkbox"}]
|
||||
[:span {"x-html" "element.label"}]]]]
|
||||
[:template {:x-if "elements.length == 0"}
|
||||
[:li {:class "px-4 pt-4 flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-500 text-gray-800 dark:text-gray-100 text-xs " "style" "border: 0 !important"}
|
||||
@@ -172,12 +167,10 @@
|
||||
(defn multi-typeahead-selected-pill- [params]
|
||||
[:div.flex-grow.flex
|
||||
[:template {:x-if "value.size > 0"}
|
||||
[:a.bg-blue-100.rounded-full.px-3
|
||||
[:span.text-left
|
||||
[:a.bg-blue-100.rounded-full.px-3
|
||||
[:span.text-left
|
||||
[:span {"x-text" "value.has('all') ? 'All' : value.size"}]
|
||||
" selected"]
|
||||
|
||||
]]
|
||||
" selected"]]]
|
||||
[:template {:x-if "value.size == 0"}
|
||||
[:span.text-left.text-gray-400 "None selected"]]
|
||||
[:div {:class "w-4 h-4 ml-2 inline text-gray-500 self-center rounded-full bg-gray-100 text-gray-500"
|
||||
@@ -223,7 +216,7 @@
|
||||
(sequential? (:value params))
|
||||
(map (fn [v] ((:value-fn params identity) v))
|
||||
(:value params))
|
||||
|
||||
|
||||
:else
|
||||
[])
|
||||
:tippy nil
|
||||
@@ -238,7 +231,7 @@
|
||||
:elements (cond-> [{:value "all" :label "All"}]
|
||||
(sequential? (:value params))
|
||||
(into (map (fn [v]
|
||||
{:value ((:value-fn params identity) v)
|
||||
{:value ((:value-fn params identity) v)
|
||||
:label ((:content-fn params identity) v)})
|
||||
(:value params))))
|
||||
:x-ref "r"})
|
||||
@@ -265,8 +258,7 @@
|
||||
[:input (-> params
|
||||
(dissoc :class :value-fn :content-fn :placeholder :x-model)
|
||||
(assoc :type "hidden"
|
||||
:value ""
|
||||
))]]
|
||||
:value ""))]]
|
||||
[:div.flex.w-full.justify-items-stretch
|
||||
(multi-typeahead-selected-pill- params)
|
||||
[:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"}
|
||||
@@ -281,7 +273,7 @@
|
||||
:x-ref "warning_pop"
|
||||
:class "hidden peer-hover:block bg-red-50 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 p-4"
|
||||
:x-text "value.warning"}]]]
|
||||
(multi-typeahead-dropdown- params) ])])
|
||||
(multi-typeahead-dropdown- params)])])
|
||||
|
||||
|
||||
(defn use-size [size]
|
||||
@@ -292,26 +284,25 @@
|
||||
|
||||
|
||||
(defn text-input- [{:keys [size error?] :as params}]
|
||||
[:input
|
||||
(-> params
|
||||
[:input
|
||||
(-> params
|
||||
(dissoc :error?)
|
||||
(assoc :type "text" :autocomplete "off")
|
||||
(update
|
||||
:class #(-> ""
|
||||
(hh/add-class default-input-classes)
|
||||
(hh/add-class %)))
|
||||
(update
|
||||
:class #(-> ""
|
||||
(hh/add-class default-input-classes)
|
||||
(hh/add-class %)))
|
||||
(update :class #(str % (use-size size))))])
|
||||
|
||||
(defn text-area- [{:keys [] :as params}]
|
||||
[:textarea
|
||||
(-> params
|
||||
[:textarea
|
||||
(-> params
|
||||
(update :class #(-> ""
|
||||
(hh/add-class default-input-classes)
|
||||
(hh/add-class %)))) ]
|
||||
)
|
||||
(hh/add-class %))))])
|
||||
|
||||
(defn money-input- [{:keys [size] :as params}]
|
||||
[:input
|
||||
[:input
|
||||
(-> params
|
||||
(update :class (fnil hh/add-class "") default-input-classes)
|
||||
(update :class hh/add-class "appearance-none text-right")
|
||||
@@ -321,7 +312,7 @@
|
||||
(dissoc :size))])
|
||||
|
||||
(defn int-input- [{:keys [size] :as params}]
|
||||
[:input
|
||||
[:input
|
||||
(-> params
|
||||
(update :class (fnil hh/add-class "") default-input-classes)
|
||||
(update :class hh/add-class "appearance-none text-right")
|
||||
@@ -331,12 +322,11 @@
|
||||
(dissoc :size))])
|
||||
|
||||
(defn date-input- [{:keys [size] :as params}]
|
||||
[:div.shrink {:x-data (hx/json {:value (:value params)
|
||||
[:div.shrink {:x-data (hx/json {:value (:value params)
|
||||
:tippy nil
|
||||
:dp nil})
|
||||
"x-effect" "console.log('changed to' +value)"
|
||||
"@change-date.camel" "$dispatch('change')"
|
||||
}
|
||||
"@change-date.camel" "$dispatch('change')"}
|
||||
[:input
|
||||
(-> params
|
||||
(update :class (fnil hh/add-class "") default-input-classes)
|
||||
@@ -349,13 +339,13 @@
|
||||
(assoc "autocomplete" "off")
|
||||
(assoc "@change" "value = $event.target.value;")
|
||||
|
||||
(assoc "@keydown.escape" "tippy.hide(); " )
|
||||
(assoc "@keydown.escape" "tippy.hide(); ")
|
||||
#_(assoc "hx-on" (hiccup/raw "changeDate: htmx.trigger(this, \"change\") "))
|
||||
(update :class #(str % (use-size size) " w-full"))
|
||||
(dissoc :size))]
|
||||
[:template {:x-ref "tooltip" }
|
||||
|
||||
[:div.shrink
|
||||
[:template {:x-ref "tooltip"}
|
||||
|
||||
[:div.shrink
|
||||
[:div
|
||||
(-> params
|
||||
(update :class (fnil hh/add-class "") default-input-classes)
|
||||
@@ -369,19 +359,19 @@
|
||||
(assoc "@htmx:before-cleanup-element" "destroyDatepicker(dp)")
|
||||
(assoc "x-destroy" "destroyDatepicker(dp)")
|
||||
(assoc "@change-date.camel" "value = dp.getDate(\"mm/dd/yyyy\");")
|
||||
|
||||
|
||||
(update :class #(str % (use-size size) " w-full"))
|
||||
(dissoc :size :name :x-model :x-modelable))]]]])
|
||||
|
||||
(defn multi-calendar-input- [{:keys [size] :as params}]
|
||||
(let [value (str/join ", "
|
||||
(for [v (:value params)
|
||||
:when v]
|
||||
(let [value (str/join ", "
|
||||
(for [v (:value params)
|
||||
:when v]
|
||||
(some-> v (atime/unparse-local atime/normal-date))))]
|
||||
[:div.shrink {:x-data (hx/json {:value value
|
||||
:dp nil })
|
||||
:dp nil})
|
||||
:x-modelable "value"
|
||||
:x-model (:x-model params) }
|
||||
:x-model (:x-model params)}
|
||||
[:template {:x-for "v in value"}
|
||||
[:input {:type "hidden" :name (:name params) :x-model "v"}]]
|
||||
[:div
|
||||
@@ -393,8 +383,8 @@
|
||||
(assoc :x-init "$nextTick(() => { dp = initMultiDatepicker($el, value); ;}); ")
|
||||
(assoc "x-effect" "if(dp) { dp.setDate(Array.from(value), {clear: true}); } ")
|
||||
(assoc ":data-date" "Array.prototype.join.call(value, ', ')")
|
||||
(assoc "@htmx:before-history-save" "destroyDatepicker(dp)" )
|
||||
(assoc "@htmx:before-cleanup-element" "destroyDatepicker(dp)" )
|
||||
(assoc "@htmx:before-history-save" "destroyDatepicker(dp)")
|
||||
(assoc "@htmx:before-cleanup-element" "destroyDatepicker(dp)")
|
||||
(assoc "x-destroy" "destroyDatepicker(dp)")
|
||||
(assoc "@change-date.camel" "value = dp.getDate(\"mm/dd/yyyy\");")
|
||||
|
||||
@@ -404,9 +394,9 @@
|
||||
(defn calendar-input- [{:keys [size] :as params}]
|
||||
(let [value (:value params)]
|
||||
[:div.shrink {:x-data (hx/json {:value value
|
||||
:dp nil })
|
||||
:dp nil})
|
||||
:x-modelable "value"
|
||||
:x-model (:x-model params) }
|
||||
:x-model (:x-model params)}
|
||||
[:input {:type "hidden" :name (:name params) :x-model "value"}]
|
||||
[:div
|
||||
(-> params
|
||||
@@ -417,8 +407,8 @@
|
||||
(assoc :x-init "$nextTick(() => { dp = initCalendar($el); ;}); ")
|
||||
(assoc "x-effect" "if(dp) { dp.setDate(value); } ")
|
||||
(assoc ":data-date" "value")
|
||||
(assoc "@htmx:before-history-save" "destroyDatepicker(dp)" )
|
||||
(assoc "@htmx:before-cleanup-element" "destroyDatepicker(dp)" )
|
||||
(assoc "@htmx:before-history-save" "destroyDatepicker(dp)")
|
||||
(assoc "@htmx:before-cleanup-element" "destroyDatepicker(dp)")
|
||||
(assoc "x-destroy" "destroyDatepicker(dp)")
|
||||
(assoc "@change-date.camel" "value = dp.getDate(\"mm/dd/yyyy\");")
|
||||
|
||||
@@ -429,12 +419,12 @@
|
||||
|
||||
(defn field-errors- [{:keys [source key]} & rest]
|
||||
(let [errors (:errors (cond-> (meta source)
|
||||
key (get key)))]
|
||||
key (get key)))]
|
||||
[:p.mt-2.text-xs.text-red-600.dark:text-red-500.h-4 (str/join ", " errors)]))
|
||||
|
||||
(defn field- [params & rest]
|
||||
[:div (-> params
|
||||
(update :class #(hh/add-class (or % "") "group" )))
|
||||
(update :class #(hh/add-class (or % "") "group")))
|
||||
(when (:label params)
|
||||
[:label {:class "block mb-2 text-sm font-medium text-gray-900 dark:text-white"} (:label params)])
|
||||
rest
|
||||
@@ -444,7 +434,7 @@
|
||||
|
||||
(defn inline-field- [params & rest]
|
||||
[:div (-> params
|
||||
(update :class #(hh/add-class (or % "") "group flex items-baseline gap-2" )))
|
||||
(update :class #(hh/add-class (or % "") "group flex items-baseline gap-2")))
|
||||
(when (:label params)
|
||||
[:label {:class "mb-2 text-sm font-medium text-gray-900 dark:text-white"} (:label params)])
|
||||
rest
|
||||
@@ -471,10 +461,10 @@
|
||||
|
||||
(defn validated-inline-field- [params & rest]
|
||||
(inline-field- (cond-> params
|
||||
true (dissoc :errors)
|
||||
(sequential? (:errors params)) (update :class #(hh/add-class (or % "") "has-error")))
|
||||
rest
|
||||
(errors- {:errors (:errors params)})))
|
||||
true (dissoc :errors)
|
||||
(sequential? (:errors params)) (update :class #(hh/add-class (or % "") "has-error")))
|
||||
rest
|
||||
(errors- {:errors (:errors params)})))
|
||||
|
||||
(defn hidden- [{:keys [name value] :as params}]
|
||||
[:input (merge {:type "hidden" :value value :name name} params)])
|
||||
|
||||
@@ -336,7 +336,7 @@
|
||||
:to-step :next-steps}))))
|
||||
|
||||
|
||||
(defn- location-select*
|
||||
(defn location-select*
|
||||
[{:keys [name account-location client-locations value]}]
|
||||
(let [options (into (cond account-location
|
||||
[[account-location account-location]]
|
||||
@@ -352,7 +352,7 @@
|
||||
:value (ffirst options)
|
||||
:class "w-full"})))
|
||||
|
||||
(defn- account-typeahead*
|
||||
(defn account-typeahead*
|
||||
[{:keys [name value client-id x-model]}]
|
||||
[:div.flex.flex-col
|
||||
(com/typeahead {:name name
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
[auto-ap.client-routes :as client-routes]
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact conn merge-query observable-query
|
||||
pull-many]]
|
||||
audit-transact audit-transact-batch conn merge-query
|
||||
observable-query pull-many]]
|
||||
[auto-ap.datomic.accounts :as d-accounts]
|
||||
[auto-ap.datomic.bank-accounts :as d-bank-accounts]
|
||||
[auto-ap.datomic.clients :as d-clients]
|
||||
@@ -16,14 +16,16 @@
|
||||
exception->4xx exception->notification
|
||||
extract-client-ids notify-if-locked]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.permissions :refer [can?]]
|
||||
[auto-ap.permissions :refer [can? wrap-must]]
|
||||
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||
[auto-ap.routes.invoice :as route]
|
||||
[auto-ap.routes.payments :as payment-route]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.rule-matching :as rm]
|
||||
[auto-ap.solr :as solr]
|
||||
[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.link-dropdown :refer [link-dropdown]]
|
||||
[auto-ap.ssr.components.multi-modal :as mm]
|
||||
@@ -33,13 +35,14 @@
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.invoice.common :refer [default-read]]
|
||||
[auto-ap.ssr.invoice.import :as invoice-import]
|
||||
[auto-ap.ssr.invoice.new-invoice-wizard :as new-invoice-wizard]
|
||||
[auto-ap.ssr.invoice.new-invoice-wizard :as new-invoice-wizard :refer [location-select*]]
|
||||
[auto-ap.ssr.pos.common :refer [date-range-field*]]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.utils
|
||||
:refer [apply-middleware-to-all-handlers assert-schema
|
||||
clj-date-schema dissoc-nil-transformer entity-id
|
||||
html-response main-transformer modal-response money
|
||||
form-validation-error html-response main-transformer
|
||||
many-entity modal-response money percentage
|
||||
ref->enum-schema round-money strip wrap-entity
|
||||
wrap-implied-route-param wrap-merge-prior-hx
|
||||
wrap-schema-enforce]]
|
||||
@@ -52,6 +55,7 @@
|
||||
[clojure.string :as str]
|
||||
[datomic.api :as dc]
|
||||
[hiccup.util :as hu]
|
||||
[iol-ion.utils :refer [random-tempid]]
|
||||
[malli.core :as mc]
|
||||
[malli.transform :as mt]
|
||||
[malli.util :as mut]))
|
||||
@@ -480,6 +484,12 @@
|
||||
"hx-include" "#invoice-filters"
|
||||
:color :red}
|
||||
"Void selected"))
|
||||
(when (can? (:identity request) {:subject :invoice :activity :bulk-edit})
|
||||
(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/bulk-edit))
|
||||
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||
"hx-include" "#invoice-filters"
|
||||
:color :secondary}
|
||||
"Bulk Edit"))
|
||||
(when (can? (:identity request) {:subject :invoice :activity :pay})
|
||||
(pay-button* {:ids (selected->ids request
|
||||
(:query-params request))}))
|
||||
@@ -1400,6 +1410,298 @@
|
||||
target-route)
|
||||
(:query-params request)))}}))
|
||||
|
||||
|
||||
(defn initial-bulk-edit-state [request]
|
||||
(mm/->MultiStepFormState {:search-params (:query-params request)
|
||||
:expense-accounts [{:db/id "123"
|
||||
:location "Shared"
|
||||
:account nil
|
||||
:percentage 1.0}]}
|
||||
[]
|
||||
{:search-params (:query-params request)
|
||||
:expense-accounts [{:db/id "123"
|
||||
:location "Shared"
|
||||
:account nil
|
||||
:percentage 1.0}]}))
|
||||
|
||||
(defn- account-typeahead*
|
||||
[{:keys [name value client-id x-model]}]
|
||||
[:div.flex.flex-col
|
||||
(com/typeahead {:name name
|
||||
:placeholder "Search..."
|
||||
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
|
||||
{ :purpose "invoice"})
|
||||
:id name
|
||||
:x-model x-model
|
||||
:value value
|
||||
:content-fn (fn [value]
|
||||
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
|
||||
client-id)))})])
|
||||
|
||||
|
||||
|
||||
;; TODO clientize
|
||||
(defn all-ids-not-locked [all-ids]
|
||||
(->> all-ids
|
||||
(dc/q '[:find ?i
|
||||
:in $ [?i ...]
|
||||
:where
|
||||
[?i :invoice/client ?c]
|
||||
[(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
|
||||
[?i :invoice/date ?d]
|
||||
[(>= ?d ?lu)]]
|
||||
(dc/db conn))
|
||||
(map first)))
|
||||
(defn- bulk-edit-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 {:name (fc/field-name) })
|
||||
: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])))
|
||||
: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 amount-field"
|
||||
: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))))
|
||||
|
||||
(defrecord AccountsStep [linear-wizard]
|
||||
mm/ModalWizardStep
|
||||
(step-name [_]
|
||||
"Expense Accounts")
|
||||
(step-key [_]
|
||||
:accounts)
|
||||
|
||||
(edit-path [_ _]
|
||||
[])
|
||||
|
||||
(step-schema [_]
|
||||
(mut/select-keys (mm/form-schema linear-wizard) #{:expense-accounts}))
|
||||
|
||||
(render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}]
|
||||
(let [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) " invoices"]
|
||||
:body (mm/default-step-body
|
||||
{}
|
||||
[:div {}
|
||||
(fc/with-field :expense-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 #(bulk-edit-account-row* {:value %
|
||||
:client-id (:invoice/client snapshot)}))
|
||||
|
||||
(com/data-grid-new-row {:colspan 4
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
::route/bulk-edit-new-account)
|
||||
:row-offset 0
|
||||
:index (count (fc/field-value)) }
|
||||
"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/bulk-edit-total)
|
||||
:hx-target "this"
|
||||
:hx-swap "innerHTML"}
|
||||
#_(invoice-expense-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/bulk-edit-balance)
|
||||
:hx-target "this"
|
||||
:hx-swap "innerHTML"}
|
||||
#_(invoice-expense-account-balance* request))
|
||||
(com/data-grid-cell {})))))])
|
||||
: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 maybe-code-accounts [invoice account-rules valid-locations]
|
||||
(with-precision 2
|
||||
(let [accounts (vec (mapcat
|
||||
(fn [ar]
|
||||
(let [cents-to-distribute (int (Math/round (Math/abs (* (:percentage ar)
|
||||
(:invoice/total invoice)
|
||||
100))))]
|
||||
(if (= "Shared" (:location ar))
|
||||
(do
|
||||
(->> valid-locations
|
||||
(map
|
||||
(fn [cents location]
|
||||
{:db/id (random-tempid)
|
||||
:invoice-expense-account/account (:account ar)
|
||||
:invoice-expense-account/amount (* 0.01 cents)
|
||||
:invoice-expense-account/location location})
|
||||
(rm/spread-cents cents-to-distribute (count valid-locations)))))
|
||||
[(cond-> {:db/id (random-tempid)
|
||||
:invoice-expense-account/account (:account ar)
|
||||
:invoice-expense-account/amount (* 0.01 cents-to-distribute)}
|
||||
(:location ar) (assoc :invoice-expense-account/location (:location ar)))])))
|
||||
account-rules))
|
||||
accounts (mapv
|
||||
(fn [a]
|
||||
(update a :invoice-expense-account/amount
|
||||
#(with-precision 2
|
||||
(double (.setScale (bigdec %) 2 java.math.RoundingMode/HALF_UP)))))
|
||||
accounts)
|
||||
leftover (with-precision 2 (.round (bigdec (- (Math/abs (:invoice/total invoice))
|
||||
(Math/abs (reduce + 0.0 (map #(:invoice-expense-account/amount %) accounts)))))
|
||||
*math-context*))
|
||||
accounts (if (seq accounts)
|
||||
(update-in accounts [(dec (count accounts)) :invoice-expense-account/amount] #(+ % (double leftover)))
|
||||
[])]
|
||||
accounts)))
|
||||
|
||||
(defn assert-percentages-add-up [{:keys [expense-accounts]}]
|
||||
(let [expense-account-total (reduce + 0 (map (fn [x] (:percentage x)) expense-accounts))]
|
||||
(when-not (dollars= 1.0 expense-account-total)
|
||||
(form-validation-error (str "Expense account total (" expense-account-total ") does not equal 100%")))))
|
||||
|
||||
(defrecord BulkEditWizard [_ 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-edit-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 [_]
|
||||
(mc/schema [:map
|
||||
[:expense-accounts
|
||||
(many-entity {:min 1}
|
||||
[:account entity-id]
|
||||
[:location [:string {:min 1 :error/message "required"}]]
|
||||
[:percentage percentage])]]))
|
||||
(submit [this {:keys [multi-form-state request-method identity] :as request}]
|
||||
(let [selected-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 selected-ids)
|
||||
invoices (pull-many (dc/db conn) '[:db/id :invoice/total {:invoice/client [:client/locations]}] (vec all-ids)) ]
|
||||
(assert-percentages-add-up (:snapshot multi-form-state))
|
||||
|
||||
(doseq [a (-> multi-form-state :snapshot :expense-accounts)
|
||||
:let [{:keys [:account/location :account/name]} (dc/pull (dc/db conn) [:account/location :account/name] (:account a))]]
|
||||
(when (and location (not= location (:location a)))
|
||||
(let [err (str "Account " name " uses location " (:location a) ", but is supposed to be " location)]
|
||||
(throw (ex-info err {:validation-error err})))))
|
||||
(alog/info ::bulk-code :count (count all-ids))
|
||||
(audit-transact-batch
|
||||
(map (fn [i]
|
||||
[:upsert-invoice {:db/id (:db/id i)
|
||||
:invoice/expense-accounts (maybe-code-accounts i (-> multi-form-state :snapshot :expense-accounts) (-> i :invoice/client :client/locations))}])
|
||||
invoices)
|
||||
(:identity request))
|
||||
|
||||
(html-response
|
||||
[:div]
|
||||
:headers (cond-> {"hx-trigger" (hx/json { "modalclose" ""
|
||||
"invalidated" ""
|
||||
"notification" (str "Successfully coded " (count all-ids) " invoices.")})
|
||||
"hx-reswap" "outerHTML"})))))
|
||||
|
||||
(def bulk-edit-wizard (->BulkEditWizard nil nil))
|
||||
|
||||
|
||||
(defn bulk-edit-total* [request]
|
||||
(let [total (->> (-> request
|
||||
:multi-form-state
|
||||
:step-params
|
||||
:expense-accounts)
|
||||
(map (fnil :percentage 0.0))
|
||||
(filter number?)
|
||||
(reduce + 0.0))]
|
||||
(format "%.1f%%" (* 100.0 total))))
|
||||
|
||||
(defn bulk-edit-balance* [request]
|
||||
(let [total (->> (-> request
|
||||
:multi-form-state
|
||||
:step-params
|
||||
:expense-accounts)
|
||||
(map (fnil :percentage 0.0))
|
||||
(filter number?)
|
||||
(reduce + 0.0))
|
||||
balance (- 100.0
|
||||
(* 100.0 total))]
|
||||
[:span {:class (when-not (dollars= 0.0 balance)
|
||||
"text-red-300")}
|
||||
(format "%.1f%%" balance)]))
|
||||
|
||||
(defn bulk-edit-total [request]
|
||||
(html-response (bulk-edit-total* request)))
|
||||
|
||||
(defn bulk-edit-balance [request]
|
||||
(html-response (bulk-edit-balance* request)))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(->
|
||||
@@ -1417,9 +1719,36 @@
|
||||
::route/legacy-paid-invoices (redirect-handler ::route/paid-page)
|
||||
::route/legacy-voided-invoices (redirect-handler ::route/voided-page)
|
||||
::route/legacy-new-invoice (redirect-handler ::route/new-wizard)
|
||||
::route/undo-autopay (-> undo-autopay
|
||||
(wrap-entity [:route-params :db/id] default-read)
|
||||
(wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
|
||||
::route/bulk-edit (-> mm/open-wizard-handler
|
||||
(mm/wrap-wizard bulk-edit-wizard)
|
||||
(mm/wrap-init-multi-form-state initial-bulk-edit-state))
|
||||
::route/bulk-edit-submit (-> mm/submit-handler
|
||||
(mm/wrap-wizard bulk-edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state)
|
||||
(wrap-must {:subject :invoice :activity :bulk-edit}))
|
||||
::route/bulk-edit-total (-> bulk-edit-total
|
||||
(mm/wrap-wizard bulk-edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state)
|
||||
(wrap-must {:subject :invoice :activity :bulk-edit}))
|
||||
::route/bulk-edit-balance (-> bulk-edit-balance
|
||||
|
||||
(mm/wrap-wizard bulk-edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state)
|
||||
(wrap-must {:subject :invoice :activity :bulk-edit}))
|
||||
::route/bulk-edit-new-account (->
|
||||
(add-new-entity-handler [:step-params :expense-accounts]
|
||||
(fn render [cursor request]
|
||||
(bulk-edit-account-row*
|
||||
{:value cursor }))
|
||||
(fn build-new-row [base _]
|
||||
(assoc base :invoice-expense-account/location "Shared")))
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:client-id {:optional true}
|
||||
[:maybe entity-id]]]))
|
||||
|
||||
::route/undo-autopay (-> undo-autopay
|
||||
(wrap-entity [:route-params :db/id] default-read)
|
||||
(wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
|
||||
::route/unvoid (-> unvoid-invoice
|
||||
(wrap-entity [:route-params :db/id] default-read)
|
||||
(wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
:post ::pay-submit}
|
||||
"/bulk-delete" {:get ::bulk-delete
|
||||
:delete ::bulk-delete-confirm}
|
||||
"/bulk-edit" {:get ::bulk-edit
|
||||
:put ::bulk-edit-submit
|
||||
"/account" ::bulk-edit-new-account
|
||||
"/total" ::bulk-edit-total
|
||||
"/balance" ::bulk-edit-balance}
|
||||
["/" [#"\d+" :db/id]] {:delete ::delete
|
||||
"/undo-autopay" ::undo-autopay
|
||||
"/unvoid" ::unvoid
|
||||
|
||||
Reference in New Issue
Block a user