Add vendor pre-population for bulk code and individual edit forms
- Add vendor-changed HTMX handlers for both bulk code and individual edit - Pre-populate default account at 100% when vendor is selected and no accounts exist - Fix render-accounts-section to render from step-params correctly - Change bulk code vendor-changed from hx-get to hx-post to include form data - Add routes for vendor-changed endpoints - Update e2e tests to cover vendor pre-population - Run lein cljfmt fix across codebase
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
:account/invoice-allowance [:db/ident]
|
||||
:account/client-overrides [:db/id
|
||||
:account-client-override/name
|
||||
{:account-client-override/client [:db/id :client/name]}]} ])
|
||||
{:account-client-override/client [:db/id :client/name]}]}])
|
||||
|
||||
(defn search- [id query client]
|
||||
(let [client-part (if (some->> client (can-see-client? id))
|
||||
@@ -71,9 +71,9 @@
|
||||
(valid-allowances (-> a allowance :db/ident))
|
||||
(= (:db/id a) vendor-account))))
|
||||
(map (fn [[n a]]
|
||||
{:label (str (:account/numeric-code a) " - " (if client-id
|
||||
{:label (str (:account/numeric-code a) " - " (if client-id
|
||||
(:account/name (d-accounts/clientize a client-id))
|
||||
n))
|
||||
n))
|
||||
:value (:db/id a)
|
||||
:location (:account/location a)
|
||||
:warning (when (= :allowance/warn (-> a allowance :db/ident))
|
||||
|
||||
@@ -15,84 +15,77 @@
|
||||
|
||||
(defn hourly-changes []
|
||||
(let [tx-instant-attr (:db/id (dc/pull (dc/db conn) '[:db/id] :db/txInstant))
|
||||
tx-lookup (->>
|
||||
(dc/tx-range
|
||||
(dc/log conn)
|
||||
(coerce/to-date (time/plus (time/now) (time/hours -24)))
|
||||
(coerce/to-date (time/now)))
|
||||
(map (fn extract-tx-instant [tx]
|
||||
(let [tx-id (->> (:data tx)
|
||||
(map (fn [d]
|
||||
(:tx d)))
|
||||
first)
|
||||
tx-instant (->> tx
|
||||
:data
|
||||
(filter (fn [d]
|
||||
(and (= (:e d) tx-id)
|
||||
(= tx-instant-attr (:a d)))))
|
||||
(map :v)
|
||||
first)]
|
||||
|
||||
tx-instant)))
|
||||
(group-by (fn hours-ago [d]
|
||||
(time/in-hours (time/interval (coerce/to-date-time d) (time/now)))
|
||||
))
|
||||
)]
|
||||
tx-lookup (->>
|
||||
(dc/tx-range
|
||||
(dc/log conn)
|
||||
(coerce/to-date (time/plus (time/now) (time/hours -24)))
|
||||
(coerce/to-date (time/now)))
|
||||
(map (fn extract-tx-instant [tx]
|
||||
(let [tx-id (->> (:data tx)
|
||||
(map (fn [d]
|
||||
(:tx d)))
|
||||
first)
|
||||
tx-instant (->> tx
|
||||
:data
|
||||
(filter (fn [d]
|
||||
(and (= (:e d) tx-id)
|
||||
(= tx-instant-attr (:a d)))))
|
||||
(map :v)
|
||||
first)]
|
||||
|
||||
tx-instant)))
|
||||
(group-by (fn hours-ago [d]
|
||||
(time/in-hours (time/interval (coerce/to-date-time d) (time/now))))))]
|
||||
|
||||
(for [h (range 24)]
|
||||
(count (tx-lookup h [])))))
|
||||
|
||||
(defn page [request]
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/admin-aside-nav
|
||||
:client-selection (:client-selection request)
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
:identity (:identity request)}
|
||||
(com/breadcrumbs {} [:a {:href (bidi/path-for ssr-routes/only-routes :auto-ap.routes.admin/page)}
|
||||
"Admin"])
|
||||
[:div.flex.space-x-4
|
||||
(com/content-card {:class "w-1/4"}
|
||||
[:div {:class "flex flex-col px-4 py-3 space-y-3"}
|
||||
[:div
|
||||
[:h1.text-2xl.mb-3.font-bold "Growth in clients"]
|
||||
[:div
|
||||
[:div {:class "w-full h-64"
|
||||
:id "client-chart"
|
||||
:data-chart (hx/json {
|
||||
:labels ["2 years ago" "1 year ago" "today"],
|
||||
:series [(for [n [2 1 0]
|
||||
:let [start (time/plus (time/now) (time/years (- n)))]]
|
||||
(->> (dc/q '[:find (count ?c)
|
||||
:in $
|
||||
:where [?c :client/code]]
|
||||
(dc/as-of (dc/db conn) (coerce/to-date start)))
|
||||
first
|
||||
first))]})}]
|
||||
[:script {:lang "javascript"}
|
||||
(hiccup/raw
|
||||
"new Chartist.Bar('#client-chart', JSON.parse(document.getElementById('client-chart').getAttribute('data-chart')))")]]]])
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/admin-aside-nav
|
||||
:client-selection (:client-selection request)
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
:identity (:identity request)}
|
||||
(com/breadcrumbs {} [:a {:href (bidi/path-for ssr-routes/only-routes :auto-ap.routes.admin/page)}
|
||||
"Admin"])
|
||||
[:div.flex.space-x-4
|
||||
(com/content-card {:class "w-1/4"}
|
||||
[:div {:class "flex flex-col px-4 py-3 space-y-3"}
|
||||
[:div
|
||||
[:h1.text-2xl.mb-3.font-bold "Growth in clients"]
|
||||
[:div
|
||||
[:div {:class "w-full h-64"
|
||||
:id "client-chart"
|
||||
:data-chart (hx/json {:labels ["2 years ago" "1 year ago" "today"],
|
||||
:series [(for [n [2 1 0]
|
||||
:let [start (time/plus (time/now) (time/years (- n)))]]
|
||||
(->> (dc/q '[:find (count ?c)
|
||||
:in $
|
||||
:where [?c :client/code]]
|
||||
(dc/as-of (dc/db conn) (coerce/to-date start)))
|
||||
first
|
||||
first))]})}]
|
||||
[:script {:lang "javascript"}
|
||||
(hiccup/raw
|
||||
"new Chartist.Bar('#client-chart', JSON.parse(document.getElementById('client-chart').getAttribute('data-chart')))")]]]])
|
||||
|
||||
(com/content-card {:class "w-1/2"}
|
||||
[:div {:class "flex flex-col px-4 py-3 space-y-3"}
|
||||
[:div
|
||||
[:h1.text-2xl.mb-3.font-bold "Changes by hour"]
|
||||
[:div
|
||||
[:div {:class "w-full h-64"
|
||||
:id "changes"
|
||||
:data-chart (hx/json {
|
||||
:labels (for [n (range -24 0)]
|
||||
(format "%d" n)),
|
||||
:series [(hourly-changes)]})}]
|
||||
[:script {:lang "javascript"}
|
||||
(hiccup/raw
|
||||
"new Chartist.Line('#changes', JSON.parse(document.getElementById('changes').getAttribute('data-chart')))")]]]])]
|
||||
)
|
||||
"Admin")
|
||||
)
|
||||
(com/content-card {:class "w-1/2"}
|
||||
[:div {:class "flex flex-col px-4 py-3 space-y-3"}
|
||||
[:div
|
||||
[:h1.text-2xl.mb-3.font-bold "Changes by hour"]
|
||||
[:div
|
||||
[:div {:class "w-full h-64"
|
||||
:id "changes"
|
||||
:data-chart (hx/json {:labels (for [n (range -24 0)]
|
||||
(format "%d" n)),
|
||||
:series [(hourly-changes)]})}]
|
||||
[:script {:lang "javascript"}
|
||||
(hiccup/raw
|
||||
"new Chartist.Line('#changes', JSON.parse(document.getElementById('changes').getAttribute('data-chart')))")]]]])])
|
||||
"Admin"))
|
||||
|
||||
(def key->handler
|
||||
{
|
||||
:auto-ap.routes.admin/page (wrap-client-redirect-unauthenticated (wrap-admin page))
|
||||
})
|
||||
{:auto-ap.routes.admin/page (wrap-client-redirect-unauthenticated (wrap-admin page))})
|
||||
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
(defn filters [request]
|
||||
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
||||
"hx-get" (bidi/path-for ssr-routes/only-routes
|
||||
:admin-account-table)
|
||||
:admin-account-table)
|
||||
"hx-target" "#entity-table"
|
||||
"hx-indicator" "#entity-table"}
|
||||
|
||||
[:fieldset.space-y-6
|
||||
[:fieldset.space-y-6
|
||||
(com/field {:label "Name"}
|
||||
(com/text-input {:name "name"
|
||||
:id "name"
|
||||
@@ -111,19 +111,19 @@
|
||||
'[(clojure.string/upper-case ?an) ?upper-an]
|
||||
'[(clojure.string/includes? ?upper-an ?ns)]]}
|
||||
:args [(str/upper-case (:name query-params))]})
|
||||
|
||||
|
||||
(some->> query-params :code)
|
||||
(merge-query {:query {:find []
|
||||
:in ['?nc]
|
||||
:where ['[?e :account/numeric-code ?nc]]}
|
||||
:args [(:code query-params)]})
|
||||
|
||||
|
||||
(some->> query-params :type)
|
||||
(merge-query {:query {:find []
|
||||
:in ['?r]
|
||||
:where ['[?e :account/type ?r] ]}
|
||||
:where ['[?e :account/type ?r]]}
|
||||
:args [(some->> query-params :type)]})
|
||||
|
||||
|
||||
true
|
||||
(merge-query {:query {:find ['?sort-default '?e]
|
||||
:where ['[?e :account/numeric-code ?un]
|
||||
@@ -202,21 +202,20 @@
|
||||
[:account/numeric-code]
|
||||
:form-params form-params)))
|
||||
_ (some->> form-params
|
||||
:account/client-overrides
|
||||
(group-by :account-client-override/client)
|
||||
(filter (fn [[_ overrides]]
|
||||
(> (count overrides) 1)))
|
||||
(map first)
|
||||
seq
|
||||
(#(form-validation-error (format "Client(s) %s have more than one override."
|
||||
(str/join ", "
|
||||
(map (fn [client]
|
||||
(format "'%s'" (pull-attr (dc/db conn)
|
||||
:client/name
|
||||
(-> client)))
|
||||
) %)))
|
||||
:form-params form-params)) ;; TODO shouldnt need to bubble this through. See if we can eliminate the passing of form and last-form.
|
||||
)
|
||||
:account/client-overrides
|
||||
(group-by :account-client-override/client)
|
||||
(filter (fn [[_ overrides]]
|
||||
(> (count overrides) 1)))
|
||||
(map first)
|
||||
seq
|
||||
(#(form-validation-error (format "Client(s) %s have more than one override."
|
||||
(str/join ", "
|
||||
(map (fn [client]
|
||||
(format "'%s'" (pull-attr (dc/db conn)
|
||||
:client/name
|
||||
(-> client)))) %)))
|
||||
:form-params form-params)) ;; TODO shouldnt need to bubble this through. See if we can eliminate the passing of form and last-form.
|
||||
)
|
||||
{:keys [tempids]} (audit-transact [[:upsert-entity (cond-> entity
|
||||
(:account/numeric-code entity) (assoc :account/code (str (:account/numeric-code entity))))]]
|
||||
(:identity request))
|
||||
@@ -241,11 +240,11 @@
|
||||
"client_id" (:db/id (:account-client-override/client o))
|
||||
"account_client_override_id" (:db/id o)})))
|
||||
(html-response
|
||||
(row* identity updated-account {:flash? true})
|
||||
:headers (cond-> {"hx-trigger" "modalclose"}
|
||||
(= :post request-method) (assoc "hx-retarget" "#entity-table tbody"
|
||||
"hx-reswap" "afterbegin")
|
||||
(= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-account)))))))
|
||||
(row* identity updated-account {:flash? true})
|
||||
:headers (cond-> {"hx-trigger" "modalclose"}
|
||||
(= :post request-method) (assoc "hx-retarget" "#entity-table tbody"
|
||||
"hx-reswap" "afterbegin")
|
||||
(= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-account)))))))
|
||||
|
||||
(defn client-override* [override]
|
||||
(com/data-grid-row (-> {:x-ref "p"
|
||||
@@ -259,165 +258,161 @@
|
||||
(com/data-grid-cell {}
|
||||
(com/validated-field {:errors (fc/field-errors)}
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:placeholder "Search..."
|
||||
:class "w-96"
|
||||
:url (bidi/path-for ssr-routes/only-routes
|
||||
:company-search)
|
||||
:value (fc/field-value)
|
||||
:content-fn #(pull-attr (dc/db conn) :client/name %)}))))
|
||||
:placeholder "Search..."
|
||||
:class "w-96"
|
||||
:url (bidi/path-for ssr-routes/only-routes
|
||||
:company-search)
|
||||
:value (fc/field-value)
|
||||
:content-fn #(pull-attr (dc/db conn) :client/name %)}))))
|
||||
(fc/with-field :account-client-override/name
|
||||
(com/data-grid-cell
|
||||
{}
|
||||
(com/validated-field {:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-96"
|
||||
:value (fc/field-value)}))))
|
||||
{}
|
||||
(com/validated-field {:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-96"
|
||||
: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 dialog* [{:keys [entity form-params form-errors]}]
|
||||
(fc/start-form form-params form-errors
|
||||
[:div {:x-data (hx/json {"accountName" (or (:account/name form-params) (:account/numeric-code entity))
|
||||
"accountCode" (or (:account/numeric-code form-params) (:account/numeric-code entity) )})
|
||||
:hx-target "this"
|
||||
}
|
||||
(com/modal
|
||||
{}
|
||||
[:form (-> {:hx-ext "response-targets"
|
||||
:hx-swap "outerHTML swap:300ms"
|
||||
:hx-target-400 "#form-errors .error-content" }
|
||||
(assoc (if (:db/id entity)
|
||||
:hx-put
|
||||
:hx-post)
|
||||
(str (bidi/path-for ssr-routes/only-routes
|
||||
:admin-transaction-rule-edit-save))))
|
||||
[:fieldset {:class "hx-disable"}
|
||||
(com/modal-card
|
||||
{:class "md:h-[600px]"}
|
||||
[:div.flex [:div.p-2 "Account"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600
|
||||
[:span {:x-text "accountCode"}]
|
||||
" - "
|
||||
[:span {:x-text "accountName"}]]]
|
||||
[:div.space-y-1
|
||||
(when-let [id (:db/id entity)]
|
||||
(com/hidden {:name "db/id"
|
||||
:value id}))
|
||||
[:div {:x-data (hx/json {"accountName" (or (:account/name form-params) (:account/numeric-code entity))
|
||||
"accountCode" (or (:account/numeric-code form-params) (:account/numeric-code entity))})
|
||||
:hx-target "this"}
|
||||
(com/modal
|
||||
{}
|
||||
[:form (-> {:hx-ext "response-targets"
|
||||
:hx-swap "outerHTML swap:300ms"
|
||||
:hx-target-400 "#form-errors .error-content"}
|
||||
(assoc (if (:db/id entity)
|
||||
:hx-put
|
||||
:hx-post)
|
||||
(str (bidi/path-for ssr-routes/only-routes
|
||||
:admin-transaction-rule-edit-save))))
|
||||
[:fieldset {:class "hx-disable"}
|
||||
(com/modal-card
|
||||
{:class "md:h-[600px]"}
|
||||
[:div.flex [:div.p-2 "Account"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600
|
||||
[:span {:x-text "accountCode"}]
|
||||
" - "
|
||||
[:span {:x-text "accountName"}]]]
|
||||
[:div.space-y-1
|
||||
(when-let [id (:db/id entity)]
|
||||
(com/hidden {:name "db/id"
|
||||
:value id}))
|
||||
|
||||
(fc/with-field :account/numeric-code
|
||||
(if (nil? (:db/id entity))
|
||||
(com/validated-field {:label "Numeric code"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:x-model "accountCode"
|
||||
:autofocus true
|
||||
:class "w-32"}))
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value (fc/field-value)})))
|
||||
(fc/with-field :account/name
|
||||
(com/validated-field {:label "Name"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:x-model "accountName"
|
||||
(fc/with-field :account/numeric-code
|
||||
(if (nil? (:db/id entity))
|
||||
(com/validated-field {:label "Numeric code"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:x-model "accountCode"
|
||||
:autofocus true
|
||||
:class "w-32"}))
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value (fc/field-value)})))
|
||||
(fc/with-field :account/name
|
||||
(com/validated-field {:label "Name"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:x-model "accountName"
|
||||
|
||||
:class "w-64"
|
||||
:value (fc/field-value)})))
|
||||
(fc/with-field :account/type
|
||||
(com/validated-field {:label "Account Type"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:class "w-36"
|
||||
:id "type"
|
||||
:value (some-> (fc/field-value) name)
|
||||
:options (ref->select-options "account-type")})))
|
||||
(fc/with-field :account/location
|
||||
(com/validated-field {:label "Location"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-16"
|
||||
:value (fc/field-value)})))
|
||||
:class "w-64"
|
||||
:value (fc/field-value)})))
|
||||
(fc/with-field :account/type
|
||||
(com/validated-field {:label "Account Type"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:class "w-36"
|
||||
:id "type"
|
||||
:value (some-> (fc/field-value) name)
|
||||
:options (ref->select-options "account-type")})))
|
||||
(fc/with-field :account/location
|
||||
(com/validated-field {:label "Location"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-16"
|
||||
:value (fc/field-value)})))
|
||||
|
||||
[:div.flex.flex-wrap.gap-4
|
||||
(fc/with-field :account/invoice-allowance
|
||||
(com/validated-field {:label "Invoice Allowance"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:value (some-> (fc/field-value) name)
|
||||
:class "w-36"
|
||||
:options (ref->select-options "allowance")})))
|
||||
(fc/with-field :account/vendor-allowance
|
||||
(com/validated-field {:label "Vendor Allowance"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:class "w-36"
|
||||
:value (some-> (fc/field-value) name)
|
||||
:options (ref->select-options "allowance")})))]
|
||||
(fc/with-field :account/applicability
|
||||
(com/validated-field {:label "Applicability"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:class "w-36"
|
||||
:value (some-> (fc/field-value) name)
|
||||
:options (ref->select-options "account-applicability")})))
|
||||
[:div.flex.flex-wrap.gap-4
|
||||
(fc/with-field :account/invoice-allowance
|
||||
(com/validated-field {:label "Invoice Allowance"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:value (some-> (fc/field-value) name)
|
||||
:class "w-36"
|
||||
:options (ref->select-options "allowance")})))
|
||||
(fc/with-field :account/vendor-allowance
|
||||
(com/validated-field {:label "Vendor Allowance"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:class "w-36"
|
||||
:value (some-> (fc/field-value) name)
|
||||
:options (ref->select-options "allowance")})))]
|
||||
(fc/with-field :account/applicability
|
||||
(com/validated-field {:label "Applicability"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:class "w-36"
|
||||
:value (some-> (fc/field-value) name)
|
||||
:options (ref->select-options "account-applicability")})))
|
||||
|
||||
(fc/with-field :account/client-overrides
|
||||
|
||||
(com/field {:label "Client Overrides" :id "client-overrides"}
|
||||
(fc/with-field :account/client-overrides
|
||||
|
||||
(com/data-grid {:headers [(com/data-grid-header {} "Client")
|
||||
(com/data-grid-header {} "Account name")
|
||||
(com/data-grid-header {})]
|
||||
:id "client-override-table"}
|
||||
(fc/cursor-map
|
||||
#(client-override* %))
|
||||
(com/field {:label "Client Overrides" :id "client-overrides"}
|
||||
|
||||
(com/data-grid-new-row {:colspan 3
|
||||
:index (count (fc/field-value))
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:admin-account-client-override-new)}
|
||||
"New override"))))]
|
||||
[:div
|
||||
(com/form-errors {:errors (:errors fc/*form-errors*)})
|
||||
(com/validated-save-button {:errors (seq form-errors)}
|
||||
"Save account")])]])]))
|
||||
(com/data-grid {:headers [(com/data-grid-header {} "Client")
|
||||
(com/data-grid-header {} "Account name")
|
||||
(com/data-grid-header {})]
|
||||
:id "client-override-table"}
|
||||
(fc/cursor-map
|
||||
#(client-override* %))
|
||||
|
||||
(defn new-client-override [{ {:keys [index]} :query-params}]
|
||||
(html-response
|
||||
(fc/start-form-with-prefix
|
||||
[:account/client-overrides (or index 0)]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:new? true}
|
||||
[]
|
||||
(client-override* fc/*current*))))
|
||||
(com/data-grid-new-row {:colspan 3
|
||||
:index (count (fc/field-value))
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:admin-account-client-override-new)}
|
||||
"New override"))))]
|
||||
[:div
|
||||
(com/form-errors {:errors (:errors fc/*form-errors*)})
|
||||
(com/validated-save-button {:errors (seq form-errors)}
|
||||
"Save account")])]])]))
|
||||
|
||||
(defn new-client-override [{{:keys [index]} :query-params}]
|
||||
(html-response
|
||||
(fc/start-form-with-prefix
|
||||
[:account/client-overrides (or index 0)]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:new? true}
|
||||
[]
|
||||
(client-override* fc/*current*))))
|
||||
|
||||
(def form-schema (mc/schema
|
||||
[:map
|
||||
[:db/id {:optional true} [:maybe entity-id]]
|
||||
[:account/numeric-code {:optional true} [:maybe :int]]
|
||||
[:account/name [:string {:min 1 :decode/string strip}]]
|
||||
[:account/location {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:account/type (ref->enum-schema "account-type")]
|
||||
[:account/applicability (ref->enum-schema "account-applicability")] ;
|
||||
[:account/invoice-allowance (ref->enum-schema "allowance")]
|
||||
[:account/vendor-allowance (ref->enum-schema "allowance")]
|
||||
[:account/client-overrides {:optional true}
|
||||
[:maybe
|
||||
(many-entity {}
|
||||
[:db/id [:or entity-id temp-id]]
|
||||
[:account-client-override/client entity-id]
|
||||
[:account-client-override/name [:string {:min 2 :decode/string strip}]])]]]))
|
||||
[:map
|
||||
[:db/id {:optional true} [:maybe entity-id]]
|
||||
[:account/numeric-code {:optional true} [:maybe :int]]
|
||||
[:account/name [:string {:min 1 :decode/string strip}]]
|
||||
[:account/location {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:account/type (ref->enum-schema "account-type")]
|
||||
[:account/applicability (ref->enum-schema "account-applicability")] ;
|
||||
[:account/invoice-allowance (ref->enum-schema "allowance")]
|
||||
[:account/vendor-allowance (ref->enum-schema "allowance")]
|
||||
[:account/client-overrides {:optional true}
|
||||
[:maybe
|
||||
(many-entity {}
|
||||
[:db/id [:or entity-id temp-id]]
|
||||
[:account-client-override/client entity-id]
|
||||
[:account-client-override/name [:string {:min 2 :decode/string strip}]])]]]))
|
||||
|
||||
(defn account-dialog [{:keys [entity form-params form-errors]}]
|
||||
(modal-response (dialog* {:entity entity
|
||||
:form-params (or (when (seq form-params)
|
||||
form-params)
|
||||
(when entity
|
||||
(mc/decode form-schema entity main-transformer))
|
||||
{})
|
||||
:form-errors form-errors})))
|
||||
|
||||
|
||||
|
||||
:form-params (or (when (seq form-params)
|
||||
form-params)
|
||||
(when entity
|
||||
(mc/decode form-schema entity main-transformer))
|
||||
{})
|
||||
:form-errors form-errors})))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
|
||||
@@ -28,14 +28,13 @@
|
||||
(com.amazonaws.services.ecs.model AssignPublicIp)))
|
||||
|
||||
(defn get-ecs-tasks []
|
||||
(->>
|
||||
(concat (:task-arns (ecs/list-tasks :max-results 50)) (:task-arns (ecs/list-tasks :desired-status "STOPPED" :max-results 50)))
|
||||
(ecs/describe-tasks :include [] :tasks)
|
||||
:tasks
|
||||
(map #(assoc % :task-definition (:task-definition (ecs/describe-task-definition :task-definition (:task-definition-arn %)))))
|
||||
(sort-by :created-at)
|
||||
reverse))
|
||||
|
||||
(->>
|
||||
(concat (:task-arns (ecs/list-tasks :max-results 50)) (:task-arns (ecs/list-tasks :desired-status "STOPPED" :max-results 50)))
|
||||
(ecs/describe-tasks :include [] :tasks)
|
||||
:tasks
|
||||
(map #(assoc % :task-definition (:task-definition (ecs/describe-task-definition :task-definition (:task-definition-arn %)))))
|
||||
(sort-by :created-at)
|
||||
reverse))
|
||||
|
||||
(defn is-background-job?
|
||||
"This function checks whether a given task is a background job.
|
||||
@@ -60,7 +59,7 @@
|
||||
(defn job-exited-successfully? [task]
|
||||
(if (= 0 (->> task
|
||||
:containers
|
||||
(filter (comp #{"integreat-app" } :name))
|
||||
(filter (comp #{"integreat-app"} :name))
|
||||
(first)
|
||||
:exit-code))
|
||||
true
|
||||
@@ -77,7 +76,7 @@
|
||||
:succeeded
|
||||
:failed))
|
||||
:name (task-definition->job-name (:task-definition task))
|
||||
:end-date (some-> (:stopped-at task) coerce/to-date-time (time/to-time-zone (time/time-zone-for-offset 0)))
|
||||
:end-date (some-> (:stopped-at task) coerce/to-date-time (time/to-time-zone (time/time-zone-for-offset 0)))
|
||||
:start-date (some-> (:created-at task) coerce/to-date-time (time/to-time-zone (time/time-zone-for-offset 0)))})
|
||||
|
||||
(defn fetch-page [request]
|
||||
@@ -85,7 +84,7 @@
|
||||
(filter is-background-job?)
|
||||
(map ecs-task->job))]
|
||||
[jobs (count jobs)]))
|
||||
(def query-schema (mc/schema [:map ]))
|
||||
(def query-schema (mc/schema [:map]))
|
||||
|
||||
(def grid-page
|
||||
(helper/build {:id "job-table"
|
||||
@@ -107,8 +106,7 @@
|
||||
:entity-name "Job"
|
||||
:query-schema query-schema
|
||||
:route :admin-job-table
|
||||
:headers [
|
||||
{:key "start"
|
||||
:headers [{:key "start"
|
||||
:name "Start"
|
||||
:render #(some-> % :start-date (atime/unparse-local atime/standard-time))}
|
||||
{:key "end"
|
||||
@@ -119,7 +117,7 @@
|
||||
:render (fn [e]
|
||||
(when (and (:start-date e)
|
||||
(:end-date e))
|
||||
(str (time/in-minutes (time/interval
|
||||
(str (time/in-minutes (time/interval
|
||||
(:start-date e)
|
||||
(:end-date e))) " minutes")))}
|
||||
{:key "name"
|
||||
@@ -150,16 +148,16 @@
|
||||
:network-configuration {:aws-vpc-configuration {:subnets ["subnet-5e675761" "subnet-8519fde2" "subnet-89bab8d4"]
|
||||
:security-groups ["sg-004e5855310c453a3" "sg-02d167406b1082698"]
|
||||
:assign-public-ip AssignPublicIp/ENABLED}}}
|
||||
args (assoc-in [:overrides :container-overrides ] [{:name "integreat-app" :environment [{:name "args" :value (pr-str args)}]}]))))
|
||||
args (assoc-in [:overrides :container-overrides] [{:name "integreat-app" :environment [{:name "args" :value (pr-str args)}]}]))))
|
||||
|
||||
(defn job-start [{:keys [form-params]}]
|
||||
(if (not (get (currently-running-jobs) (:name form-params)))
|
||||
(let [new-job (run-task
|
||||
(-> (:name form-params)
|
||||
(str/replace #"-" "_")
|
||||
(str/replace #":" "")
|
||||
(str "_" (:dd-env env)))
|
||||
(dissoc form-params :name))]
|
||||
(-> (:name form-params)
|
||||
(str/replace #"-" "_")
|
||||
(str/replace #":" "")
|
||||
(str "_" (:dd-env env)))
|
||||
(dissoc form-params :name))]
|
||||
{:message (str "task " (str new-job) " started.")})
|
||||
(form-validation-error "This job is already running"
|
||||
:form-params form-params)))
|
||||
@@ -170,107 +168,101 @@
|
||||
[(fc/with-field :ledger-url
|
||||
(com/validated-field {:label "Url"
|
||||
:errors (fc/field-errors)}
|
||||
[:div.flex.place-items-center.gap-2
|
||||
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
|
||||
(com/text-input {:placeholder "ledger-data.csv"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)} )]))]
|
||||
[:div.flex.place-items-center.gap-2
|
||||
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
|
||||
(com/text-input {:placeholder "ledger-data.csv"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)})]))]
|
||||
(= "register-invoice-import" name)
|
||||
[
|
||||
(fc/with-field :invoice-url
|
||||
(com/validated-field {:label "Url"
|
||||
:errors (fc/field-errors)}
|
||||
[:div.flex.place-items-center.gap-2
|
||||
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
|
||||
(com/text-input {:placeholder "invoice-data.csv"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)} )]))]
|
||||
[(fc/with-field :invoice-url
|
||||
(com/validated-field {:label "Url"
|
||||
:errors (fc/field-errors)}
|
||||
[:div.flex.place-items-center.gap-2
|
||||
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
|
||||
(com/text-input {:placeholder "invoice-data.csv"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)})]))]
|
||||
(= "load-historical-sales" name)
|
||||
[
|
||||
(fc/with-field :client
|
||||
(com/validated-field {:label "Client"
|
||||
:errors (fc/field-errors)}
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:placeholder "Search..."
|
||||
:url (bidi/path-for ssr-routes/only-routes
|
||||
:company-search)})))
|
||||
(fc/with-field :days
|
||||
[(fc/with-field :client
|
||||
(com/validated-field {:label "Client"
|
||||
:errors (fc/field-errors)}
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:placeholder "Search..."
|
||||
:url (bidi/path-for ssr-routes/only-routes
|
||||
:company-search)})))
|
||||
(fc/with-field :days
|
||||
(com/validated-field {:label "Days to load"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:placeholder "60"
|
||||
:name (fc/field-name)
|
||||
:value (fc/field-value)} )))]
|
||||
:else nil))
|
||||
:value (fc/field-value)})))]
|
||||
:else nil)))
|
||||
|
||||
|
||||
)
|
||||
|
||||
(defn subform [{{:keys [name]} :query-params }]
|
||||
(defn subform [{{:keys [name]} :query-params}]
|
||||
(html-response
|
||||
(fc/start-form {} nil
|
||||
(subform* {:name name}))))
|
||||
(fc/start-form {} nil
|
||||
(subform* {:name name}))))
|
||||
|
||||
(defn job-start-dialog [{:keys [form-errors form-params] :as request}]
|
||||
(fc/start-form (or form-params {}) form-errors
|
||||
(modal-response
|
||||
(com/modal ;; TODO we need a cleaner way to have forms that wrap the whole. In this cas
|
||||
{}
|
||||
[:form {:hx-post (bidi/path-for ssr-routes/only-routes :admin-job-start)
|
||||
:class "h-full w-full"}
|
||||
[:fieldset {:class "hx-disable h-full w-full"}
|
||||
(com/modal-card {}
|
||||
[:div.m-2 "New job"]
|
||||
[:div.space-y-6
|
||||
(modal-response
|
||||
(com/modal ;; TODO we need a cleaner way to have forms that wrap the whole. In this cas
|
||||
{}
|
||||
[:form {:hx-post (bidi/path-for ssr-routes/only-routes :admin-job-start)
|
||||
:class "h-full w-full"}
|
||||
[:fieldset {:class "hx-disable h-full w-full"}
|
||||
(com/modal-card {}
|
||||
[:div.m-2 "New job"]
|
||||
[:div.space-y-6
|
||||
|
||||
(fc/with-field :name
|
||||
(com/validated-field {:label "Job"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:class "w-64"
|
||||
:options [["" ""]
|
||||
["yodlee2" "Yodlee Import"]
|
||||
["yodlee2-accounts" "Yodlee Account Import"]
|
||||
["intuit" "Intuit import"]
|
||||
["plaid" "Plaid import"]
|
||||
["bulk-journal-import" "Bulk Journal Import"]
|
||||
["square-import-job" "Square Import"]
|
||||
["register-invoice-import" "Register Invoice Import "]
|
||||
["ezcater-upsert" "Upsert recent ezcater orders"]
|
||||
["load-historical-sales" "Load Historical Square Sales"]
|
||||
["export-backup" "Export Backup"]]
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:admin-job-subform)
|
||||
:hx-target "#sub-form"
|
||||
:hx-swap "innerHTML"})))
|
||||
(fc/with-field :name
|
||||
(com/validated-field {:label "Job"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:class "w-64"
|
||||
:options [["" ""]
|
||||
["yodlee2" "Yodlee Import"]
|
||||
["yodlee2-accounts" "Yodlee Account Import"]
|
||||
["intuit" "Intuit import"]
|
||||
["plaid" "Plaid import"]
|
||||
["bulk-journal-import" "Bulk Journal Import"]
|
||||
["square-import-job" "Square Import"]
|
||||
["register-invoice-import" "Register Invoice Import "]
|
||||
["ezcater-upsert" "Upsert recent ezcater orders"]
|
||||
["load-historical-sales" "Load Historical Square Sales"]
|
||||
["export-backup" "Export Backup"]]
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:admin-job-subform)
|
||||
:hx-target "#sub-form"
|
||||
:hx-swap "innerHTML"})))
|
||||
|
||||
[:div#sub-form (subform* {:name (fc/with-field :name (fc/field-value))}) ]]
|
||||
[:div
|
||||
|
||||
(com/form-errors {:errors (:errors fc/*form-errors*)})
|
||||
(com/validated-save-button {:errors form-errors} "Run job")])]]))))
|
||||
[:div#sub-form (subform* {:name (fc/with-field :name (fc/field-value))})]]
|
||||
[:div
|
||||
|
||||
(com/form-errors {:errors (:errors fc/*form-errors*)})
|
||||
(com/validated-save-button {:errors form-errors} "Run job")])]]))))
|
||||
|
||||
(def form-schema (mc/schema [:map
|
||||
[:name [:string {:min 1}]]
|
||||
[:ledger-url {:optional true} [:string {:min 1}]]
|
||||
[:invoice-url {:optional true} [:string {:min 1}]]
|
||||
[:client {:optional true} entity-id]
|
||||
[:days {:optional true} [:int {:min 1 :max 120}]]
|
||||
]))
|
||||
[:days {:optional true} [:int {:min 1 :max 120}]]]))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(->>
|
||||
{:admin-jobs (helper/page-route grid-page)
|
||||
:admin-job-table (helper/table-route grid-page)
|
||||
:admin-job-subform (-> subform (wrap-schema-enforce :query-schema [:map [:name {:optional true} [:maybe :string]]]))
|
||||
:admin-job-start (-> job-start
|
||||
(wrap-schema-enforce :form-schema form-schema)
|
||||
(wrap-nested-form-params)
|
||||
(wrap-form-4xx-2 job-start-dialog))
|
||||
:admin-job-start-dialog job-start-dialog})
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-admin)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
(apply-middleware-to-all-handlers
|
||||
(->>
|
||||
{:admin-jobs (helper/page-route grid-page)
|
||||
:admin-job-table (helper/table-route grid-page)
|
||||
:admin-job-subform (-> subform (wrap-schema-enforce :query-schema [:map [:name {:optional true} [:maybe :string]]]))
|
||||
:admin-job-start (-> job-start
|
||||
(wrap-schema-enforce :form-schema form-schema)
|
||||
(wrap-nested-form-params)
|
||||
(wrap-form-4xx-2 job-start-dialog))
|
||||
:admin-job-start-dialog job-start-dialog})
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-admin)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
(ns auto-ap.ssr.admin.clients
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact conn merge-query pull-attr pull-id
|
||||
pull-many query2]]
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact conn merge-query pull-attr pull-id
|
||||
pull-many query2]]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||
@@ -11,7 +11,7 @@
|
||||
[auto-ap.routes.indicators :as indicators]
|
||||
[auto-ap.routes.queries :as q]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.solr :as solr]
|
||||
[auto-ap.square.core3 :as square]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
@@ -26,11 +26,11 @@
|
||||
[auto-ap.ssr.indicators :as i]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.utils
|
||||
:refer [apply-middleware-to-all-handlers entity-id
|
||||
form-validation-error html-response many-entity
|
||||
many-entity-custom modal-response ref->enum-schema strip
|
||||
temp-id wrap-entity wrap-merge-prior-hx
|
||||
wrap-schema-enforce]]
|
||||
:refer [apply-middleware-to-all-handlers entity-id
|
||||
form-validation-error html-response many-entity
|
||||
many-entity-custom modal-response ref->enum-schema strip
|
||||
temp-id wrap-entity wrap-merge-prior-hx
|
||||
wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[cheshire.core :as cheshire]
|
||||
@@ -47,7 +47,6 @@
|
||||
(:import
|
||||
[java.util UUID]))
|
||||
|
||||
|
||||
;; TODO make more reusable malli schemas, use unions if it would be helpful
|
||||
;; TODO copy save logic from graphql version
|
||||
;; TODO cash drawer shift
|
||||
@@ -67,8 +66,6 @@
|
||||
[:enum
|
||||
"" "all" "only-mine"]]]]]))
|
||||
|
||||
|
||||
|
||||
(defn filters [request]
|
||||
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
||||
"hx-get" (bidi/path-for ssr-routes/only-routes
|
||||
@@ -178,7 +175,6 @@
|
||||
:where ['[?e :client/groups ?g]]}
|
||||
:args [(clojure.string/upper-case (:group query-params))]})
|
||||
|
||||
|
||||
(not (str/blank? (some-> query-params :code)))
|
||||
(merge-query {:query {:in ['?code]
|
||||
:where ['[?e :client/code ?code]]}
|
||||
@@ -293,8 +289,6 @@
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
|
||||
|
||||
|
||||
(def bank-account-schema [:and [:map
|
||||
[:db/id [:or entity-id temp-id]]
|
||||
[:bank-account/name :string]
|
||||
@@ -383,10 +377,10 @@
|
||||
[:maybe (many-entity-custom {}
|
||||
[:and
|
||||
[:map
|
||||
|
||||
|
||||
[:db/id [:or entity-id temp-id]]
|
||||
[:bank-account/name :string]
|
||||
|
||||
|
||||
[:bank-account/code :string]
|
||||
[:bank-account/type [:maybe (ref->enum-schema "bank-account-type")]]
|
||||
[:bank-account/numeric-code {:optional true} [:maybe :int]]
|
||||
@@ -401,15 +395,15 @@
|
||||
[:bank-account/intuit-bank-account {:optional true} [:maybe entity-id]]
|
||||
[:bank-account/include-in-reports {:default false}
|
||||
[:boolean {:decode/string {:enter #(if (= % "on") true
|
||||
|
||||
|
||||
(boolean %))}}]]
|
||||
[:bank-account/visible {:default false}
|
||||
[:boolean {:decode/string {:enter #(if (= % "on") true
|
||||
|
||||
|
||||
(boolean %))}}]]
|
||||
[:bank-account/use-date-instead-of-post-date? {:default false}
|
||||
[:boolean {:decode/string {:enter #(if (= % "on") true
|
||||
|
||||
|
||||
(boolean %))}}]]
|
||||
[:bank-account/start-date {:optional true} [:maybe {:decode/arbitrary (fn [m]
|
||||
(if (string? m)
|
||||
@@ -443,10 +437,6 @@
|
||||
[:client/week-b-credits {:optional true} [:maybe :double]]
|
||||
[:client/week-b-debits {:optional true} [:maybe :double]]]))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(defn email-contact-row [email-contact-cursor]
|
||||
(com/data-grid-row
|
||||
(-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? email-contact-cursor))))})
|
||||
@@ -526,12 +516,10 @@
|
||||
(com/data-grid-cell {:class "align-top"}
|
||||
(com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x))))
|
||||
|
||||
|
||||
(defn- dialog-header [step]
|
||||
[:div.flex [:div.p-2 (mm/step-name step)] [:p.ml-2.rounded.bg-gray-50.p-2.dark:bg-gray-600
|
||||
[:span {:x-text "clientName"}]]])
|
||||
|
||||
|
||||
(defrecord InfoModal [linear-wizard]
|
||||
mm/ModalWizardStep
|
||||
(step-name [_]
|
||||
@@ -598,7 +586,6 @@
|
||||
(mm/default-step-footer linear-wizard this :validation-route ::route/navigate)
|
||||
:validation-route ::route/navigate)))
|
||||
|
||||
|
||||
(defn match-row [_]
|
||||
(com/data-grid-row
|
||||
{:x-ref "p"
|
||||
@@ -644,8 +631,6 @@
|
||||
(com/data-grid-cell {:class "align-top"}
|
||||
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
|
||||
|
||||
|
||||
|
||||
(defrecord MatchesModal [linear-wizard]
|
||||
mm/ModalWizardStep
|
||||
(step-name [_]
|
||||
@@ -699,7 +684,6 @@
|
||||
(step-key [_]
|
||||
:contact)
|
||||
|
||||
|
||||
(edit-path [_ _]
|
||||
[])
|
||||
|
||||
@@ -798,7 +782,6 @@
|
||||
:to (mm/encode-step-key [:bank-account (fc/field-value (:db/id bank-account))])})}
|
||||
svg/pencil)]])])
|
||||
|
||||
|
||||
(defmulti bank-account-card (comp deref :bank-account/type))
|
||||
(defmethod bank-account-card :bank-account-type/cash [bank-account]
|
||||
(bank-account-card-base {:bg-color "bg-green-50"
|
||||
@@ -821,7 +804,6 @@
|
||||
:icon svg/check
|
||||
:bank-account bank-account}))
|
||||
|
||||
|
||||
(defmulti bank-account-form (comp deref :bank-account/type))
|
||||
(defmethod bank-account-form :bank-account-type/cash [bank-account]
|
||||
[:div
|
||||
@@ -904,8 +886,6 @@
|
||||
:checked (fc/field-value)}
|
||||
"Visible for payment"))]])
|
||||
|
||||
|
||||
|
||||
(defn- plaid-account-select [client-id]
|
||||
(fc/with-field :bank-account/plaid-account
|
||||
(com/validated-field {:errors (fc/field-errors)
|
||||
@@ -1048,7 +1028,6 @@
|
||||
[:div#days-indicator
|
||||
(i/days-ago* (some-> (fc/field-value)))]])
|
||||
|
||||
|
||||
(fc/with-field :bank-account/include-in-reports
|
||||
(com/checkbox {:name (fc/field-name)
|
||||
:value (boolean (fc/field-value))
|
||||
@@ -1224,8 +1203,6 @@
|
||||
(yodlee-account-select (:db/id (:snapshot fc/*form-data*)))
|
||||
(intuit-account-select (:db/id (:snapshot fc/*form-data*)))])
|
||||
|
||||
|
||||
|
||||
(defn new-bank-account-card []
|
||||
[:div {:class "w-[30em]"}
|
||||
(com/card {:class "w-full border-dotted bg-gray-50"}
|
||||
@@ -1255,7 +1232,6 @@
|
||||
|
||||
(edit-path [_ _] [])
|
||||
|
||||
|
||||
(step-schema [_]
|
||||
(mut/select-keys (mm/form-schema linear-wizard) #{}))
|
||||
|
||||
@@ -1284,7 +1260,6 @@
|
||||
:validation-route ::route/navigate)]
|
||||
:validation-route ::route/navigate)))
|
||||
|
||||
|
||||
(defn square-location-table []
|
||||
[:div#square-locations
|
||||
[:div.htmx-indicator
|
||||
@@ -1367,7 +1342,7 @@
|
||||
:hx-include "#square-token"
|
||||
:hx-trigger "click"
|
||||
:hx-indicator "#square-locations"
|
||||
:hx-target "#square-locations" }
|
||||
:hx-target "#square-locations"}
|
||||
"Refresh")]
|
||||
|
||||
(fc/with-field :client/square-locations
|
||||
@@ -1447,8 +1422,6 @@
|
||||
(filterv #(not= (get-in multi-form-state [:step-params :db/id]) (:db/id %)) bank-accounts)))
|
||||
(mm/select-state [] nil))))
|
||||
|
||||
|
||||
|
||||
(defrecord CashFlowModal [linear-wizard]
|
||||
mm/ModalWizardStep
|
||||
(step-name [_]
|
||||
@@ -1663,7 +1636,6 @@
|
||||
#(mm/select-state % [] {})
|
||||
#(assoc-in % [:snapshot :client/bank-accounts] new-bank-accounts)))))))
|
||||
|
||||
|
||||
(def sales-summary-query
|
||||
"[:find ?d4 (sum ?total) (sum ?tax) (sum ?tip) (sum ?service-charge) (sum ?discount) (sum ?returns)
|
||||
:with ?s
|
||||
@@ -1792,9 +1764,6 @@
|
||||
[?cds :cash-drawer-shift/opened-cash ?opened-cash]
|
||||
[(iol-ion.query/excel-date ?date) ?d4]]")
|
||||
|
||||
|
||||
|
||||
|
||||
(defn setup-sales-queries-impl [client-id]
|
||||
(let [{client-code :client/code feature-flags :client/feature-flags} (dc/pull (dc/db conn) '[:client/code :client/feature-flags] client-id)
|
||||
is-new-square? ((set feature-flags) "new-square")]
|
||||
@@ -1840,7 +1809,6 @@
|
||||
(cheshire/generate-string (format (slurp (io/resource which)) url)))}
|
||||
children))
|
||||
|
||||
|
||||
(defn biweekly-sales-powerquery [request]
|
||||
(setup-sales-queries-impl (:db/id (:route-params request)))
|
||||
(modal-response
|
||||
@@ -1872,7 +1840,6 @@
|
||||
|
||||
(com/modal-footer {} [:div])))))
|
||||
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{::route/page (helper/page-route grid-page)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.routes.admin.excel-invoices :as route]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.components.inputs :as inputs]
|
||||
@@ -16,8 +16,8 @@
|
||||
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[auto-ap.ssr.utils
|
||||
:refer [apply-middleware-to-all-handlers html-response wrap-form-4xx-2
|
||||
wrap-schema-enforce]]
|
||||
:refer [apply-middleware-to-all-handlers html-response wrap-form-4xx-2
|
||||
wrap-schema-enforce]]
|
||||
[auto-ap.utils :refer [by]]
|
||||
[bidi.bidi :as bidi]
|
||||
[clj-time.coerce :as coerce]
|
||||
@@ -38,7 +38,6 @@
|
||||
|
||||
invoice)
|
||||
|
||||
|
||||
(defn reset-id [i]
|
||||
(update i :invoice-number
|
||||
(fn [n] (if (re-matches #"#+" n)
|
||||
@@ -85,7 +84,6 @@
|
||||
(get (by (comp :db/id :vendor-schedule-payment-dom/client) :vendor-schedule-payment-dom/dom (:vendor/schedule-payment-dom vendor))
|
||||
client-id))
|
||||
|
||||
|
||||
(defn invoice-rows->transaction [rows user]
|
||||
(->> rows
|
||||
(mapcat (fn [{:keys [vendor-id total client-id date invoice-number default-location check automatically-paid-when-due account-id]}]
|
||||
@@ -121,8 +119,7 @@
|
||||
(let [[[bank-account]] (seq (dc/q '[:find ?ba
|
||||
:in $ ?c
|
||||
:where [?c :client/bank-accounts ?ba]
|
||||
[?ba :bank-account/type :bank-account-type/cash]
|
||||
]
|
||||
[?ba :bank-account/type :bank-account-type/cash]]
|
||||
(dc/db conn)
|
||||
client-id))]
|
||||
[:upsert-transaction #:transaction {:amount (- (:invoice/total invoice))
|
||||
@@ -130,18 +127,17 @@
|
||||
:client (:invoice/client invoice)
|
||||
:status "POSTED"
|
||||
:bank-account bank-account
|
||||
:db/id #_ {:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id)
|
||||
:id #_ {:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id)
|
||||
:db/id #_{:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id)
|
||||
:id #_{:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id)
|
||||
:raw-id transaction-id
|
||||
:vendor (:invoice/vendor invoice)
|
||||
:description-original "Cash payment"
|
||||
:date (coerce/to-date date)
|
||||
:approval-status :transaction-approval-status/approved
|
||||
:accounts [{:db/id (str #_ {:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id) "-account")
|
||||
:accounts [{:db/id (str #_{:clj-kondo/ignore [:unresolved-var]} (digest/sha-256 transaction-id) "-account")
|
||||
:transaction-account/account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"]))
|
||||
:transaction-account/location "A"
|
||||
:transaction-account/amount (Math/abs (:invoice/total invoice))}]}]))
|
||||
]
|
||||
:transaction-account/amount (Math/abs (:invoice/total invoice))}]}]))]
|
||||
[[:propose-invoice (d-invoices/code-invoice (validate-invoice (remove-nils invoice))
|
||||
account-id)]
|
||||
(some-> payment remove-nils)
|
||||
@@ -154,17 +150,16 @@
|
||||
(map #(str/split % #"\t"))
|
||||
(map #(into {} (map (fn [c k] [k c]) % columns))))
|
||||
vendor-name->vendor (->>
|
||||
(set (map :vendor-name tabulated))
|
||||
(dc/q '[:find ?n ?v
|
||||
:in $ [?n ...]
|
||||
:where [?v :vendor/name ?n]]
|
||||
(dc/db conn)
|
||||
)
|
||||
(into {}))
|
||||
all-clients (merge (into {}(dc/q '[:find ?n (pull ?v [:db/id :client/locations])
|
||||
:in $
|
||||
:where [?v :client/name ?n]]
|
||||
(dc/db conn)))
|
||||
(set (map :vendor-name tabulated))
|
||||
(dc/q '[:find ?n ?v
|
||||
:in $ [?n ...]
|
||||
:where [?v :vendor/name ?n]]
|
||||
(dc/db conn))
|
||||
(into {}))
|
||||
all-clients (merge (into {} (dc/q '[:find ?n (pull ?v [:db/id :client/locations])
|
||||
:in $
|
||||
:where [?v :client/name ?n]]
|
||||
(dc/db conn)))
|
||||
(into {}
|
||||
(dc/q '[:find ?n (pull ?v [:db/id :client/locations])
|
||||
:in $
|
||||
@@ -190,16 +185,16 @@
|
||||
(let [parsed-invoice-rows (parse-invoice-rows excel-rows)
|
||||
existing-rows (set (d-invoices/get-existing-set))
|
||||
grouped-rows (group-by
|
||||
(fn [i]
|
||||
(cond (seq (:errors i))
|
||||
:error
|
||||
(fn [i]
|
||||
(cond (seq (:errors i))
|
||||
:error
|
||||
|
||||
(existing-rows [(:vendor-id i) (:client-id i) (:invoice-number i)])
|
||||
:exists
|
||||
(existing-rows [(:vendor-id i) (:client-id i) (:invoice-number i)])
|
||||
:exists
|
||||
|
||||
:else
|
||||
:new))
|
||||
parsed-invoice-rows)
|
||||
:else
|
||||
:new))
|
||||
parsed-invoice-rows)
|
||||
vendors-not-found (->> parsed-invoice-rows
|
||||
(filter #(and (nil? (:vendor-id %))
|
||||
(not= "Cash" (:check %))))
|
||||
@@ -208,9 +203,9 @@
|
||||
(audit-transact (invoice-rows->transaction (:new grouped-rows) user)
|
||||
user)
|
||||
{:imported (count (:new grouped-rows))
|
||||
:already-imported (count (:exists grouped-rows))
|
||||
:vendors-not-found vendors-not-found
|
||||
:errors (map #(dissoc % :date) (:error grouped-rows))}))
|
||||
:already-imported (count (:exists grouped-rows))
|
||||
:vendors-not-found vendors-not-found
|
||||
:errors (map #(dissoc % :date) (:error grouped-rows))}))
|
||||
|
||||
(def sample "6/16/17 Acme Bread NMKT-CB 3/26/56 12:00 AM $54.00 Naschmarkt X 7/31/17 8:26 AM 8/1/17 3:57 PM 31000
|
||||
6/20/17 Acme Bread NMKT-CB 3/19/58 12:00 AM $54.00 Naschmarkt X 7/31/17 8:26 AM 8/1/17 3:57 PM
|
||||
@@ -218,100 +213,98 @@
|
||||
|
||||
(defn form* [{:keys [form-params form-errors]} & children]
|
||||
(fc/start-form form-params form-errors
|
||||
[:form {:hx-post (bidi/path-for ssr-routes/only-routes ::route/import) :hx-swap "outerHTML"}
|
||||
[:div {:class "flex flex-col px-4 py-3 space-y-3 w-full"}
|
||||
[:h1.text-2xl.mb-3.font-bold "Import invoices from excel"]
|
||||
[:form {:hx-post (bidi/path-for ssr-routes/only-routes ::route/import) :hx-swap "outerHTML"}
|
||||
[:div {:class "flex flex-col px-4 py-3 space-y-3 w-full"}
|
||||
[:h1.text-2xl.mb-3.font-bold "Import invoices from excel"]
|
||||
|
||||
(fc/with-field :tsv
|
||||
(com/validated-field {:label "Tab-separated invoices"
|
||||
:errors (fc/field-errors)}
|
||||
[:textarea {:class (hh/add-class "w-full h-96" inputs/default-input-classes) :placeholder (hiccup/raw sample)
|
||||
:name (fc/field-name)
|
||||
}
|
||||
(fc/field-value)]))
|
||||
(com/form-errors {:errors (:errors fc/*form-errors*)})
|
||||
(com/validated-save-button {:color :primary
|
||||
:class "place-self-end w-32"
|
||||
:errors (seq form-errors)}
|
||||
"Import")
|
||||
children]]))
|
||||
(fc/with-field :tsv
|
||||
(com/validated-field {:label "Tab-separated invoices"
|
||||
:errors (fc/field-errors)}
|
||||
[:textarea {:class (hh/add-class "w-full h-96" inputs/default-input-classes) :placeholder (hiccup/raw sample)
|
||||
:name (fc/field-name)}
|
||||
(fc/field-value)]))
|
||||
(com/form-errors {:errors (:errors fc/*form-errors*)})
|
||||
(com/validated-save-button {:color :primary
|
||||
:class "place-self-end w-32"
|
||||
:errors (seq form-errors)}
|
||||
"Import")
|
||||
children]]))
|
||||
|
||||
(defn page [{:keys [form-params form-errors] :as request}]
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/admin-aside-nav
|
||||
:client-selection (:client-selection request)
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
:identity (:identity request)
|
||||
:request request}
|
||||
(com/breadcrumbs {} [:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
|
||||
"Admin"])
|
||||
[:div.flex.space-x-4
|
||||
(com/content-card
|
||||
{:class "w-3/4"}
|
||||
(form* {:form-params {}
|
||||
:form-errors []}))])
|
||||
"Admin"))
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/admin-aside-nav
|
||||
:client-selection (:client-selection request)
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
:identity (:identity request)
|
||||
:request request}
|
||||
(com/breadcrumbs {} [:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
|
||||
"Admin"])
|
||||
[:div.flex.space-x-4
|
||||
(com/content-card
|
||||
{:class "w-3/4"}
|
||||
(form* {:form-params {}
|
||||
:form-errors []}))])
|
||||
"Admin"))
|
||||
|
||||
(defn form [{:keys [form-params form-errors] :as request}]
|
||||
(html-response
|
||||
(form* {:form-params (or form-params {})
|
||||
:form-errors (or form-errors [])})))
|
||||
(html-response
|
||||
(form* {:form-params (or form-params {})
|
||||
:form-errors (or form-errors [])})))
|
||||
|
||||
(defn import [{:keys [form-params form-errors] :as request}]
|
||||
(html-response
|
||||
(let [result (bulk-upload-invoices (:tsv form-params) (:identity request))]
|
||||
(form* {:form-params form-params
|
||||
:form-errors form-errors}
|
||||
[:div.flex.space-x-4
|
||||
(com/pill {:color :primary}
|
||||
(format "%d imported" (:imported result)))
|
||||
(com/pill {:color :secondary}
|
||||
(format "%d extant" (:already-imported result)))
|
||||
(when (seq (:vendors-not-found result))
|
||||
(list
|
||||
(com/pill {:color :yellow
|
||||
"@mouseover" "show=true"
|
||||
"@mouseout" "show=false"
|
||||
"x-tooltip" "{content: ()=>$refs.tooltip.innerHTML ,
|
||||
allowHTML: true}" }
|
||||
(html-response
|
||||
(let [result (bulk-upload-invoices (:tsv form-params) (:identity request))]
|
||||
(form* {:form-params form-params
|
||||
:form-errors form-errors}
|
||||
[:div.flex.space-x-4
|
||||
(com/pill {:color :primary}
|
||||
(format "%d imported" (:imported result)))
|
||||
(com/pill {:color :secondary}
|
||||
(format "%d extant" (:already-imported result)))
|
||||
(when (seq (:vendors-not-found result))
|
||||
(list
|
||||
(com/pill {:color :yellow
|
||||
"@mouseover" "show=true"
|
||||
"@mouseout" "show=false"
|
||||
"x-tooltip" "{content: ()=>$refs.tooltip.innerHTML ,
|
||||
allowHTML: true}"}
|
||||
|
||||
(format "%d vendors not found" (count (:vendors-not-found result))))
|
||||
[:template {:x-ref "tooltip"}
|
||||
[:ul
|
||||
(for [n (take 5 (:vendors-not-found result))]
|
||||
[:li n])]]))]
|
||||
(format "%d vendors not found" (count (:vendors-not-found result))))
|
||||
[:template {:x-ref "tooltip"}
|
||||
[:ul
|
||||
(for [n (take 5 (:vendors-not-found result))]
|
||||
[:li n])]]))]
|
||||
|
||||
(when (seq (:errors result))
|
||||
(com/field {:label "Errors"}
|
||||
(com/data-grid
|
||||
{:headers [(com/data-grid-header {} "Date")
|
||||
(com/data-grid-header {} "Invoice #")
|
||||
(com/data-grid-header {} "Client")
|
||||
(com/data-grid-header {} "Vendor")
|
||||
(com/data-grid-header {} "Amount")
|
||||
(com/data-grid-header {} "Errors")]}
|
||||
(for [r (:errors result)]
|
||||
(com/data-grid-row
|
||||
{}
|
||||
(com/data-grid-cell {} (:raw-date r))
|
||||
(com/data-grid-cell {} (:invoice-number r))
|
||||
(com/data-grid-cell {} (:client-name r))
|
||||
(com/data-grid-cell {} (:vendor-name r))
|
||||
(com/data-grid-cell {} (:amount r))
|
||||
(com/data-grid-cell {} (str/join ", " (map :info (:errors r)))))))))))))
|
||||
(when (seq (:errors result))
|
||||
(com/field {:label "Errors"}
|
||||
(com/data-grid
|
||||
{:headers [(com/data-grid-header {} "Date")
|
||||
(com/data-grid-header {} "Invoice #")
|
||||
(com/data-grid-header {} "Client")
|
||||
(com/data-grid-header {} "Vendor")
|
||||
(com/data-grid-header {} "Amount")
|
||||
(com/data-grid-header {} "Errors")]}
|
||||
(for [r (:errors result)]
|
||||
(com/data-grid-row
|
||||
{}
|
||||
(com/data-grid-cell {} (:raw-date r))
|
||||
(com/data-grid-cell {} (:invoice-number r))
|
||||
(com/data-grid-cell {} (:client-name r))
|
||||
(com/data-grid-cell {} (:vendor-name r))
|
||||
(com/data-grid-cell {} (:amount r))
|
||||
(com/data-grid-cell {} (str/join ", " (map :info (:errors r)))))))))))))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(->>
|
||||
{::route/page page
|
||||
::route/import (-> import
|
||||
(wrap-schema-enforce :form-schema [:map [:tsv :string]])
|
||||
(wrap-nested-form-params)
|
||||
(wrap-form-4xx-2 form))
|
||||
})
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-admin)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
(apply-middleware-to-all-handlers
|
||||
(->>
|
||||
{::route/page page
|
||||
::route/import (-> import
|
||||
(wrap-schema-enforce :form-schema [:map [:tsv :string]])
|
||||
(wrap-nested-form-params)
|
||||
(wrap-form-4xx-2 form))})
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-admin)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
[bidi.bidi :as bidi]))
|
||||
|
||||
(defn tx-rows->changes [history]
|
||||
(->> history
|
||||
(->> history
|
||||
(group-by (fn [[a _ t]]
|
||||
[a t]))
|
||||
(map (fn [[[a t] changes]]
|
||||
@@ -59,7 +59,6 @@
|
||||
:else
|
||||
(pr-str v)))
|
||||
|
||||
|
||||
(defn inspect [{{:keys [entity-id]} :params :as request}]
|
||||
(alog/info ::inspect
|
||||
:request request)
|
||||
@@ -151,7 +150,7 @@
|
||||
[:div.mt-4
|
||||
[:form.flex.gap-2 {"hx-target" "#history-table"
|
||||
"hx-get" (bidi/path-for ssr-routes/only-routes
|
||||
:admin-history)
|
||||
:admin-history)
|
||||
"hx-select" "#history-table"
|
||||
"hx-swap" "innerHTML"
|
||||
"hx-push-url" "true"}
|
||||
@@ -187,6 +186,5 @@
|
||||
(if entity-id
|
||||
(result-table {:entity-id entity-id})
|
||||
[:div#history-table])
|
||||
[:div#inspector]
|
||||
])
|
||||
[:div#inspector]])
|
||||
"History")))
|
||||
|
||||
@@ -37,11 +37,11 @@
|
||||
(defn filters [request]
|
||||
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
||||
"hx-get" (bidi/path-for ssr-routes/only-routes
|
||||
::route/table)
|
||||
::route/table)
|
||||
"hx-target" "#entity-table"
|
||||
"hx-indicator" "#entity-table"}
|
||||
|
||||
[:fieldset.space-y-6
|
||||
[:fieldset.space-y-6
|
||||
(date-range-field {:value {:start (:start-date (:query-params request))
|
||||
:end (:end-date (:query-params request))}
|
||||
:id "date-range"})
|
||||
@@ -53,12 +53,12 @@
|
||||
:options (ref->select-options "import-source" :allow-nil? true)}))
|
||||
|
||||
#_(com/field {:label "Code"}
|
||||
(com/text-input {:name "code"
|
||||
:id "code"
|
||||
:class "hot-filter"
|
||||
:value (:code (:query-params request))
|
||||
:placeholder "11101"
|
||||
:size :small}))]])
|
||||
(com/text-input {:name "code"
|
||||
:id "code"
|
||||
:class "hot-filter"
|
||||
:value (:code (:query-params request))
|
||||
:placeholder "11101"
|
||||
:size :small}))]])
|
||||
|
||||
(def default-read '[:db/id
|
||||
[:import-batch/date :xform clj-time.coerce/from-date]
|
||||
@@ -72,9 +72,9 @@
|
||||
(defn fetch-ids [db request]
|
||||
(let [query-params (:query-params request)
|
||||
query (cond-> {:query {:find []
|
||||
:in '[$ ]
|
||||
:in '[$]
|
||||
:where '[]}
|
||||
:args [db ]}
|
||||
:args [db]}
|
||||
(:sort query-params) (add-sorter-fields {"source" ['[?e :import-batch/source ?s]
|
||||
'[?s :db/ident ?s2]
|
||||
'[(name ?s2) ?sort-source]]
|
||||
@@ -84,8 +84,8 @@
|
||||
"user" ['[?e :import-batch/user-name ?sort-user]]
|
||||
"date" ['[?e :import-batch/date ?sort-date]]
|
||||
"type" ['[?e :account/type ?t]
|
||||
'[?t :db/ident ?ti]
|
||||
'[(name ?ti) ?sort-type]]}
|
||||
'[?t :db/ident ?ti]
|
||||
'[(name ?ti) ?sort-type]]}
|
||||
query-params)
|
||||
|
||||
(or (:start-date query-params)
|
||||
@@ -96,7 +96,7 @@
|
||||
(merge-query {:query '{:in [?start-date]
|
||||
:where [[(>= ?d ?start-date)]]}
|
||||
:args [(-> query-params :start-date c/to-date)]})
|
||||
|
||||
|
||||
(:end-date query-params)
|
||||
(merge-query {:query '{:in [?end-date]
|
||||
:where [[(< ?d ?end-date)]]}
|
||||
@@ -184,17 +184,17 @@
|
||||
(def row* (partial helper/row* grid-page))
|
||||
(def table* (partial helper/table* grid-page))
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(->>
|
||||
{::route/page (helper/page-route grid-page)
|
||||
::route/table (helper/table-route grid-page)})
|
||||
(fn [h]
|
||||
(-> h
|
||||
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)
|
||||
(wrap-admin)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
(apply-middleware-to-all-handlers
|
||||
(->>
|
||||
{::route/page (helper/page-route grid-page)
|
||||
::route/table (helper/table-route grid-page)})
|
||||
(fn [h]
|
||||
(-> h
|
||||
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)
|
||||
(wrap-admin)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
(ns auto-ap.ssr.admin.transaction-rules
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact conn merge-query pull-attr pull-many
|
||||
query2 remove-nils]]
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact conn merge-query pull-attr pull-many
|
||||
query2 remove-nils]]
|
||||
[auto-ap.datomic.accounts :as d-accounts]
|
||||
[auto-ap.datomic.transactions :as d-transactions]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.query-params :as query-params :refer [wrap-copy-qp-pqp]]
|
||||
[auto-ap.routes.admin.transaction-rules :as route]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
: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]
|
||||
@@ -23,12 +23,12 @@
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.utils
|
||||
:refer [apply-middleware-to-all-handlers
|
||||
default-grid-fields-schema entity-id
|
||||
field-validation-error form-validation-error
|
||||
html-response many-entity modal-response money percentage
|
||||
ref->enum-schema ref->radio-options regex temp-id
|
||||
wrap-entity wrap-merge-prior-hx wrap-schema-enforce]]
|
||||
:refer [apply-middleware-to-all-handlers
|
||||
default-grid-fields-schema entity-id
|
||||
field-validation-error form-validation-error
|
||||
html-response many-entity modal-response money percentage
|
||||
ref->enum-schema ref->radio-options regex temp-id
|
||||
wrap-entity wrap-merge-prior-hx wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[bidi.bidi :as bidi]
|
||||
@@ -40,10 +40,10 @@
|
||||
[malli.util :as mut]))
|
||||
|
||||
(def query-schema (mc/schema
|
||||
[:maybe
|
||||
(into [:map {}
|
||||
[:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]] ]
|
||||
default-grid-fields-schema)]))
|
||||
[:maybe
|
||||
(into [:map {}
|
||||
[:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]]]
|
||||
default-grid-fields-schema)]))
|
||||
(defn filters [request]
|
||||
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
||||
"hx-get" (bidi/path-for ssr-routes/only-routes
|
||||
@@ -154,7 +154,7 @@
|
||||
|
||||
(not (str/blank? (:client-group query-params)))
|
||||
(merge-query {:query {:in ['?client-group]
|
||||
:where ['[?e :transaction-rule/client-group ?client-group] ]}
|
||||
:where ['[?e :transaction-rule/client-group ?client-group]]}
|
||||
:args [(clojure.string/upper-case (:client-group query-params))]})
|
||||
|
||||
true
|
||||
@@ -288,10 +288,6 @@
|
||||
[:transaction-rule/bank-account]
|
||||
:form-params form-params)))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(def transaction-read '[{:transaction/client [:client/name]
|
||||
:transaction/bank-account [:bank-account/name]}
|
||||
:transaction/description-original
|
||||
@@ -369,8 +365,6 @@
|
||||
'[(>= ?dom ?dom-gte)]]}
|
||||
:args [dom-gte]})
|
||||
|
||||
|
||||
|
||||
true
|
||||
(merge-query {:query {:where ['[?e :transaction/id]]}}))
|
||||
results (->>
|
||||
@@ -436,7 +430,7 @@
|
||||
:content-fn (fn [value]
|
||||
(let [a (dc/pull (dc/db conn) d-accounts/default-read value)]
|
||||
(when value
|
||||
(str
|
||||
(str
|
||||
(:account/numeric-code a)
|
||||
" - "
|
||||
(:account/name (d-accounts/clientize a
|
||||
@@ -505,7 +499,6 @@
|
||||
(com/data-grid-cell {:class "align-top"}
|
||||
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
|
||||
|
||||
|
||||
(defn all-ids-not-locked [all-ids]
|
||||
(->> all-ids
|
||||
(dc/q '[:find ?t
|
||||
@@ -621,18 +614,18 @@
|
||||
{}
|
||||
(com/modal-header {} [:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"]])
|
||||
(com/modal-body {} [:form#my-form
|
||||
{:hx-post (bidi/path-for ssr-routes/only-routes ::route/execute
|
||||
:db/id (:db/id entity))
|
||||
:hx-indicator "#code"}
|
||||
[:div
|
||||
{:hx-get (bidi/path-for ssr-routes/only-routes ::route/check-badges)
|
||||
:hx-trigger "change"
|
||||
:hx-target "#transaction-test-results .gutter"
|
||||
:hx-include "this"}
|
||||
(transaction-rule-test-table* {:entity entity
|
||||
:clients clients
|
||||
:checkboxes? true
|
||||
:only-uncoded? true})]])
|
||||
{:hx-post (bidi/path-for ssr-routes/only-routes ::route/execute
|
||||
:db/id (:db/id entity))
|
||||
:hx-indicator "#code"}
|
||||
[:div
|
||||
{:hx-get (bidi/path-for ssr-routes/only-routes ::route/check-badges)
|
||||
:hx-trigger "change"
|
||||
:hx-target "#transaction-test-results .gutter"
|
||||
:hx-include "this"}
|
||||
(transaction-rule-test-table* {:entity entity
|
||||
:clients clients
|
||||
:checkboxes? true
|
||||
:only-uncoded? true})]])
|
||||
(com/modal-footer {} [:div.flex.justify-end (com/validated-save-button {:form "my-form" :id "code"} "Code transactions")])))
|
||||
:headers (-> {}
|
||||
(assoc "hx-trigger-after-settle" "modalnext")
|
||||
@@ -656,7 +649,7 @@
|
||||
(edit-path [_ _] [])
|
||||
|
||||
(step-schema [_]
|
||||
(mm/form-schema linear-wizard))
|
||||
(mm/form-schema linear-wizard))
|
||||
|
||||
(render-step [this request]
|
||||
(mm/default-render-step
|
||||
@@ -825,11 +818,11 @@
|
||||
(com/validated-field {:label "Approval status"
|
||||
:errors (fc/field-errors)}
|
||||
(com/radio-card {:options (ref->radio-options "transaction-approval-status")
|
||||
:value (fc/field-value)
|
||||
:name (fc/field-name)
|
||||
:size :small
|
||||
:orientation :horizontal})))]]])
|
||||
:footer
|
||||
:value (fc/field-value)
|
||||
:name (fc/field-name)
|
||||
:size :small
|
||||
:orientation :horizontal})))]]])
|
||||
:footer
|
||||
(mm/default-step-footer linear-wizard this :validation-route ::route/navigate)
|
||||
:validation-route ::route/navigate)))
|
||||
|
||||
@@ -893,25 +886,25 @@
|
||||
nil)))
|
||||
(form-schema [_] form-schema)
|
||||
(submit [_ {:keys [multi-form-state request-method identity] :as request}]
|
||||
|
||||
(let [transaction-rule (:snapshot multi-form-state)
|
||||
_ (validate-transaction-rule transaction-rule)
|
||||
entity (cond-> transaction-rule
|
||||
(:transaction-rule/client-group transaction-rule) (update :transaction-rule/client-group str/upper-case)
|
||||
(= :post request-method) (assoc :db/id "new")
|
||||
true (assoc :transaction-rule/note (entity->note transaction-rule)))
|
||||
{:keys [tempids]} (audit-transact [[:upsert-entity entity]]
|
||||
(:identity request))
|
||||
updated-rule (dc/pull (dc/db conn)
|
||||
default-read
|
||||
(or (get tempids (:db/id entity)) (:db/id entity)))]
|
||||
(html-response
|
||||
(row* identity updated-rule {:flash? true})
|
||||
:headers (cond-> {"hx-trigger" "modalclose"}
|
||||
(= :post request-method) (assoc "hx-retarget" "#entity-table tbody"
|
||||
"hx-reswap" "afterbegin")
|
||||
(= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-rule))
|
||||
"hx-reswap" "outerHTML"))))))
|
||||
|
||||
(let [transaction-rule (:snapshot multi-form-state)
|
||||
_ (validate-transaction-rule transaction-rule)
|
||||
entity (cond-> transaction-rule
|
||||
(:transaction-rule/client-group transaction-rule) (update :transaction-rule/client-group str/upper-case)
|
||||
(= :post request-method) (assoc :db/id "new")
|
||||
true (assoc :transaction-rule/note (entity->note transaction-rule)))
|
||||
{:keys [tempids]} (audit-transact [[:upsert-entity entity]]
|
||||
(:identity request))
|
||||
updated-rule (dc/pull (dc/db conn)
|
||||
default-read
|
||||
(or (get tempids (:db/id entity)) (:db/id entity)))]
|
||||
(html-response
|
||||
(row* identity updated-rule {:flash? true})
|
||||
:headers (cond-> {"hx-trigger" "modalclose"}
|
||||
(= :post request-method) (assoc "hx-retarget" "#entity-table tbody"
|
||||
"hx-reswap" "afterbegin")
|
||||
(= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-rule))
|
||||
"hx-reswap" "outerHTML"))))))
|
||||
(def rule-wizard (->TransactionRuleWizard nil nil nil))
|
||||
|
||||
(def key->handler
|
||||
@@ -1003,10 +996,10 @@
|
||||
{}))))})
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)
|
||||
(wrap-admin)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
(:require
|
||||
[auto-ap.cursor :as cursor]
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact audit-transact-batch audit-transact-batch
|
||||
conn merge-query pull-attr pull-many query2]]
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact audit-transact-batch audit-transact-batch
|
||||
conn merge-query pull-attr pull-many query2]]
|
||||
[auto-ap.datomic.accounts :as d-accounts]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||
[auto-ap.routes.admin.vendors :as route]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.solr :as solr]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler
|
||||
@@ -23,12 +23,12 @@
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.utils
|
||||
:refer [apply-middleware-to-all-handlers
|
||||
default-grid-fields-schema entity-id
|
||||
form-validation-error html-response many-entity
|
||||
modal-response ref->enum-schema ref->select-options strip
|
||||
temp-id wrap-entity wrap-form-4xx-2 wrap-merge-prior-hx
|
||||
wrap-schema-enforce]]
|
||||
:refer [apply-middleware-to-all-handlers
|
||||
default-grid-fields-schema entity-id
|
||||
form-validation-error html-response many-entity
|
||||
modal-response ref->enum-schema ref->select-options strip
|
||||
temp-id wrap-entity wrap-form-4xx-2 wrap-merge-prior-hx
|
||||
wrap-schema-enforce]]
|
||||
[bidi.bidi :as bidi]
|
||||
[clojure.string :as str]
|
||||
[datomic.api :as dc]
|
||||
@@ -41,7 +41,7 @@
|
||||
(into [:map {}
|
||||
[:name {:optional true :default nil} [:maybe [:string {:string/decode strip}]]]
|
||||
#_[:role {:optional true} [:maybe (ref->enum-schema "user-role")]]
|
||||
#_[:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]] ]
|
||||
#_[:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]]]
|
||||
default-grid-fields-schema)]))
|
||||
(defn filters [request]
|
||||
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
||||
@@ -60,16 +60,16 @@
|
||||
:size :small}))
|
||||
(com/field {:label "Type"}
|
||||
(com/radio-card {:size :small
|
||||
:name "type"
|
||||
:value (:type (:query-params request))
|
||||
:options [{:value ""
|
||||
:content "All"}
|
||||
{:value "only-hidden"
|
||||
:content "Only hidden"}
|
||||
{:value "only-global"
|
||||
:content "Only global"}
|
||||
#_{:value "potential-duplicates"
|
||||
:content "Potential duplicates"}]}))]])
|
||||
:name "type"
|
||||
:value (:type (:query-params request))
|
||||
:options [{:value ""
|
||||
:content "All"}
|
||||
{:value "only-hidden"
|
||||
:content "Only hidden"}
|
||||
{:value "only-global"
|
||||
:content "Only global"}
|
||||
#_{:value "potential-duplicates"
|
||||
:content "Potential duplicates"}]}))]])
|
||||
|
||||
(def default-read '[:db/id
|
||||
:vendor/name
|
||||
@@ -203,8 +203,6 @@
|
||||
(def row* (partial helper/row* grid-page))
|
||||
(def table* (partial helper/table* grid-page))
|
||||
|
||||
|
||||
|
||||
(defn merge-submit [{:keys [form-params request-method identity] :as request}]
|
||||
(if (= (:source-vendor form-params)
|
||||
(:target-vendor form-params))
|
||||
@@ -245,7 +243,6 @@
|
||||
(= i (dec (count steps))) (assoc :last? true))
|
||||
n)))))
|
||||
|
||||
|
||||
;; TODO add plaid merchant
|
||||
;; TODO each client only used once
|
||||
|
||||
@@ -285,7 +282,6 @@
|
||||
(com/data-grid-cell {:class "align-top"}
|
||||
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))))
|
||||
|
||||
|
||||
(defn automatically-paid-when-due-row [terms-override-cursor]
|
||||
(com/data-grid-row
|
||||
(-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? terms-override-cursor))))})
|
||||
@@ -303,15 +299,12 @@
|
||||
:value (fc/field-value)
|
||||
:value-fn :db/id
|
||||
|
||||
|
||||
:content-fn #(pull-attr (dc/db conn) :client/name (:db/id %))
|
||||
:size :small})))
|
||||
|
||||
|
||||
(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-typeahead*
|
||||
[{:keys [name value client-id x-model]}]
|
||||
[:div.flex.flex-col
|
||||
@@ -370,12 +363,6 @@
|
||||
(com/data-grid-cell {:class "align-top"}
|
||||
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(defn dialog* [{:keys [entity form-params form-errors] :as params}]
|
||||
(alog/peek ::dialog-entity form-params)
|
||||
(fc/start-form form-params form-errors
|
||||
@@ -868,7 +855,6 @@
|
||||
|
||||
(def vendor-wizard (->VendorWizard :info))
|
||||
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(->>
|
||||
@@ -921,11 +907,11 @@
|
||||
(fn [cursor _] (account-override-row cursor)))})
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)
|
||||
(wrap-admin)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
:headers {"Location" "/login"}
|
||||
:session {}})
|
||||
|
||||
|
||||
(defn impersonate [request]
|
||||
{:status 200
|
||||
:session {:identity (dissoc (jwt/unsign (get-in request [:query-params "jwt"])
|
||||
@@ -39,23 +38,22 @@
|
||||
next (assoc "state" (hu/url-encode next))))))))
|
||||
|
||||
(defn- page-contents [request]
|
||||
[:div#app { "@notification.document" "notificationDetails=event.detail.value; showNotification=true"
|
||||
[:div#app {"@notification.document" "notificationDetails=event.detail.value; showNotification=true"
|
||||
|
||||
:x-data (hx/json {:showError false
|
||||
:errorDetails ""
|
||||
:showNotification false
|
||||
:notificationDetails ""})
|
||||
"@htmx:response-error.camel" "errorDetails = $event.detail.xhr.response; showError=true;"
|
||||
}
|
||||
[:div#app-contents.flex.overflow-hidden
|
||||
[:div#main-content {:class "relative w-full h-full overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content " }
|
||||
"@htmx:response-error.camel" "errorDetails = $event.detail.xhr.response; showError=true;"}
|
||||
[:div#app-contents.flex.overflow-hidden
|
||||
[:div#main-content {:class "relative w-full h-full overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content "}
|
||||
[:div#notification-holder
|
||||
[:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg {:x-show "showNotification" }
|
||||
[:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg {:x-show "showNotification"}
|
||||
[:div.relative
|
||||
[:button.absolute.right-2.top-2.w-6.h-6.z-50.text-blue-400
|
||||
{ "@click" "showNotification=false"}
|
||||
{"@click" "showNotification=false"}
|
||||
svg/filled-x]]
|
||||
|
||||
|
||||
[:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-blue-800.bg-blue-50.dark:bg-gray-800.dark:text-blue-400.border-blue-300.rounded-lg.border.max-h-96
|
||||
{:x-show "showNotification"
|
||||
"x-transition:enter" "transition duration-300 transform ease-in-out"
|
||||
@@ -64,16 +62,16 @@
|
||||
"x-transition:leave" "transition duration-300 transform ease-in-out"
|
||||
"x-transition:leave-start" "opacity-100 translate-y-0"
|
||||
"x-transition:leave-end" "opacity-0 translate-y-full"}
|
||||
|
||||
|
||||
[:div {:class "p-4 text-lg w-full" :role "alert"}
|
||||
[:div.text-sm
|
||||
[:pre#notification-details.text-xs {:x-html "notificationDetails"}]]]]]]
|
||||
[:div {:x-show "showError"
|
||||
[:div {:x-show "showError"
|
||||
:x-init ""}
|
||||
[:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg
|
||||
[:div.relative
|
||||
[:button.absolute.right-2.top-2.w-6.h-6.z-50.text-red-600
|
||||
{ "@click" "showError=false"}
|
||||
{"@click" "showError=false"}
|
||||
svg/filled-x]]
|
||||
|
||||
[:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-red-800.bg-red-50.dark:bg-gray-800.dark:text-red-400.border-red-300.rounded-lg.border.max-h-96
|
||||
@@ -81,7 +79,7 @@
|
||||
"x-transition:enter" "transition duration-300"
|
||||
"x-transition:enter-start" "opacity-0"
|
||||
"x-transition:enter-end" "opacity-100"}
|
||||
|
||||
|
||||
[:div {:class "p-4 mb-4 text-lg w-full" :role "alert"}
|
||||
[:div.inline-block.w-8.h-8.mr-2 svg/alert]
|
||||
[:span.font-medium "Oh, drat! An unexpected error has occurred."]
|
||||
@@ -94,14 +92,13 @@
|
||||
|
||||
[:div.p-4
|
||||
[:img {:src "/img/logo-big.png"}]
|
||||
[:div
|
||||
[:a.button.is-large.is-primary {:href (login-url (get (:query-params request) "redirect-to"))} "Login with Google"]]
|
||||
"HELLO"])
|
||||
]]] ])
|
||||
[:div
|
||||
[:a.button.is-large.is-primary {:href (login-url (get (:query-params request) "redirect-to"))} "Login with Google"]]
|
||||
"HELLO"])]]]])
|
||||
|
||||
(defn login [request]
|
||||
(base-page
|
||||
request
|
||||
(page-contents request)
|
||||
|
||||
|
||||
"Dashboard"))
|
||||
@@ -2,18 +2,17 @@
|
||||
(:require [auto-ap.ssr.form-cursor :as fc]
|
||||
[auto-ap.ssr.utils :refer [html-response wrap-schema-enforce]]))
|
||||
|
||||
|
||||
(defn add-new-entity-handler
|
||||
([path render-fn] (add-new-entity-handler path
|
||||
render-fn
|
||||
(fn default-data [base _]
|
||||
(fn default-data [base _]
|
||||
base)))
|
||||
([path render-fn build-data]
|
||||
(-> (fn new-entity [{{:keys [index]} :query-params :as request}]
|
||||
(html-response
|
||||
(fc/start-form-with-prefix (conj path (or index 0))
|
||||
(build-data {:db/id (str (java.util.UUID/randomUUID))
|
||||
:new? true} request)
|
||||
:new? true} request)
|
||||
[]
|
||||
(render-fn fc/*current* request))))
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
|
||||
@@ -33,18 +33,17 @@
|
||||
(com/content-card {:class " w-[748px]"
|
||||
:hx-target "this"
|
||||
:hx-swap "outerHTML"}
|
||||
[:div.col-span-1.p-4 {:class "p-4 sm:p-6 space-y-4 overflow-visible "
|
||||
}
|
||||
[:div.col-span-1.p-4 {:class "p-4 sm:p-6 space-y-4 overflow-visible "}
|
||||
[:h3 {:class "mb-4 text-xl font-semibold dark:text-white"}
|
||||
"Signature"]
|
||||
[:div#signature-notification.notification.block {:style {:display "none"}}]
|
||||
[:div {:x-data (hx/json {"signature" nil
|
||||
"editing" false
|
||||
"existing" (boolean signature-file)})
|
||||
:hx-put (bidi/path-for ssr-routes/only-routes
|
||||
:company-update-signature)
|
||||
:hx-trigger "accepted"
|
||||
:hx-vals "js:{signatureData: event.detail.signatureData}"}
|
||||
"editing" false
|
||||
"existing" (boolean signature-file)})
|
||||
:hx-put (bidi/path-for ssr-routes/only-routes
|
||||
:company-update-signature)
|
||||
:hx-trigger "accepted"
|
||||
:hx-vals "js:{signatureData: event.detail.signatureData}"}
|
||||
[:div.htmx-indicator
|
||||
[:div.bg-gray-100.flex.items-center.text-green-500.justify-center.rounded.rounded-lg.border.border-gray-400 {:style {:width "696px" :height "261px"}}
|
||||
(svg/spinner {:class "w-4 h-4 text-primary-300"})
|
||||
@@ -58,7 +57,6 @@
|
||||
:x-show "existing && !editing"}])
|
||||
[:canvas.rounded.rounded-lg.border.border-gray-300
|
||||
|
||||
|
||||
{:style {:width 696
|
||||
:height 261}
|
||||
:x-init "signature= new SignaturePad($el); signature.off()"
|
||||
@@ -67,7 +65,6 @@
|
||||
:height 261
|
||||
:x-show "existing ? editing: true"}]]
|
||||
|
||||
|
||||
[:div.flex.gap-2.justify-end
|
||||
(com/button {:color :primary
|
||||
:x-show "!editing"
|
||||
@@ -83,7 +80,7 @@
|
||||
:x-show "editing"}
|
||||
"Accept")]]
|
||||
|
||||
[:div
|
||||
[:div
|
||||
[:div.flex.justify-center " - or -"]
|
||||
[:form {:hx-post (bidi/path-for ssr-routes/only-routes
|
||||
:company-upload-signature)
|
||||
@@ -92,18 +89,17 @@
|
||||
#_#_:hx-target "#signature-notification"
|
||||
:hx-swap "outerHTML"
|
||||
:id "upload"
|
||||
:hx-trigger "z"
|
||||
}
|
||||
[:div.htmx-indicator
|
||||
[:div.bg-gray-100.flex.items-center.text-green-500.justify-center.rounded.rounded-lg.border.border-gray-400 {:style {:width "696px" :height "261px"}}
|
||||
(svg/spinner {:class "w-4 h-4 text-primary-300"})
|
||||
[:div.ml-3 "Loading..."]]]
|
||||
[:div.htmx-indicator-hidden
|
||||
[:div.border-2.border-dashed.rounded-lg.p-4.w-full.text-center.cursor-pointer.h-64.flex.items-center.justify-center.text-lg.relative
|
||||
{:x-data (hx/json {"files" nil
|
||||
"hovering" false})
|
||||
:x-dispatch:z "files"
|
||||
":class" "{'bg-blue-100': !hovering,
|
||||
:hx-trigger "z"}
|
||||
[:div.htmx-indicator
|
||||
[:div.bg-gray-100.flex.items-center.text-green-500.justify-center.rounded.rounded-lg.border.border-gray-400 {:style {:width "696px" :height "261px"}}
|
||||
(svg/spinner {:class "w-4 h-4 text-primary-300"})
|
||||
[:div.ml-3 "Loading..."]]]
|
||||
[:div.htmx-indicator-hidden
|
||||
[:div.border-2.border-dashed.rounded-lg.p-4.w-full.text-center.cursor-pointer.h-64.flex.items-center.justify-center.text-lg.relative
|
||||
{:x-data (hx/json {"files" nil
|
||||
"hovering" false})
|
||||
:x-dispatch:z "files"
|
||||
":class" "{'bg-blue-100': !hovering,
|
||||
'border-blue-300': !hovering,
|
||||
'text-blue-700': !hovering,
|
||||
'bg-green-100': hovering,
|
||||
@@ -111,23 +107,20 @@
|
||||
'text-green-700': hovering
|
||||
}"}
|
||||
|
||||
[:input {:type "file"
|
||||
:name "file"
|
||||
:class "absolute inset-0 m-0 p-0 w-full h-full outline-none opacity-0",
|
||||
:x-on:change "files = $event.target.files;",
|
||||
:x-on:dragover "hovering = true",
|
||||
:x-on:dragleave "hovering = false",
|
||||
:x-on:drop "hovering = false"}]
|
||||
[:div.flex.flex-col.space-2
|
||||
[:div
|
||||
[:ul {:x-show "files != null"}
|
||||
[:template {:x-for "f in files"}
|
||||
[:li (com/pill {:color :primary :x-text "f.name"})]]]]
|
||||
|
||||
|
||||
[:input {:type "file"
|
||||
:name "file"
|
||||
:class "absolute inset-0 m-0 p-0 w-full h-full outline-none opacity-0",
|
||||
:x-on:change "files = $event.target.files;",
|
||||
:x-on:dragover "hovering = true",
|
||||
:x-on:dragleave "hovering = false",
|
||||
:x-on:drop "hovering = false"}]
|
||||
[:div.flex.flex-col.space-2
|
||||
[:div
|
||||
[:ul {:x-show "files != null"}
|
||||
[:template {:x-for "f in files"}
|
||||
[:li (com/pill {:color :primary :x-text "f.name"})]]]]
|
||||
|
||||
|
||||
[:div.htmx-indicator-hidden "Drop a signature file (696x261 pixels jpeg) here."]]]] ]]])))
|
||||
[:div.htmx-indicator-hidden "Drop a signature file (696x261 pixels jpeg) here."]]]]]]])))
|
||||
|
||||
(defn upload-signature-data [{{:strs [signatureData]} :form-params client :client :as request}]
|
||||
(let [prefix "data:image/png;base64,"]
|
||||
@@ -149,66 +142,66 @@
|
||||
|
||||
(defn upload-signature-file [{{:strs [signatureData]} :form-params client :client user :identity :as request}]
|
||||
(assert-can-see-client user client)
|
||||
(let [{:strs [file]} (:multipart-params request) ]
|
||||
(try
|
||||
(let [signature-id (str (UUID/randomUUID)) ]
|
||||
(s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env)
|
||||
:key (str signature-id ".jpg")
|
||||
:input-stream (io/input-stream (:tempfile file))
|
||||
:metadata {:content-type "image/jpeg"
|
||||
:content-length (:length (:tempfile file))}
|
||||
:canned-acl "public-read")
|
||||
@(dc/transact conn [{:db/id (:db/id client)
|
||||
:client/signature-file (str "https://integreat-signature-images.s3.amazonaws.com/" signature-id ".jpg")}])
|
||||
(html-response
|
||||
(signature request)))
|
||||
(catch Exception e
|
||||
(println e)
|
||||
#_(-> result
|
||||
(assoc :error? true)
|
||||
(update :results conj {:filename filename
|
||||
:response (.getMessage e)
|
||||
:sample (:sample (ex-data e))
|
||||
:template (:template (ex-data e))}))))
|
||||
(let [{:strs [file]} (:multipart-params request)]
|
||||
(try
|
||||
(let [signature-id (str (UUID/randomUUID))]
|
||||
(s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env)
|
||||
:key (str signature-id ".jpg")
|
||||
:input-stream (io/input-stream (:tempfile file))
|
||||
:metadata {:content-type "image/jpeg"
|
||||
:content-length (:length (:tempfile file))}
|
||||
:canned-acl "public-read")
|
||||
@(dc/transact conn [{:db/id (:db/id client)
|
||||
:client/signature-file (str "https://integreat-signature-images.s3.amazonaws.com/" signature-id ".jpg")}])
|
||||
(html-response
|
||||
(signature request)))
|
||||
(catch Exception e
|
||||
(println e)
|
||||
#_(-> result
|
||||
(assoc :error? true)
|
||||
(update :results conj {:filename filename
|
||||
:response (.getMessage e)
|
||||
:sample (:sample (ex-data e))
|
||||
:template (:template (ex-data e))}))))
|
||||
#_(html-response [:div#page-notification.p-4.rounded-lg
|
||||
{:class (if (:error? results)
|
||||
"bg-red-50 text-red-700"
|
||||
"bg-primary-50 text-primary-700")}
|
||||
[:table
|
||||
[:thead
|
||||
[:tr [:td "File"] [:td "Result"]
|
||||
[:td "Template"]
|
||||
(if (:error? results)
|
||||
[:td "Sample match"])]
|
||||
#_[:tr "Result"]
|
||||
#_[:tr "Template"]]
|
||||
(for [r (:results results)]
|
||||
[:tr
|
||||
[:td.p-2.border
|
||||
{:class (if (:error? results)
|
||||
"bg-red-50 text-red-700 border-red-300"
|
||||
"bg-primary-50 text-primary-700 border-green-500")}
|
||||
(:filename r)]
|
||||
[:td.p-2.border
|
||||
{:class (if (:error? results)
|
||||
"bg-red-50 text-red-700 border-red-300"
|
||||
"bg-primary-50 text-primary-700 border-green-500")}
|
||||
(:response r)]
|
||||
[:td.p-2.border
|
||||
{:class (if (:error? results)
|
||||
"bg-red-50 text-red-700 border-red-300"
|
||||
"bg-primary-50 text-primary-700 border-green-500")}
|
||||
"Template: " (:template r)]
|
||||
(if (:error? results)
|
||||
{:class (if (:error? results)
|
||||
"bg-red-50 text-red-700"
|
||||
"bg-primary-50 text-primary-700")}
|
||||
[:table
|
||||
[:thead
|
||||
[:tr [:td "File"] [:td "Result"]
|
||||
[:td "Template"]
|
||||
(if (:error? results)
|
||||
[:td "Sample match"])]
|
||||
#_[:tr "Result"]
|
||||
#_[:tr "Template"]]
|
||||
(for [r (:results results)]
|
||||
[:tr
|
||||
[:td.p-2.border
|
||||
{:class "bg-red-50 text-red-700 border-red-300"}
|
||||
{:class (if (:error? results)
|
||||
"bg-red-50 text-red-700 border-red-300"
|
||||
"bg-primary-50 text-primary-700 border-green-500")}
|
||||
(:filename r)]
|
||||
[:td.p-2.border
|
||||
{:class (if (:error? results)
|
||||
"bg-red-50 text-red-700 border-red-300"
|
||||
"bg-primary-50 text-primary-700 border-green-500")}
|
||||
(:response r)]
|
||||
[:td.p-2.border
|
||||
{:class (if (:error? results)
|
||||
"bg-red-50 text-red-700 border-red-300"
|
||||
"bg-primary-50 text-primary-700 border-green-500")}
|
||||
"Template: " (:template r)]
|
||||
(if (:error? results)
|
||||
[:td.p-2.border
|
||||
{:class "bg-red-50 text-red-700 border-red-300"}
|
||||
|
||||
[:ul
|
||||
(for [[k v] (dissoc (:sample r) :template :source-url :full-text :text)]
|
||||
[:li (name k) ": " (str v)])]
|
||||
#_(:template r)])])]]
|
||||
:headers
|
||||
{"hx-trigger" "invalidated"})))
|
||||
[:ul
|
||||
(for [[k v] (dissoc (:sample r) :template :source-url :full-text :text)]
|
||||
[:li (name k) ": " (str v)])]
|
||||
#_(:template r)])])]]
|
||||
:headers
|
||||
{"hx-trigger" "invalidated"})))
|
||||
|
||||
(defn main-content* [{:keys [client identity] :as request}]
|
||||
(if-not client
|
||||
@@ -276,7 +269,6 @@
|
||||
|
||||
(def search (wrap-json-response search))
|
||||
|
||||
|
||||
(defn bank-account-search [{:keys [route-params query-params clients]}]
|
||||
(let [valid-client-ids (set (map :db/id clients))
|
||||
selected-client-id (Long/parseLong (get route-params :db/id))
|
||||
|
||||
@@ -24,32 +24,32 @@
|
||||
|
||||
(def query-schema (mc/schema
|
||||
[:maybe
|
||||
(into [:map {} ]
|
||||
(into [:map {}]
|
||||
default-grid-fields-schema)]))
|
||||
|
||||
(def vendor-read '[:db/id
|
||||
:vendor/name
|
||||
{:vendor/legal-entity-1099-type [:db/ident]}
|
||||
{:vendor/legal-entity-tin-type [:db/ident]}
|
||||
{:vendor/address [:address/street1
|
||||
:address/city
|
||||
:address/state
|
||||
:address/zip]}
|
||||
:vendor/name
|
||||
{:vendor/legal-entity-1099-type [:db/ident]}
|
||||
{:vendor/legal-entity-tin-type [:db/ident]}
|
||||
{:vendor/address [:address/street1
|
||||
:address/city
|
||||
:address/state
|
||||
:address/zip]}
|
||||
{:vendor/default-account [:account/name]}
|
||||
:vendor/legal-entity-tin
|
||||
:vendor/legal-entity-name
|
||||
:vendor/legal-entity-first-name
|
||||
:vendor/legal-entity-middle-name
|
||||
:vendor/legal-entity-last-name])
|
||||
:vendor/legal-entity-tin
|
||||
:vendor/legal-entity-name
|
||||
:vendor/legal-entity-first-name
|
||||
:vendor/legal-entity-middle-name
|
||||
:vendor/legal-entity-last-name])
|
||||
|
||||
(defn sum-for-client-vendor [client-id vendor-id]
|
||||
(ffirst (dc/q '[:find
|
||||
(sum ?a)
|
||||
:with ?d
|
||||
:with ?d
|
||||
:in $ ?c ?v
|
||||
:where
|
||||
[?p :payment/client ?c]
|
||||
[?p :payment/date ?d ]
|
||||
[?p :payment/date ?d]
|
||||
[(>= ?d #inst "2025-01-01T08:00")]
|
||||
[(< ?d #inst "2026-01-01T08:00")]
|
||||
[?p :payment/type :payment-type/check]
|
||||
@@ -64,11 +64,11 @@
|
||||
(pull ?c [:client/code :db/id])
|
||||
(pull ?v vendor-read)
|
||||
(sum ?a)
|
||||
:with ?d
|
||||
:with ?d
|
||||
:in $ [?c ...] vendor-read
|
||||
:where
|
||||
[?p :payment/client ?c]
|
||||
[?p :payment/date ?d ]
|
||||
[?p :payment/date ?d]
|
||||
[(>= ?d #inst "2025-01-01T08:00")]
|
||||
[(< ?d #inst "2026-01-01T08:00")]
|
||||
[?p :payment/type :payment-type/check]
|
||||
@@ -78,105 +78,101 @@
|
||||
trimmed-clients
|
||||
vendor-read)
|
||||
all (->> results
|
||||
(filter (fn [[_ _ a]]
|
||||
(>= (or a 0.0) 600.0)))
|
||||
(sort-by (fn [[client _ amount]]
|
||||
[(:client/code client ) amount]))
|
||||
(into []))
|
||||
(filter (fn [[_ _ a]]
|
||||
(>= (or a 0.0) 600.0)))
|
||||
(sort-by (fn [[client _ amount]]
|
||||
[(:client/code client) amount]))
|
||||
(into []))
|
||||
paginated (apply-pagination-raw {:start (:start query-params)
|
||||
:per-page (:per-page query-params)} all)]
|
||||
[(:entries paginated) (:count paginated)]))
|
||||
|
||||
(def grid-page
|
||||
(helper/build
|
||||
{:id "entity-table"
|
||||
:nav com/company-aside-nav
|
||||
:id-fn (comp :db/id second)
|
||||
:fetch-page fetch-page
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company)}
|
||||
"My Company"]
|
||||
(helper/build
|
||||
{:id "entity-table"
|
||||
:nav com/company-aside-nav
|
||||
:id-fn (comp :db/id second)
|
||||
:fetch-page fetch-page
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company)}
|
||||
"My Company"]
|
||||
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company-1099)}
|
||||
"1099 Vendor Info"]]
|
||||
:title "1099 Vendors"
|
||||
:entity-name "Vendors"
|
||||
:query-schema query-schema
|
||||
:route :company-1099-vendor-table
|
||||
:row-buttons (fn [request e]
|
||||
[(com/icon-button {:hx-get (url (bidi/path-for ssr-routes/only-routes
|
||||
:company-1099-vendor-dialog
|
||||
:vendor-id (:db/id (second e)))
|
||||
{:client-id (:db/id (first e))})}
|
||||
svg/pencil)])
|
||||
:headers [{:key "Client"
|
||||
:name "Client"
|
||||
:sort-key "client"
|
||||
:render (comp :client/code first)}
|
||||
{:key "vendor-name"
|
||||
:name "Vendor Name"
|
||||
:sort-key "vendor"
|
||||
:render (fn [[_ vendor]]
|
||||
[:div.flex.whitespace-nowrap.items-center.gap-4
|
||||
[:div [:div (:vendor/name vendor)]
|
||||
[:div.text-sm.text-gray-400
|
||||
(or (-> vendor :vendor/legal-entity-name not-empty)
|
||||
(str (-> vendor :vendor/legal-entity-first-name) " "
|
||||
(-> vendor :vendor/legal-entity-middle-name) " "
|
||||
(-> vendor :vendor/legal-entity-last-name)))]]
|
||||
(when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)]
|
||||
(com/pill
|
||||
{:class "text-xs font-medium"
|
||||
:color :primary}
|
||||
(str/capitalize t99-type))
|
||||
)])}
|
||||
{:key "tin"
|
||||
:name "TIN"
|
||||
:sort-key "tin"
|
||||
:show-starting "md"
|
||||
:render (fn [[_ vendor]]
|
||||
[:div.flex.gap-4
|
||||
(when-let [tin (-> vendor :vendor/legal-entity-tin)]
|
||||
[:span {:class "text-xs font-medium py-0.5 "}
|
||||
tin])
|
||||
(when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)]
|
||||
(com/pill {:class "text-xs font-medium"
|
||||
:color :yellow}
|
||||
(name tin-type)))]
|
||||
)}
|
||||
{:key "expense-account"
|
||||
:name "Expense Account"
|
||||
:show-starting "md"
|
||||
:render (fn [[_ vendor]]
|
||||
[:div.flex.gap-4
|
||||
(when-let [tin (-> vendor :vendor/default-account :account/name)]
|
||||
[:span {:class "text-xs font-medium py-0.5 "}
|
||||
tin]) ])}
|
||||
{:key "address"
|
||||
:name "Address"
|
||||
:sort-key "address"
|
||||
:show-starting "lg"
|
||||
:render (fn [[_ vendor]]
|
||||
(if (-> vendor :vendor/address :address/street1)
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company-1099)}
|
||||
"1099 Vendor Info"]]
|
||||
:title "1099 Vendors"
|
||||
:entity-name "Vendors"
|
||||
:query-schema query-schema
|
||||
:route :company-1099-vendor-table
|
||||
:row-buttons (fn [request e]
|
||||
[(com/icon-button {:hx-get (url (bidi/path-for ssr-routes/only-routes
|
||||
:company-1099-vendor-dialog
|
||||
:vendor-id (:db/id (second e)))
|
||||
{:client-id (:db/id (first e))})}
|
||||
svg/pencil)])
|
||||
:headers [{:key "Client"
|
||||
:name "Client"
|
||||
:sort-key "client"
|
||||
:render (comp :client/code first)}
|
||||
{:key "vendor-name"
|
||||
:name "Vendor Name"
|
||||
:sort-key "vendor"
|
||||
:render (fn [[_ vendor]]
|
||||
[:div.flex.whitespace-nowrap.items-center.gap-4
|
||||
[:div [:div (:vendor/name vendor)]
|
||||
[:div.text-sm.text-gray-400
|
||||
(or (-> vendor :vendor/legal-entity-name not-empty)
|
||||
(str (-> vendor :vendor/legal-entity-first-name) " "
|
||||
(-> vendor :vendor/legal-entity-middle-name) " "
|
||||
(-> vendor :vendor/legal-entity-last-name)))]]
|
||||
(when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)]
|
||||
(com/pill
|
||||
{:class "text-xs font-medium"
|
||||
:color :primary}
|
||||
(str/capitalize t99-type)))])}
|
||||
{:key "tin"
|
||||
:name "TIN"
|
||||
:sort-key "tin"
|
||||
:show-starting "md"
|
||||
:render (fn [[_ vendor]]
|
||||
[:div.flex.gap-4
|
||||
(when-let [tin (-> vendor :vendor/legal-entity-tin)]
|
||||
[:span {:class "text-xs font-medium py-0.5 "}
|
||||
tin])
|
||||
(when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)]
|
||||
(com/pill {:class "text-xs font-medium"
|
||||
:color :yellow}
|
||||
(name tin-type)))])}
|
||||
{:key "expense-account"
|
||||
:name "Expense Account"
|
||||
:show-starting "md"
|
||||
:render (fn [[_ vendor]]
|
||||
[:div.flex.gap-4
|
||||
(when-let [tin (-> vendor :vendor/default-account :account/name)]
|
||||
[:span {:class "text-xs font-medium py-0.5 "}
|
||||
tin])])}
|
||||
{:key "address"
|
||||
:name "Address"
|
||||
:sort-key "address"
|
||||
:show-starting "lg"
|
||||
:render (fn [[_ vendor]]
|
||||
(if (-> vendor :vendor/address :address/street1)
|
||||
[:div
|
||||
[:div (-> vendor :vendor/address :address/street1)] " "
|
||||
[:div
|
||||
[:div (-> vendor :vendor/address :address/street1)] " "
|
||||
[:div
|
||||
(-> vendor :vendor/address :address/street2)] " "
|
||||
[:div
|
||||
(-> vendor :vendor/address :address/city) " "
|
||||
(-> vendor :vendor/address :address/state) ","
|
||||
(-> vendor :vendor/address :address/zip)]]
|
||||
[:p.text-sm.italic.text-gray-400 "No address"]))}
|
||||
{:key "paid"
|
||||
:name "Paid"
|
||||
:sort-key "paid"
|
||||
:render (fn [[_ _ paid]]
|
||||
(com/pill {:class "text-xs font-medium"
|
||||
:color :primary}
|
||||
"Paid $" (Math/round paid)))}]}))
|
||||
|
||||
|
||||
(-> vendor :vendor/address :address/street2)] " "
|
||||
[:div
|
||||
(-> vendor :vendor/address :address/city) " "
|
||||
(-> vendor :vendor/address :address/state) ","
|
||||
(-> vendor :vendor/address :address/zip)]]
|
||||
[:p.text-sm.italic.text-gray-400 "No address"]))}
|
||||
{:key "paid"
|
||||
:name "Paid"
|
||||
:sort-key "paid"
|
||||
:render (fn [[_ _ paid]]
|
||||
(com/pill {:class "text-xs font-medium"
|
||||
:color :primary}
|
||||
"Paid $" (Math/round paid)))}]}))
|
||||
|
||||
(def table* (partial helper/table* grid-page))
|
||||
(def row* (partial helper/row* grid-page))
|
||||
@@ -185,7 +181,6 @@
|
||||
{:keys [vendor-id]} :route-params
|
||||
{:keys [client-id]} :query-params}]
|
||||
|
||||
|
||||
(assert-can-see-client identity client-id)
|
||||
|
||||
@(dc/transact conn [[:upsert-entity (-> form-params
|
||||
@@ -198,30 +193,28 @@
|
||||
(:address/zip a)
|
||||
(:db/id a))
|
||||
a
|
||||
nil)) ))]])
|
||||
(html-response
|
||||
nil))))]])
|
||||
(html-response
|
||||
|
||||
(row* identity [(dc/pull (dc/db conn) [:db/id :client/code] client-id)
|
||||
(dc/pull (dc/db conn) vendor-read vendor-id)
|
||||
(sum-for-client-vendor client-id vendor-id)
|
||||
] {:flash? true})
|
||||
:headers {"hx-trigger" "modalclose"
|
||||
"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" vendor-id)}))
|
||||
(row* identity [(dc/pull (dc/db conn) [:db/id :client/code] client-id)
|
||||
(dc/pull (dc/db conn) vendor-read vendor-id)
|
||||
(sum-for-client-vendor client-id vendor-id)] {:flash? true})
|
||||
:headers {"hx-trigger" "modalclose"
|
||||
"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" vendor-id)}))
|
||||
|
||||
(def default-vendor-read '[* {[:vendor/legal-entity-1099-type :xform iol-ion.query/ident] [:db/ident]
|
||||
[:vendor/legal-entity-tin-type :xform iol-ion.query/ident] [:db/ident]}])
|
||||
|
||||
|
||||
(def form-schema (mc/schema [:map
|
||||
[:vendor/address {:default {}}
|
||||
[:maybe
|
||||
[:map
|
||||
[:map
|
||||
[:db/id {:optional true} [:maybe entity-id]]
|
||||
[:address/street1 {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:address/street2 {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:address/city {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:address/state {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:address/zip {:optional true} [:maybe [:re { :error/message "invalid zip"
|
||||
[:address/zip {:optional true} [:maybe [:re {:error/message "invalid zip"
|
||||
:decode/string strip} #"^(\d{5}|)$"]]]]]]
|
||||
[:vendor/legal-entity-name {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:vendor/legal-entity-first-name {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
@@ -237,131 +230,131 @@
|
||||
(when entity
|
||||
(mc/decode form-schema entity main-transformer))
|
||||
{})
|
||||
form-errors
|
||||
(modal-response
|
||||
(com/modal
|
||||
{}
|
||||
[:form {:hx-post (url (bidi/path-for ssr-routes/only-routes
|
||||
:company-1099-vendor-save
|
||||
:request-method :post
|
||||
:vendor-id vendor-id)
|
||||
{:client-id client-id})
|
||||
:class "w-full h-full max-w-2xl"
|
||||
:hx-swap "outerHTML swap:300ms"}
|
||||
form-errors
|
||||
(modal-response
|
||||
(com/modal
|
||||
{}
|
||||
[:form {:hx-post (url (bidi/path-for ssr-routes/only-routes
|
||||
:company-1099-vendor-save
|
||||
:request-method :post
|
||||
:vendor-id vendor-id)
|
||||
{:client-id client-id})
|
||||
:class "w-full h-full max-w-2xl"
|
||||
:hx-swap "outerHTML swap:300ms"}
|
||||
|
||||
[:fieldset {:class "hx-disable w-full h-full"}
|
||||
(com/modal-card
|
||||
{}
|
||||
[:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name entity)]]
|
||||
[:div.grid.grid-cols-6.gap-x-4.gap-y-2
|
||||
|
||||
(fc/with-field-default :vendor/address {}
|
||||
(println "ADDRESS" fc/*current*)
|
||||
(list [:h4.text-xl.border-b.col-span-6 "Address"]
|
||||
[:div.col-span-6
|
||||
(fc/with-field :db/id
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value (fc/field-value)}))
|
||||
[:fieldset {:class "hx-disable w-full h-full"}
|
||||
(com/modal-card
|
||||
{}
|
||||
[:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name entity)]]
|
||||
[:div.grid.grid-cols-6.gap-x-4.gap-y-2
|
||||
|
||||
(fc/with-field :address/street1
|
||||
(com/validated-field {:label "Street 1"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-full"
|
||||
:value (fc/field-value)
|
||||
:placeholder "1700 Pennsylvania Ave"
|
||||
:autofocus true})))]
|
||||
[:div.col-span-6
|
||||
(fc/with-field :address/street2
|
||||
(com/validated-field {:label "Street 2"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-full"
|
||||
:value (fc/field-value)
|
||||
:placeholder "Suite 200"})))]
|
||||
[:div.col-span-3
|
||||
(fc/with-field :address/city
|
||||
(com/validated-field {:label "City"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-full"
|
||||
:value (fc/field-value)
|
||||
:placeholder "Cupertino"})))]
|
||||
[:div.col-span-1
|
||||
(fc/with-field :address/state
|
||||
(com/validated-field {:label "State"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-full"
|
||||
:value (fc/field-value)
|
||||
:placeholder "CA"})))]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :address/zip
|
||||
(com/validated-field {:label "Zip"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-full"
|
||||
:value (fc/field-value)
|
||||
:placeholder "98102"})))]))
|
||||
|
||||
[:h4.text-xl.border-b.col-span-6 "Legal Entity"]
|
||||
[:div.col-span-6
|
||||
(fc/with-field :vendor/legal-entity-name
|
||||
(com/validated-field {:label "Legal Entity Name"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-full"
|
||||
:value (fc/field-value)
|
||||
:placeholder "Good Restaurant LLC"})))]
|
||||
[:div.col-span-6.text-center " - OR -"]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :vendor/legal-entity-first-name
|
||||
(com/validated-field {:label "First Name"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:placeholder "John"})))]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :vendor/legal-entity-middle-name
|
||||
(com/validated-field {:label "Middle Name"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:placeholder "C."})))]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :vendor/legal-entity-last-name
|
||||
(com/validated-field {:label "Last Name"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:placeholder "Riley"})))]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :vendor/legal-entity-tin
|
||||
(com/validated-field {:label "TIN"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:placeholder "John"})))]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :vendor/legal-entity-tin-type
|
||||
(com/validated-field {:label "TIN Type"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:allow-blank? true
|
||||
:value (some-> (fc/field-value) name)
|
||||
:options [["ein" "EIN"]
|
||||
["ssn" "SSN"]]})))]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :vendor/legal-entity-1099-type
|
||||
(com/validated-field {:label "1099 Type"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:allow-blank? true
|
||||
:value (some-> (fc/field-value) name)
|
||||
:options (ref->select-options "legal-entity-1099-type")})))]]
|
||||
[:div
|
||||
(com/form-errors {:errors (:errors fc/*form-errors*)})
|
||||
(com/validated-save-button {:errors form-errors} "Save vendor")])]]))))
|
||||
(fc/with-field-default :vendor/address {}
|
||||
(println "ADDRESS" fc/*current*)
|
||||
(list [:h4.text-xl.border-b.col-span-6 "Address"]
|
||||
[:div.col-span-6
|
||||
(fc/with-field :db/id
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value (fc/field-value)}))
|
||||
|
||||
(fc/with-field :address/street1
|
||||
(com/validated-field {:label "Street 1"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-full"
|
||||
:value (fc/field-value)
|
||||
:placeholder "1700 Pennsylvania Ave"
|
||||
:autofocus true})))]
|
||||
[:div.col-span-6
|
||||
(fc/with-field :address/street2
|
||||
(com/validated-field {:label "Street 2"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-full"
|
||||
:value (fc/field-value)
|
||||
:placeholder "Suite 200"})))]
|
||||
[:div.col-span-3
|
||||
(fc/with-field :address/city
|
||||
(com/validated-field {:label "City"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-full"
|
||||
:value (fc/field-value)
|
||||
:placeholder "Cupertino"})))]
|
||||
[:div.col-span-1
|
||||
(fc/with-field :address/state
|
||||
(com/validated-field {:label "State"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-full"
|
||||
:value (fc/field-value)
|
||||
:placeholder "CA"})))]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :address/zip
|
||||
(com/validated-field {:label "Zip"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-full"
|
||||
:value (fc/field-value)
|
||||
:placeholder "98102"})))]))
|
||||
|
||||
[:h4.text-xl.border-b.col-span-6 "Legal Entity"]
|
||||
[:div.col-span-6
|
||||
(fc/with-field :vendor/legal-entity-name
|
||||
(com/validated-field {:label "Legal Entity Name"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:class "w-full"
|
||||
:value (fc/field-value)
|
||||
:placeholder "Good Restaurant LLC"})))]
|
||||
[:div.col-span-6.text-center " - OR -"]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :vendor/legal-entity-first-name
|
||||
(com/validated-field {:label "First Name"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:placeholder "John"})))]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :vendor/legal-entity-middle-name
|
||||
(com/validated-field {:label "Middle Name"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:placeholder "C."})))]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :vendor/legal-entity-last-name
|
||||
(com/validated-field {:label "Last Name"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:placeholder "Riley"})))]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :vendor/legal-entity-tin
|
||||
(com/validated-field {:label "TIN"
|
||||
:errors (fc/field-errors)}
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:placeholder "John"})))]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :vendor/legal-entity-tin-type
|
||||
(com/validated-field {:label "TIN Type"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:allow-blank? true
|
||||
:value (some-> (fc/field-value) name)
|
||||
:options [["ein" "EIN"]
|
||||
["ssn" "SSN"]]})))]
|
||||
[:div.col-span-2
|
||||
(fc/with-field :vendor/legal-entity-1099-type
|
||||
(com/validated-field {:label "1099 Type"
|
||||
:errors (fc/field-errors)}
|
||||
(com/select {:name (fc/field-name)
|
||||
:allow-blank? true
|
||||
:value (some-> (fc/field-value) name)
|
||||
:options (ref->select-options "legal-entity-1099-type")})))]]
|
||||
[:div
|
||||
(com/form-errors {:errors (:errors fc/*form-errors*)})
|
||||
(com/validated-save-button {:errors form-errors} "Save vendor")])]]))))
|
||||
|
||||
(def vendor-table (helper/table-route grid-page))
|
||||
(def page (helper/page-route grid-page))
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
[hiccup2.core :as hiccup]
|
||||
[malli.core :as mc]))
|
||||
(def query-schema (mc/schema
|
||||
[:maybe
|
||||
(into [:map {} ]
|
||||
default-grid-fields-schema)]))
|
||||
[:maybe
|
||||
(into [:map {}]
|
||||
default-grid-fields-schema)]))
|
||||
|
||||
(def default-read '[:db/id
|
||||
:plaid-item/external-id
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
{:plaid-item/accounts [:db/id
|
||||
{:bank-account/_plaid-account [{:bank-account/integration-status
|
||||
[{ [ :integration-status/state :xform iol-ion.query/ident] [:db/ident]}
|
||||
[{[:integration-status/state :xform iol-ion.query/ident] [:db/ident]}
|
||||
:integration-status/message
|
||||
:integration-status/last-attempt
|
||||
:integration-status/last-updated]}]}
|
||||
@@ -66,7 +66,6 @@
|
||||
true (apply-sort-3 query-params)
|
||||
true (apply-pagination query-params))))
|
||||
|
||||
|
||||
(defn hydrate-results [ids db _]
|
||||
(let [results (pull-many-by-id db default-read ids)]
|
||||
(->> ids
|
||||
@@ -78,15 +77,12 @@
|
||||
[(hydrate-results ids-to-retrieve db request)
|
||||
matching-count]))
|
||||
|
||||
|
||||
|
||||
(defn plaid-link-script [token]
|
||||
(format "window.plaid = Plaid.create(
|
||||
{ token: \"%s\",
|
||||
onSuccess: function (x) { htmx.trigger(\"#link-account\", \"linked\", {\"public_token\": x})}
|
||||
})", token))
|
||||
|
||||
|
||||
(defn link [{{client-code "client_code" public-token "public_token"} :form-params
|
||||
:keys [identity]
|
||||
:as request}]
|
||||
@@ -99,24 +95,24 @@
|
||||
(alog/info ::linking-plaid :id identity :client-code client-code)
|
||||
(assert-can-see-client identity (pull-attr (dc/db conn) :db/id [:client/code client-code]))
|
||||
(let [access-token (:access_token (p/exchange-public-token public-token client-code))
|
||||
account-result (p/get-accounts access-token )
|
||||
account-result (p/get-accounts access-token)
|
||||
item {:plaid-item/client [:client/code client-code]
|
||||
:plaid-item/external-id (-> account-result :item :item_id )
|
||||
:plaid-item/external-id (-> account-result :item :item_id)
|
||||
:plaid-item/access-token access-token
|
||||
:plaid-item/status (or (some-> account-result :item :error)
|
||||
"SUCCESS")
|
||||
"SUCCESS")
|
||||
:plaid-item/last-updated (coerce/to-date (time/now))
|
||||
:db/id "plaid-item"}]
|
||||
|
||||
@(dc/transact conn (->> (:accounts account-result)
|
||||
(map (fn [a]
|
||||
(let [balance (some-> a :balances :current (* 0.01))]
|
||||
(cond-> {:plaid-account/external-id (:account_id a)
|
||||
:plaid-account/number (:mask a)
|
||||
:plaid-account/name (str (:name a) " " (:mask a))
|
||||
:plaid-item/_accounts "plaid-item"}
|
||||
balance (assoc :plaid-account/balance balance)))))
|
||||
(into [item])))
|
||||
(map (fn [a]
|
||||
(let [balance (some-> a :balances :current (* 0.01))]
|
||||
(cond-> {:plaid-account/external-id (:account_id a)
|
||||
:plaid-account/number (:mask a)
|
||||
:plaid-account/name (str (:name a) " " (:mask a))
|
||||
:plaid-item/_accounts "plaid-item"}
|
||||
balance (assoc :plaid-account/balance balance)))))
|
||||
(into [item])))
|
||||
(alog/info ::access-token-was :token access-token)
|
||||
{:headers {"Hx-redirect" (bidi/path-for ssr-routes/only-routes
|
||||
:company-plaid)}}))
|
||||
@@ -141,115 +137,110 @@
|
||||
(com/button-icon {} svg/refresh)
|
||||
"Start relink")])))
|
||||
|
||||
(def grid-page
|
||||
(helper/build
|
||||
{:id "plaid-table"
|
||||
:nav com/company-aside-nav
|
||||
:fetch-page fetch-page
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company)}
|
||||
"My Company"]
|
||||
|
||||
(def grid-page
|
||||
(helper/build
|
||||
{:id "plaid-table"
|
||||
:nav com/company-aside-nav
|
||||
:fetch-page fetch-page
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company)}
|
||||
"My Company"]
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company-plaid)}
|
||||
"Plaid"]]
|
||||
:title "Plaid Accounts"
|
||||
:entity-name "Plaid accounts"
|
||||
:query-schema query-schema
|
||||
:route :company-plaid-table
|
||||
:action-buttons (fn [request]
|
||||
(when-let [client-code (:client/code (:client request))]
|
||||
[[:div {:hx-post (str (bidi/path-for ssr-routes/only-routes
|
||||
:company-plaid-link
|
||||
:request-method :post))
|
||||
:hx-vals (hiccup/raw (format "js:{client_code: \"%s\", public_token: event.detail.public_token}", client-code))
|
||||
:hx-trigger "linked"}
|
||||
[:script (hiccup/raw (plaid-link-script (p/get-link-token client-code)))]
|
||||
(com/button {:color :primary
|
||||
:id "link-account"
|
||||
:onClick "window.plaid.open()"}
|
||||
(com/button-icon {} svg/refresh)
|
||||
(format "Link %s account" client-code))]]))
|
||||
:row-buttons (fn [request e]
|
||||
[[:div (com/button {:hx-put (str (bidi/path-for ssr-routes/only-routes
|
||||
:company-plaid-relink)
|
||||
"?plaid-item-id=" (:db/id e))
|
||||
:color :primary
|
||||
:hx-target "closest div"}
|
||||
"Reauthenticate")]])
|
||||
:headers [{:key "plaid-item"
|
||||
:name "Plaid Item"
|
||||
:sort-key "external-id"
|
||||
:render :plaid-item/external-id}
|
||||
{:key "integreat-plaid-status"
|
||||
:name "Integreat ↔ Plaid status"
|
||||
:render (fn [e]
|
||||
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company-plaid)}
|
||||
"Plaid"]]
|
||||
:title "Plaid Accounts"
|
||||
:entity-name "Plaid accounts"
|
||||
:query-schema query-schema
|
||||
:route :company-plaid-table
|
||||
:action-buttons (fn [request]
|
||||
(when-let [client-code (:client/code (:client request))]
|
||||
[[:div {:hx-post (str (bidi/path-for ssr-routes/only-routes
|
||||
:company-plaid-link
|
||||
:request-method :post))
|
||||
:hx-vals (hiccup/raw (format "js:{client_code: \"%s\", public_token: event.detail.public_token}", client-code))
|
||||
:hx-trigger "linked"}
|
||||
[:script (hiccup/raw (plaid-link-script (p/get-link-token client-code)))]
|
||||
(com/button {:color :primary
|
||||
:id "link-account"
|
||||
:onClick "window.plaid.open()"}
|
||||
(com/button-icon {} svg/refresh)
|
||||
(format "Link %s account" client-code))]]))
|
||||
:row-buttons (fn [request e]
|
||||
[[:div (com/button {:hx-put (str (bidi/path-for ssr-routes/only-routes
|
||||
:company-plaid-relink)
|
||||
"?plaid-item-id=" (:db/id e))
|
||||
:color :primary
|
||||
:hx-target "closest div"}
|
||||
"Reauthenticate")]])
|
||||
:headers [{:key "plaid-item"
|
||||
:name "Plaid Item"
|
||||
:sort-key "external-id"
|
||||
:render :plaid-item/external-id}
|
||||
{:key "integreat-plaid-status"
|
||||
:name "Integreat ↔ Plaid status"
|
||||
:render (fn [e]
|
||||
|
||||
(let [bad-integration (->> (:plaid-item/accounts e)
|
||||
(map (comp
|
||||
first
|
||||
:bank-account/_plaid-account))
|
||||
(filter (comp #{:integration-state/failed :integration-state/unauthorized}
|
||||
:integration-status/state
|
||||
:bank-account/integration-status))
|
||||
first
|
||||
:bank-account/integration-status)]
|
||||
[:div
|
||||
|
||||
[:div.cursor-pointer (com/pill (cond-> {:color :primary}
|
||||
bad-integration (assoc :color :red
|
||||
:x-tooltip "{content: ()=>$refs.tooltip.innerHTML, theme: 'light', allowHTML: true}"))
|
||||
(let [bad-integration (->> (:plaid-item/accounts e)
|
||||
(map (comp
|
||||
first
|
||||
:bank-account/_plaid-account))
|
||||
(filter (comp #{:integration-state/failed :integration-state/unauthorized}
|
||||
:integration-status/state
|
||||
:bank-account/integration-status))
|
||||
first
|
||||
:bank-account/integration-status)]
|
||||
[:div
|
||||
|
||||
[:div.inline-flex.gap-2
|
||||
(or
|
||||
(some-> bad-integration
|
||||
:integration-status/state
|
||||
name
|
||||
str/capitalize)
|
||||
"Success")
|
||||
(when bad-integration
|
||||
" (detail)")
|
||||
|
||||
|
||||
(when bad-integration
|
||||
[:template {:x-ref "tooltip"}
|
||||
[:div.text-red-700
|
||||
(:integration-status/message bad-integration)]])])]
|
||||
[:div.grid.grid-cols-2.gap-1.auto-cols-min.grid-flow-row.shrink
|
||||
[:div "Attempted: "] [:div (atime/unparse-local (coerce/to-date-time (:integration-status/last-attempt e)) atime/normal-date)]
|
||||
[:div "Last Updated: "] [:div (atime/unparse-local (coerce/to-date-time (:integration-status/last-updated e)) atime/normal-date)]]]))}
|
||||
{:key "plaid-bank-status"
|
||||
:name "Plaid ↔ Bank Status"
|
||||
:sort-key "plaid-bank-status"
|
||||
:render (fn [e]
|
||||
(when-let [status (:plaid-item/status e)]
|
||||
[:div [:div (com/pill {:color :primary}
|
||||
status)]
|
||||
[:div (atime/unparse-local (coerce/to-date-time (:plaid-item/last-updated e)) atime/normal-date)]]))}
|
||||
|
||||
{:key "accounts"
|
||||
:name "Accounts"
|
||||
:show-starting "md"
|
||||
:render (fn [e]
|
||||
[:ul
|
||||
(for [a (:plaid-item/accounts e)]
|
||||
[:li [:svg.inline {:data-jdenticon-value (:db/id a) :width "24" :height "24"}] (:plaid-account/name a) " - " (:plaid-account/number a) " - updated "
|
||||
(atime/unparse-local (:plaid-account/last-synced a) atime/normal-date)])])}]}))
|
||||
[:div.cursor-pointer (com/pill (cond-> {:color :primary}
|
||||
bad-integration (assoc :color :red
|
||||
:x-tooltip "{content: ()=>$refs.tooltip.innerHTML, theme: 'light', allowHTML: true}"))
|
||||
|
||||
[:div.inline-flex.gap-2
|
||||
(or
|
||||
(some-> bad-integration
|
||||
:integration-status/state
|
||||
name
|
||||
str/capitalize)
|
||||
"Success")
|
||||
(when bad-integration
|
||||
" (detail)")
|
||||
|
||||
(when bad-integration
|
||||
[:template {:x-ref "tooltip"}
|
||||
[:div.text-red-700
|
||||
(:integration-status/message bad-integration)]])])]
|
||||
[:div.grid.grid-cols-2.gap-1.auto-cols-min.grid-flow-row.shrink
|
||||
[:div "Attempted: "] [:div (atime/unparse-local (coerce/to-date-time (:integration-status/last-attempt e)) atime/normal-date)]
|
||||
[:div "Last Updated: "] [:div (atime/unparse-local (coerce/to-date-time (:integration-status/last-updated e)) atime/normal-date)]]]))}
|
||||
{:key "plaid-bank-status"
|
||||
:name "Plaid ↔ Bank Status"
|
||||
:sort-key "plaid-bank-status"
|
||||
:render (fn [e]
|
||||
(when-let [status (:plaid-item/status e)]
|
||||
[:div [:div (com/pill {:color :primary}
|
||||
status)]
|
||||
[:div (atime/unparse-local (coerce/to-date-time (:plaid-item/last-updated e)) atime/normal-date)]]))}
|
||||
|
||||
{:key "accounts"
|
||||
:name "Accounts"
|
||||
:show-starting "md"
|
||||
:render (fn [e]
|
||||
[:ul
|
||||
(for [a (:plaid-item/accounts e)]
|
||||
[:li [:svg.inline {:data-jdenticon-value (:db/id a) :width "24" :height "24"}] (:plaid-account/name a) " - " (:plaid-account/number a) " - updated "
|
||||
(atime/unparse-local (:plaid-account/last-synced a) atime/normal-date)])])}]}))
|
||||
|
||||
(def page (helper/page-route grid-page))
|
||||
(def table (helper/table-route grid-page))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{
|
||||
:company-plaid page
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{:company-plaid page
|
||||
:company-plaid-table table
|
||||
:company-plaid-link link
|
||||
:company-plaid-relink relink
|
||||
|
||||
}
|
||||
:company-plaid-relink relink}
|
||||
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
|
||||
@@ -27,13 +27,12 @@
|
||||
(def query-schema (mc/schema
|
||||
[:maybe
|
||||
(into [:map {:date-range [:date-range :start-date :end-date]}
|
||||
|
||||
|
||||
[:start-date {:optional true}
|
||||
[:maybe clj-date-schema]]
|
||||
[:end-date {:optional true}
|
||||
[:maybe clj-date-schema]]
|
||||
[:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]]
|
||||
]
|
||||
[:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]]]
|
||||
default-grid-fields-schema)]))
|
||||
(def default-read '[:db/id :report/client [:report/created :xform clj-time.coerce/from-date] :report/url :report/name :report/creator])
|
||||
|
||||
@@ -43,22 +42,20 @@
|
||||
query (cond-> {:query {:find []
|
||||
:in '[$ [?c ...]]
|
||||
:where '[[?e :report/client ?c]]}
|
||||
:args [db (:trimmed-clients request)]}
|
||||
:args [db (:trimmed-clients request)]}
|
||||
|
||||
|
||||
(:sort query-params) (add-sorter-fields {"client" ['[?e :report/client ?c]
|
||||
'[?c :client/name ?sort-client]]
|
||||
"created" ['[?e :report/created ?sort-created]]
|
||||
"creator" ['[?e :report/creator ?sort-creator]]
|
||||
"name" ['[?e :report/name ?sort-name]
|
||||
]}
|
||||
query-params)
|
||||
'[?c :client/name ?sort-client]]
|
||||
"created" ['[?e :report/created ?sort-created]]
|
||||
"creator" ['[?e :report/creator ?sort-creator]]
|
||||
"name" ['[?e :report/name ?sort-name]]}
|
||||
query-params)
|
||||
|
||||
true
|
||||
(merge-query {:query {:find ['?sort-default '?e] :where ['[?e :report/created ?sort-default]]}}))]
|
||||
(->> (query2 query)
|
||||
(apply-sort-3 (update query-params :sort conj {:sort-key "default-2" :asc true}))
|
||||
(apply-pagination query-params))))
|
||||
(apply-sort-3 (update query-params :sort conj {:sort-key "default-2" :asc true}))
|
||||
(apply-pagination query-params))))
|
||||
|
||||
(defn hydrate-results [ids db request]
|
||||
(let [results (->> (pull-many db default-read ids)
|
||||
@@ -67,7 +64,7 @@
|
||||
(->> ids
|
||||
(map results)
|
||||
(filter identity)
|
||||
|
||||
|
||||
(map first)
|
||||
(filter (fn [r]
|
||||
(let [used-clients (set (map :db/id (:report/client r)))]
|
||||
@@ -78,7 +75,7 @@
|
||||
(defn fetch-page [args]
|
||||
(let [db (dc/db conn)
|
||||
{ids-to-retrieve :ids matching-count :count} (fetch-ids db args)]
|
||||
|
||||
|
||||
[(->> (hydrate-results ids-to-retrieve db args))
|
||||
matching-count]))
|
||||
|
||||
@@ -115,7 +112,7 @@
|
||||
:sort-key "creator"
|
||||
:render (fn [report]
|
||||
(when (:report/creator report)
|
||||
(com/pill {:color :primary }
|
||||
(com/pill {:color :primary}
|
||||
(:report/creator report))))}
|
||||
{:key "created"
|
||||
:name "Created"
|
||||
@@ -129,7 +126,7 @@
|
||||
(def page (helper/page-route grid-page))
|
||||
|
||||
(defn delete-report [{:keys [form-params identity]}]
|
||||
|
||||
|
||||
(let [[id-to-delete key] (first (dc/q '[:find ?i ?k
|
||||
:in $ ?i
|
||||
:where [?i :report/key ?k]]
|
||||
@@ -137,29 +134,28 @@
|
||||
(some-> (get form-params "id") not-empty Long/parseLong)))
|
||||
report (dc/pull (dc/db conn) default-read id-to-delete)]
|
||||
(assert-can-see-client identity (:report/client report))
|
||||
(when id-to-delete
|
||||
(when id-to-delete
|
||||
(s3/delete-object :bucket-name (:data-bucket env)
|
||||
:key key)
|
||||
@(dc/transact conn [[:db/retractEntity id-to-delete]]))
|
||||
(html-response
|
||||
(row* identity
|
||||
report
|
||||
{:flash? true
|
||||
:delete-after-settle? true}))))
|
||||
|
||||
(html-response
|
||||
(row* identity
|
||||
report
|
||||
{:flash? true
|
||||
:delete-after-settle? true}))))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(->>
|
||||
(into
|
||||
{:company-reports page
|
||||
:company-reports-table table
|
||||
:company-reports-delete delete-report}
|
||||
company-expense-report/key->handler)
|
||||
(into
|
||||
{:company-reports page
|
||||
:company-reports-table table
|
||||
:company-reports-delete delete-report}
|
||||
company-expense-report/key->handler)
|
||||
(into company-reconciliation-report/key->handler))
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(ns auto-ap.ssr.company.reports.expense
|
||||
(ns auto-ap.ssr.company.reports.expense
|
||||
(:require [auto-ap.datomic :refer [conn merge-query]]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.logging :as alog]
|
||||
@@ -20,73 +20,71 @@
|
||||
[hiccup2.core :as hiccup]))
|
||||
|
||||
(defn lookup-breakdown-data [request]
|
||||
(let [query (cond-> {:query '{:find [?cn ?user-date (sum ?amt)]
|
||||
:with [?e]
|
||||
:in [$ [?clients ?start ?end]]
|
||||
:where
|
||||
[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]
|
||||
(not [?e :invoice/status :invoice-status/voided])
|
||||
[?e :invoice/date ?d]
|
||||
[?e :invoice/client ?c]
|
||||
[?e :invoice/expense-accounts ?iea]
|
||||
[?iea :invoice-expense-account/amount ?amt]
|
||||
[?c :client/name ?cn]
|
||||
[(clj-time.coerce/to-date-time ?d) ?user-date]]}
|
||||
:args
|
||||
[(dc/db conn)
|
||||
[(extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
(some-> (time/plus (time/now) (time/days -65)) coerce/to-date)
|
||||
(some-> (time/now) coerce/to-date)]]}
|
||||
(let [query (cond-> {:query '{:find [?cn ?user-date (sum ?amt)]
|
||||
:with [?e]
|
||||
:in [$ [?clients ?start ?end]]
|
||||
:where
|
||||
[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]
|
||||
(not [?e :invoice/status :invoice-status/voided])
|
||||
[?e :invoice/date ?d]
|
||||
[?e :invoice/client ?c]
|
||||
[?e :invoice/expense-accounts ?iea]
|
||||
[?iea :invoice-expense-account/amount ?amt]
|
||||
[?c :client/name ?cn]
|
||||
[(clj-time.coerce/to-date-time ?d) ?user-date]]}
|
||||
:args
|
||||
[(dc/db conn)
|
||||
[(extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
(some-> (time/plus (time/now) (time/days -65)) coerce/to-date)
|
||||
(some-> (time/now) coerce/to-date)]]}
|
||||
|
||||
(:vendor-id (:query-params request))
|
||||
(merge-query {:query '{:in [?v]
|
||||
:where [ [?e :invoice/vendor ?v]]}
|
||||
:args [ (:db/id (:vendor-id (:query-params request)))]})
|
||||
|
||||
(:account-id (:query-params request))
|
||||
(merge-query {:query '{:in [?a]
|
||||
:where [ [?iea :invoice-expense-account/account ?a]]}
|
||||
:args [ (:db/id (:account-id (:query-params request)))]}))]
|
||||
|
||||
(dc/query query)))
|
||||
(:vendor-id (:query-params request))
|
||||
(merge-query {:query '{:in [?v]
|
||||
:where [[?e :invoice/vendor ?v]]}
|
||||
:args [(:db/id (:vendor-id (:query-params request)))]})
|
||||
|
||||
(:account-id (:query-params request))
|
||||
(merge-query {:query '{:in [?a]
|
||||
:where [[?iea :invoice-expense-account/account ?a]]}
|
||||
:args [(:db/id (:account-id (:query-params request)))]}))]
|
||||
|
||||
(dc/query query)))
|
||||
|
||||
(defn lookup-invoice-total-data [request]
|
||||
(let [start (:start-date (:query-params request) (time/plus (time/now) (time/days -30)))
|
||||
end (:end-date (:query-params request) (time/now))
|
||||
query (cond-> {:query '{:find [?cn ?vn (sum ?t)]
|
||||
:with [ ?e]
|
||||
:in [$ [?clients ?start ?end]]
|
||||
:where
|
||||
[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]
|
||||
(not [?e :invoice/status :invoice-status/voided])
|
||||
[?e :invoice/client ?c]
|
||||
[?e :invoice/total ?t]
|
||||
[?e :invoice/vendor ?v]
|
||||
[?v :vendor/name ?vn]
|
||||
[?c :client/name ?cn]
|
||||
]}
|
||||
:args
|
||||
[(dc/db conn)
|
||||
[(extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
(some-> start coerce/to-date)
|
||||
(some-> end coerce/to-date)]]})]
|
||||
|
||||
(dc/query query)))
|
||||
(let [start (:start-date (:query-params request) (time/plus (time/now) (time/days -30)))
|
||||
end (:end-date (:query-params request) (time/now))
|
||||
query (cond-> {:query '{:find [?cn ?vn (sum ?t)]
|
||||
:with [?e]
|
||||
:in [$ [?clients ?start ?end]]
|
||||
:where
|
||||
[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]
|
||||
(not [?e :invoice/status :invoice-status/voided])
|
||||
[?e :invoice/client ?c]
|
||||
[?e :invoice/total ?t]
|
||||
[?e :invoice/vendor ?v]
|
||||
[?v :vendor/name ?vn]
|
||||
[?c :client/name ?cn]]}
|
||||
:args
|
||||
[(dc/db conn)
|
||||
[(extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
(some-> start coerce/to-date)
|
||||
(some-> end coerce/to-date)]]})]
|
||||
|
||||
(defn week-seq
|
||||
(dc/query query)))
|
||||
|
||||
(defn week-seq
|
||||
([c] (week-seq c (atime/last-monday)))
|
||||
([c starting] (reverse (for [n (range c)
|
||||
:let [start (time/minus starting (time/weeks n))
|
||||
end (time/minus starting (time/weeks (dec n)))]]
|
||||
[(atime/as-local-time (coerce/to-date-time start)) (atime/as-local-time (coerce/to-date-time end))]))))
|
||||
|
||||
|
||||
(defn- best-week [d weeks]
|
||||
(reduce
|
||||
(fn [acc [start end]]
|
||||
@@ -97,11 +95,10 @@
|
||||
nil
|
||||
weeks))
|
||||
|
||||
|
||||
(defn expense-breakdown-card* [request]
|
||||
(com/card {:class "w-full h-full" :id "expense-breakdown-report"}
|
||||
[:div {:class "flex flex-col px-8 py-8 space-y-3 w-full h-full"}
|
||||
|
||||
|
||||
[:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-breakdown-card)
|
||||
:hx-trigger "change"
|
||||
:hx-target "#expense-breakdown-report"
|
||||
@@ -157,14 +154,14 @@
|
||||
(for [d weeks]
|
||||
(get-in lookup [ea d] 0)))]
|
||||
[:canvas {:x-data (hx/json {:chart nil
|
||||
:labels x-axis
|
||||
:datasets (map (fn [s a] {:label a
|
||||
:data s
|
||||
:borderWidth 1})
|
||||
series
|
||||
distinct-accounts)})
|
||||
:x-init
|
||||
"new Chart($el, {
|
||||
:labels x-axis
|
||||
:datasets (map (fn [s a] {:label a
|
||||
:data s
|
||||
:borderWidth 1})
|
||||
series
|
||||
distinct-accounts)})
|
||||
:x-init
|
||||
"new Chart($el, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
@@ -186,7 +183,7 @@
|
||||
[:div {:class "flex flex-col px-8 py-8 space-y-3"}
|
||||
[:div
|
||||
[:h1.text-2xl.mb-3.font-bold "Invoice totals by vendor"]
|
||||
[:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-invoice-total-card )
|
||||
[:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-invoice-total-card)
|
||||
:hx-trigger "change"
|
||||
:hx-target "#invoice-totals-report"
|
||||
:hx-swap "outerHTML"}
|
||||
@@ -201,7 +198,7 @@
|
||||
(com/date-input {:name (fc/field-name)
|
||||
:class "w-64"
|
||||
:value (some-> (fc/field-value)
|
||||
(atime/unparse-local atime/normal-date)) })]))
|
||||
(atime/unparse-local atime/normal-date))})]))
|
||||
(fc/with-field :end-date
|
||||
(com/validated-field {:label "End"
|
||||
:errors (fc/field-errors)}
|
||||
@@ -209,13 +206,12 @@
|
||||
(com/date-input {:name (fc/field-name)
|
||||
:class "w-64"
|
||||
:value (some-> (fc/field-value)
|
||||
(atime/unparse-local atime/normal-date)) })]))])]
|
||||
(atime/unparse-local atime/normal-date))})]))])]
|
||||
[:div {:class "overflow-scroll min-w-full max-h-[700px]"}
|
||||
(let [data (lookup-invoice-total-data request)
|
||||
companies (sort (set (map first data)))
|
||||
vendors (sort (set (map second data)))
|
||||
result (by (juxt first second) last data)
|
||||
]
|
||||
result (by (juxt first second) last data)]
|
||||
(com/data-grid
|
||||
{:headers (into
|
||||
[(com/data-grid-header {:class "sticky left-0 z-60 bg-gray-100"} "Vendor")]
|
||||
@@ -231,7 +227,7 @@
|
||||
(com/data-grid-cell
|
||||
{}
|
||||
(or (some->> (get result [company vendor])
|
||||
(format "$%,.2f" ))
|
||||
(format "$%,.2f"))
|
||||
[:span.text-gray-200 "-"])))))))]]]))
|
||||
|
||||
(defn page [request]
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
(com/data-grid-cell {:class class}
|
||||
(when (> (count (:missing-transactions row)) 0)
|
||||
[:div
|
||||
(com/button { :x-tooltip.on.click "{content: ()=>$refs.tooltip.innerHTML, theme: 'light', allowHTML: true}" }
|
||||
(com/button {:x-tooltip.on.click "{content: ()=>$refs.tooltip.innerHTML, theme: 'light', allowHTML: true}"}
|
||||
[:div.flex.gap-2.items-center
|
||||
(count (:missing-transactions row))
|
||||
[:div.w-4.h-4 svg/question]])
|
||||
@@ -67,13 +67,12 @@
|
||||
(com/data-grid-cell {}
|
||||
(format "$%,.2f" (:transaction/amount r))))))]]))))))])
|
||||
|
||||
|
||||
(defn reconciliation-card* [{:keys [request report]}]
|
||||
(com/content-card {:class "w-full" :id "reconciliation-report"}
|
||||
[:div {:class "flex flex-col px-8 py-8 space-y-3"}
|
||||
[:div
|
||||
[:h1.text-2xl.mb-3.font-bold "Bank Reconciliation Report"]
|
||||
|
||||
|
||||
[:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-reconciliation-report-card)
|
||||
:hx-target "#reconciliation-report"
|
||||
:hx-swap "outerHTML"}
|
||||
@@ -88,7 +87,7 @@
|
||||
(com/date-input {:name (fc/field-name)
|
||||
:class "w-64"
|
||||
:value (some-> (fc/field-value)
|
||||
(atime/unparse-local atime/normal-date)) })]))
|
||||
(atime/unparse-local atime/normal-date))})]))
|
||||
(fc/with-field :end-date
|
||||
(com/validated-field {:label "End"
|
||||
:errors (fc/field-errors)}
|
||||
@@ -96,12 +95,11 @@
|
||||
(com/date-input {:name (fc/field-name)
|
||||
:class "w-64"
|
||||
:value (some-> (fc/field-value)
|
||||
(atime/unparse-local atime/normal-date)) })]))
|
||||
(atime/unparse-local atime/normal-date))})]))
|
||||
(com/button {:color :primary :class "self-center w-24"} "Run")])]
|
||||
(if report
|
||||
(if report
|
||||
(report* {:request request :report report})
|
||||
[:div "Please choose a time range to run the report"])
|
||||
]]))
|
||||
[:div "Please choose a time range to run the report"])]]))
|
||||
|
||||
(defn page [request]
|
||||
(base-page
|
||||
@@ -134,7 +132,7 @@
|
||||
url/map->query))
|
||||
|
||||
(defn get-report-data [start-date end-date client-ids]
|
||||
(let [client-codes (map first (dc/q '[:find ?cc :in $ [?c ...] :where [?c :client/code ?cc]] (dc/db conn ) client-ids))]
|
||||
(let [client-codes (map first (dc/q '[:find ?cc :in $ [?c ...] :where [?c :client/code ?cc]] (dc/db conn) client-ids))]
|
||||
(for [[ib ba c] (seq (apply get-intuit-bank-accounts (dc/db conn) client-codes))
|
||||
:let [raw-transactions (get-transactions (atime/unparse-local start-date atime/iso-date)
|
||||
(atime/unparse-local end-date atime/iso-date)
|
||||
@@ -169,11 +167,11 @@
|
||||
:requires-feedback-count (:transaction-approval-status/requires-feedback found-transactions 0)
|
||||
:missing-transactions missing-transactions})))
|
||||
|
||||
(defn card [{ {:keys [start-date end-date]} :query-params :as request}]
|
||||
(defn card [{{:keys [start-date end-date]} :query-params :as request}]
|
||||
(let [client-ids (extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
(:client-id request)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
report (get-report-data start-date end-date client-ids)]
|
||||
(html-response
|
||||
(reconciliation-card* {:request request
|
||||
@@ -182,7 +180,7 @@
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{:company-reconciliation-report page
|
||||
{:company-reconciliation-report page
|
||||
:company-reconciliation-report-card card}
|
||||
(fn [h]
|
||||
(-> h
|
||||
@@ -191,4 +189,4 @@
|
||||
[:start-date {:optional true}
|
||||
[:maybe clj-date-schema]]
|
||||
[:end-date {:optional true}
|
||||
[:maybe clj-date-schema]] ])))))
|
||||
[:maybe clj-date-schema]]])))))
|
||||
@@ -33,36 +33,34 @@
|
||||
:yodlee-provider-account/client [:client/code]}])
|
||||
|
||||
(def query-schema (mc/schema
|
||||
[:maybe
|
||||
(into [:map {}
|
||||
[:client-id {:optional true} [:maybe entity-id]] ]
|
||||
default-grid-fields-schema)]))
|
||||
[:maybe
|
||||
(into [:map {}
|
||||
[:client-id {:optional true} [:maybe entity-id]]]
|
||||
default-grid-fields-schema)]))
|
||||
|
||||
(defn fetch-ids [db request]
|
||||
(let [query-params (:query-params request)
|
||||
query (cond-> {:query {:find []
|
||||
:in ['$ '[?xx ...]]
|
||||
:where ['[?e :yodlee-provider-account/id]
|
||||
'[?e :yodlee-provider-account/client ?xx]]}
|
||||
:args [db (:trimmed-clients request)]}
|
||||
:in ['$ '[?xx ...]]
|
||||
:where ['[?e :yodlee-provider-account/id]
|
||||
'[?e :yodlee-provider-account/client ?xx]]}
|
||||
:args [db (:trimmed-clients request)]}
|
||||
|
||||
|
||||
(:sort query-params) (add-sorter-fields {"status" ['[?e :yodlee-provider-account/status ?sort-status]]
|
||||
"client" ['[?e :yodlee-provider-account/client ?c]
|
||||
'[?c :client/code ?sort-client]]
|
||||
"provider-account" ['[?e :yodlee-provider-account/id ?sort-provider-account]]
|
||||
"last-updated" ['[?e :yodlee-provider-account/last-updated ?sort-last-updated]]}
|
||||
query-params)
|
||||
true
|
||||
(merge-query {:query {:find ['?e ]
|
||||
:where ['[?e :yodlee-provider-account/id]]}}))]
|
||||
(:sort query-params) (add-sorter-fields {"status" ['[?e :yodlee-provider-account/status ?sort-status]]
|
||||
"client" ['[?e :yodlee-provider-account/client ?c]
|
||||
'[?c :client/code ?sort-client]]
|
||||
"provider-account" ['[?e :yodlee-provider-account/id ?sort-provider-account]]
|
||||
"last-updated" ['[?e :yodlee-provider-account/last-updated ?sort-last-updated]]}
|
||||
query-params)
|
||||
true
|
||||
(merge-query {:query {:find ['?e]
|
||||
:where ['[?e :yodlee-provider-account/id]]}}))]
|
||||
(->> query
|
||||
|
||||
(query2)
|
||||
(apply-sort-3 query-params)
|
||||
(apply-pagination query-params))))
|
||||
|
||||
|
||||
(defn hydrate-results [ids db _]
|
||||
(let [results (->> (pull-many db default-read ids)
|
||||
(group-by :db/id))]
|
||||
@@ -70,26 +68,24 @@
|
||||
(map results)
|
||||
(map first))))
|
||||
|
||||
|
||||
(defn fetch-page [request]
|
||||
(let [db (dc/db conn)
|
||||
{ids-to-retrieve :ids matching-count :count} (fetch-ids db request)]
|
||||
[(->> (hydrate-results ids-to-retrieve db request))
|
||||
matching-count]))
|
||||
|
||||
|
||||
(defn fastlink-dialog [{:keys [client]}]
|
||||
(modal-response
|
||||
(com/modal
|
||||
{}
|
||||
(com/modal-card
|
||||
{}
|
||||
[:div.flex [:div.p-2 "Yodlee Fastlink"] ]
|
||||
[:div
|
||||
[:div#fa-spot]
|
||||
[:script {:lang "text/javascript"}
|
||||
(hiccup/raw
|
||||
(format "
|
||||
(com/modal
|
||||
{}
|
||||
(com/modal-card
|
||||
{}
|
||||
[:div.flex [:div.p-2 "Yodlee Fastlink"]]
|
||||
[:div
|
||||
[:div#fa-spot]
|
||||
[:script {:lang "text/javascript"}
|
||||
(hiccup/raw
|
||||
(format "
|
||||
fastlink.open({fastLinkURL: '%s',
|
||||
accessToken: '%s',
|
||||
params: {'configName': 'Aggregation'},
|
||||
@@ -100,25 +96,24 @@ fastlink.open({fastLinkURL: '%s',
|
||||
}},
|
||||
'fa-spot');
|
||||
|
||||
" (:yodlee2-fastlink env) (yodlee/get-access-token (:client/code client))))]
|
||||
]
|
||||
[:div]))))
|
||||
" (:yodlee2-fastlink env) (yodlee/get-access-token (:client/code client))))]]
|
||||
[:div]))))
|
||||
|
||||
(defn reauthenticate [{:keys [form-params identity]}]
|
||||
(assert-can-see-client identity (-> (dc/pull (dc/db conn) '[{:yodlee-provider-account/client [:db/id]}] (Long/parseLong (get form-params "id")))
|
||||
:yodlee-provider-account/client
|
||||
:db/id))
|
||||
(html-response
|
||||
(com/modal
|
||||
{}
|
||||
(com/modal-card
|
||||
{}
|
||||
[:div.flex [:div.p-2 "Yodlee Fastlink"] ]
|
||||
[:div
|
||||
[:div#fa-spot]
|
||||
[:script {:lang "text/javascript"}
|
||||
(hiccup/raw
|
||||
(format "
|
||||
(com/modal
|
||||
{}
|
||||
(com/modal-card
|
||||
{}
|
||||
[:div.flex [:div.p-2 "Yodlee Fastlink"]]
|
||||
[:div
|
||||
[:div#fa-spot]
|
||||
[:script {:lang "text/javascript"}
|
||||
(hiccup/raw
|
||||
(format "
|
||||
fastlink.open({fastLinkURL: '%s',
|
||||
accessToken: '%s',
|
||||
params: {'configName': 'Aggregation',
|
||||
@@ -127,94 +122,93 @@ fastlink.open({fastLinkURL: '%s',
|
||||
'fa-spot');
|
||||
|
||||
"
|
||||
(:yodlee2-fastlink env)
|
||||
(yodlee/get-access-token (-> (dc/pull (dc/db conn)
|
||||
[{:yodlee-provider-account/client [:client/code]}]
|
||||
(Long/parseLong (get form-params "id")))
|
||||
:yodlee-provider-account/client
|
||||
:client/code))
|
||||
(pull-attr (dc/db conn) :yodlee-provider-account/id (Long/parseLong (get form-params "id")))))]]
|
||||
[:div]))))
|
||||
(:yodlee2-fastlink env)
|
||||
(yodlee/get-access-token (-> (dc/pull (dc/db conn)
|
||||
[{:yodlee-provider-account/client [:client/code]}]
|
||||
(Long/parseLong (get form-params "id")))
|
||||
:yodlee-provider-account/client
|
||||
:client/code))
|
||||
(pull-attr (dc/db conn) :yodlee-provider-account/id (Long/parseLong (get form-params "id")))))]]
|
||||
[:div]))))
|
||||
|
||||
(def grid-page
|
||||
(helper/build
|
||||
{:id "yodlee-table"
|
||||
:nav com/company-aside-nav
|
||||
:id-fn :db/id
|
||||
:fetch-page fetch-page
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company)}
|
||||
"My Company"]
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company-yodlee)}
|
||||
"Yodlee"]]
|
||||
:title "Yodlee Accounts"
|
||||
:entity-name "Yodlee accounts"
|
||||
:query-schema query-schema
|
||||
:route :company-yodlee-table
|
||||
:action-buttons (fn [request]
|
||||
[[:div.flex.flex-col.flex-shrink
|
||||
[:div.flex-shrink
|
||||
(com/button {:color :primary
|
||||
:on-click "openFastlink()"
|
||||
:disabled (if (:client request)
|
||||
false
|
||||
true)
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:company-yodlee-fastlink-dialog)
|
||||
:hx-target "#modal-holder"}
|
||||
(com/button-icon {} svg/refresh)
|
||||
"Link new account")]
|
||||
(when-not (:client request)
|
||||
[:div.text-xs "Note: please select a specific customer to link a new account."])]])
|
||||
:row-buttons (fn [request _]
|
||||
[
|
||||
(com/button {:hx-put (bidi/path-for ssr-routes/only-routes
|
||||
:company-yodlee-provider-account-reauthenticate)
|
||||
:color :primary
|
||||
:hx-target "#modal-holder"}
|
||||
"Reauthenticate")
|
||||
(when (is-admin? (:identity request))
|
||||
(com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes
|
||||
:company-yodlee-provider-account-refresh)
|
||||
:hx-target "closest tr"}
|
||||
svg/refresh))])
|
||||
:headers [{:key "client"
|
||||
:name "Client"
|
||||
:sort-key "client"
|
||||
:hide? (fn [args]
|
||||
(= (count (:clients args)) 1))
|
||||
:render #(-> % :yodlee-provider-account/client :client/code)}
|
||||
{:key "provider-account"
|
||||
:name "Provider Account"
|
||||
:sort-key "provider-account"
|
||||
:render :yodlee-provider-account/id}
|
||||
{:key "status"
|
||||
:name "Status"
|
||||
:sort-key "status"
|
||||
:render #(when-let [status (:yodlee-provider-account/status %)]
|
||||
(com/pill {:color (if (not= status "SUCCESS")
|
||||
:yellow
|
||||
:primary) }
|
||||
status))}
|
||||
{:key "detailed-status"
|
||||
:name "Detailed Status"
|
||||
:sort-key "detailed-status"
|
||||
:render #(when-let [status (:yodlee-provider-account/detailed-status %)]
|
||||
status)}
|
||||
(helper/build
|
||||
{:id "yodlee-table"
|
||||
:nav com/company-aside-nav
|
||||
:id-fn :db/id
|
||||
:fetch-page fetch-page
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company)}
|
||||
"My Company"]
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company-yodlee)}
|
||||
"Yodlee"]]
|
||||
:title "Yodlee Accounts"
|
||||
:entity-name "Yodlee accounts"
|
||||
:query-schema query-schema
|
||||
:route :company-yodlee-table
|
||||
:action-buttons (fn [request]
|
||||
[[:div.flex.flex-col.flex-shrink
|
||||
[:div.flex-shrink
|
||||
(com/button {:color :primary
|
||||
:on-click "openFastlink()"
|
||||
:disabled (if (:client request)
|
||||
false
|
||||
true)
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:company-yodlee-fastlink-dialog)
|
||||
:hx-target "#modal-holder"}
|
||||
(com/button-icon {} svg/refresh)
|
||||
"Link new account")]
|
||||
(when-not (:client request)
|
||||
[:div.text-xs "Note: please select a specific customer to link a new account."])]])
|
||||
:row-buttons (fn [request _]
|
||||
[(com/button {:hx-put (bidi/path-for ssr-routes/only-routes
|
||||
:company-yodlee-provider-account-reauthenticate)
|
||||
:color :primary
|
||||
:hx-target "#modal-holder"}
|
||||
"Reauthenticate")
|
||||
(when (is-admin? (:identity request))
|
||||
(com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes
|
||||
:company-yodlee-provider-account-refresh)
|
||||
:hx-target "closest tr"}
|
||||
svg/refresh))])
|
||||
:headers [{:key "client"
|
||||
:name "Client"
|
||||
:sort-key "client"
|
||||
:hide? (fn [args]
|
||||
(= (count (:clients args)) 1))
|
||||
:render #(-> % :yodlee-provider-account/client :client/code)}
|
||||
{:key "provider-account"
|
||||
:name "Provider Account"
|
||||
:sort-key "provider-account"
|
||||
:render :yodlee-provider-account/id}
|
||||
{:key "status"
|
||||
:name "Status"
|
||||
:sort-key "status"
|
||||
:render #(when-let [status (:yodlee-provider-account/status %)]
|
||||
(com/pill {:color (if (not= status "SUCCESS")
|
||||
:yellow
|
||||
:primary)}
|
||||
status))}
|
||||
{:key "detailed-status"
|
||||
:name "Detailed Status"
|
||||
:sort-key "detailed-status"
|
||||
:render #(when-let [status (:yodlee-provider-account/detailed-status %)]
|
||||
status)}
|
||||
|
||||
{:key "last-updated"
|
||||
:name "Last Updated"
|
||||
:sort-key "last-updated"
|
||||
:render #(atime/unparse-local (:yodlee-provider-account/last-updated %)
|
||||
atime/normal-date)}
|
||||
{:key "accounts"
|
||||
:name "Accounts"
|
||||
:show-starting "md"
|
||||
:render (fn [e]
|
||||
[:ul
|
||||
(for [a (:yodlee-provider-account/accounts e)]
|
||||
[:li (:yodlee-account/name a) " - " (:yodlee-account/number a)])])}]}))
|
||||
{:key "last-updated"
|
||||
:name "Last Updated"
|
||||
:sort-key "last-updated"
|
||||
:render #(atime/unparse-local (:yodlee-provider-account/last-updated %)
|
||||
atime/normal-date)}
|
||||
{:key "accounts"
|
||||
:name "Accounts"
|
||||
:show-starting "md"
|
||||
:render (fn [e]
|
||||
[:ul
|
||||
(for [a (:yodlee-provider-account/accounts e)]
|
||||
[:li (:yodlee-account/name a) " - " (:yodlee-account/number a)])])}]}))
|
||||
|
||||
(def page (helper/page-route grid-page))
|
||||
(def table (helper/table-route grid-page))
|
||||
@@ -224,26 +218,23 @@ fastlink.open({fastLinkURL: '%s',
|
||||
(yodlee/refresh-provider-account (:client/code (:yodlee-provider-account/client provider-account))
|
||||
(:yodlee-provider-account/id provider-account))
|
||||
(html-response
|
||||
(helper/row*
|
||||
grid-page
|
||||
identity
|
||||
provider-account
|
||||
{:flash? true}))))
|
||||
(helper/row*
|
||||
grid-page
|
||||
identity
|
||||
provider-account
|
||||
{:flash? true}))))
|
||||
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{
|
||||
:company-yodlee page
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{:company-yodlee page
|
||||
:company-yodlee-table table
|
||||
:company-yodlee-fastlink-dialog fastlink-dialog
|
||||
}
|
||||
:company-yodlee-fastlink-dialog fastlink-dialog}
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)
|
||||
(wrap-client-redirect-unauthenticated)
|
||||
(wrap-secure)))))
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)
|
||||
(wrap-client-redirect-unauthenticated)
|
||||
(wrap-secure)))))
|
||||
@@ -15,7 +15,6 @@
|
||||
[hiccup2.core :as hiccup]
|
||||
[iol-ion.query :refer [can-see-client?]]))
|
||||
|
||||
|
||||
(defn dropdown-search-results* [{:keys [options]}]
|
||||
[:ul
|
||||
(for [{:keys [id name group]} options]
|
||||
@@ -44,9 +43,6 @@
|
||||
:hx-trigger "click"}
|
||||
name])]])])
|
||||
|
||||
|
||||
|
||||
|
||||
(defn get-clients [identity query]
|
||||
(let [raw-query (not-empty (strip-special query))
|
||||
cleansed-query (not-empty (cleanse-query query))
|
||||
@@ -89,11 +85,11 @@
|
||||
"localStorage.setItem(\"last-client-id\", \"" (:db/id client) "\")" "\n"
|
||||
"localStorage.setItem(\"last-selected-clients\", " (json/write-str (json/write-str client-selection))
|
||||
#_(cond (:group client-selection)
|
||||
(:group client-selection)
|
||||
(:selected client-selection)
|
||||
(:selected client-selection)
|
||||
:else
|
||||
client-selection) ")")]
|
||||
(:group client-selection)
|
||||
(:selected client-selection)
|
||||
(:selected client-selection)
|
||||
:else
|
||||
client-selection) ")")]
|
||||
[:div
|
||||
[:button#company-dropdown-button {:class "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||
"x-tooltip.on.click" "{content: ()=>$refs.tooltip.innerHTML, theme: 'light', onMount(i) { htmx.process(i.popper); }, allowHTML: true, interactive:true}"
|
||||
@@ -103,11 +99,11 @@
|
||||
"My Companies"
|
||||
(= :all client-selection)
|
||||
"All Companies"
|
||||
|
||||
|
||||
(and client
|
||||
(= 1 (count clients)))
|
||||
(:client/name client)
|
||||
|
||||
|
||||
:else
|
||||
(str (count clients) " Companies"))
|
||||
[:div.w-4.h-4.ml-2
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
":style" (format "selected == '%s' ? 'max-height: ' + $el.scrollHeight + 'px' : ''" (:selector params))))
|
||||
(for [c children]
|
||||
[:li
|
||||
(update-in c [1 1 :class ] (fn [c]
|
||||
(hh/add-class (or c "") " flex items-center p-2 pl-11 w-full text-base font-normal rounded-lg transition duration-75 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700")))])])
|
||||
(update-in c [1 1 :class] (fn [c]
|
||||
(hh/add-class (or c "") " flex items-center p-2 pl-11 w-full text-base font-normal rounded-lg transition duration-75 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700")))])])
|
||||
|
||||
(defn left-aside- [{:keys [nav page-specific]} & _]
|
||||
[:aside {:id "left-nav",
|
||||
@@ -83,7 +83,6 @@
|
||||
[:div {:class "overflow-y-auto py-5 px-3 h-full bg-gray-50 border-r border-gray-200 dark:bg-gray-800 dark:border-gray-700"}
|
||||
nav
|
||||
|
||||
|
||||
(when page-specific
|
||||
[:div {:class " pt-5 mt-5 space-y-2 border-t border-gray-200 dark:border-gray-700"}
|
||||
page-specific])]])
|
||||
@@ -94,7 +93,7 @@
|
||||
"invoices"
|
||||
|
||||
(#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts ::ss-routes/page} (:matched-route request))
|
||||
"sales"
|
||||
"sales"
|
||||
(#{::payment-routes/all-page ::payment-routes/pending-page ::payment-routes/cleared-page ::payment-routes/voided-page} (:matched-route request))
|
||||
"payments"
|
||||
(#{::transaction-routes/page ::transaction-routes/approved-page ::transaction-routes/unapproved-page ::transaction-routes/requires-feedback-page :transaction-insights} (:matched-route request))
|
||||
@@ -108,7 +107,7 @@
|
||||
|
||||
[:li
|
||||
(menu-button- {:icon svg/pie
|
||||
:href (bidi/path-for ssr-routes/only-routes
|
||||
:href (bidi/path-for ssr-routes/only-routes
|
||||
::dashboard/page)}
|
||||
"Dashboard")]
|
||||
|
||||
@@ -147,7 +146,6 @@
|
||||
:hx-boost "true"}
|
||||
"Voided")
|
||||
|
||||
|
||||
(when (can? (:identity request)
|
||||
{:subject :invoice
|
||||
:activity :import})
|
||||
@@ -156,7 +154,6 @@
|
||||
:active? (= ::invoice-route/import-page (:matched-route request))
|
||||
:hx-boost "true"} "Import"))
|
||||
|
||||
|
||||
#_(when (can? (:identity request)
|
||||
{:subject :invoice
|
||||
:activity :import})
|
||||
@@ -168,7 +165,6 @@
|
||||
"Glimpse"
|
||||
(tags/pill- {:color :secondary} "Beta")]))
|
||||
|
||||
|
||||
(when (can? (:identity request)
|
||||
{:subject :ar-invoice
|
||||
:activity :read})
|
||||
@@ -213,12 +209,12 @@
|
||||
:hx-boost "true"}
|
||||
|
||||
"Refunds")
|
||||
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
|
||||
:pos-cash-drawer-shifts)
|
||||
"?date-range=week")
|
||||
:active? (= :pos-cash-drawer-shifts (:matched-route request))
|
||||
:hx-boost "true"}
|
||||
"Cash drawer shifts")
|
||||
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
|
||||
:pos-cash-drawer-shifts)
|
||||
"?date-range=week")
|
||||
:active? (= :pos-cash-drawer-shifts (:matched-route request))
|
||||
:hx-boost "true"}
|
||||
"Cash drawer shifts")
|
||||
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
|
||||
::ss-routes/page)
|
||||
"?date-range=week")
|
||||
@@ -288,7 +284,6 @@
|
||||
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
|
||||
:transaction-insights)} "Insights")))]
|
||||
|
||||
|
||||
(when (can? (:identity request)
|
||||
{:subject :ledger-page})
|
||||
(list
|
||||
@@ -314,7 +309,7 @@
|
||||
[:div.flex.gap-2
|
||||
"External Register"
|
||||
(tags/pill- {:color :secondary} "WIP")]))
|
||||
|
||||
|
||||
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::ledger-routes/profit-and-loss))
|
||||
:active? (= ::ledger-routes/profit-and-loss (:matched-route request))
|
||||
@@ -322,7 +317,7 @@
|
||||
[:div.flex.gap-2
|
||||
"Profit and loss"
|
||||
(tags/pill- {:color :secondary} "WIP")])
|
||||
|
||||
|
||||
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::ledger-routes/cash-flows))
|
||||
:active? (= ::ledger-routes/cash-flows (:matched-route request))
|
||||
@@ -330,7 +325,7 @@
|
||||
[:div.flex.gap-2
|
||||
"Cash flows"
|
||||
(tags/pill- {:color :secondary} "WIP")])
|
||||
|
||||
|
||||
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::ledger-routes/balance-sheet))
|
||||
:active? (= ::ledger-routes/balance-sheet (:matched-route request))
|
||||
@@ -338,8 +333,7 @@
|
||||
[:div.flex.gap-2
|
||||
"Balance Sheet"
|
||||
(tags/pill- {:color :secondary} "WIP")])
|
||||
|
||||
|
||||
|
||||
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::ledger-routes/external-import-page)
|
||||
{:date-range "month"})
|
||||
@@ -349,7 +343,6 @@
|
||||
"External Import"
|
||||
(tags/pill- {:color :secondary} "WIP")]))))]))
|
||||
|
||||
|
||||
(defn company-aside-nav- [request]
|
||||
[:ul {:class "space-y-2" :hx-boost "true"}
|
||||
[:li
|
||||
@@ -465,7 +458,6 @@
|
||||
:hx-boost true}
|
||||
"Background Jobs")]
|
||||
|
||||
|
||||
(menu-button- {:icon svg/arrow-in
|
||||
"@click.prevent" "if (selected == 'import') {selected = null } else { selected = 'import'} "}
|
||||
"Import")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(ns auto-ap.ssr.components.bank-account-icon
|
||||
(ns auto-ap.ssr.components.bank-account-icon
|
||||
(:require [auto-ap.ssr.hiccup-helper :as hh]
|
||||
[auto-ap.ssr.svg :as svg]))
|
||||
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
[:a {:href "#", :class "inline-flex w-4 h-4 mr-2 items-center text-sm font-medium text-gray-700 hover:text-blue-600 dark:text-gray-400 dark:hover:text-white"}
|
||||
[:div.w-4.h-4 svg/home]]]
|
||||
(for [p steps]
|
||||
[:li
|
||||
[:li
|
||||
[:div {:class "flex items-center"}
|
||||
|
||||
[:div {:class "w-6 h-6 text-gray-400",}
|
||||
|
||||
[:div {:class "w-6 h-6 text-gray-400"}
|
||||
svg/breadcrumb-component]
|
||||
|
||||
|
||||
(update-in p [1 :class] str " ml-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ml-2 dark:text-gray-400 dark:hover:text-white")]])
|
||||
#_[:li {:aria-current "page"}
|
||||
[:div {:class "flex items-center"}
|
||||
[:svg {:aria-hidden "true", :class "w-6 h-6 text-gray-400", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
|
||||
[:path {:fill-rule "evenodd", :d "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", :clip-rule "evenodd"}]]
|
||||
[:span {:class "ml-1 text-sm font-medium text-gray-500 md:ml-2 dark:text-gray-400"} "Flowbite"]]]]])
|
||||
[:div {:class "flex items-center"}
|
||||
[:svg {:aria-hidden "true", :class "w-6 h-6 text-gray-400", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
|
||||
[:path {:fill-rule "evenodd", :d "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", :clip-rule "evenodd"}]]
|
||||
[:span {:class "ml-1 text-sm font-medium text-gray-500 md:ml-2 dark:text-gray-400"} "Flowbite"]]]]])
|
||||
|
||||
@@ -113,9 +113,9 @@
|
||||
(= :secondary (:color params)) (str " text-white bg-blue-500 hover:bg-blue-600 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700")
|
||||
(= :primary (:color params)) (str " text-white bg-green-500 hover:bg-green-600 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 ")
|
||||
(= :secondary-light (:color params)) (str " text-blue-800 bg-white-200 border-gray-100 border hover:bg-blue-100 focus:ring-blue-100 dark:bg-blue-400 dark:hover:bg-blue-800 ")
|
||||
|
||||
|
||||
(not (nil? (:color params)))
|
||||
(str " text-white " (bg-colors (:color params) (:disabled params)))
|
||||
(str " text-white " (bg-colors (:color params) (:disabled params)))
|
||||
|
||||
(nil? (:color params))
|
||||
(str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 font-medium border border-gray-300 dark:border-gray-700")))
|
||||
@@ -126,7 +126,7 @@
|
||||
(svg/spinner {:class "inline w-4 h-4 text-white"})
|
||||
[:div.ml-3 "Loading..."]])
|
||||
(into [:div.inline-flex.gap-2.items-center.justify-center {:class (when (:indicator? params true)
|
||||
"htmx-indicator-hidden")}]
|
||||
"htmx-indicator-hidden")}]
|
||||
children)])
|
||||
|
||||
(defn icon-button- [params & children]
|
||||
@@ -162,8 +162,6 @@
|
||||
[:div.ml-3 "Loading..."]]
|
||||
(into [:div.htmx-indicator-hidden] children)])
|
||||
|
||||
|
||||
|
||||
(defn group-button- [{:keys [size] :or {size :normal} :as params} & children]
|
||||
(into [:button (cond-> params
|
||||
true (assoc :type (or (:type params) "button"))
|
||||
@@ -191,7 +189,7 @@
|
||||
(defn navigation-button- [{:keys [class next-arrow?] :or {next-arrow? true} :as params} & children]
|
||||
[:button
|
||||
(-> params
|
||||
(update :class (fnil hh/add-class "")
|
||||
(update :class (fnil hh/add-class "")
|
||||
"p-4 text-green-700 border border-gray-300 rounded-lg bg-gray-50
|
||||
dark:bg-gray-800 dark:border-green-800 dark:text-green-400
|
||||
focus:ring-green-400 focus:ring-2
|
||||
@@ -211,7 +209,7 @@
|
||||
{:class "space-y-4 w-72"}
|
||||
(for [n children]
|
||||
[:li n])
|
||||
|
||||
|
||||
#_[:li
|
||||
[:div
|
||||
{:class
|
||||
@@ -231,7 +229,6 @@
|
||||
[:span {:class "sr-only"} "Confirmation"]
|
||||
[:h3 {:class "font-medium"} "5. Confirmation"]]]]])
|
||||
|
||||
|
||||
(defn validated-save-button- [{:keys [errors class] :as params} & children]
|
||||
(button- (-> {:color (or (:color params) :primary)
|
||||
:type "submit" :class (cond-> (or class "")
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[clojure.string :as str]))
|
||||
|
||||
(defn card- [params & children]
|
||||
(into [:div (update params :class
|
||||
(into [:div (update params :class
|
||||
#(cond-> (or % "")
|
||||
(not (str/includes? (or % "") "bg-")) (hh/add-class "dark:bg-gray-800 bg-white ")
|
||||
true (hh/add-class "shadow-md sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 overflow-hidden")))]
|
||||
@@ -13,6 +13,6 @@
|
||||
(defn content-card- [params & children]
|
||||
[:section (merge params {:class (hh/add-class " py-3 sm:py-5" (:class params))})
|
||||
[:div {:class (:max-w params "max-w-screen-2xl")}
|
||||
(into
|
||||
[:div {:class "relative overflow-scroll shadow-md dark:bg-gray-800 sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 bg-white"}]
|
||||
children)]])
|
||||
(into
|
||||
[:div {:class "relative overflow-scroll shadow-md dark:bg-gray-800 sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 bg-white"}]
|
||||
children)]])
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
"@click" (format "$dispatch('sorted', {key: '%s'})" (:sort-key params))
|
||||
:style (:style params)}]
|
||||
(if (:sort-key params)
|
||||
[(into [:a {:href "#"} ] rest)]
|
||||
[(into [:a {:href "#"}] rest)]
|
||||
rest)))
|
||||
|
||||
(defn sort-header- [params & rest]
|
||||
[:th.px-4.py-3 {:scope "col" :class (:class params)
|
||||
"@click" (format "$dispatch('sorted', {key: '%s'})" (:sort-key params)) }
|
||||
(into [:a {:href "#"} ] rest)])
|
||||
"@click" (format "$dispatch('sorted', {key: '%s'})" (:sort-key params))}
|
||||
(into [:a {:href "#"}] rest)])
|
||||
|
||||
(defn row- [params & rest]
|
||||
(into [:tr (update params
|
||||
@@ -31,11 +31,11 @@
|
||||
(defn cell- [params & rest]
|
||||
(into [:td.px-4.py-2 (update params
|
||||
:class #(str (-> ""
|
||||
(hh/add-class (or % ""))))) ]
|
||||
(hh/add-class (or % "")))))]
|
||||
rest))
|
||||
|
||||
(defn right-stack-cell- [params & rest]
|
||||
(cell- params (into [:div.flex.flex-row-reverse.items-center.justify-between
|
||||
(cell- params (into [:div.flex.flex-row-reverse.items-center.justify-between
|
||||
rest])))
|
||||
|
||||
(defn checkbox-header- [params & rest]
|
||||
@@ -46,17 +46,17 @@
|
||||
|
||||
(defn data-grid-
|
||||
[{:keys [headers thead-params id] :as params} & rest]
|
||||
[:div.shrink.overflow-y-scroll
|
||||
[:div.shrink.overflow-y-scroll
|
||||
[:table (merge {:class "w-full text-sm text-left text-gray-500 dark:text-gray-400 shrink"}
|
||||
(dissoc params :headers :thead-params))
|
||||
[:thead (update thead-params :class #(-> "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 group-[.raw]:sticky group-[.raw]:z-10 group-[.raw]:top-0"
|
||||
(hh/add-class (or % ""))))
|
||||
(into
|
||||
[:tr]
|
||||
headers)]
|
||||
(into
|
||||
[:tbody {}]
|
||||
rest)]])
|
||||
(into
|
||||
[:tr]
|
||||
headers)]
|
||||
(into
|
||||
[:tbody {}]
|
||||
rest)]])
|
||||
|
||||
;; needed for tailwind
|
||||
;; lg:table-cell md:table-cell
|
||||
@@ -81,35 +81,35 @@
|
||||
rows] :as params} & children]
|
||||
(let [card (if raw? raw-table-card content-card-)]
|
||||
(card
|
||||
(cond-> { :id id :class (cond-> "group" raw? (hh/add-class "raw h-full flex flex-col overflow-hidden")) }
|
||||
root-params (merge root-params)
|
||||
route (assoc
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
route
|
||||
:request-method :get)
|
||||
:hx-trigger "clientSelected from:body, invalidated from:body"
|
||||
:hx-swap "outerHTML swap:300ms"))
|
||||
[:div {:class " flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-baseline lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"}
|
||||
[:h1.text-2xl.mb-3.font-bold title]
|
||||
[:div {:class "flex items-center flex-1 space-x-4"}
|
||||
[:h5
|
||||
(when subtitle
|
||||
[:span subtitle])]]
|
||||
(into [:div {:class "group-[.raw]:hidden flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"}]
|
||||
action-buttons)]
|
||||
[:div {:class "overflow-x-auto contents"}
|
||||
(data-grid- {:headers headers
|
||||
:thead-params thead-params}
|
||||
rows)]
|
||||
(cond-> {:id id :class (cond-> "group" raw? (hh/add-class "raw h-full flex flex-col overflow-hidden"))}
|
||||
root-params (merge root-params)
|
||||
route (assoc
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
route
|
||||
:request-method :get)
|
||||
:hx-trigger "clientSelected from:body, invalidated from:body"
|
||||
:hx-swap "outerHTML swap:300ms"))
|
||||
[:div {:class " flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-baseline lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"}
|
||||
[:h1.text-2xl.mb-3.font-bold title]
|
||||
[:div {:class "flex items-center flex-1 space-x-4"}
|
||||
[:h5
|
||||
(when subtitle
|
||||
[:span subtitle])]]
|
||||
(into [:div {:class "group-[.raw]:hidden flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"}]
|
||||
action-buttons)]
|
||||
[:div {:class "overflow-x-auto contents"}
|
||||
(data-grid- {:headers headers
|
||||
:thead-params thead-params}
|
||||
rows)]
|
||||
|
||||
(when (or paginate?
|
||||
(nil? paginate?))
|
||||
[:div {:class "contents group-[.raw]:block"}
|
||||
(paginator- {:start start
|
||||
:end (Math/min (+ start per-page) total)
|
||||
:per-page per-page
|
||||
:total total
|
||||
:a-params (fn [page]
|
||||
(when (or paginate?
|
||||
(nil? paginate?))
|
||||
[:div {:class "contents group-[.raw]:block"}
|
||||
(paginator- {:start start
|
||||
:end (Math/min (+ start per-page) total)
|
||||
:per-page per-page
|
||||
:total total
|
||||
:a-params (fn [page]
|
||||
;; TODO it might be good to have a more global form defined in the specific page
|
||||
;; with elements that are part of item
|
||||
;; that way this is not deeply coupled
|
||||
@@ -117,44 +117,43 @@
|
||||
;; TODO the other way to think about this is that we want the request to include
|
||||
;; all of the correct parameters, not parameters to merge with the current ones.
|
||||
;; think sorting, filters, pagination
|
||||
{:hx-get (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
route
|
||||
:request-method :get)
|
||||
{:start (* page per-page)})
|
||||
:hx-target (str "#" id)
|
||||
:hx-swap "outerHTML show:#app:top"
|
||||
:hx-indicator (str "#" id)})
|
||||
:per-page-params {:hx-get (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
{:hx-get (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
route
|
||||
:request-method :get))
|
||||
:hx-trigger "change"
|
||||
:hx-include "this"
|
||||
:hx-target (str "#" id) ;
|
||||
:request-method :get)
|
||||
{:start (* page per-page)})
|
||||
:hx-target (str "#" id)
|
||||
:hx-swap "outerHTML show:#app:top"
|
||||
:hx-indicator (str "#" id)}})])
|
||||
children
|
||||
[:div {:class "htmx-indicator absolute -translate-x-1/2 -translate-y-1/2 top-2/4 left-1/2 overflow-hidden w-full h-full"}
|
||||
[:div {:class "flex items-center justify-center w-full h-full border border-gray-200 rounded-lg bg-gray-50 dark:bg-gray-800 dark:border-gray-700 bg-opacity-50" }
|
||||
[:div {:class "px-3 py-1 text-xs font-medium leading-none text-center text-blue-800 bg-blue-200 rounded-full animate-pulse dark:bg-blue-900 dark:text-blue-200"} "loading..."]]])))
|
||||
:hx-indicator (str "#" id)})
|
||||
:per-page-params {:hx-get (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
route
|
||||
:request-method :get))
|
||||
:hx-trigger "change"
|
||||
:hx-include "this"
|
||||
:hx-target (str "#" id) ;
|
||||
:hx-swap "outerHTML show:#app:top"
|
||||
:hx-indicator (str "#" id)}})])
|
||||
children
|
||||
[:div {:class "htmx-indicator absolute -translate-x-1/2 -translate-y-1/2 top-2/4 left-1/2 overflow-hidden w-full h-full"}
|
||||
[:div {:class "flex items-center justify-center w-full h-full border border-gray-200 rounded-lg bg-gray-50 dark:bg-gray-800 dark:border-gray-700 bg-opacity-50"}
|
||||
[:div {:class "px-3 py-1 text-xs font-medium leading-none text-center text-blue-800 bg-blue-200 rounded-full animate-pulse dark:bg-blue-900 dark:text-blue-200"} "loading..."]]])))
|
||||
|
||||
(defn new-row- [{:keys [index colspan tr-params row-offset] :as params} & content]
|
||||
(row-
|
||||
(merge {:class "new-row"
|
||||
"x-on:htmx:after-settle.camel" "let options=$el.parentNode.querySelectorAll('tr'); let target=options[options.length-2]; $nextTick(() => $focus.within(target).first())"
|
||||
(merge {:class "new-row"
|
||||
"x-on:htmx:after-settle.camel" "let options=$el.parentNode.querySelectorAll('tr'); let target=options[options.length-2]; $nextTick(() => $focus.within(target).first())"
|
||||
|
||||
:x-data (hx/json {:newRowIndex index
|
||||
:offset (or row-offset 0)}) }
|
||||
tr-params)
|
||||
(cell- {:colspan colspan
|
||||
:class "bg-gray-100"}
|
||||
[:div.flex.justify-center
|
||||
(a-button- (merge
|
||||
(dissoc params :index :colspan)
|
||||
{"@click.prevent" "$dispatch('newRow', {index: (newRowIndex++)})"
|
||||
:color :secondary
|
||||
:hx-trigger "newRow"
|
||||
:hx-vals (hiccup/raw "js:{index: event.detail.index }")
|
||||
:hx-target "closest .new-row"
|
||||
:hx-swap "beforebegin"
|
||||
})
|
||||
content)])))
|
||||
:x-data (hx/json {:newRowIndex index
|
||||
:offset (or row-offset 0)})}
|
||||
tr-params)
|
||||
(cell- {:colspan colspan
|
||||
:class "bg-gray-100"}
|
||||
[:div.flex.justify-center
|
||||
(a-button- (merge
|
||||
(dissoc params :index :colspan)
|
||||
{"@click.prevent" "$dispatch('newRow', {index: (newRowIndex++)})"
|
||||
:color :secondary
|
||||
:hx-trigger "newRow"
|
||||
:hx-vals (hiccup/raw "js:{index: event.detail.index }")
|
||||
:hx-target "closest .new-row"
|
||||
:hx-swap "beforebegin"})
|
||||
content)])))
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]))
|
||||
|
||||
|
||||
(defn modal-
|
||||
"This modal function is used to create a modal window with a stack that allows for transitioning between modals.
|
||||
|
||||
@@ -26,16 +25,16 @@
|
||||
:class (fn [c] (-> c
|
||||
(or "")
|
||||
(hh/add-class "w-full p-4 modal-card flex max-h-[inherit]"))))
|
||||
[:div {:class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content w-full flex flex-col max-h-full overflow-hidden" }
|
||||
[:div {:class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content w-full flex flex-col max-h-full overflow-hidden"}
|
||||
[:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} header]
|
||||
[:div {:class "px-6 py-2 space-y-6 overflow-y-scroll w-full shrink"}
|
||||
|
||||
|
||||
content]
|
||||
(when footer [:div {:class "p-4"}
|
||||
[:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex (hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"})
|
||||
[:span {:class "w-2 h-2 mr-1 bg-red-500 rounded-full"}] [:span.px-2.py-0.5 "An unexpected error has occured. Integreat staff have been notified."]]
|
||||
(when (:error params )
|
||||
[:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex { :class "dark:bg-red-900 dark:text-red-300"}
|
||||
(when (:error params)
|
||||
[:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex {:class "dark:bg-red-900 dark:text-red-300"}
|
||||
[:span {:class "w-2 h-2 mr-1 bg-red-500 rounded-full"}]
|
||||
[:span.px-2.py-0.5 (:error params)]])
|
||||
[:div {:class "shrink-0"}
|
||||
@@ -45,7 +44,6 @@
|
||||
[:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"}
|
||||
children])
|
||||
|
||||
|
||||
(defn modal-header-attachment- [params & children]
|
||||
[:div {:class "flex items-start justify-between p-4 border-b shrink-0"}
|
||||
children])
|
||||
@@ -56,7 +54,7 @@
|
||||
|
||||
(defn modal-footer- [params & children]
|
||||
[:div {:class "p-4 border-t"}
|
||||
[:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex
|
||||
[:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex
|
||||
(hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"})
|
||||
(hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"})
|
||||
[:span {:class "w-2 h-2 bg-red-500 rounded-full"}]
|
||||
@@ -66,7 +64,7 @@
|
||||
|
||||
(defn modal-card-advanced- [params & children]
|
||||
[:div (merge params
|
||||
{:class (hh/add-class "modal-card bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col max-h-screen max-w-screen" (:class params "")) })
|
||||
{:class (hh/add-class "modal-card bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col max-h-screen max-w-screen" (:class params ""))})
|
||||
children])
|
||||
|
||||
(defn success-modal- [{:keys [title]} & children]
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
[clojure.string :as str]
|
||||
[hiccup2.core :as hiccup]))
|
||||
|
||||
|
||||
(def default-input-classes
|
||||
["bg-gray-50" "border" "text-sm" "rounded-lg" "" "block"
|
||||
"p-2.5" "border-gray-300" "text-gray-900" "focus:ring-blue-500" "focus:border-blue-500"
|
||||
@@ -149,7 +148,6 @@
|
||||
[: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")})
|
||||
@@ -178,7 +176,6 @@
|
||||
:x-show "value.size > 0"}
|
||||
svg/x]])
|
||||
|
||||
|
||||
(defn multi-typeahead- [params]
|
||||
[:div.relative {:x-data (hx/json {:baseUrl (str (:url params))
|
||||
:reset_elements (js-fn "function(e) {
|
||||
@@ -268,21 +265,17 @@
|
||||
:x-effect "if (value.warning) { $nextTick(()=> warning_badge.update()) }"}
|
||||
(tags/badge- {:class "peer"} "!")
|
||||
|
||||
|
||||
[:div {:x-show "value.warning"
|
||||
: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)])])
|
||||
|
||||
|
||||
(defn use-size [size]
|
||||
(if (= :small size)
|
||||
(str " " "text-xs p-2")
|
||||
(str " " "text-sm p-2.25")))
|
||||
|
||||
|
||||
|
||||
(defn text-input- [{:keys [size error?] :as params}]
|
||||
[:input
|
||||
(-> params
|
||||
@@ -415,8 +408,6 @@
|
||||
(update :class #(str % (use-size size) " w-full"))
|
||||
(dissoc :size :name :x-model :x-modelable))]]))
|
||||
|
||||
|
||||
|
||||
(defn field-errors- [{:keys [source key]} & rest]
|
||||
(let [errors (:errors (cond-> (meta source)
|
||||
key (get key)))]
|
||||
@@ -469,8 +460,6 @@
|
||||
(defn hidden- [{:keys [name value] :as params}]
|
||||
[:input (merge {:type "hidden" :value value :name name} params)])
|
||||
|
||||
|
||||
|
||||
(defn toggle- [params & children]
|
||||
[:label {:class "inline-flex items-center cursor-pointer"}
|
||||
[:input (merge {:type "checkbox", :class "sr-only peer"} params)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(ns auto-ap.ssr.components.link-dropdown
|
||||
(ns auto-ap.ssr.components.link-dropdown
|
||||
(:require [auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]))
|
||||
@@ -8,8 +8,7 @@
|
||||
[:div {:x-data (hx/json {})}
|
||||
|
||||
(com/a-icon-button {:class "relative"
|
||||
"@click.prevent" "$tooltip($refs.tooltip, {content: ()=>$refs.tooltip.innerHTML, theme: 'light', allowHTML: true, interactive:true, popperOptions: {strategy: 'fixed', modifiers: [{name: 'flip', options: {fallbackPlacements: ['top']}}]}})"
|
||||
}
|
||||
"@click.prevent" "$tooltip($refs.tooltip, {content: ()=>$refs.tooltip.innerHTML, theme: 'light', allowHTML: true, interactive:true, popperOptions: {strategy: 'fixed', modifiers: [{name: 'flip', options: {fallbackPlacements: ['top']}}]}})"}
|
||||
svg/paperclip
|
||||
(com/badge {:color "blue"} (count links)))
|
||||
[:template {:x-ref "tooltip"}
|
||||
|
||||
@@ -15,12 +15,11 @@
|
||||
[malli.core :as mc]
|
||||
[malli.core :as m]))
|
||||
|
||||
|
||||
(def default-form-props {:hx-ext "response-targets"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-target-400 "#form-errors .error-content"
|
||||
:hx-trigger "submit"
|
||||
:hx-target "this" })
|
||||
:hx-target "this"})
|
||||
|
||||
(defprotocol ModalWizardStep
|
||||
(step-key [this])
|
||||
@@ -57,7 +56,6 @@
|
||||
(or (get-in (:snapshot multi-form-state) edit-path)
|
||||
default)))
|
||||
|
||||
|
||||
(defn merge-multi-form-state [{:keys [snapshot edit-path step-params] :as multi-form-state}]
|
||||
(let [cursor (cursor/cursor (or snapshot {}))
|
||||
;; this hack makes sure that, in the event of a missing vector entry, will make sure to add it first
|
||||
@@ -87,8 +85,6 @@
|
||||
(fn encode-step-key [sk]
|
||||
(mc/encode step-key-schema sk main-transformer))))
|
||||
|
||||
|
||||
|
||||
(defn render-timeline [linear-wizard current-step validation-route]
|
||||
(let [step-names (map #(step-name (get-step linear-wizard %)) (steps linear-wizard))
|
||||
active-index (.indexOf step-names (step-name current-step))]
|
||||
@@ -148,9 +144,9 @@
|
||||
next-button-content]}]
|
||||
[:div.flex.justify-end
|
||||
[:div.flex.items-baseline.gap-x-4
|
||||
(let [step-errors (:step-params fc/*form-errors*)]
|
||||
(com/form-errors {:errors (or (:errors step-errors)
|
||||
(when (sequential? step-errors) step-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))
|
||||
(step-key step))
|
||||
(when validation-route
|
||||
@@ -172,7 +168,7 @@
|
||||
(com/modal-card-advanced
|
||||
{"@keydown.enter.prevent.stop" "if ($refs.next ) {$refs.next.click()}"
|
||||
:class (str
|
||||
(or width-height-class " md:w-[750px] md:h-[600px] ")
|
||||
(or width-height-class " md:w-[750px] md:h-[600px] ")
|
||||
" w-full h-full
|
||||
group-[.forward]/transition:htmx-swapping:opacity-0
|
||||
group-[.forward]/transition:htmx-swapping:-translate-x-1/4
|
||||
@@ -261,7 +257,7 @@
|
||||
:oob (or oob []))))
|
||||
|
||||
(def next-handler
|
||||
|
||||
|
||||
(-> (fn [{:keys [wizard] :as request}]
|
||||
(let [current-step (get-current-step wizard)]
|
||||
(if (satisfies? CustomNext current-step)
|
||||
@@ -361,8 +357,6 @@
|
||||
(render-wizard wizard request)])
|
||||
(get query-params :replace-modal) (assoc-in [:headers "hx-trigger"] "modalswap")))
|
||||
|
||||
|
||||
|
||||
(defn wrap-init-multi-form-state [handler get-multi-form-state]
|
||||
(->
|
||||
(fn init-multi-form [request]
|
||||
|
||||
@@ -6,16 +6,15 @@
|
||||
[config.core :refer [env]]
|
||||
[hiccup2.core :as hiccup]))
|
||||
|
||||
(defn page- [{:keys [nav page-specific client clients client-selection identity app-params request] :or {app-params {}} } & children]
|
||||
[:div#app { "@notification.document" "notificationDetails=event.detail.value; showNotification=true"
|
||||
(defn page- [{:keys [nav page-specific client clients client-selection identity app-params request] :or {app-params {}}} & children]
|
||||
[:div#app {"@notification.document" "notificationDetails=event.detail.value; showNotification=true"
|
||||
|
||||
:x-data (hx/json {:leftNavShow true
|
||||
:showError false
|
||||
:errorDetails ""
|
||||
:showNotification false
|
||||
:notificationDetails ""})
|
||||
"@htmx:response-error.camel" "errorDetails = $event.detail.xhr.response; showError=true;"
|
||||
}
|
||||
"@htmx:response-error.camel" "errorDetails = $event.detail.xhr.response; showError=true;"}
|
||||
(navbar- {:client-selection client-selection
|
||||
:clients clients
|
||||
:client client
|
||||
@@ -29,33 +28,32 @@
|
||||
:page-specific page-specific})
|
||||
[:div#main-content {:class "relative w-full h-full overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content lg:pl-64"
|
||||
":class" "leftNavShow ? 'lg:pl-64' : ''"
|
||||
:x-effect "leftNavShow ? $el.classList.add('lg:pl-64') : $el.classList.remove('lg:pl-64')"
|
||||
}
|
||||
:x-effect "leftNavShow ? $el.classList.add('lg:pl-64') : $el.classList.remove('lg:pl-64')"}
|
||||
[:div#notification-holder
|
||||
[:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg {:x-show "showNotification" }
|
||||
[:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg {:x-show "showNotification"}
|
||||
[:div.relative
|
||||
[:button.absolute.right-2.top-2.w-6.h-6.z-50.text-blue-400
|
||||
{ "@click" "showNotification=false"}
|
||||
{"@click" "showNotification=false"}
|
||||
svg/filled-x]]
|
||||
|
||||
|
||||
[:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-blue-800.bg-blue-50.dark:bg-gray-800.dark:text-blue-400.border-blue-300.rounded-lg.border.max-h-96
|
||||
{:x-show "showNotification"
|
||||
"x-transition:enter" "transition duration-300 transform ease-in-out"
|
||||
"x-transition:enter-start" "opacity-0 translate-y-full"
|
||||
"x-transition:enter-end" "opacity-100 translate-y-0"
|
||||
"x-transition:leave" "transition duration-300 transform ease-in-out"
|
||||
"x-transition:leave-start" "opacity-100 translate-y-0"
|
||||
"x-transition:leave-end" "opacity-0 translate-y-full"}
|
||||
|
||||
"x-transition:enter" "transition duration-300 transform ease-in-out"
|
||||
"x-transition:enter-start" "opacity-0 translate-y-full"
|
||||
"x-transition:enter-end" "opacity-100 translate-y-0"
|
||||
"x-transition:leave" "transition duration-300 transform ease-in-out"
|
||||
"x-transition:leave-start" "opacity-100 translate-y-0"
|
||||
"x-transition:leave-end" "opacity-0 translate-y-full"}
|
||||
|
||||
[:div {:class "p-4 text-lg w-full" :role "alert"}
|
||||
[:div.text-sm
|
||||
[:pre#notification-details.text-xs {:x-html "notificationDetails"}]]]]]]
|
||||
[:div {:x-show "showError"
|
||||
:x-init ""}
|
||||
[:div {:x-show "showError"
|
||||
:x-init ""}
|
||||
[:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg
|
||||
[:div.relative
|
||||
[:button.absolute.right-2.top-2.w-6.h-6.z-50.text-red-600
|
||||
{ "@click" "showError=false"}
|
||||
{"@click" "showError=false"}
|
||||
svg/filled-x]]
|
||||
|
||||
[:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-red-800.bg-red-50.dark:bg-gray-800.dark:text-red-400.border-red-300.rounded-lg.border.max-h-96
|
||||
@@ -63,7 +61,7 @@
|
||||
"x-transition:enter" "transition duration-300"
|
||||
"x-transition:enter-start" "opacity-0"
|
||||
"x-transition:enter-end" "opacity-100"}
|
||||
|
||||
|
||||
[:div {:class "p-4 mb-4 text-lg w-full" :role "alert"}
|
||||
[:div.inline-block.w-8.h-8.mr-2 svg/alert]
|
||||
[:span.font-medium "Oh, drat! An unexpected error has occurred."]
|
||||
@@ -73,6 +71,4 @@
|
||||
[:pre#error-details.text-xs {:x-show "expandError" :x-text "errorDetails"}]]]]]]
|
||||
(into
|
||||
[:div.p-4]
|
||||
children)]]
|
||||
|
||||
])
|
||||
children)]]])
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
x
|
||||
(> y z)
|
||||
z
|
||||
:else
|
||||
:else
|
||||
y))
|
||||
|
||||
(def elipsis-button
|
||||
@@ -24,42 +24,41 @@
|
||||
current-page (long (Math/floor (/ start per-page)))
|
||||
first-page-button (bound 0 (- current-page buttons-before) (- total-pages max-buttons))
|
||||
all-buttons (into [] (for [x (range total-pages)]
|
||||
[:li
|
||||
[:li
|
||||
[:a (-> (a-params x)
|
||||
(update
|
||||
:class #(cond-> %
|
||||
true (str " flex items-center justify-center px-3 py-2 text-sm leading-tight border ")
|
||||
(update
|
||||
:class #(cond-> %
|
||||
true (str " flex items-center justify-center px-3 py-2 text-sm leading-tight border ")
|
||||
|
||||
(= current-page x)
|
||||
(str " text-primary-600 bg-primary-50 border-primary-300 hover:bg-primary-100 hover:text-primary-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white")
|
||||
(= current-page x)
|
||||
(str " text-primary-600 bg-primary-50 border-primary-300 hover:bg-primary-100 hover:text-primary-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white")
|
||||
|
||||
(not= current-page x)
|
||||
(str " text-gray-500 bg-white border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white")))
|
||||
(not= current-page x)
|
||||
(str " text-gray-500 bg-white border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white")))
|
||||
(assoc :href "#"))
|
||||
[:div.htmx-indicator.flex.items-center
|
||||
(svg/spinner {:class "inline w-4 h-4 text-black"})]
|
||||
[:div.htmx-indicator-hidden
|
||||
[:div.htmx-indicator-hidden
|
||||
(inc x)]]]))
|
||||
|
||||
|
||||
last-page-button (Math/min (long total-pages) (long (+ max-buttons first-page-button)))
|
||||
|
||||
extended-last-page-button (when (not= last-page-button total-pages)
|
||||
(list
|
||||
elipsis-button
|
||||
(last all-buttons)))
|
||||
elipsis-button
|
||||
(last all-buttons)))
|
||||
|
||||
extended-first-page-button (when (not= first-page-button 0)
|
||||
(list
|
||||
(first all-buttons)
|
||||
elipsis-button))]
|
||||
(first all-buttons)
|
||||
elipsis-button))]
|
||||
[:nav.flex.items-center.space-x-3
|
||||
[:span.text-sm.text-gray-500 "Per page"]
|
||||
(inputs/select- (merge per-page-params
|
||||
{:options [[25 "25"]
|
||||
[50 "50"]
|
||||
[100 "100"]
|
||||
[200 "200"]]
|
||||
[50 "50"]
|
||||
[100 "100"]
|
||||
[200 "200"]]
|
||||
:value per-page
|
||||
:name "per-page"}))
|
||||
[:ul {:class "inline-flex items-stretch -space-x-px"}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(ns auto-ap.ssr.components.periods
|
||||
(ns auto-ap.ssr.components.periods
|
||||
(:require
|
||||
[auto-ap.ssr.components.buttons :as buttons]
|
||||
[auto-ap.ssr.components.inputs :as inputs]
|
||||
@@ -19,7 +19,7 @@
|
||||
(atime/unparse-local atime/normal-date))})
|
||||
|
||||
:x-effect "console.log('periods are', periods)"
|
||||
:x-init "$watch('periods', ds => source_date= ds.length > 0 ? ds[0].end : null)" }
|
||||
:x-init "$watch('periods', ds => source_date= ds.length > 0 ? ds[0].end : null)"}
|
||||
[:template {:x-for "(v,n) in periods" ":key" "n"}
|
||||
[:div
|
||||
[:input {:type "hidden"
|
||||
@@ -29,62 +29,61 @@
|
||||
":name" "'periods[' + n + '][end]'"
|
||||
:x-model "v.end"}]]]
|
||||
(buttons/a-button- {"x-tooltip.on.click.theme.dropdown.placement.bottom.interactive" "{content: ()=> $refs.tooltip.innerHTML, allowHTML: true, appendTo: $root}"
|
||||
:indicator? false}
|
||||
[:template {:x-if "periods.length == 0"}
|
||||
[:span.text-left.text-gray-400 "None selected"]]
|
||||
[:template {:x-if "periods.length < 3 && periods.length > 0"}
|
||||
[:span.inline-flex.gap-2
|
||||
[:template {:x-for "p in periods"}
|
||||
(tags/pill- {:color :secondary}
|
||||
[:span {:x-text "p.start"}]
|
||||
" - "
|
||||
[:span {:x-text "p.end"}])]]]
|
||||
[:template {:x-if "periods.length >= 3"}
|
||||
(tags/pill- {:color :secondary}
|
||||
[:span {:x-text "periods.length"}]
|
||||
" periods selected")]
|
||||
[:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"}
|
||||
svg/drop-down])
|
||||
:indicator? false}
|
||||
[:template {:x-if "periods.length == 0"}
|
||||
[:span.text-left.text-gray-400 "None selected"]]
|
||||
[:template {:x-if "periods.length < 3 && periods.length > 0"}
|
||||
[:span.inline-flex.gap-2
|
||||
[:template {:x-for "p in periods"}
|
||||
(tags/pill- {:color :secondary}
|
||||
[:span {:x-text "p.start"}]
|
||||
" - "
|
||||
[:span {:x-text "p.end"}])]]]
|
||||
[:template {:x-if "periods.length >= 3"}
|
||||
(tags/pill- {:color :secondary}
|
||||
[:span {:x-text "periods.length"}]
|
||||
" periods selected")]
|
||||
[:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"}
|
||||
svg/drop-down])
|
||||
[:template {:x-ref "tooltip"}
|
||||
[:div.p-4.gap-2 {:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 ring-1 p-4 w-[700px] "}
|
||||
[:div.flex.flex-col.gap-2
|
||||
(tabs/tabs-
|
||||
{:tabs [{:name "Quick"
|
||||
:content [:div.flex.flex.gap-2
|
||||
(inputs/calendar-input- {:placeholder "12/21/2020" :x-model "source_date"})
|
||||
[:div.flex.flex-col.gap-2
|
||||
(buttons/a-button- {"@click" "periods=getFourWeekPeriodsPeriods(source_date)"} [:span "13 periods, ending "
|
||||
[:span {:x-text "source_date"}]])
|
||||
(buttons/a-button- {"@click" "periods=[calendarYearPeriod(source_date)]"} [:span "Calendar year ("
|
||||
[:span {:x-text "parseMMDDYYYY(source_date).getFullYear()"}]
|
||||
")"])
|
||||
(buttons/a-button- {"@click" "periods=getTwelveCalendarMonthsPeriods(source_date)"} [:span "12 months, ending "
|
||||
[:span {:x-text "parseMMDDYYYY(source_date).toLocaleString('default', { month: 'long' })"}]])
|
||||
[:hr {:class "h-px my-1 bg-gray-200 border-0 dark:bg-gray-700"} ]
|
||||
(buttons/a-button- {"@click" "periods=getLastMonthPeriods()"} "Last Month")
|
||||
(buttons/a-button- {"@click" "periods=getMonthToDatePeriods()"} "Month to date")
|
||||
(buttons/a-button- {"@click" "periods=getYearToDatePeriods()"} "Year to date")
|
||||
(buttons/a-button- {"@click" "periods=[]"} "Clear")]]}
|
||||
{:name "Advanced"
|
||||
:content [:div.flex.gap-4 {:class "overflow-hidden max-h-[300px]"
|
||||
:x-data (hx/json {:calendarTarget "0"
|
||||
:calendarWhich "start"})
|
||||
"@change-date.camel" "$el.querySelectorAll('.text-inputs.' + calendarWhich)[calendarTarget].focus()"}
|
||||
(inputs/calendar-input- {:x-model "periods[calendarTarget][calendarWhich]"})
|
||||
[:div.flex.flex-col.gap-4.p-2
|
||||
[:div.overflow-y-scroll.flex.flex-col.gap-4
|
||||
[:template {:x-for "(p, i) in periods" ":key" "i"}
|
||||
[:div.flex.gap-4.
|
||||
(inputs/text-input- { :class "text-inputs start" :x-model "periods[i].start" "@focus" "calendarTarget =i; calendarWhich='start'" })
|
||||
(inputs/text-input- { :class "text-inputs end" :x-model "periods[i].end" "@focus" "calendarTarget =i; calendarWhich='end'"})
|
||||
(buttons/a-icon-button- {"@click.prevent.stop" "periods=periods.filter((_, i2) => i !== i2); calendarTarget=0"} svg/x)]
|
||||
#_(com/pill {:color :secondary}
|
||||
[:span {:x-text "p.start"}]
|
||||
" - "
|
||||
[:span {:x-text "p.end"}])]]
|
||||
(buttons/button- {"@click.prevent.stop" "periods.push({start: '', end: ''}); calendarTarget=0" :class "w-32"} "Add new period")]
|
||||
]}]
|
||||
:active "Quick"}) ]]]])
|
||||
{:tabs [{:name "Quick"
|
||||
:content [:div.flex.flex.gap-2
|
||||
(inputs/calendar-input- {:placeholder "12/21/2020" :x-model "source_date"})
|
||||
[:div.flex.flex-col.gap-2
|
||||
(buttons/a-button- {"@click" "periods=getFourWeekPeriodsPeriods(source_date)"} [:span "13 periods, ending "
|
||||
[:span {:x-text "source_date"}]])
|
||||
(buttons/a-button- {"@click" "periods=[calendarYearPeriod(source_date)]"} [:span "Calendar year ("
|
||||
[:span {:x-text "parseMMDDYYYY(source_date).getFullYear()"}]
|
||||
")"])
|
||||
(buttons/a-button- {"@click" "periods=getTwelveCalendarMonthsPeriods(source_date)"} [:span "12 months, ending "
|
||||
[:span {:x-text "parseMMDDYYYY(source_date).toLocaleString('default', { month: 'long' })"}]])
|
||||
[:hr {:class "h-px my-1 bg-gray-200 border-0 dark:bg-gray-700"}]
|
||||
(buttons/a-button- {"@click" "periods=getLastMonthPeriods()"} "Last Month")
|
||||
(buttons/a-button- {"@click" "periods=getMonthToDatePeriods()"} "Month to date")
|
||||
(buttons/a-button- {"@click" "periods=getYearToDatePeriods()"} "Year to date")
|
||||
(buttons/a-button- {"@click" "periods=[]"} "Clear")]]}
|
||||
{:name "Advanced"
|
||||
:content [:div.flex.gap-4 {:class "overflow-hidden max-h-[300px]"
|
||||
:x-data (hx/json {:calendarTarget "0"
|
||||
:calendarWhich "start"})
|
||||
"@change-date.camel" "$el.querySelectorAll('.text-inputs.' + calendarWhich)[calendarTarget].focus()"}
|
||||
(inputs/calendar-input- {:x-model "periods[calendarTarget][calendarWhich]"})
|
||||
[:div.flex.flex-col.gap-4.p-2
|
||||
[:div.overflow-y-scroll.flex.flex-col.gap-4
|
||||
[:template {:x-for "(p, i) in periods" ":key" "i"}
|
||||
[:div.flex.gap-4.
|
||||
(inputs/text-input- {:class "text-inputs start" :x-model "periods[i].start" "@focus" "calendarTarget =i; calendarWhich='start'"})
|
||||
(inputs/text-input- {:class "text-inputs end" :x-model "periods[i].end" "@focus" "calendarTarget =i; calendarWhich='end'"})
|
||||
(buttons/a-icon-button- {"@click.prevent.stop" "periods=periods.filter((_, i2) => i !== i2); calendarTarget=0"} svg/x)]
|
||||
#_(com/pill {:color :secondary}
|
||||
[:span {:x-text "p.start"}]
|
||||
" - "
|
||||
[:span {:x-text "p.end"}])]]
|
||||
(buttons/button- {"@click.prevent.stop" "periods.push({start: '', end: ''}); calendarTarget=0" :class "w-32"} "Add new period")]]}]
|
||||
:active "Quick"})]]]])
|
||||
|
||||
(defn dates-dropdown- [{:keys [value name]}]
|
||||
[:div {:x-data (hx/json {:dates (map #(atime/unparse-local % atime/normal-date) value)})}
|
||||
@@ -122,16 +121,16 @@
|
||||
(buttons/a-button- {"@click" "dates=[]"} "Clear")]]}
|
||||
{:name "Advanced oooo"
|
||||
:content [:div.flex.gap-4 {:class "overflow-hidden max-h-[300px]"
|
||||
:x-data (hx/json {:calendarTarget "0" })
|
||||
:x-data (hx/json {:calendarTarget "0"})
|
||||
"@change-date.camel" "$el.querySelectorAll('.text-inputs')[calendarTarget].focus();"}
|
||||
(inputs/calendar-input- {:x-model "dates[calendarTarget]" })
|
||||
(inputs/calendar-input- {:x-model "dates[calendarTarget]"})
|
||||
[:div.flex.flex-col.gap-4.p-2
|
||||
[:div.overflow-y-scroll.flex.flex-col.gap-4
|
||||
[:template {:x-for "(p, i) in dates" ":key" "i"}
|
||||
[:div.flex.gap-4.
|
||||
(inputs/text-input- {:x-model "dates[i]"
|
||||
(inputs/text-input- {:x-model "dates[i]"
|
||||
"@focus" "calendarTarget =i; "
|
||||
:class "text-inputs"})
|
||||
(buttons/a-icon-button- {"@click.prevent.stop" "dates=dates.filter((_, i2) => i !== i2); calendarTarget=0"} svg/x)] ]]
|
||||
(buttons/a-icon-button- {"@click.prevent.stop" "dates=dates.filter((_, i2) => i !== i2); calendarTarget=0"} svg/x)]]]
|
||||
(buttons/button- {"@click.prevent.stop" "dates.push(null); calendarTarget=0" :class "w-32"} "Add new period")]]}]
|
||||
:active "Quick"})]]]])
|
||||
@@ -1,28 +1,26 @@
|
||||
(ns auto-ap.ssr.components.tabs
|
||||
(ns auto-ap.ssr.components.tabs
|
||||
(:require
|
||||
[auto-ap.ssr.hx :as hx]))
|
||||
|
||||
(defn tabs- [{:keys [tabs active]}]
|
||||
[:div.flex.flex-col.gap-2 {:x-data (hx/json {:activeTab active})}
|
||||
[:div {:class "text-sm font-medium text-center text-gray-500 border-b border-gray-200 dark:text-gray-400 dark:border-gray-700" }
|
||||
[:div {:class "text-sm font-medium text-center text-gray-500 border-b border-gray-200 dark:text-gray-400 dark:border-gray-700"}
|
||||
[:ul {:class "flex flex-wrap -mb-px"}
|
||||
(for [tab tabs]
|
||||
[:li {:class "me-2"}
|
||||
[:a {:href "#"
|
||||
:x-data (hx/json {:tabName (:name tab)})
|
||||
":data-active" (format "activeTab==tabName")
|
||||
"@click" (format "activeTab=tabName" )
|
||||
:class "inline-block data-[active]:text-blue-600 data-[active]:border-blue-600 p-4 border-b-2 rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300"}
|
||||
"@click" (format "activeTab=tabName")
|
||||
:class "inline-block data-[active]:text-blue-600 data-[active]:border-blue-600 p-4 border-b-2 rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300"}
|
||||
(:name tab)]])
|
||||
|
||||
|
||||
|
||||
|
||||
#_[:li
|
||||
[:a {:class "inline-block p-4 text-gray-400 rounded-t-lg cursor-not-allowed dark:text-gray-500"} "Disabled"]]]]
|
||||
(for [tab tabs]
|
||||
[:div {:x-data (hx/json {:tabName (:name tab)})
|
||||
:x-show (format "activeTab==tabName")
|
||||
:x-show (format "activeTab==tabName")
|
||||
"x-transition:enter" "transition-opacity duration-300"
|
||||
"x-transition:enter-start" "opacity-0"
|
||||
"x-transition:enter-end" "opacity-100"}
|
||||
(:content tab) ])])
|
||||
"x-transition:enter-start" "opacity-0"
|
||||
"x-transition:enter-end" "opacity-100"}
|
||||
(:content tab)])])
|
||||
@@ -1,7 +1,6 @@
|
||||
(ns auto-ap.ssr.components.tags
|
||||
(:require [auto-ap.ssr.hiccup-helper :as hh]))
|
||||
|
||||
|
||||
(defn pill- [params & children]
|
||||
(into
|
||||
[:span (cond-> params
|
||||
@@ -23,7 +22,6 @@
|
||||
(defn badge- [params & children]
|
||||
[:div (merge params {:class (-> (hh/add-class "absolute inline-flex items-center z-10 justify-center w-6 h-6 text-xs font-black text-white
|
||||
border-3 border-white rounded-full -top-2 -right-2 dark:border-gray-900"
|
||||
(:class params)
|
||||
)
|
||||
(:class params))
|
||||
(hh/add-class (or (some-> (:color params) (#(str "bg-" % "-300")))
|
||||
"bg-red-300")))}) children])
|
||||
|
||||
@@ -30,14 +30,14 @@
|
||||
(if active?
|
||||
[:li {:class "flex items-center text-primary-600 font-medium dark:text-primary-500"}
|
||||
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border-2 border-primary-600 rounded-full shrink-0 dark:border-primary-500"}]
|
||||
children ]
|
||||
children]
|
||||
[:li {:class (cond-> "flex items-center"
|
||||
(not visited?) (hh/add-class "text-gray-400"))}
|
||||
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border border-gray-500 rounded-full shrink-0 dark:border-gray-400"}
|
||||
(when visited?
|
||||
[:svg {:class "w-3 h-3 text-primary-600 dark:text-primary-500", :aria-hidden "true", :xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 16 12"}
|
||||
[:path {:stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "2", :d "M1 5.917 5.724 10.5 15 1.5"}]])]
|
||||
children ]))
|
||||
children]))
|
||||
|
||||
(defn vertical-timeline [params & children]
|
||||
[:ol {:class (hh/add-class "flex flex-col items-start space-y-2 text-xs text-center text-gray-500 bg-gray-100 dark:text-gray-400 sm:text-base dark:bg-gray-800 sm:space-y-4 px-2"
|
||||
|
||||
@@ -10,15 +10,14 @@
|
||||
[:div {:class "flex items-center ml-3 mr-10"}
|
||||
[:div
|
||||
[:button#user-menu-button {:type "button", :class "flex text-sm bg-gray-800 rounded-full focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600", :aria-expanded "false"
|
||||
"@click" "$tooltip($refs.tooltip, {content: ()=>$refs.tooltip.innerHTML, theme: $store.darkMode.on ? 'dark' : 'light', allowHTML: true, interactive:true})"
|
||||
}
|
||||
"@click" "$tooltip($refs.tooltip, {content: ()=>$refs.tooltip.innerHTML, theme: $store.darkMode.on ? 'dark' : 'light', allowHTML: true, interactive:true})"}
|
||||
[:span {:class "sr-only"} "Open user menu"]
|
||||
[:img {:class "w-8 h-8 rounded-full", :src (pull-attr (dc/db conn) :user/profile-image-url (:db/id identity)) :alt "user photo" :referrerpolicy "no-referrer"}]]]
|
||||
[:template {:class ""
|
||||
:x-ref "tooltip"}
|
||||
:x-ref "tooltip"}
|
||||
[:div {:class "px-4 py-3", :role "none"}
|
||||
[:p {:class "text-sm text-gray-900 dark:text-white", :role "none"} (:user/name identity)]
|
||||
[:p {:class "text-sm font-medium text-gray-900 truncate dark:text-gray-300", :role "none"} (pull-attr (dc/db conn) :user/email (:db/id identity))] ]
|
||||
[:p {:class "text-sm font-medium text-gray-900 truncate dark:text-gray-300", :role "none"} (pull-attr (dc/db conn) :user/email (:db/id identity))]]
|
||||
[:ul {:class "py-1", :role "none"}
|
||||
[:li
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes :company), :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "My Company"]]
|
||||
@@ -27,7 +26,7 @@
|
||||
:class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "Admin"])
|
||||
[:li
|
||||
[:a {:href "#", :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"
|
||||
"@click.prevent" "$store.darkMode.toggle()" }
|
||||
"@click.prevent" "$store.darkMode.toggle()"}
|
||||
"Night Mode"]]
|
||||
[:li
|
||||
[:a {:href "/logout", :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "Sign out"]]]] ])
|
||||
[:a {:href "/logout", :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "Sign out"]]]]])
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
(:require [auto-ap.permissions :refer [wrap-must]]
|
||||
[auto-ap.routes.ezcater-xls :as ezcater-xls]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated wrap-secure]]
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated wrap-secure]]
|
||||
[auto-ap.ssr.account :as account]
|
||||
[auto-ap.ssr.admin :as admin]
|
||||
[auto-ap.ssr.not-found :as not-found]
|
||||
@@ -43,7 +43,6 @@
|
||||
|
||||
;; from auto-ap.ssr-routes, because they're shared
|
||||
|
||||
|
||||
(def key->handler
|
||||
(-> {:logout auth/logout
|
||||
:login auth/login
|
||||
@@ -86,17 +85,17 @@
|
||||
(into company-1099/key->handler)
|
||||
(into invoice/key->handler)
|
||||
(into import-batch/key->handler)
|
||||
(into pos-sales/key->handler)
|
||||
(into pos-expected-deposits/key->handler)
|
||||
(into pos-tenders/key->handler)
|
||||
(into pos-cash-drawer-shifts/key->handler)
|
||||
(into pos-refunds/key->handler)
|
||||
(into pos-sales-summaries/key->handler)
|
||||
(into users/key->handler)
|
||||
(into admin-accounts/key->handler)
|
||||
(into admin-excel-invoices/key->handler)
|
||||
(into admin/key->handler)
|
||||
(into admin-jobs/key->handler)
|
||||
(into pos-sales/key->handler)
|
||||
(into pos-expected-deposits/key->handler)
|
||||
(into pos-tenders/key->handler)
|
||||
(into pos-cash-drawer-shifts/key->handler)
|
||||
(into pos-refunds/key->handler)
|
||||
(into pos-sales-summaries/key->handler)
|
||||
(into users/key->handler)
|
||||
(into admin-accounts/key->handler)
|
||||
(into admin-excel-invoices/key->handler)
|
||||
(into admin/key->handler)
|
||||
(into admin-jobs/key->handler)
|
||||
(into admin-vendors/key->handler)
|
||||
(into admin-clients/key->handler)
|
||||
(into admin-rules/key->handler)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(ns auto-ap.ssr.dashboard
|
||||
(ns auto-ap.ssr.dashboard
|
||||
(:require
|
||||
[auto-ap.datomic :refer [conn]]
|
||||
[auto-ap.graphql.ledger :refer [get-profit-and-loss-raw]]
|
||||
@@ -25,11 +25,11 @@
|
||||
[hiccup.util :as hu]))
|
||||
|
||||
(defn bank-accounts-card [request]
|
||||
(html-response
|
||||
(html-response
|
||||
(com/card {:class "h-full"}
|
||||
[:div.p-4.h-full
|
||||
[:h1.text-2xl.font-bold "Bank Accounts"]
|
||||
[:div (hx/htmx-transition-appear {:class "h-full overflow-scroll" })
|
||||
[:div (hx/htmx-transition-appear {:class "h-full overflow-scroll"})
|
||||
(for [c (:valid-trimmed-client-ids request)
|
||||
b (:client/bank-accounts (dc/pull (dc/db conn) '[{:client/bank-accounts
|
||||
|
||||
@@ -58,43 +58,42 @@
|
||||
(#(str "Synced " %)))]
|
||||
|
||||
#_(when-let [n (cond (-> b :bank-account/intuit-bank-account)
|
||||
"Intuit"
|
||||
(-> b :bank-account/yodlee-account)
|
||||
"Yodlee"
|
||||
(-> b :bank-account/plaid-account)
|
||||
"Plaid"
|
||||
:else
|
||||
nil)]
|
||||
(list
|
||||
[:div (str n " Balance")]
|
||||
[:div.text-right (format "$%,.2f" (or (-> b :bank-account/intuit-bank-account :intuit-bank-account/current-balance)
|
||||
(-> b :bank-account/yodlee-account :yodlee-account/available-balance)
|
||||
(-> b :bank-account/plaid-account :plaid-account/balance)
|
||||
0.0))]
|
||||
|
||||
"Intuit"
|
||||
(-> b :bank-account/yodlee-account)
|
||||
"Yodlee"
|
||||
(-> b :bank-account/plaid-account)
|
||||
"Plaid"
|
||||
:else
|
||||
nil)]
|
||||
(list
|
||||
[:div (str n " Balance")]
|
||||
[:div.text-right (format "$%,.2f" (or (-> b :bank-account/intuit-bank-account :intuit-bank-account/current-balance)
|
||||
(-> b :bank-account/yodlee-account :yodlee-account/available-balance)
|
||||
(-> b :bank-account/plaid-account :plaid-account/balance)
|
||||
0.0))]
|
||||
|
||||
[:div.text-xs.text-gray-400.text-right (or (some-> (:bank-account/intuit-bank-account b)
|
||||
(:intuit-bank-account/last-synced)
|
||||
(atime/unparse-local atime/standard-time)
|
||||
(#(str "Synced " %)))
|
||||
(some-> (:bank-account/yodlee-account b)
|
||||
(:yodlee-account/last-synced)
|
||||
(atime/unparse-local atime/standard-time)
|
||||
(#(str "Synced " %)))
|
||||
(some-> (:bank-account/plaid-account b)
|
||||
(:plaid-account/last-synced)
|
||||
(atime/unparse-local atime/standard-time)
|
||||
(#(str "Synced " %))))]
|
||||
(when-let [pending-balance (-> b :bank-account/yodlee-account :yodlee-account/pending-balance)]
|
||||
(list
|
||||
[:div (str n " Pending Txs")]
|
||||
[:div.text-right (format "$%,.2f" pending-balance)]))
|
||||
[:div.inline-flex.justify-end.text-xs.text-gray-400.it]))
|
||||
[:div.text-xs.text-gray-400.text-right (or (some-> (:bank-account/intuit-bank-account b)
|
||||
(:intuit-bank-account/last-synced)
|
||||
(atime/unparse-local atime/standard-time)
|
||||
(#(str "Synced " %)))
|
||||
(some-> (:bank-account/yodlee-account b)
|
||||
(:yodlee-account/last-synced)
|
||||
(atime/unparse-local atime/standard-time)
|
||||
(#(str "Synced " %)))
|
||||
(some-> (:bank-account/plaid-account b)
|
||||
(:plaid-account/last-synced)
|
||||
(atime/unparse-local atime/standard-time)
|
||||
(#(str "Synced " %))))]
|
||||
(when-let [pending-balance (-> b :bank-account/yodlee-account :yodlee-account/pending-balance)]
|
||||
(list
|
||||
[:div (str n " Pending Txs")]
|
||||
[:div.text-right (format "$%,.2f" pending-balance)]))
|
||||
[:div.inline-flex.justify-end.text-xs.text-gray-400.it]))
|
||||
#_[:div.inline-flex.justify-between.items-baseline]]])]])))
|
||||
|
||||
(defn sales-chart-card [request]
|
||||
(html-response
|
||||
(let [ totals
|
||||
(html-response
|
||||
(let [totals
|
||||
(->> (dc/q '[:find ?sd (sum ?total)
|
||||
:with ?e
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
@@ -113,7 +112,7 @@
|
||||
[:canvas.w-full.h-full.p-8 {:x-data (hx/json {:chart nil
|
||||
:labels (map first totals)
|
||||
:data (map second totals)})
|
||||
:x-init
|
||||
:x-init
|
||||
"new Chart($el, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
@@ -136,8 +135,8 @@
|
||||
});"}]]))))
|
||||
|
||||
(defn expense-pie-card [request]
|
||||
(html-response
|
||||
(let [ totals
|
||||
(html-response
|
||||
(let [totals
|
||||
(->> (dc/q '[:find ?an (sum ?amt)
|
||||
:with ?iea
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
@@ -179,19 +178,18 @@
|
||||
});"}]]))))
|
||||
|
||||
(defn pnl-card [request]
|
||||
(html-response
|
||||
(com/card {:class "w-full h-full p-4"}
|
||||
(html-response
|
||||
(com/card {:class "w-full h-full p-4"}
|
||||
[:h1.text-2xl.font-bold.text-gray-700
|
||||
"Profit and Loss, last month" ]
|
||||
(let [ data (<-graphql (get-profit-and-loss-raw (:valid-trimmed-client-ids request)
|
||||
[{:start (time/plus (time/now) (time/months -1))
|
||||
:end (time/now)}]))
|
||||
"Profit and Loss, last month"]
|
||||
(let [data (<-graphql (get-profit-and-loss-raw (:valid-trimmed-client-ids request)
|
||||
[{:start (time/plus (time/now) (time/months -1))
|
||||
:end (time/now)}]))
|
||||
data (r/->PNLData {} (:accounts (first (:periods data))) {})
|
||||
sales (r/aggregate-accounts (r/filter-categories data [ :sales]))
|
||||
expenses (r/aggregate-accounts (r/filter-categories data [ :cogs :payroll :controllable :fixed-overhead :ownership-controllable ]))]
|
||||
(list
|
||||
#_(when (not= (count all-clients) (count clients))
|
||||
)
|
||||
sales (r/aggregate-accounts (r/filter-categories data [:sales]))
|
||||
expenses (r/aggregate-accounts (r/filter-categories data [:cogs :payroll :controllable :fixed-overhead :ownership-controllable]))]
|
||||
(list
|
||||
#_(when (not= (count all-clients) (count clients)))
|
||||
[:canvas.w-full.h-full.p-8 {:x-data (hx/json {:chart nil
|
||||
:labels [(format "Income $%,.2f" sales) (format "Expenses $%,.2f" expenses)]
|
||||
:data [sales expenses]})
|
||||
@@ -217,12 +215,12 @@
|
||||
}
|
||||
}
|
||||
});"}]
|
||||
[:div
|
||||
[:div
|
||||
"Income: " (format "$%,.2f" sales)]
|
||||
[:div
|
||||
[:div
|
||||
"Expenses: " (format "$%,.2f" expenses)])))))
|
||||
(defn tasks-card [request]
|
||||
(html-response
|
||||
(html-response
|
||||
(com/card {:class "w-full h-full p-4"}
|
||||
[:h1.text-2xl.font-bold.text-gray-700
|
||||
"Tasks"]
|
||||
@@ -237,7 +235,7 @@
|
||||
[(:valid-trimmed-client-ids request)
|
||||
(coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/years -1))))
|
||||
nil]))
|
||||
|
||||
|
||||
[uncategorized-transaction-count uncategorized-transaction-amount]
|
||||
(first (dc/q '[:find (count ?e) (sum ?am)
|
||||
:in $ [?clients ?start-date ?end-date]
|
||||
@@ -248,25 +246,23 @@
|
||||
[(:valid-trimmed-client-ids request)
|
||||
(coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/years -1))))
|
||||
nil]))]
|
||||
(list
|
||||
(list
|
||||
(when (not= 0 (or unpaid-invoice-count 0))
|
||||
[:div.bg-gray-50.rounded.p-4
|
||||
[:span "You have " (str unpaid-invoice-count) " unpaid invoices with an outstanding balance of " (format "$%,.2f" unpaid-invoice-amount) ". " ]
|
||||
|
||||
[:span "You have " (str unpaid-invoice-count) " unpaid invoices with an outstanding balance of " (format "$%,.2f" unpaid-invoice-amount) ". "]
|
||||
|
||||
(com/link {:href (hu/url (bidi.bidi/path-for ssr-routes/only-routes ::i-routes/unpaid-page)
|
||||
{:date-range "year"})
|
||||
}
|
||||
|
||||
"Pay now")
|
||||
])
|
||||
{:date-range "year"})}
|
||||
|
||||
"Pay now")])
|
||||
(when (not= 0 (or uncategorized-transaction-count 0))
|
||||
[:div.bg-gray-50.rounded.p-4
|
||||
[:span "You have " (str uncategorized-transaction-count) " transactions needing your feedback. " ]
|
||||
|
||||
[:span "You have " (str uncategorized-transaction-count) " transactions needing your feedback. "]
|
||||
|
||||
(com/link {:href (str (bidi.bidi/path-for ssr-routes/only-routes ::transaction-routes/requires-feedback-page)
|
||||
"?date-range="
|
||||
(url/url-encode (pr-str {:start (atime/unparse-local (time/plus (time/now) (time/years -1)) atime/iso-date) :end (atime/unparse-local (time/now) atime/iso-date)}))) }
|
||||
|
||||
(url/url-encode (pr-str {:start (atime/unparse-local (time/plus (time/now) (time/years -1)) atime/iso-date) :end (atime/unparse-local (time/now) atime/iso-date)})))}
|
||||
|
||||
"Review now")])))])))
|
||||
|
||||
(defn stub-card [params & children]
|
||||
@@ -280,35 +276,33 @@
|
||||
[:div.htmx-indicator (svg/spinner {:class "inline w-32 h-32 text-green-500"})]]))
|
||||
|
||||
(defn- page-contents [request]
|
||||
[:div.mb-8
|
||||
[:div {:class "grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-4 mb-8"}
|
||||
[:div.h-96 (stub-card {:title "Expenses"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/expense-card)
|
||||
:hx-trigger "load"} )]
|
||||
[:div.h-96
|
||||
(stub-card {:title "Tasks"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/tasks-card)
|
||||
:hx-trigger "load"} )]
|
||||
[:div {:class " row-span-2 h-[49rem]"}
|
||||
(stub-card {:title "Bank Accounts"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/bank-accounts-card)
|
||||
:hx-trigger "load"} )
|
||||
]
|
||||
|
||||
[:div.h-96
|
||||
(stub-card {:title "Gross Sales, last 14 days"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/sales-card)
|
||||
:hx-trigger "load"})
|
||||
]
|
||||
[:div.h-96
|
||||
(stub-card {:title "Profit and Loss, last month"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/pnl-card)
|
||||
:hx-trigger "load"}) ]
|
||||
[:div.col-span-2.h-96
|
||||
(stub-card {:title "Expense breakdown"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-breakdown-card)
|
||||
:hx-trigger "load"} )]
|
||||
[:div]] ])
|
||||
[:div.mb-8
|
||||
[:div {:class "grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-4 mb-8"}
|
||||
[:div.h-96 (stub-card {:title "Expenses"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/expense-card)
|
||||
:hx-trigger "load"})]
|
||||
[:div.h-96
|
||||
(stub-card {:title "Tasks"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/tasks-card)
|
||||
:hx-trigger "load"})]
|
||||
[:div {:class " row-span-2 h-[49rem]"}
|
||||
(stub-card {:title "Bank Accounts"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/bank-accounts-card)
|
||||
:hx-trigger "load"})]
|
||||
|
||||
[:div.h-96
|
||||
(stub-card {:title "Gross Sales, last 14 days"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/sales-card)
|
||||
:hx-trigger "load"})]
|
||||
[:div.h-96
|
||||
(stub-card {:title "Profit and Loss, last month"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/pnl-card)
|
||||
:hx-trigger "load"})]
|
||||
[:div.col-span-2.h-96
|
||||
(stub-card {:title "Expense breakdown"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-breakdown-card)
|
||||
:hx-trigger "load"})]
|
||||
[:div]]])
|
||||
|
||||
(defn page [request]
|
||||
(base-page
|
||||
@@ -334,12 +328,12 @@
|
||||
"Dashboard"))
|
||||
|
||||
(def key->handler
|
||||
( apply-middleware-to-all-handlers
|
||||
{::d-routes/page page
|
||||
::d-routes/expense-card expense-pie-card
|
||||
::d-routes/pnl-card pnl-card
|
||||
::d-routes/sales-card sales-chart-card
|
||||
::d-routes/bank-accounts-card bank-accounts-card
|
||||
::d-routes/tasks-card tasks-card }
|
||||
(fn [h]
|
||||
(wrap-client-redirect-unauthenticated (wrap-admin h)))))
|
||||
(apply-middleware-to-all-handlers
|
||||
{::d-routes/page page
|
||||
::d-routes/expense-card expense-pie-card
|
||||
::d-routes/pnl-card pnl-card
|
||||
::d-routes/sales-card sales-chart-card
|
||||
::d-routes/bank-accounts-card bank-accounts-card
|
||||
::d-routes/tasks-card tasks-card}
|
||||
(fn [h]
|
||||
(wrap-client-redirect-unauthenticated (wrap-admin h)))))
|
||||
@@ -8,8 +8,6 @@
|
||||
(def ^:dynamic *prev-cursor* nil)
|
||||
(def ^:dynamic *current* nil)
|
||||
|
||||
|
||||
|
||||
(defmacro start-form [form-data errors & rest]
|
||||
`(binding [*form-data* ~form-data
|
||||
*form-errors* (or ~errors {})]
|
||||
@@ -37,13 +35,13 @@
|
||||
(defmacro with-field-default [field default & rest]
|
||||
`(let [new-cursor# (get *current* ~field ~default)
|
||||
new-cursor2# (if (not (deref new-cursor#))
|
||||
(do
|
||||
(cursor/transact! *current*
|
||||
(fn [c#]
|
||||
(assoc c# ~field ~default)))
|
||||
(get *current* ~field ~default))
|
||||
|
||||
new-cursor#)]
|
||||
(do
|
||||
(cursor/transact! *current*
|
||||
(fn [c#]
|
||||
(assoc c# ~field ~default)))
|
||||
(get *current* ~field ~default))
|
||||
|
||||
new-cursor#)]
|
||||
(with-cursor new-cursor2#
|
||||
~@rest)))
|
||||
|
||||
@@ -71,7 +69,6 @@
|
||||
(and (sequential? errors)
|
||||
(every? string? errors)))))
|
||||
|
||||
|
||||
(defn cursor-map
|
||||
([f] (cursor-map *current* f))
|
||||
([cursor f]
|
||||
|
||||
@@ -1,39 +1,37 @@
|
||||
(ns auto-ap.ssr.grid-page-helper
|
||||
(:require
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.query-params :as query-params]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-client-redirect-unauthenticated wrap-secure]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.hiccup-helper :as hh]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[auto-ap.ssr.utils :refer [html-response main-transformer]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[cemerick.url :as url]
|
||||
[clojure.string :as str]
|
||||
[hiccup.util :as hu]
|
||||
[malli.core :as m]
|
||||
[malli.transform :as mt2]
|
||||
[taoensso.encore :refer [filter-vals]]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.data.csv :as csv]))
|
||||
|
||||
|
||||
(ns auto-ap.ssr.grid-page-helper
|
||||
(:require
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.query-params :as query-params]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-client-redirect-unauthenticated wrap-secure]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.hiccup-helper :as hh]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[auto-ap.ssr.utils :refer [html-response main-transformer]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[cemerick.url :as url]
|
||||
[clojure.string :as str]
|
||||
[hiccup.util :as hu]
|
||||
[malli.core :as m]
|
||||
[malli.transform :as mt2]
|
||||
[taoensso.encore :refer [filter-vals]]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.data.csv :as csv]))
|
||||
|
||||
(defn row* [{:keys [check-box-warning? check-boxes?] :as gridspec} user entity {:keys [flash? delete-after-settle? request class] :as options}]
|
||||
(let [cells (if check-boxes?
|
||||
[(com/data-grid-cell {:class "relative"}
|
||||
[(com/data-grid-cell {:class "relative"}
|
||||
(let [cb (com/checkbox {:name "id" :value ((:id-fn gridspec) entity)
|
||||
:x-model "selected"})]
|
||||
(if (and check-box-warning? (check-box-warning? entity))
|
||||
(do
|
||||
[:div.bg-yellow-100.absolute.inset-0.flex.items-center.px-4.py-2
|
||||
[:div {:class "absolute inset-0 bg-yellow-50 z-0",
|
||||
(if (and check-box-warning? (check-box-warning? entity))
|
||||
(do
|
||||
[:div.bg-yellow-100.absolute.inset-0.flex.items-center.px-4.py-2
|
||||
[:div {:class "absolute inset-0 bg-yellow-50 z-0",
|
||||
|
||||
:style "background-image: linear-gradient(135deg, rgba(0, 0, 0, 0.1) 12.5%, transparent 12.5%, transparent 50%, rgba(0, 0, 0, 0.1) 50%, rgba(0, 0, 0, 0.1) 62.5%, transparent 62.5%, transparent);\n background-size: 10px 10px;"}]
|
||||
|
||||
@@ -63,7 +61,7 @@
|
||||
(cond-> {:class (cond-> (or class "")
|
||||
flash? (hh/add-class "live-added group"))
|
||||
:data-id ((:id-fn gridspec) entity)}
|
||||
delete-after-settle?
|
||||
delete-after-settle?
|
||||
(assoc "@htmx:after-settle.camel" "setTimeout(() => $el.remove(), 400)"))
|
||||
cells)))
|
||||
|
||||
@@ -89,7 +87,7 @@
|
||||
[:div.h-4.w-4 svg/x]]]]))
|
||||
"default sort"))
|
||||
|
||||
(defn create-break-table-fn [break-table grid-spec ]
|
||||
(defn create-break-table-fn [break-table grid-spec]
|
||||
(let [last (atom nil)]
|
||||
(fn [request entity]
|
||||
(let [break-table-value (break-table request entity)]
|
||||
@@ -106,7 +104,6 @@
|
||||
"desc")))
|
||||
s)))
|
||||
|
||||
|
||||
(defn table* [grid-spec user {{:keys [start per-page flash-id sort]} :parsed-query-params :as request}]
|
||||
(alog/info ::TABLE-QP
|
||||
:qp (:query-params request)
|
||||
@@ -123,7 +120,7 @@
|
||||
:raw? (:raw? grid-spec)
|
||||
:title [:div.flex.gap-2 (if (string? (:title grid-spec))
|
||||
(:title grid-spec)
|
||||
((:title grid-spec) request)) ]
|
||||
((:title grid-spec) request))]
|
||||
:route (:route grid-spec)
|
||||
:root-params {:x-data (hx/json {:sort (sort->query sort)})
|
||||
"x-hx-val:sort" "sort"}
|
||||
@@ -154,11 +151,11 @@
|
||||
"selected" "all-selected"))
|
||||
:color :secondary-light}
|
||||
[:div.w-4.h-4 svg/download])))
|
||||
:rows
|
||||
(let [break-table-fn (some-> grid-spec :break-table ( create-break-table-fn grid-spec))]
|
||||
:rows
|
||||
(let [break-table-fn (some-> grid-spec :break-table (create-break-table-fn grid-spec))]
|
||||
(for [entity entities
|
||||
row (if-let [break-table-row (when break-table-fn (break-table-fn request entity))]
|
||||
|
||||
|
||||
[break-table-row (row* grid-spec user entity {:flash? (= flash-id ((:id-fn grid-spec) entity)) :request request})]
|
||||
[(row* grid-spec user entity {:flash? (= flash-id ((:id-fn grid-spec) entity)) :request request})])]
|
||||
row))
|
||||
@@ -203,9 +200,6 @@
|
||||
[])))
|
||||
(com/data-grid-header {}))})))
|
||||
|
||||
|
||||
|
||||
|
||||
(defn wrap-trim-client-ids [handler]
|
||||
(fn trim-client-ids [request]
|
||||
(let [valid-clients (extract-client-ids (:clients request)
|
||||
@@ -218,8 +212,7 @@
|
||||
set)]
|
||||
(handler (assoc request :trimmed-clients valid-clients)))))
|
||||
|
||||
|
||||
(defn table-route [grid-spec & {:keys [push-url?] :or { push-url? true}}]
|
||||
(defn table-route [grid-spec & {:keys [push-url?] :or {push-url? true}}]
|
||||
(cond-> (fn table [{:keys [identity] :as request}]
|
||||
|
||||
(html-response (table*
|
||||
@@ -247,7 +240,7 @@
|
||||
;; make it so that it rerenders the date range component, along with a hx-trigger change header
|
||||
(defn csv-route [{:keys [fetch-page headers page->csv-entities]} & {:keys []}]
|
||||
(cond-> (fn csv-route [{:keys [identity] :as request}]
|
||||
|
||||
|
||||
(let [page-results (fetch-page (assoc-in request [:query-params :per-page] Long/MAX_VALUE))
|
||||
csv-entities ((or page->csv-entities (fn [[entities]] entities)) page-results)
|
||||
csv-content (with-open [i (java.io.StringWriter.)]
|
||||
@@ -255,13 +248,13 @@
|
||||
(into [(for [h headers
|
||||
:when ((:render-for h #{:html :csv}) :csv)]
|
||||
(:name h))]
|
||||
(for [e csv-entities ]
|
||||
(for [e csv-entities]
|
||||
(for [h headers
|
||||
:when ((:render-for h #{:html :csv})
|
||||
:csv)]
|
||||
((or (:render-csv h) (comp str (:render h))) e)))))
|
||||
(.toString i))]
|
||||
|
||||
|
||||
{:headers {"Content-Type" "text/csv"}
|
||||
:body csv-content}))
|
||||
true (wrap-trim-client-ids)
|
||||
@@ -285,7 +278,7 @@
|
||||
:request request}
|
||||
(apply com/breadcrumbs {} (:breadcrumbs grid-spec))
|
||||
(when (:above-grid grid-spec)
|
||||
( (:above-grid grid-spec) request))
|
||||
((:above-grid grid-spec) request))
|
||||
[:div {:x-data (hx/json {:selected [] :all_selected false :type (:entity-name grid-spec)})
|
||||
"x-on:copy" "if (selected.length > 0) {$clipboard(JSON.stringify({'type': type, 'selected': selected}))}"
|
||||
"x-on:client-selected.document" "selected=[]; all_selected=false"
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
[hiccup.util :as hu]
|
||||
[clojure.set :as set]))
|
||||
|
||||
|
||||
(defprotocol ClassHelper
|
||||
(add-class [this add])
|
||||
(remove-class [this remove])
|
||||
@@ -70,7 +69,6 @@
|
||||
(replace-tw (string->class-list this)
|
||||
tw)))
|
||||
|
||||
|
||||
(str (hiccup/html [:div {:class (-> "hello bryce hello-1 hello-2"
|
||||
(replace-wildcard ["hello-" "b"] ["hi" "there"]))}]))
|
||||
(str (hiccup/html [:div {:class (-> "p-1.5 "
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
[cheshire.generate :refer [add-encoder]]
|
||||
[clojure.string :as str]))
|
||||
|
||||
|
||||
(defn vals [m]
|
||||
(cheshire/generate-string m))
|
||||
|
||||
@@ -15,18 +14,17 @@
|
||||
|
||||
(add-encoder jsfn jsf)
|
||||
|
||||
|
||||
(defn json [m]
|
||||
(let [starting-point (cheshire/generate-string m)]
|
||||
(if (map? m)
|
||||
(reduce
|
||||
(fn [starting-point [k v]]
|
||||
(if (instance? jsfn v)
|
||||
(-> (str/replace starting-point (re-pattern (str "(?s)\"__" (.-name v) "__(.*?)__end__\"")) "$1" )
|
||||
(str/replace "\\n" "\n"))
|
||||
starting-point))
|
||||
starting-point
|
||||
m)
|
||||
(reduce
|
||||
(fn [starting-point [k v]]
|
||||
(if (instance? jsfn v)
|
||||
(-> (str/replace starting-point (re-pattern (str "(?s)\"__" (.-name v) "__(.*?)__end__\"")) "$1")
|
||||
(str/replace "\\n" "\n"))
|
||||
starting-point))
|
||||
starting-point
|
||||
m)
|
||||
starting-point)))
|
||||
|
||||
(defn random-alpha-string []
|
||||
@@ -67,7 +65,7 @@
|
||||
alpine-disappear)
|
||||
(dissoc params :data-key)))
|
||||
|
||||
(defn alpine-mount-then-disappear [{:keys [data-key] :as params :or {data-key "show"}} ]
|
||||
(defn alpine-mount-then-disappear [{:keys [data-key] :as params :or {data-key "show"}}]
|
||||
(merge (-> {:x-data (json {data-key true})
|
||||
:x-init (format "$nextTick(() => %s=false)" (name data-key))
|
||||
:x-show (name data-key)}
|
||||
@@ -85,13 +83,12 @@
|
||||
(format "\"%s\": $data.%s || ''" field alpine-field))
|
||||
|
||||
field->alpine-field)))))
|
||||
|
||||
|
||||
(defn trigger-click-or-enter [m]
|
||||
(assoc m :hx-trigger "click, keyup[keyCode==13]"))
|
||||
|
||||
(defn htmx-transition-appear [params]
|
||||
(-> params
|
||||
(-> params
|
||||
(update :class (fn [c]
|
||||
(-> (or c "")
|
||||
(hh/add-class "opacity-100 transition htmx-added:opacity-0 duration-300")))))
|
||||
)
|
||||
(hh/add-class "opacity-100 transition htmx-added:opacity-0 duration-300"))))))
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
[clj-time.core :as t]))
|
||||
|
||||
(defn days-ago* [date]
|
||||
(if date
|
||||
(if date
|
||||
(let [start (c/to-date-time date)
|
||||
today (t/now)]
|
||||
|
||||
@@ -33,4 +33,4 @@
|
||||
{::route/days-ago (wrap-schema-enforce days-ago
|
||||
:query-schema
|
||||
[:map [:date {:optional false}
|
||||
clj-date-schema ]])})
|
||||
clj-date-schema]])})
|
||||
@@ -3,7 +3,7 @@
|
||||
(def default-read '[:db/id
|
||||
:invoice/invoice-number
|
||||
:invoice/total
|
||||
{ :invoice/uploader [:user/name]}
|
||||
{:invoice/uploader [:user/name]}
|
||||
:invoice/outstanding-balance
|
||||
:invoice/source-url
|
||||
:invoice/location
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
(def bucket-name (:data-bucket env))
|
||||
|
||||
(defn lookup [tx]
|
||||
(->> (:expense-documents tx)
|
||||
(->> (:expense-documents tx)
|
||||
(mapcat :summary-fields)
|
||||
(concat (->> tx :expense-documents ))
|
||||
(concat (->> tx :expense-documents))
|
||||
(map (fn [sf]
|
||||
(-> sf
|
||||
(update :label-detection dissoc :geometry)
|
||||
@@ -53,22 +53,22 @@
|
||||
(clojure.string/replace c #"\W+" " "))
|
||||
|
||||
(defn deduplicate [xs]
|
||||
(first
|
||||
(reduce
|
||||
(fn [[so-far seen-parsed?] [raw parsed]]
|
||||
(if (seen-parsed? parsed)
|
||||
[so-far seen-parsed?]
|
||||
[(conj so-far [raw parsed])
|
||||
(conj seen-parsed? parsed)]))
|
||||
[[] #{}]
|
||||
xs)))
|
||||
(first
|
||||
(reduce
|
||||
(fn [[so-far seen-parsed?] [raw parsed]]
|
||||
(if (seen-parsed? parsed)
|
||||
[so-far seen-parsed?]
|
||||
[(conj so-far [raw parsed])
|
||||
(conj seen-parsed? parsed)]))
|
||||
[[] #{}]
|
||||
xs)))
|
||||
|
||||
(defn textract->textract-invoice [request id tx]
|
||||
(let [lookup (lookup tx)
|
||||
valid-client-ids (extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
(:client-id request)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
total-options (->> (stack-rank #{"AMOUNT_DUE"} lookup)
|
||||
(map (fn [t]
|
||||
[t (some->> t
|
||||
@@ -103,10 +103,10 @@
|
||||
[(pull-attr (dc/db conn) :client/name c) c]))))
|
||||
vendor-name-options (->> (stack-rank #{"VENDOR_NAME"} lookup)
|
||||
(mapcat (fn [t]
|
||||
(for [m (->> (solr/query solr/impl "vendors" {"query" (format "name:(%s) ", t) "fields" "score, *"})
|
||||
(filter (fn [d] (> (:score d) 2.0)))
|
||||
(map (comp #(Long/parseLong %) :id)))]
|
||||
[t m])))
|
||||
(for [m (->> (solr/query solr/impl "vendors" {"query" (format "name:(%s) ", t) "fields" "score, *"})
|
||||
(filter (fn [d] (> (:score d) 2.0)))
|
||||
(map (comp #(Long/parseLong %) :id)))]
|
||||
[t m])))
|
||||
(deduplicate))
|
||||
date-options (->> (stack-rank #{"INVOICE_RECEIPT_DATE" "ORDER_DATE" "DELIVERY_DATE"} lookup)
|
||||
(map (fn [t]
|
||||
@@ -120,22 +120,22 @@
|
||||
[t t]))
|
||||
(deduplicate))]
|
||||
#:textract-invoice
|
||||
{:db/id id
|
||||
:textract-status "SUCCEEDED"
|
||||
:total (first total-options)
|
||||
:total-options (seq total-options)
|
||||
:customer-identifier (first customer-identifier-options)
|
||||
:customer-identifier-options (seq customer-identifier-options)
|
||||
:location [nil ""]
|
||||
:vendor-name (first vendor-name-options)
|
||||
:vendor-name-options (seq vendor-name-options)
|
||||
:date (first date-options)
|
||||
:date-options (seq date-options)
|
||||
:invoice-number (first invoice-number-options)
|
||||
:invoice-number-options (seq invoice-number-options)}))
|
||||
{:db/id id
|
||||
:textract-status "SUCCEEDED"
|
||||
:total (first total-options)
|
||||
:total-options (seq total-options)
|
||||
:customer-identifier (first customer-identifier-options)
|
||||
:customer-identifier-options (seq customer-identifier-options)
|
||||
:location [nil ""]
|
||||
:vendor-name (first vendor-name-options)
|
||||
:vendor-name-options (seq vendor-name-options)
|
||||
:date (first date-options)
|
||||
:date-options (seq date-options)
|
||||
:invoice-number (first invoice-number-options)
|
||||
:invoice-number-options (seq invoice-number-options)}))
|
||||
|
||||
(defn upload-form* []
|
||||
[:div
|
||||
[:div
|
||||
[:form.bg-blue-100.border-2.border-dashed.rounded-lg.border-blue-300.p-4.max-w-md.w-md.text-center.cursor-pointer
|
||||
{:action (bidi/path-for ssr-routes/only-routes
|
||||
:invoice-glimpse-upload)
|
||||
@@ -144,7 +144,7 @@
|
||||
"Drop an invoice here"]
|
||||
[:script
|
||||
(hiccup/raw
|
||||
"
|
||||
"
|
||||
invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
success: function(file, response) {
|
||||
window.location.href = file.xhr.responseURL;
|
||||
@@ -154,14 +154,14 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
}); ")]])
|
||||
|
||||
(defn customer-identifier-id->customer-identifier-client [[ci client]]
|
||||
(when client
|
||||
(when client
|
||||
(let [real-client (dc/pull (dc/db conn)
|
||||
[:client/name :db/id]
|
||||
client)]
|
||||
[ci [(:db/id real-client) (:client/name real-client)]])))
|
||||
|
||||
(defn vendor-name-tuple->vendor-tuple [[vn vendor]]
|
||||
(when vendor
|
||||
(when vendor
|
||||
(let [real-vendor (dc/pull (dc/db conn)
|
||||
[:vendor/name :db/id]
|
||||
vendor)]
|
||||
@@ -170,9 +170,9 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
(defn get-job [id]
|
||||
(-> (dc/pull (dc/db conn) '[*] id)
|
||||
(update :textract-invoice/customer-identifier customer-identifier-id->customer-identifier-client)
|
||||
(update :textract-invoice/customer-identifier-options #(map customer-identifier-id->customer-identifier-client %) )
|
||||
(update :textract-invoice/customer-identifier-options #(map customer-identifier-id->customer-identifier-client %))
|
||||
(update :textract-invoice/vendor-name vendor-name-tuple->vendor-tuple)
|
||||
(update :textract-invoice/vendor-name-options #(map vendor-name-tuple->vendor-tuple %) )))
|
||||
(update :textract-invoice/vendor-name-options #(map vendor-name-tuple->vendor-tuple %))))
|
||||
|
||||
(defn refresh-job [request id]
|
||||
(let [{:keys [:db/id :textract-invoice/job-id :textract-invoice/textract-status]} (get-job id)]
|
||||
@@ -185,7 +185,6 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
@(dc/transact conn [{:db/id id :textract-invoice/textract-status new-status}]))))
|
||||
(get-job id)))
|
||||
|
||||
|
||||
(defn pill-list* [{:keys [selected options class ->text ->value id field]}]
|
||||
(let [options (->> options
|
||||
(filter (complement #{selected}))
|
||||
@@ -194,7 +193,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
(com/pill {:color :secondary}
|
||||
(com/link {:hx-patch (str (bidi/path-for ssr-routes/only-routes :invoice-glimpse-update-textract-invoice :textract-invoice-id id) "?" (url/map->query {field (if ->value (->value x) (->text x))}))
|
||||
:hx-target "closest form"
|
||||
:href "#"} (->text x)))]) ))]
|
||||
:href "#"} (->text x)))])))]
|
||||
(when (seq options)
|
||||
[:div.col-span-6.col-start-1.text-xs
|
||||
"Alternates: "
|
||||
@@ -224,21 +223,21 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
(format "%s (%s)" client-name customer-identifier))
|
||||
:->value (fn [[client-identifier [id client-name]]]
|
||||
id)})
|
||||
|
||||
[:div.col-span-2.col-start-1
|
||||
|
||||
[:div.col-span-2.col-start-1
|
||||
(com/field {:label "Location (blank will use default location)"}
|
||||
(com/text-input {:name "location"
|
||||
:value (-> textract-invoice
|
||||
:textract-invoice/location
|
||||
second)
|
||||
:placeholder "Location"}))]
|
||||
:value (-> textract-invoice
|
||||
:textract-invoice/location
|
||||
second)
|
||||
:placeholder "Location"}))]
|
||||
#_(pill-list* {:selected (:textract-invoice/location textract-invoice)
|
||||
:options (:textract-invoice/location-options textract-invoice)
|
||||
:id (:db/id textract-invoice)
|
||||
:field "location"
|
||||
:->text (fn [[_ amount]]
|
||||
(str amount))})
|
||||
|
||||
:options (:textract-invoice/location-options textract-invoice)
|
||||
:id (:db/id textract-invoice)
|
||||
:field "location"
|
||||
:->text (fn [[_ amount]]
|
||||
(str amount))})
|
||||
|
||||
[:div.col-span-6
|
||||
(com/field {:label "Vendor"}
|
||||
(com/text-input {:name (path->name [:invoice/vendor])
|
||||
@@ -269,9 +268,9 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
:id (:db/id textract-invoice)
|
||||
:field "date"
|
||||
:->text (fn [[_ date]]
|
||||
(-> date
|
||||
(coerce/to-date-time)
|
||||
(atime/unparse-local atime/normal-date)))})
|
||||
(-> date
|
||||
(coerce/to-date-time)
|
||||
(atime/unparse-local atime/normal-date)))})
|
||||
[:div.col-span-2.col-start-1
|
||||
(com/field {:label "Total"}
|
||||
(com/money-input {:name "total"
|
||||
@@ -284,7 +283,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
:id (:db/id textract-invoice)
|
||||
:field "total"
|
||||
:->text (fn [[_ amount]]
|
||||
(str amount))})
|
||||
(str amount))})
|
||||
[:div.col-span-2.col-start-1
|
||||
(com/field {:label "Invoice Number"}
|
||||
(com/text-input {:name "invoice-number"
|
||||
@@ -311,11 +310,11 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
:hx-trigger "load delay:5s"
|
||||
:hx-swap "outerHTML"}
|
||||
"Analyzing job " (some-> textract-invoice
|
||||
:textract-invoice/job-id
|
||||
(subs 0 8)) "..."]
|
||||
:textract-invoice/job-id
|
||||
(subs 0 8)) "..."]
|
||||
(= "SUCCEEDED" (:textract-invoice/textract-status textract-invoice))
|
||||
[:div.px-4
|
||||
|
||||
|
||||
[:div.flex.flex-row.space-x-4
|
||||
[:div {:style {:width "805"}}
|
||||
(com/card {}
|
||||
@@ -335,15 +334,15 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
:invoice-glimpse)}
|
||||
(com/button {:color :secondary} "New glimpse")]])]
|
||||
[:p.text-sm.italic "Import your invoices with the power of AI. Please only use PDFs with a single invoice in them."]
|
||||
|
||||
(when id
|
||||
|
||||
(when id
|
||||
(job-progress* request id))
|
||||
(when-not id
|
||||
(when-not id
|
||||
(upload-form*))])])
|
||||
|
||||
(defn begin-textract-file [s3-location]
|
||||
(let [tempid (random-tempid)
|
||||
|
||||
|
||||
id (get-in @(dc/transact conn [{:db/id tempid
|
||||
:textract-invoice/textract-status "IN_PROGRESS"
|
||||
:textract-invoice/pdf-url (str "https://" bucket-name "/" s3-location)}])
|
||||
@@ -364,7 +363,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
[_ invoice-number] (:textract-invoice/invoice-number textract-invoice)
|
||||
vendor (dc/pull (dc/db conn) d-vendors/default-read vendor-id)
|
||||
location (when (and client-id)
|
||||
(or location
|
||||
(or location
|
||||
(->> (dc/pull (dc/db conn) '[:client/locations] client-id)
|
||||
:client/locations
|
||||
first)))
|
||||
@@ -374,7 +373,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
due)]
|
||||
(alog/peek ::temp-textract-invoice textract-invoice)
|
||||
(when (and client-id date invoice-number vendor-id total)
|
||||
(alog/peek ::TEMP-invoice
|
||||
(alog/peek ::TEMP-invoice
|
||||
(cond-> {:db/id (random-tempid)
|
||||
:invoice/client client-id
|
||||
:invoice/client-identifier (first (:textract-invoice/customer-identifier textract-invoice))
|
||||
@@ -382,7 +381,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
:invoice/invoice-number invoice-number
|
||||
:invoice/total total
|
||||
:invoice/date date
|
||||
|
||||
|
||||
:invoice/location location
|
||||
:invoice/import-status :import-status/imported
|
||||
:invoice/outstanding-balance total
|
||||
@@ -408,14 +407,14 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
(get (:params request) "file"))]
|
||||
(mu/log ::uploading-file
|
||||
:file file)
|
||||
(try
|
||||
(try
|
||||
(let [s3-location (str "textract-files/" (UUID/randomUUID) "." (last (str/split (:filename file) #"[\\.]")))
|
||||
_ (with-open [stream (io/input-stream (:tempfile file))]
|
||||
(s3/put-object (:data-bucket env)
|
||||
s3-location
|
||||
stream
|
||||
{:content-type "application/pdf"
|
||||
:content-length (.length (:tempfile file))}))
|
||||
(s3/put-object (:data-bucket env)
|
||||
s3-location
|
||||
stream
|
||||
{:content-type "application/pdf"
|
||||
:content-length (.length (:tempfile file))}))
|
||||
textract-invoice (begin-textract-file s3-location)]
|
||||
{:headers {"Location"
|
||||
(str (bidi/path-for ssr-routes/only-routes
|
||||
@@ -437,7 +436,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
new-invoice-id (get-in @(dc/transact conn [[:propose-invoice new-invoice]])
|
||||
[:tempids (:db/id new-invoice)])
|
||||
_ (when new-invoice-id @(dc/transact conn [{:db/id (:db/id current-job)
|
||||
:textract-invoice/invoice new-invoice-id}]))]
|
||||
:textract-invoice/invoice new-invoice-id}]))]
|
||||
(if new-invoice-id
|
||||
(html-response (page* request nil)
|
||||
:headers {"hx-push-url" (bidi/path-for ssr-routes/only-routes :invoice-glimpse)
|
||||
@@ -456,36 +455,36 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
(mu/log ::method
|
||||
:method request-method)
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/main-aside-nav
|
||||
:client-selection (:client-selection request)
|
||||
:client (:client request)
|
||||
:clients (:clients request)
|
||||
:request request
|
||||
:identity (:identity request)
|
||||
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:invoice-glimpse)
|
||||
:hx-trigger "clientSelected from:body"
|
||||
:hx-select "#app-contents"
|
||||
:hx-swap "outerHTML swap:300ms"}}
|
||||
(com/breadcrumbs {}
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:admin)}
|
||||
"Invoice"]
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:invoice-glimpse)}
|
||||
"Glimpse"])
|
||||
(page* request (some-> request
|
||||
:route-params
|
||||
:textract-invoice-id
|
||||
Long/parseLong)))
|
||||
request
|
||||
(com/page {:nav com/main-aside-nav
|
||||
:client-selection (:client-selection request)
|
||||
:client (:client request)
|
||||
:clients (:clients request)
|
||||
:request request
|
||||
:identity (:identity request)
|
||||
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:invoice-glimpse)
|
||||
:hx-trigger "clientSelected from:body"
|
||||
:hx-select "#app-contents"
|
||||
:hx-swap "outerHTML swap:300ms"}}
|
||||
(com/breadcrumbs {}
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:admin)}
|
||||
"Invoice"]
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:invoice-glimpse)}
|
||||
"Glimpse"])
|
||||
(page* request (some-> request
|
||||
:route-params
|
||||
:textract-invoice-id
|
||||
Long/parseLong)))
|
||||
|
||||
"Invoice Glimpse"))
|
||||
"Invoice Glimpse"))
|
||||
|
||||
(defn textract-invoice [request]
|
||||
(if (get-in request [:headers "hx-request"])
|
||||
(html-response (job-progress* request (some-> request
|
||||
:route-params
|
||||
:textract-invoice-id
|
||||
Long/parseLong)))
|
||||
:route-params
|
||||
:textract-invoice-id
|
||||
Long/parseLong)))
|
||||
(page request)))
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
(:require
|
||||
[amazonica.aws.s3 :as s3]
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact conn merge-query observable-query
|
||||
pull-attr pull-many random-tempid]]
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact conn merge-query observable-query
|
||||
pull-attr pull-many random-tempid]]
|
||||
[auto-ap.datomic.clients :as d-clients]
|
||||
[auto-ap.datomic.invoices :as d-invoices]
|
||||
[auto-ap.datomic.vendors :as d-vendors]
|
||||
@@ -24,24 +24,23 @@
|
||||
[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 clj-date-schema
|
||||
entity-id html-response main-transformer ref->enum-schema
|
||||
strip wrap-entity wrap-implied-route-param
|
||||
wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[bidi.bidi :as bidi]
|
||||
[clj-time.coerce :as coerce :refer [to-date]]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[com.brunobonacci.mulog :as mu]
|
||||
[config.core :refer [env]]
|
||||
[datomic.api :as dc]
|
||||
[hiccup2.core :as hiccup]
|
||||
[malli.core :as mc])
|
||||
:refer [apply-middleware-to-all-handlers clj-date-schema
|
||||
entity-id html-response main-transformer ref->enum-schema
|
||||
strip wrap-entity wrap-implied-route-param
|
||||
wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[bidi.bidi :as bidi]
|
||||
[clj-time.coerce :as coerce :refer [to-date]]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.string :as str]
|
||||
[com.brunobonacci.mulog :as mu]
|
||||
[config.core :refer [env]]
|
||||
[datomic.api :as dc]
|
||||
[hiccup2.core :as hiccup]
|
||||
[malli.core :as mc])
|
||||
(:import [java.util UUID]))
|
||||
|
||||
|
||||
(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"}
|
||||
@@ -108,7 +107,6 @@
|
||||
:size :small})])
|
||||
(exact-match-id* request)]])
|
||||
|
||||
|
||||
(defn fetch-ids [db {:keys [query-params route-params] :as request}]
|
||||
(let [valid-clients (extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
@@ -131,8 +129,6 @@
|
||||
(some-> (:start-date query-params) coerce/to-date)
|
||||
(some-> (:end-date query-params) coerce/to-date)]]}
|
||||
|
||||
|
||||
|
||||
(:client-id query-params)
|
||||
(merge-query {:query {:in ['?client-id]
|
||||
:where ['[?e :invoice/client ?client-id]]}
|
||||
@@ -144,7 +140,6 @@
|
||||
'[?client-id :client/code ?client-code]]}
|
||||
:args [(:client-code query-params)]})
|
||||
|
||||
|
||||
(:start (:due-range query-params)) (merge-query {:query {:in '[?start-due]
|
||||
:where ['[?e :invoice/due ?due]
|
||||
'[(>= ?due ?start-due)]]}
|
||||
@@ -155,7 +150,6 @@
|
||||
'[(<= ?due ?end-due)]]}
|
||||
:args [(coerce/to-date (:end (:due-range query-params)))]})
|
||||
|
||||
|
||||
(:import-status query-params)
|
||||
(merge-query {:query {:in ['?import-status]
|
||||
:where ['[?e :invoice/import-status ?import-status]]}
|
||||
@@ -232,7 +226,6 @@
|
||||
(apply-sort-3 (assoc query-params :default-asc? false))
|
||||
(apply-pagination query-params))))
|
||||
|
||||
|
||||
(defn hydrate-results [ids db _]
|
||||
(let [results (->> (pull-many db default-read ids)
|
||||
(group-by :db/id))
|
||||
@@ -307,7 +300,6 @@
|
||||
(assoc-in [:query-params :start] 0)
|
||||
(assoc-in [:query-params :per-page] 250))))
|
||||
|
||||
|
||||
:else
|
||||
selected)
|
||||
ids (->> (dc/q '[:find ?i
|
||||
@@ -318,29 +310,27 @@
|
||||
(map first))]
|
||||
ids))
|
||||
|
||||
(def upload-schema
|
||||
[:map
|
||||
(def upload-schema
|
||||
[:map
|
||||
[:force-client {:optional true}
|
||||
[:maybe entity-id]]
|
||||
[:force-vendor {:optional true}
|
||||
[:maybe entity-id]]
|
||||
[:force-chatgpt {:optional true :default false}
|
||||
[:maybe [ :boolean {:decode/string {:enter #(if (= % "on") true
|
||||
[:force-chatgpt {:optional true :default false}
|
||||
[:maybe [:boolean {:decode/string {:enter #(if (= % "on") true
|
||||
|
||||
(boolean %))}}]]]
|
||||
(boolean %))}}]]]
|
||||
[:force-location {:optional true}
|
||||
[:maybe [:string {:decode/string strip :min 2 :max 2}]]]])
|
||||
|
||||
(defn upload-form [{:keys [form-params form-errors] :as request}]
|
||||
(com/content-card {}
|
||||
|
||||
|
||||
[:div.px-4.py-3.space-y-4
|
||||
|
||||
[:div.flex.justify-between.items-center [:h1.text-2xl.mb-3.font-bold "Import new invoices"]
|
||||
]
|
||||
|
||||
[:div.flex.justify-between.items-center [:h1.text-2xl.mb-3.font-bold "Import new invoices"]]
|
||||
[:div#page-notification.notification.block {:style {:display "none"}}]
|
||||
|
||||
|
||||
|
||||
[:form
|
||||
{:hx-post (bidi/path-for ssr-routes/only-routes
|
||||
::route/import-file)
|
||||
@@ -351,7 +341,7 @@
|
||||
(fc/start-form
|
||||
form-params form-errors
|
||||
[:div.flex.gap-4.items-center
|
||||
|
||||
|
||||
(fc/with-field :force-client
|
||||
(com/validated-field {:label "Force client"
|
||||
:errors (fc/field-errors)}
|
||||
@@ -366,7 +356,7 @@
|
||||
(fc/with-field :force-location
|
||||
(com/validated-field {:label "Force location"
|
||||
:errors (fc/field-errors)}
|
||||
|
||||
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
:size 2})))
|
||||
@@ -382,15 +372,15 @@
|
||||
:value (fc/field-value)
|
||||
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})]))
|
||||
(fc/with-field :force-chatgpt
|
||||
(com/validated-field { :errors (fc/field-errors)
|
||||
(com/validated-field {:errors (fc/field-errors)
|
||||
:label " "}
|
||||
(com/checkbox {:name (fc/field-name)
|
||||
:error? (fc/error?) }
|
||||
:error? (fc/error?)}
|
||||
"Only use ChatGPT")))])
|
||||
|
||||
|
||||
[:div.border-2.border-dashed.rounded-lg.p-4.w-full.text-center.cursor-pointer.h-64.flex.items-center.justify-center.text-lg.relative
|
||||
{ :x-data (hx/json {"files" nil
|
||||
"hovering" false})
|
||||
{:x-data (hx/json {"files" nil
|
||||
"hovering" false})
|
||||
":class" "{'bg-blue-100': !hovering,
|
||||
'border-blue-300': !hovering,
|
||||
'text-blue-700': !hovering,
|
||||
@@ -399,8 +389,6 @@
|
||||
'text-green-700': hovering
|
||||
}"
|
||||
:x-ref "box"}
|
||||
|
||||
|
||||
|
||||
[:input {:type "file"
|
||||
:name "file"
|
||||
@@ -410,13 +398,12 @@
|
||||
:x-on:dragover "hovering = true",
|
||||
:x-on:dragleave "hovering = false",
|
||||
:x-on:drop "hovering = false"}]
|
||||
[:div.flex.flex-col.space-2
|
||||
[:div
|
||||
[:div.flex.flex-col.space-2
|
||||
[:div
|
||||
[:ul {:x-show "files != null"}
|
||||
[:template {:x-for "f in files" }
|
||||
[:li (com/pill {:color :primary :x-text "f.name"}) ]
|
||||
]]]
|
||||
|
||||
[:template {:x-for "f in files"}
|
||||
[:li (com/pill {:color :primary :x-text "f.name"})]]]]
|
||||
|
||||
[:div.htmx-indicator-hidden "Drop files to upload here"]]]
|
||||
(com/button {:color :primary :class "w-32 mt-3"} "Upload")]]))
|
||||
|
||||
@@ -435,14 +422,12 @@
|
||||
:query-schema query-schema
|
||||
:action-buttons (fn [request]
|
||||
(let [[_ _ outstanding total] (:page-results request)]
|
||||
[
|
||||
(when (can? (:identity request) {:subject :invoice :activity :import})
|
||||
[(when (can? (:identity request) {:subject :invoice :activity :import})
|
||||
(com/button {:hx-put (str (bidi/path-for ssr-routes/only-routes ::route/bulk-approve))
|
||||
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||
"hx-include" "#invoice-filters"
|
||||
:color :primary
|
||||
":disabled" "$data.all_selected || ($data.selected && $data.selected.length > 0) ? false: true "
|
||||
}
|
||||
":disabled" "$data.all_selected || ($data.selected && $data.selected.length > 0) ? false: true "}
|
||||
"Approve selected"))
|
||||
(when (can? (:identity request) {:subject :invoice :activity :import})
|
||||
(com/button {:hx-delete (str (bidi/path-for ssr-routes/only-routes ::route/bulk-disapprove))
|
||||
@@ -463,8 +448,8 @@
|
||||
(when (and (= :import-status/pending (:invoice/import-status entity))
|
||||
(can? (:identity request) {:subject :invoice :activity :import}))
|
||||
(com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes
|
||||
::route/approve
|
||||
:db/id (:db/id entity))}
|
||||
::route/approve
|
||||
:db/id (:db/id entity))}
|
||||
svg/thumbs-up))])
|
||||
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
|
||||
@@ -515,11 +500,11 @@
|
||||
|
||||
(defn disapprove [{invoice :entity :as request identity :identity}]
|
||||
(when-not (= :import-status/pending (:invoice/import-status invoice))
|
||||
(throw (ex-info (str "Cannot disapprove an invoice if it is not pending." (:invoice/import-status invoice))
|
||||
(throw (ex-info (str "Cannot disapprove an invoice if it is not pending." (:invoice/import-status invoice))
|
||||
{:type :notification})))
|
||||
(exception->notification
|
||||
#(assert-can-see-client identity (:db/id (:invoice/client invoice))))
|
||||
|
||||
|
||||
(audit-transact [[:db/retractEntity (:db/id invoice)]] identity)
|
||||
|
||||
(html-response (row* (:identity request) invoice
|
||||
@@ -528,13 +513,13 @@
|
||||
|
||||
(defn approve [{invoice :entity :as request identity :identity}]
|
||||
(when-not (= :import-status/pending (:invoice/import-status invoice))
|
||||
(throw (ex-info (str "Cannot approve an invoice if it is not pending." (:invoice/import-status invoice))
|
||||
(throw (ex-info (str "Cannot approve an invoice if it is not pending." (:invoice/import-status invoice))
|
||||
{:type :notification})))
|
||||
(exception->notification
|
||||
#(do (assert-can-see-client identity (:db/id (:invoice/client invoice)))
|
||||
(assert-not-locked (-> invoice :invoice/client :db/id) (-> invoice :invoice/date))))
|
||||
|
||||
(audit-transact [ [:upsert-invoice {:db/id (:db/id invoice) :invoice/import-status :import-status/imported}]] identity)
|
||||
|
||||
(audit-transact [[:upsert-invoice {:db/id (:db/id invoice) :invoice/import-status :import-status/imported}]] identity)
|
||||
|
||||
(html-response (row* (:identity request) invoice
|
||||
{:class "live-added"})
|
||||
@@ -542,51 +527,49 @@
|
||||
|
||||
(defn bulk-disapprove [request]
|
||||
(let [ids (selected->ids request (:form-params request))
|
||||
updates (map
|
||||
(fn [i] [:db/retractEntity i])
|
||||
ids) ]
|
||||
(audit-transact updates (:identity request) )
|
||||
updates (map
|
||||
(fn [i] [:db/retractEntity i])
|
||||
ids)]
|
||||
(audit-transact updates (:identity request))
|
||||
|
||||
(html-response [:div]
|
||||
:headers {"hx-trigger" (hx/json { :notification (format "Successfully disapproved %d invoices."
|
||||
:headers {"hx-trigger" (hx/json {:notification (format "Successfully disapproved %d invoices."
|
||||
(count ids))
|
||||
:invalidated "invalidated"})})))
|
||||
|
||||
(defn bulk-approve [request]
|
||||
(let [ids (selected->ids request (:form-params request))]
|
||||
(exception->notification
|
||||
(exception->notification
|
||||
#(doseq [i ids
|
||||
:let [invoice (dc/pull (dc/db conn) '[{:invoice/client [:db/id]}
|
||||
:invoice/date] i)]]
|
||||
(assert-can-see-client (:identity request) (-> invoice :invoice/client :db/id))
|
||||
(assert-not-locked (-> invoice :invoice/client :db/id) (-> invoice :invoice/date))))
|
||||
:let [invoice (dc/pull (dc/db conn) '[{:invoice/client [:db/id]}
|
||||
:invoice/date] i)]]
|
||||
(assert-can-see-client (:identity request) (-> invoice :invoice/client :db/id))
|
||||
(assert-not-locked (-> invoice :invoice/client :db/id) (-> invoice :invoice/date))))
|
||||
(let [transactions (map (fn [i] [:upsert-invoice {:db/id i :invoice/import-status :import-status/imported}]) ids)]
|
||||
(audit-transact transactions (:identity request)))
|
||||
(html-response [:div]
|
||||
:headers {"hx-trigger" (hx/json { :notification (format "Successfully approved %d invoices."
|
||||
:headers {"hx-trigger" (hx/json {:notification (format "Successfully approved %d invoices."
|
||||
(count ids))
|
||||
:invalidated "invalidated"})})))
|
||||
|
||||
#_(defn upload-invoices [{{files :file
|
||||
files-2 "file"
|
||||
client :client
|
||||
client-2 "client"
|
||||
location :location
|
||||
location-2 "location"
|
||||
vendor :vendor
|
||||
vendor-2 "vendor"} :params
|
||||
user :identity}]
|
||||
(let [files (or files files-2)
|
||||
client (or client client-2)
|
||||
location (or location location-2)
|
||||
vendor (some-> (or vendor vendor-2)
|
||||
(Long/parseLong))
|
||||
]
|
||||
))
|
||||
files-2 "file"
|
||||
client :client
|
||||
client-2 "client"
|
||||
location :location
|
||||
location-2 "location"
|
||||
vendor :vendor
|
||||
vendor-2 "vendor"} :params
|
||||
user :identity}]
|
||||
(let [files (or files files-2)
|
||||
client (or client client-2)
|
||||
location (or location location-2)
|
||||
vendor (some-> (or vendor vendor-2)
|
||||
(Long/parseLong))]))
|
||||
|
||||
(defn match-vendor [vendor-code forced-vendor vendor-search]
|
||||
(when (and (not forced-vendor) (str/blank? vendor-code))
|
||||
(if vendor-search
|
||||
(if vendor-search
|
||||
(throw (ex-info (format "No vendor found. Searched for '%s'. Please supply an forced vendor."
|
||||
vendor-search)
|
||||
{:vendor-code vendor-code}))
|
||||
@@ -594,10 +577,10 @@
|
||||
{:vendor-code vendor-code}))))
|
||||
(let [vendor-id (or forced-vendor
|
||||
(->> (dc/q
|
||||
{:find ['?vendor]
|
||||
:in ['$ '?vendor-name]
|
||||
:where ['[?vendor :vendor/name ?vendor-name]]}
|
||||
(dc/db conn) vendor-code)
|
||||
{:find ['?vendor]
|
||||
:in ['$ '?vendor-name]
|
||||
:where ['[?vendor :vendor/name ?vendor-name]]}
|
||||
(dc/db conn) vendor-code)
|
||||
first
|
||||
first))]
|
||||
(when-not vendor-id
|
||||
@@ -605,9 +588,9 @@
|
||||
{:vendor-code vendor-code})))
|
||||
|
||||
(if-let [matching-vendor (->> (dc/q
|
||||
{:find [(list 'pull '?vendor-id d-vendors/default-read)]
|
||||
:in ['$ '?vendor-id]}
|
||||
(dc/db conn) vendor-id)
|
||||
{:find [(list 'pull '?vendor-id d-vendors/default-read)]
|
||||
:in ['$ '?vendor-id]}
|
||||
(dc/db conn) vendor-id)
|
||||
first
|
||||
first)]
|
||||
matching-vendor
|
||||
@@ -617,7 +600,7 @@
|
||||
(defn import->invoice [{:keys [invoice-number source-url customer-identifier account-number total date vendor-code text full-text client-override vendor-search vendor-override location-override import-status]} user]
|
||||
(when-not total
|
||||
(throw (Exception. "Couldn't parse total from file.")))
|
||||
(when-not date
|
||||
(when-not date
|
||||
(throw (Exception. "Couldn't parse date from file.")))
|
||||
(let [matching-client (cond
|
||||
client-override client-override
|
||||
@@ -629,9 +612,9 @@
|
||||
:client-override client-override
|
||||
:matching (when matching-client
|
||||
(dc/pull (dc/db conn) [:client/name :client/code] matching-client)))
|
||||
|
||||
|
||||
matching-vendor (match-vendor vendor-code vendor-override vendor-search)
|
||||
|
||||
|
||||
matching-location (or (when-not (str/blank? location-override)
|
||||
location-override)
|
||||
(parse/best-location-match (dc/pull (dc/db conn)
|
||||
@@ -658,22 +641,20 @@
|
||||
(defn validate-invoice [invoice user]
|
||||
(let [missing-keys (for [k [:invoice/invoice-number :invoice/client :invoice/vendor :invoice/total :invoice/outstanding-balance :invoice/date]
|
||||
:when (not (get invoice k))]
|
||||
k
|
||||
)]
|
||||
(cond
|
||||
(not (:invoice/client invoice))
|
||||
(do
|
||||
k)]
|
||||
(cond
|
||||
(not (:invoice/client invoice))
|
||||
(do
|
||||
(alog/warn ::no-client :invoice invoice)
|
||||
(assoc invoice :error-message (str "Searched clients for '" (:invoice/client-identifier invoice) "'. No client found in file. Select a client first.")))
|
||||
|
||||
(not (can-see-client? user (:invoice/client invoice)))
|
||||
(do
|
||||
(alog/warn ::cant-see-client :invoice invoice )
|
||||
(assoc invoice :error-message "No access for the client in this file.")
|
||||
)
|
||||
|
||||
(do
|
||||
(alog/warn ::cant-see-client :invoice invoice)
|
||||
(assoc invoice :error-message "No access for the client in this file."))
|
||||
|
||||
(seq missing-keys)
|
||||
(do
|
||||
(do
|
||||
(alog/warn ::mising-keys :keys missing-keys)
|
||||
(assoc invoice :error-message (str "Missing the key " missing-keys)))
|
||||
:else
|
||||
@@ -686,33 +667,31 @@
|
||||
count)]
|
||||
(map #(assoc % :invoice/source-url-admin-only (boolean (> client-count 1))) is)))
|
||||
|
||||
|
||||
(defn import-uploaded-invoice [user imports]
|
||||
(alog/info ::importing-uploaded :count (count imports)
|
||||
:bc (or user "NOO"))
|
||||
(let [potential-invoices (->> imports
|
||||
(map #(import->invoice % user))
|
||||
(map #(validate-invoice % user))
|
||||
admin-only-if-multiple-clients
|
||||
)
|
||||
errored-invoices (->> potential-invoices
|
||||
admin-only-if-multiple-clients)
|
||||
errored-invoices (->> potential-invoices
|
||||
(filter #(:error-message %)))
|
||||
successful-invoices (->> potential-invoices
|
||||
(filter #(not (:error-message %))))
|
||||
proposed-invoices (->> potential-invoices
|
||||
(filter #(not (:error-message %)))
|
||||
(mapv d-invoices/code-invoice)
|
||||
(mapv (fn [i] [:propose-invoice i])))]
|
||||
|
||||
(mapv (fn [i] [:propose-invoice i])))]
|
||||
|
||||
(alog/info ::creating-invoice :invoices proposed-invoices)
|
||||
(let [tx (audit-transact proposed-invoices user)]
|
||||
#_(when-not (seq (dc/q '[:find ?i
|
||||
:in $ [?i ...]
|
||||
:where [?i :invoice/invoice-number]]
|
||||
(:db-after tx)
|
||||
(map :e (:tx-data tx))))
|
||||
(throw (ex-info "No new invoices found."
|
||||
{:template (:template (first imports))})))
|
||||
:in $ [?i ...]
|
||||
:where [?i :invoice/invoice-number]]
|
||||
(:db-after tx)
|
||||
(map :e (:tx-data tx))))
|
||||
(throw (ex-info "No new invoices found."
|
||||
{:template (:template (first imports))})))
|
||||
{:tx tx
|
||||
:errored-invoices errored-invoices
|
||||
:successful-invoices successful-invoices
|
||||
@@ -730,7 +709,7 @@
|
||||
"text/csv"
|
||||
"application/pdf")
|
||||
:content-length (.length tempfile)})
|
||||
imports (->> (if force-chatgpt
|
||||
imports (->> (if force-chatgpt
|
||||
(parse/glimpse2 (.getPath tempfile))
|
||||
(parse/parse-file (.getPath tempfile) filename :allow-glimpse? true))
|
||||
(map #(assoc %
|
||||
@@ -740,16 +719,16 @@
|
||||
:source-url (str "https://" (:data-bucket env)
|
||||
"/"
|
||||
s3-location))))]
|
||||
(try
|
||||
|
||||
(try
|
||||
|
||||
(import-uploaded-invoice identity imports)
|
||||
|
||||
(catch Exception e
|
||||
(alog/warn ::couldnt-import-upload
|
||||
:error e
|
||||
:template (:template ( first imports)))
|
||||
:template (:template (first imports)))
|
||||
(throw (ex-info (ex-message e)
|
||||
{:template (:template ( first imports))
|
||||
{:template (:template (first imports))
|
||||
:sample (first imports)}
|
||||
e)))))
|
||||
(catch Exception e
|
||||
@@ -767,23 +746,21 @@
|
||||
(fn [result {:keys [filename tempfile]}]
|
||||
(try
|
||||
(let [i (import-internal tempfile filename force-client force-location force-vendor force-chatgpt (:identity request))]
|
||||
(alog/info ::failure-error-count :count (count (:errored-invoices i)) )
|
||||
|
||||
(-> result
|
||||
(update :error? #(or %
|
||||
(alog/info ::failure-error-count :count (count (:errored-invoices i)))
|
||||
|
||||
(-> result
|
||||
(update :error? #(or %
|
||||
(boolean (seq (:errored-invoices i)))))
|
||||
|
||||
|
||||
(update :files conj {:filename filename
|
||||
:error? (boolean (seq (:errored-invoices i)))
|
||||
:successful-invoices (count (:successful-invoices i))
|
||||
:errors [:div
|
||||
[:p.text-green-500 [:b (count (:successful-invoices i)) " succeeded in total."]
|
||||
]
|
||||
[:p [:b (count (:errored-invoices i)) " failed in total."]
|
||||
]
|
||||
[:ul
|
||||
:errors [:div
|
||||
[:p.text-green-500 [:b (count (:successful-invoices i)) " succeeded in total."]]
|
||||
[:p [:b (count (:errored-invoices i)) " failed in total."]]
|
||||
[:ul
|
||||
(for [e (take 5 (:errored-invoices i))]
|
||||
[:li (:error-message e)]) ]]
|
||||
[:li (:error-message e)])]]
|
||||
:template (:template (first (:imports i)))})))
|
||||
(catch Exception e
|
||||
(-> result
|
||||
@@ -793,11 +770,10 @@
|
||||
:response (.getMessage e)
|
||||
:sample (:sample (ex-data e))
|
||||
:template (:template (ex-data e))})))))
|
||||
{:error? false
|
||||
:files []
|
||||
}
|
||||
{:error? false
|
||||
:files []}
|
||||
file)]
|
||||
|
||||
|
||||
(html-response [:div#page-notification.p-4.rounded-lg
|
||||
[:table
|
||||
[:thead
|
||||
@@ -835,34 +811,34 @@
|
||||
{"hx-trigger" "invalidated"})))
|
||||
|
||||
#_(defn wrap-test [handler]
|
||||
(fn [request]
|
||||
(clojure.pprint/pprint (:multipart-params request))
|
||||
(handler request )))
|
||||
(fn [request]
|
||||
(clojure.pprint/pprint (:multipart-params request))
|
||||
(handler request)))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(apply-middleware-to-all-handlers
|
||||
{::route/import-page
|
||||
(->
|
||||
(helper/page-route grid-page)
|
||||
(wrap-implied-route-param :status nil))
|
||||
::route/import-table
|
||||
::route/import-table
|
||||
(-> (helper/table-route grid-page)
|
||||
(wrap-implied-route-param :status nil))
|
||||
|
||||
|
||||
::route/disapprove (-> disapprove
|
||||
(wrap-entity [:route-params :db/id] default-read)
|
||||
(wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
|
||||
::route/approve (-> approve
|
||||
(wrap-entity [:route-params :db/id] default-read)
|
||||
(wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
|
||||
(wrap-entity [:route-params :db/id] default-read)
|
||||
(wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
|
||||
::route/bulk-disapprove (-> bulk-disapprove
|
||||
(wrap-schema-enforce :form-schema query-schema))
|
||||
::route/bulk-approve (-> bulk-approve
|
||||
(wrap-schema-enforce :form-schema query-schema))
|
||||
(wrap-schema-enforce :form-schema query-schema))
|
||||
::route/import-file (-> import-file
|
||||
(wrap-schema-enforce :multipart-schema upload-schema))}
|
||||
(fn [a]
|
||||
(-> a
|
||||
(-> a
|
||||
(wrap-must {:subject :invoice :activity :import})))))
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
(ns auto-ap.ssr.invoice.new-invoice-wizard
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [audit-transact conn pull-attr]]
|
||||
:refer [audit-transact conn pull-attr]]
|
||||
[auto-ap.datomic.accounts :as d-accounts]
|
||||
[auto-ap.datomic.invoices :as d-invoices]
|
||||
[auto-ap.graphql.utils :refer [assert-can-see-client assert-not-locked
|
||||
@@ -9,7 +9,7 @@
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.routes.invoice :as route]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-client-redirect-unauthenticated]]
|
||||
:refer [wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.solr :as solr]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
|
||||
@@ -21,10 +21,10 @@
|
||||
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.utils
|
||||
:refer [->db-id apply-middleware-to-all-handlers check-allowance
|
||||
check-location-belongs clj-date-schema entity-id
|
||||
form-validation-error html-response money strip
|
||||
wrap-schema-enforce]]
|
||||
:refer [->db-id apply-middleware-to-all-handlers check-allowance
|
||||
check-location-belongs clj-date-schema entity-id
|
||||
form-validation-error html-response money strip
|
||||
wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[clj-time.coerce :as coerce]
|
||||
@@ -48,10 +48,6 @@
|
||||
[:vendor-terms-override/client :vendor-terms-override/terms]}]
|
||||
vendor-id))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(defn check-vendor-default-account [vendor-id]
|
||||
(some? (:vendor/default-account (get-vendor vendor-id))))
|
||||
|
||||
@@ -83,7 +79,7 @@
|
||||
[:invoice-expense-account/location :string]
|
||||
[:invoice-expense-account/amount :double]]
|
||||
[:fn {:error/fn (fn [r x] (:type r))
|
||||
:error/path [:invoice-expense-account/location]}
|
||||
:error/path [:invoice-expense-account/location]}
|
||||
(fn [iea]
|
||||
(check-location-belongs (:invoice-expense-account/location iea)
|
||||
(:invoice-expense-account/account iea)))]]]]])
|
||||
@@ -95,7 +91,6 @@
|
||||
true
|
||||
(and invoice-number vendor client)))]])
|
||||
|
||||
|
||||
(defn clientize-vendor [{:vendor/keys [terms-overrides automatically-paid-when-due default-account account-overrides] :as vendor} client-id]
|
||||
(if (nil? vendor)
|
||||
nil
|
||||
@@ -142,7 +137,6 @@
|
||||
{:value (name :customize)
|
||||
:content [:div "Customize accounts"]}])}))))
|
||||
|
||||
|
||||
(defrecord BasicDetailsStep [linear-wizard]
|
||||
mm/ModalWizardStep
|
||||
(step-name [_]
|
||||
@@ -181,7 +175,6 @@
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value (fc/field-value)})))
|
||||
|
||||
|
||||
(fc/with-field :customize-due-and-scheduled?
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value (fc/field-value)
|
||||
@@ -220,11 +213,10 @@
|
||||
[:div.mb-4
|
||||
;; TODO DO NOT MERGE UNTIL THIS IS FIXED
|
||||
#_[:span.text-sm.text-gray-500 "Can't find the vendor? "
|
||||
(com/link {:href ...
|
||||
:target "new"}
|
||||
"Add new vendor")
|
||||
" in a new window, then return here."]]
|
||||
|
||||
(com/link {:href ...
|
||||
:target "new"}
|
||||
"Add new vendor")
|
||||
" in a new window, then return here."]]
|
||||
|
||||
[:div.flex.items-center.gap-2
|
||||
(fc/with-field :invoice/date
|
||||
@@ -333,7 +325,6 @@
|
||||
#_(mm/navigate-handler {:request request
|
||||
:to-step :next-steps}))))
|
||||
|
||||
|
||||
(defn location-select*
|
||||
[{:keys [name account-location client-locations value]}]
|
||||
(let [options (into (cond account-location
|
||||
@@ -433,8 +424,8 @@
|
||||
(filter number?)
|
||||
(reduce + 0.0))
|
||||
balance (-
|
||||
(-> request :multi-form-state :snapshot :invoice/total)
|
||||
total)]
|
||||
(-> request :multi-form-state :snapshot :invoice/total)
|
||||
total)]
|
||||
[:span {:class (when-not (dollars= 0.0 balance)
|
||||
"text-red-300")}
|
||||
(format "$%,.2f" balance)]))
|
||||
@@ -569,7 +560,6 @@
|
||||
nil
|
||||
:validation-route ::route/new-wizard-navigate)))
|
||||
|
||||
|
||||
(defn assert-no-conflicting [{:invoice/keys [invoice-number client vendor] :db/keys [id]}]
|
||||
(when (seq (d-invoices/find-conflicting {:invoice/invoice-number invoice-number
|
||||
:invoice/vendor (->db-id vendor)
|
||||
@@ -577,13 +567,11 @@
|
||||
:db/id id}))
|
||||
(form-validation-error (str "Invoice '" invoice-number "' already exists."))))
|
||||
|
||||
|
||||
(defn assert-invoice-amounts-add-up [{:keys [:invoice/expense-accounts :invoice/total]}]
|
||||
(let [expense-account-total (reduce + 0 (map (fn [x] (:invoice-expense-account/amount x)) expense-accounts))]
|
||||
(when-not (dollars= total expense-account-total)
|
||||
(form-validation-error (str "Expense account total (" expense-account-total ") does not equal invoice total (" total ")")))))
|
||||
|
||||
|
||||
(defn- calculate-spread
|
||||
"Helper function to calculate the amount to be assigned to each location"
|
||||
[shared-amount total-locations]
|
||||
@@ -592,7 +580,6 @@
|
||||
{:base-amount base-amount
|
||||
:remainder remainder}))
|
||||
|
||||
|
||||
(defn- spread-expense-account
|
||||
"Spreads the expense account amount across the given locations"
|
||||
[locations expense-account]
|
||||
@@ -628,7 +615,6 @@
|
||||
(update first-eas :invoice-expense-account/amount #(+ % leftover))
|
||||
rest))))
|
||||
|
||||
|
||||
(defn maybe-spread-locations
|
||||
"Converts any expense account for a \"Shared\" location into a separate expense account for all valid locations for that client"
|
||||
([invoice]
|
||||
@@ -643,8 +629,6 @@
|
||||
(apply-total-delta-to-account ($->cents (:invoice/total invoice)))
|
||||
(map (fn [ea] (update ea :invoice-expense-account/amount cents->$))))))))
|
||||
|
||||
|
||||
|
||||
(defrecord NewWizard2 [_ current-step]
|
||||
mm/LinearModalWizard
|
||||
(hydrate-from-request
|
||||
@@ -728,13 +712,12 @@
|
||||
|
||||
(exception->4xx #(assert-not-locked client-id (:invoice/date invoice)))
|
||||
(let [transaction-result (audit-transact [transaction] (:identity request))]
|
||||
(try
|
||||
(try
|
||||
(solr/touch-with-ledger (get-in transaction-result [:tempids "invoice"]))
|
||||
(catch Exception e
|
||||
(alog/error ::cant-save-solr
|
||||
:error e
|
||||
))
|
||||
)
|
||||
(alog/error ::cant-save-solr
|
||||
:error e)))
|
||||
|
||||
(if extant?
|
||||
|
||||
(html-response
|
||||
@@ -750,7 +733,6 @@
|
||||
|
||||
(def new-wizard (->NewWizard2 nil nil))
|
||||
|
||||
|
||||
(defn initial-new-wizard-state [request]
|
||||
(mm/->MultiStepFormState {:invoice/date (time/now)
|
||||
:customize-accounts :default}
|
||||
@@ -758,9 +740,6 @@
|
||||
{:invoice/date (time/now)
|
||||
:customize-accounts :default}))
|
||||
|
||||
|
||||
|
||||
|
||||
(defn initial-edit-wizard-state [request]
|
||||
(let [entity (dc/pull (dc/db conn) default-read (:db/id (:route-params request)))
|
||||
entity (select-keys entity (mut/keys new-form-schema))]
|
||||
@@ -779,7 +758,6 @@
|
||||
:client-locations (some->> client-id
|
||||
(pull-attr (dc/db conn) :client/locations))})))
|
||||
|
||||
|
||||
(defn due-date [{:keys [multi-form-state]}]
|
||||
(let [vendor (clientize-vendor (get-vendor (:invoice/vendor (:step-params multi-form-state)))
|
||||
(->db-id (:invoice/client (:step-params multi-form-state))))
|
||||
@@ -816,7 +794,6 @@
|
||||
:error? false
|
||||
:placeholder "1/1/2024"}))))
|
||||
|
||||
|
||||
(defn account-prediction [{:keys [multi-form-state form-errors] :as request}]
|
||||
(html-response
|
||||
(account-prediction* request)))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
(ns auto-ap.ssr.invoices
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact audit-transact-batch conn merge-query
|
||||
observable-query pull-many]]
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
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]
|
||||
@@ -23,7 +23,7 @@
|
||||
[auto-ap.routes.payments :as payment-route]
|
||||
[auto-ap.routes.transactions :as transaction-routes]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
: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]
|
||||
@@ -41,13 +41,13 @@
|
||||
[auto-ap.ssr.components.date-range :as dr]
|
||||
[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
|
||||
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]]
|
||||
:refer [apply-middleware-to-all-handlers assert-schema
|
||||
clj-date-schema dissoc-nil-transformer entity-id
|
||||
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]]
|
||||
[auto-ap.time :as atime]
|
||||
[auto-ap.utils :refer [by dollars-0? dollars=]]
|
||||
[bidi.bidi :as bidi]
|
||||
@@ -63,7 +63,6 @@
|
||||
[malli.util :as mut]
|
||||
[slingshot.slingshot :refer [try+]]))
|
||||
|
||||
|
||||
(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"}
|
||||
@@ -105,9 +104,9 @@
|
||||
(:db/id (:client request))))
|
||||
:class "filter-trigger"}))
|
||||
(dr/date-range-field {:value {:start (:start-date (:query-params request))
|
||||
:end (:end-date (:query-params request))}
|
||||
:id "date-range"
|
||||
:apply-button? true})
|
||||
:end (:end-date (:query-params request))}
|
||||
:id "date-range"
|
||||
:apply-button? true})
|
||||
(com/field {:label "Check #"}
|
||||
(com/text-input {:name "check-number"
|
||||
:id "check-number"
|
||||
@@ -122,7 +121,7 @@
|
||||
:value (:invoice-number (:query-params request))
|
||||
:placeholder "e.g., ABC-456"
|
||||
:size :small}))
|
||||
|
||||
|
||||
(com/field {:label "Amount"}
|
||||
[:div.flex.space-x-4.items-baseline
|
||||
(com/money-input {:name "amount-gte"
|
||||
@@ -143,7 +142,6 @@
|
||||
:size :small})])
|
||||
(exact-match-id* request)]])
|
||||
|
||||
|
||||
(defn fetch-ids [db {:keys [query-params route-params] :as request}]
|
||||
(let [valid-clients (extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
@@ -165,7 +163,6 @@
|
||||
(some-> (:start-date query-params) coerce/to-date)
|
||||
(some-> (:end-date query-params) coerce/to-date)]]}
|
||||
|
||||
|
||||
(:client-id query-params)
|
||||
(merge-query {:query {:in ['?client-id]
|
||||
:where ['[?e :invoice/client ?client-id]]}
|
||||
@@ -177,7 +174,6 @@
|
||||
'[?client-id :client/code ?client-code]]}
|
||||
:args [(:client-code query-params)]})
|
||||
|
||||
|
||||
(:start (:due-range query-params)) (merge-query {:query {:in '[?start-due]
|
||||
:where ['[?e :invoice/due ?due]
|
||||
'[(>= ?due ?start-due)]]}
|
||||
@@ -188,14 +184,13 @@
|
||||
'[(<= ?due ?end-due)]]}
|
||||
:args [(coerce/to-date (:end (:due-range query-params)))]})
|
||||
|
||||
|
||||
(:import-status query-params)
|
||||
(merge-query {:query {:in ['?import-status]
|
||||
:where ['[?e :invoice/import-status ?import-status]]}
|
||||
:args [(:import-status query-params)]})
|
||||
|
||||
(not (:import-status query-params))
|
||||
(merge-query {:query { :where ['[?e :invoice/import-status :import-status/imported]]} })
|
||||
(merge-query {:query {:where ['[?e :invoice/import-status :import-status/imported]]}})
|
||||
|
||||
(:status route-params)
|
||||
(merge-query {:query {:in ['?status]
|
||||
@@ -269,7 +264,6 @@
|
||||
(apply-sort-3 (assoc query-params :default-asc? false))
|
||||
(apply-pagination query-params))))
|
||||
|
||||
|
||||
(defn hydrate-results [ids db _]
|
||||
(let [results (->> (pull-many db default-read ids)
|
||||
(group-by :db/id))
|
||||
@@ -279,31 +273,30 @@
|
||||
refunds))
|
||||
|
||||
(defn sum-outstanding [ids]
|
||||
|
||||
(->>
|
||||
(dc/q {:find ['?id '?o]
|
||||
:in ['$ '[?id ...]]
|
||||
:where ['[?id :invoice/outstanding-balance ?o]]}
|
||||
(dc/db conn)
|
||||
ids)
|
||||
|
||||
(->>
|
||||
(dc/q {:find ['?id '?o]
|
||||
:in ['$ '[?id ...]]
|
||||
:where ['[?id :invoice/outstanding-balance ?o]]}
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(map last)
|
||||
(reduce
|
||||
+
|
||||
0.0)))
|
||||
|
||||
(defn sum-total-amount [ids]
|
||||
|
||||
(->>
|
||||
(dc/q {:find ['?id '?o]
|
||||
:in ['$ '[?id ...]]
|
||||
:where ['[?id :invoice/total ?o]]
|
||||
}
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(map last)
|
||||
(reduce
|
||||
+
|
||||
0.0)))
|
||||
|
||||
(->>
|
||||
(dc/q {:find ['?id '?o]
|
||||
:in ['$ '[?id ...]]
|
||||
:where ['[?id :invoice/total ?o]]}
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(map last)
|
||||
(reduce
|
||||
+
|
||||
0.0)))
|
||||
|
||||
(defn fetch-page [request]
|
||||
(let [db (dc/db conn)
|
||||
@@ -351,7 +344,6 @@
|
||||
(assoc-in [:query-params :start] 0)
|
||||
(assoc-in [:query-params :per-page] 250))))
|
||||
|
||||
|
||||
:else
|
||||
selected)]
|
||||
ids))
|
||||
@@ -369,21 +361,20 @@
|
||||
|
||||
(defn can-undo-autopayment [invoice]
|
||||
(try+
|
||||
(assert-can-undo-autopayment invoice)
|
||||
(assert-can-undo-autopayment invoice)
|
||||
true
|
||||
(catch [:type :warning] {}
|
||||
false)))
|
||||
|
||||
false)))
|
||||
|
||||
(defn pay-button* [params]
|
||||
(let [ids (:ids params)
|
||||
ids (if (seq ids)
|
||||
ids (if (seq ids)
|
||||
(map first
|
||||
(dc/q '[:find ?i
|
||||
:in $ [?i ...]
|
||||
:where (not [?i :invoice/scheduled-payment])]
|
||||
(dc/db conn)
|
||||
ids))
|
||||
(dc/q '[:find ?i
|
||||
:in $ [?i ...]
|
||||
:where (not [?i :invoice/scheduled-payment])]
|
||||
(dc/db conn)
|
||||
ids))
|
||||
ids)
|
||||
selected-client-count (if (seq ids)
|
||||
(ffirst
|
||||
@@ -417,18 +408,17 @@
|
||||
outstanding-balances)
|
||||
total (reduce + 0.0 vendor-totals)
|
||||
paying-credit? (and (> (count ids) 1)
|
||||
(= 1 (count vendor-totals))
|
||||
at-least-one-positive-payment
|
||||
(dollars-0? total))]
|
||||
|
||||
(= 1 (count vendor-totals))
|
||||
at-least-one-positive-payment
|
||||
(dollars-0? total))]
|
||||
|
||||
[:div (cond-> {:hx-target "this"
|
||||
|
||||
|
||||
:hx-trigger "click from:#pay-button"
|
||||
:x-tooltip "{allowHTML: true, content: () => $refs.template.innerHTML, appendTo: $root}"}
|
||||
paying-credit? (assoc :hx-post (bidi/path-for ssr-routes/only-routes ::route/pay-using-credit))
|
||||
(not paying-credit? ) (assoc :hx-get (bidi/path-for ssr-routes/only-routes
|
||||
::route/pay-wizard)))
|
||||
(not paying-credit?) (assoc :hx-get (bidi/path-for ssr-routes/only-routes
|
||||
::route/pay-wizard)))
|
||||
(com/button {:color :primary
|
||||
:id "pay-button"
|
||||
:disabled (or (= (count (:ids params)) 0)
|
||||
@@ -445,14 +435,13 @@
|
||||
(cond
|
||||
paying-credit?
|
||||
"Pay invoices using credit"
|
||||
|
||||
|
||||
(> (count ids) 0)
|
||||
|
||||
|
||||
(format "Pay %d invoices ($%,.2f)"
|
||||
(count ids)
|
||||
(or total 0.0))
|
||||
|
||||
|
||||
|
||||
(or (= 0 (count ids))
|
||||
(> selected-client-count 1))
|
||||
(list "Pay " (com/badge {} "!"))
|
||||
@@ -474,13 +463,11 @@
|
||||
:else
|
||||
[:div "Click to choose a bank account"])]]))
|
||||
|
||||
|
||||
(defn pay-button [request]
|
||||
(html-response
|
||||
(pay-button* {:ids (selected->ids request
|
||||
(:query-params request))})))
|
||||
|
||||
|
||||
;; TODO test as a real user
|
||||
(def grid-page
|
||||
(helper/build {:id "entity-table"
|
||||
@@ -493,9 +480,9 @@
|
||||
:oob-render
|
||||
(fn [request]
|
||||
[(assoc-in (dr/date-range-field {:value {:start (:start-date (:query-params request))
|
||||
:end (:end-date (:query-params request))}
|
||||
:id "date-range"
|
||||
:apply-button? true}) [1 :hx-swap-oob] true)
|
||||
:end (:end-date (:query-params request))}
|
||||
:id "date-range"
|
||||
:apply-button? true}) [1 :hx-swap-oob] true)
|
||||
(assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)])
|
||||
:query-schema query-schema
|
||||
:parse-query-params (fn [p]
|
||||
@@ -547,12 +534,11 @@
|
||||
:db/id (:db/id entity))}
|
||||
svg/undo))
|
||||
(when (and (can? (:identity request) {:subject :invoice :activity :edit})
|
||||
(can-undo-autopayment entity)
|
||||
)
|
||||
(can-undo-autopayment entity))
|
||||
(com/button {:hx-put (bidi/path-for ssr-routes/only-routes
|
||||
::route/undo-autopay
|
||||
:db/id (:db/id entity))}
|
||||
"Undo autopay"))])
|
||||
::route/undo-autopay
|
||||
:db/id (:db/id entity))}
|
||||
"Undo autopay"))])
|
||||
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
|
||||
"Invoices"]]
|
||||
@@ -573,7 +559,7 @@
|
||||
(= 1 (count (:client/locations (:client args))))))
|
||||
:render (fn [x] [:div.flex.items-center.gap-2 (-> x :invoice/client :client/name)
|
||||
(map #(com/pill {:color :primary} (-> % :invoice-expense-account/location))
|
||||
(:invoice/expense-accounts x)) ])}
|
||||
(:invoice/expense-accounts x))])}
|
||||
{:key "vendor"
|
||||
:name "Vendor"
|
||||
:sort-key "vendor"
|
||||
@@ -593,13 +579,12 @@
|
||||
:name "Due"
|
||||
:show-starting "xl" ;; xl:table-cell
|
||||
:render (fn [{:invoice/keys [due]}]
|
||||
(if-let [due-date (some-> due (atime/unparse-local atime/normal-date)) ]
|
||||
(let [
|
||||
today (time/now)
|
||||
(if-let [due-date (some-> due (atime/unparse-local atime/normal-date))]
|
||||
(let [today (time/now)
|
||||
[start end] (if (time/before? due today)
|
||||
[due today]
|
||||
[today due])
|
||||
i (time/interval start end )
|
||||
i (time/interval start end)
|
||||
days (if (time/before? due today)
|
||||
(- (time/in-days i))
|
||||
(time/in-days i))]
|
||||
@@ -607,23 +592,23 @@
|
||||
[:div.text-primary-700 "today"]
|
||||
(> days 0)
|
||||
[:div.text-primary-700 (format "in %d days", days)]
|
||||
:else
|
||||
[:div.text-red-700 (format "%d days ago", (- days))]))))}
|
||||
:else
|
||||
[:div.text-red-700 (format "%d days ago", (- days))]))))}
|
||||
{:key "status"
|
||||
:name "Status"
|
||||
:render (fn [{:invoice/keys [status scheduled-payment]}]
|
||||
(cond (= status :invoice-status/paid)
|
||||
(com/pill {:color :primary} "Paid")
|
||||
(= status :invoice-status/voided)
|
||||
(com/pill {:color :red} "Voided")
|
||||
|
||||
scheduled-payment
|
||||
(com/pill {:color :yellow} "Scheduled")
|
||||
(com/pill {:color :primary} "Paid")
|
||||
(= status :invoice-status/voided)
|
||||
(com/pill {:color :red} "Voided")
|
||||
|
||||
(= status :invoice-status/unpaid)
|
||||
(com/pill {:color :secondary} "Unpaid")
|
||||
:else
|
||||
""))}
|
||||
scheduled-payment
|
||||
(com/pill {:color :yellow} "Scheduled")
|
||||
|
||||
(= status :invoice-status/unpaid)
|
||||
(com/pill {:color :secondary} "Unpaid")
|
||||
:else
|
||||
""))}
|
||||
{:key "accounts"
|
||||
:name "Account"
|
||||
:show-starting "lg"
|
||||
@@ -656,32 +641,32 @@
|
||||
:class "w-8"
|
||||
:render (fn [i]
|
||||
(link-dropdown
|
||||
(into []
|
||||
(concat (->> i
|
||||
:invoice/payments
|
||||
(map :invoice-payment/payment)
|
||||
(filter (fn [p]
|
||||
(not= :payment-status/voided
|
||||
(:payment/status p))))
|
||||
(mapcat (fn [p]
|
||||
(cond-> [{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::payment-route/all-page)
|
||||
{:exact-match-id (:db/id p)})
|
||||
:content (str (format "$%,.2f" (:payment/amount p))
|
||||
(some-> (:payment/date p) coerce/to-date-time (atime/unparse-local atime/normal-date) (#(str " payment on " %))))}]
|
||||
(:payment/transaction p) (conj {:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/all-page)
|
||||
{:exact-match-id (:db/id (first (:payment/transaction p)))})
|
||||
:color :secondary
|
||||
:content "Transaction"})))))
|
||||
(when (:invoice/journal-entry i)
|
||||
[{:link (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/all-page)
|
||||
{:exact-match-id (:db/id (first (:invoice/journal-entry i)))})
|
||||
:color :yellow
|
||||
:content "Ledger entry"}])
|
||||
(when (:invoice/source-url i)
|
||||
[{:link (:invoice/source-url i)
|
||||
:color :secondary
|
||||
:content "File"}])))))}]}))
|
||||
(into []
|
||||
(concat (->> i
|
||||
:invoice/payments
|
||||
(map :invoice-payment/payment)
|
||||
(filter (fn [p]
|
||||
(not= :payment-status/voided
|
||||
(:payment/status p))))
|
||||
(mapcat (fn [p]
|
||||
(cond-> [{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::payment-route/all-page)
|
||||
{:exact-match-id (:db/id p)})
|
||||
:content (str (format "$%,.2f" (:payment/amount p))
|
||||
(some-> (:payment/date p) coerce/to-date-time (atime/unparse-local atime/normal-date) (#(str " payment on " %))))}]
|
||||
(:payment/transaction p) (conj {:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/all-page)
|
||||
{:exact-match-id (:db/id (first (:payment/transaction p)))})
|
||||
:color :secondary
|
||||
:content "Transaction"})))))
|
||||
(when (:invoice/journal-entry i)
|
||||
[{:link (hu/url (bidi/path-for ssr-routes/only-routes ::ledger-routes/all-page)
|
||||
{:exact-match-id (:db/id (first (:invoice/journal-entry i)))})
|
||||
:color :yellow
|
||||
:content "Ledger entry"}])
|
||||
(when (:invoice/source-url i)
|
||||
[{:link (:invoice/source-url i)
|
||||
:color :secondary
|
||||
:content "File"}])))))}]}))
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
|
||||
@@ -722,18 +707,16 @@
|
||||
(defn undo-autopay [{:as request :keys [identity entity]}]
|
||||
(let [invoice entity
|
||||
id (:db/id entity)
|
||||
_ (assert-can-see-client identity (:db/id (:invoice/client invoice)))
|
||||
]
|
||||
_ (assert-can-see-client identity (:db/id (:invoice/client invoice)))]
|
||||
(alog/info ::undoing-autopay :transaction :tx)
|
||||
(assert-can-undo-autopayment invoice)
|
||||
(audit-transact
|
||||
[[:upsert-invoice {:db/id id
|
||||
:invoice/status :invoice-status/unpaid
|
||||
:invoice/outstanding-balance (:invoice/total entity)
|
||||
:invoice/scheduled-payment nil}]]
|
||||
(audit-transact
|
||||
[[:upsert-invoice {:db/id id
|
||||
:invoice/status :invoice-status/unpaid
|
||||
:invoice/outstanding-balance (:invoice/total entity)
|
||||
:invoice/scheduled-payment nil}]]
|
||||
identity)
|
||||
|
||||
|
||||
(html-response
|
||||
(row* identity (dc/pull (dc/db conn) default-read id) {:flash? true
|
||||
:request request})
|
||||
@@ -847,8 +830,6 @@
|
||||
id)
|
||||
(count all-ids)))
|
||||
|
||||
|
||||
|
||||
(defn bulk-delete-dialog-confirm [request]
|
||||
(alog/peek (:form-params request))
|
||||
(let [ids (selected->ids request (:form-params request))
|
||||
@@ -861,8 +842,7 @@
|
||||
(count ids))})})))
|
||||
|
||||
#_(defn pay-invoices-from-balance [context {invoices :invoices
|
||||
client-id :client_id} _]
|
||||
)
|
||||
client-id :client_id} _])
|
||||
|
||||
(defn pay-using-credit [request]
|
||||
(alog/peek (:form-params request))
|
||||
@@ -896,7 +876,6 @@
|
||||
0.001))
|
||||
invoices)
|
||||
|
||||
|
||||
total-to-pay (reduce + 0 (map :invoice/outstanding-balance invoices-to-be-paid))
|
||||
_ (when (<= total-to-pay 0.001)
|
||||
(throw (ex-info "Select some invoices that need to be paid" {:type :form-validation})))
|
||||
@@ -926,8 +905,6 @@
|
||||
[total-to-pay []])))
|
||||
(into {}))
|
||||
|
||||
|
||||
|
||||
vendor-id (:db/id (:invoice/vendor (first invoices)))
|
||||
payment {:db/id (str vendor-id)
|
||||
:payment/amount total-to-pay
|
||||
@@ -949,7 +926,6 @@
|
||||
:notification (format "Successfully paid %d invoices."
|
||||
(count invoices))})})))
|
||||
|
||||
|
||||
(defn does-amount-exceed-outstanding? [amount outstanding-balance]
|
||||
(let [outstanding-balance (round-money outstanding-balance)
|
||||
amount (round-money amount)]
|
||||
@@ -1018,12 +994,11 @@
|
||||
:to (mm/encode-step-key :payment-details)})}
|
||||
"Credit")
|
||||
(com/button {:x-ref "button"
|
||||
"@click.prevent.capture" "$tooltip($refs.tooltip.innerHTML, {allowHTML: true, onMount(i) {htmx.process(i.popper)}, interactive:true, theme:\"light\", timeout:5000})" }
|
||||
"@click.prevent.capture" "$tooltip($refs.tooltip.innerHTML, {allowHTML: true, onMount(i) {htmx.process(i.popper)}, interactive:true, theme:\"light\", timeout:5000})"}
|
||||
"Pay"))
|
||||
[:template { :x-ref "tooltip"}
|
||||
[:div.flex.flex-col.gap-2 {
|
||||
:data-key "vis"
|
||||
:class "p-4 w-max" }
|
||||
[:template {:x-ref "tooltip"}
|
||||
[:div.flex.flex-col.gap-2 {:data-key "vis"
|
||||
:class "p-4 w-max"}
|
||||
(when (= :bank-account-type/check
|
||||
(:bank-account/type bank-account))
|
||||
(com/button {:color :primary
|
||||
@@ -1094,7 +1069,6 @@
|
||||
:can-handwrite? can-handwrite?
|
||||
:credit-only? credit-only?}))
|
||||
|
||||
|
||||
(defn can-handwrite? [invoices]
|
||||
(let [selected-vendors (set (map (comp :db/id :invoice/vendor) invoices))]
|
||||
(and
|
||||
@@ -1110,7 +1084,6 @@
|
||||
(reduce + 0.0 (map :invoice/outstanding-balance is))))
|
||||
(every? #(<= % 0.0))))
|
||||
|
||||
|
||||
(defrecord ChoosePaymentMethodModal [linear-wizard]
|
||||
mm/ModalWizardStep
|
||||
(step-name [_]
|
||||
@@ -1195,7 +1168,7 @@
|
||||
(format "Pay in full ($%,.2f)" total)))}
|
||||
{:value "advanced"
|
||||
:content "Customize payments"}]})
|
||||
|
||||
|
||||
[:div.space-y-4
|
||||
(fc/with-field :invoices
|
||||
(com/validated-field
|
||||
@@ -1345,7 +1318,6 @@
|
||||
(:snapshot multi-form-state)
|
||||
mt/strip-extra-keys-transformer)
|
||||
|
||||
|
||||
_ (assert-schema payment-form-schema snapshot)
|
||||
|
||||
_ (exception->4xx
|
||||
@@ -1354,7 +1326,7 @@
|
||||
(= "" (:check-number snapshot)))
|
||||
(throw (Exception. "Check number is required")))
|
||||
true))
|
||||
|
||||
|
||||
result (exception->4xx
|
||||
#(do
|
||||
(when (:handwritten-date snapshot)
|
||||
@@ -1378,7 +1350,7 @@
|
||||
:payment-type/credit
|
||||
:else :payment-type/debit)
|
||||
identity
|
||||
(:handwritten-date snapshot))
|
||||
(:handwritten-date snapshot))
|
||||
(catch Exception e
|
||||
(println e))))))]
|
||||
(modal-response
|
||||
@@ -1455,11 +1427,10 @@
|
||||
(defn redirect-handler [target-route]
|
||||
(fn handle [request]
|
||||
{:status 302
|
||||
:headers {"Location" (str (hu/url (bidi.bidi/path-for ssr-routes/only-routes
|
||||
:headers {"Location" (str (hu/url (bidi.bidi/path-for ssr-routes/only-routes
|
||||
target-route)
|
||||
(:query-params request)))}}))
|
||||
|
||||
|
||||
(defn initial-bulk-edit-state [request]
|
||||
(mm/->MultiStepFormState {:search-params (:query-params request)
|
||||
:expense-accounts [{:db/id "123"
|
||||
@@ -1479,7 +1450,7 @@
|
||||
(com/typeahead {:name name
|
||||
:placeholder "Search..."
|
||||
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
|
||||
{ :purpose "invoice"})
|
||||
{:purpose "invoice"})
|
||||
:id name
|
||||
:x-model x-model
|
||||
:value value
|
||||
@@ -1487,8 +1458,6 @@
|
||||
(: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
|
||||
@@ -1527,7 +1496,7 @@
|
||||
(com/validated-field
|
||||
{:errors (fc/field-errors)
|
||||
:x-hx-val:account-id "accountId"
|
||||
:hx-vals (hx/json {:name (fc/field-name) })
|
||||
: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)
|
||||
@@ -1536,7 +1505,7 @@
|
||||
(location-select* {:name (fc/field-name)
|
||||
:account-location (:account/location (cond->> (:account @value)
|
||||
(nat-int? (:account @value)) (dc/pull (dc/db conn)
|
||||
'[:account/location])))
|
||||
'[:account/location])))
|
||||
:value (fc/field-value)}))))
|
||||
(fc/with-field :percentage
|
||||
(com/data-grid-cell
|
||||
@@ -1544,10 +1513,10 @@
|
||||
(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))}))))
|
||||
: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))))
|
||||
|
||||
@@ -1587,7 +1556,7 @@
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
::route/bulk-edit-new-account)
|
||||
:row-offset 0
|
||||
:index (count (fc/field-value)) }
|
||||
:index (count (fc/field-value))}
|
||||
"New account")
|
||||
(com/data-grid-row {}
|
||||
(com/data-grid-cell {})
|
||||
@@ -1617,7 +1586,6 @@
|
||||
: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
|
||||
@@ -1667,9 +1635,9 @@
|
||||
(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)))
|
||||
(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
|
||||
@@ -1683,44 +1651,43 @@
|
||||
(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) }
|
||||
(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])]]))
|
||||
(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"})))))
|
||||
(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
|
||||
@@ -1740,7 +1707,7 @@
|
||||
(filter number?)
|
||||
(reduce + 0.0))
|
||||
balance (- 100.0
|
||||
(* 100.0 total))]
|
||||
(* 100.0 total))]
|
||||
[:span {:class (when-not (dollars= 0.0 balance)
|
||||
"text-red-300")}
|
||||
(format "%.1f%%" balance)]))
|
||||
@@ -1769,31 +1736,31 @@
|
||||
::route/legacy-voided-invoices (redirect-handler ::route/voided-page)
|
||||
::route/legacy-new-invoice (redirect-handler ::route/new-wizard)
|
||||
::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]]]))
|
||||
(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)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
(ns auto-ap.ssr.ledger
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [audit-transact audit-transact-batch conn pull-many
|
||||
remove-nils]]
|
||||
:refer [audit-transact audit-transact-batch conn pull-many
|
||||
remove-nils]]
|
||||
[auto-ap.datomic.accounts :as a]
|
||||
[auto-ap.graphql.utils :refer [assert-admin assert-can-see-client
|
||||
exception->notification notify-if-locked]]
|
||||
@@ -11,7 +11,7 @@
|
||||
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||
[auto-ap.routes.ledger :as route]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-client-redirect-unauthenticated]]
|
||||
:refer [wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.solr :as solr]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
@@ -30,11 +30,11 @@
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[auto-ap.ssr.utils
|
||||
:refer [apply-middleware-to-all-handlers clj-date-schema
|
||||
html-response main-transformer money strip
|
||||
wrap-form-4xx-2 wrap-implied-route-param
|
||||
wrap-merge-prior-hx wrap-schema-decode
|
||||
wrap-schema-enforce]]
|
||||
:refer [apply-middleware-to-all-handlers clj-date-schema
|
||||
html-response main-transformer money strip
|
||||
wrap-form-4xx-2 wrap-implied-route-param
|
||||
wrap-merge-prior-hx wrap-schema-decode
|
||||
wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[bidi.bidi :as bidi]
|
||||
@@ -50,8 +50,6 @@
|
||||
[malli.core :as mc]
|
||||
[slingshot.slingshot :refer [throw+]]))
|
||||
|
||||
|
||||
|
||||
(comment
|
||||
(mc/decode query-schema
|
||||
{:start " "}
|
||||
@@ -67,14 +65,10 @@
|
||||
(assoc-in [:query-params :start] 0)
|
||||
(assoc-in [:query-params :per-page] 250))))
|
||||
|
||||
|
||||
:else
|
||||
selected)]
|
||||
ids))
|
||||
|
||||
|
||||
|
||||
|
||||
(defn delete [{invoice :entity :as request identity :identity}]
|
||||
(exception->notification
|
||||
#(when-not (= :invoice-status/unpaid (:invoice/status invoice))
|
||||
@@ -101,10 +95,9 @@
|
||||
identity)
|
||||
|
||||
(html-response (ledger.common/row* (:identity request) (dc/pull (dc/db conn) default-read (:db/id invoice))
|
||||
{:class "live-removed"})
|
||||
{:class "live-removed"})
|
||||
:headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id invoice))}))
|
||||
|
||||
|
||||
(defn wrap-ensure-bank-account-belongs [handler]
|
||||
(fn [{:keys [query-params client] :as request}]
|
||||
(let [bank-account-belongs? (get (set (map :db/id (:client/bank-accounts client)))
|
||||
@@ -131,7 +124,7 @@
|
||||
(clojure.pprint/pprint (fc/field-errors))
|
||||
(when (seq (fc/field-value))
|
||||
|
||||
[:div {:x-data (hx/json { "showTable" false})}
|
||||
[:div {:x-data (hx/json {"showTable" false})}
|
||||
[:form {:hx-post (bidi.bidi/path-for ssr-routes/only-routes ::route/external-import-import)
|
||||
:autocomplete "off"}
|
||||
(when (:just-parsed? request)
|
||||
@@ -140,7 +133,7 @@
|
||||
[:div.inline-flex.gap-2
|
||||
(->> (:form-errors request)
|
||||
:table
|
||||
( #(if (map? %) ( vals %) %))
|
||||
(#(if (map? %) (vals %) %))
|
||||
(mapcat identity)
|
||||
(group-by last)
|
||||
(map (fn [[k v]]
|
||||
@@ -148,12 +141,12 @@
|
||||
(com/pill {:color :yellow}
|
||||
(format "%d warnings" (count v)))
|
||||
(com/pill {:color :red}
|
||||
(format "%d errors" (count v)))))))] ])
|
||||
(format "%d errors" (count v)))))))]])
|
||||
[:div.flex.gap-4.items-center
|
||||
(com/checkbox {"@click" "showTable=!showTable"}
|
||||
"Show table")
|
||||
(com/button {:color :primary} "Import")]
|
||||
[:div { :x-show "showTable"}
|
||||
[:div {:x-show "showTable"}
|
||||
(com/data-grid-card {:id "ledger-import-data"
|
||||
:route nil
|
||||
:title "Data to import"
|
||||
@@ -230,11 +223,11 @@
|
||||
(let [errors (seq (fc/field-errors))]
|
||||
(cond errors
|
||||
[:div
|
||||
{ "x-tooltip" "{content: ()=>$refs.tt.innerHTML , allowHTML: true}"}
|
||||
{"x-tooltip" "{content: ()=>$refs.tt.innerHTML , allowHTML: true}"}
|
||||
[:div.w-8.h-8.rounded-full.p-2.flex.items-start {:class
|
||||
(if (seq (filter
|
||||
(fn [[_ status]]
|
||||
|
||||
|
||||
(= :error status))
|
||||
errors))
|
||||
"bg-red-50 text-red-300"
|
||||
@@ -246,29 +239,29 @@
|
||||
[:li m])]]]
|
||||
:else
|
||||
nil))]))))}
|
||||
|
||||
|
||||
[:div.flex.m-4.flex-row-reverse
|
||||
(com/button {:color :primary} "Import")])]]])))])
|
||||
|
||||
(defn external-import-text-form* [request]
|
||||
(fc/start-form
|
||||
(or (:form-params request) {}) (:form-errors request)
|
||||
[:form#parse-form {:x-data (hx/json {"clipboard" nil})
|
||||
:hx-post (bidi.bidi/path-for ssr-routes/only-routes ::route/external-import-parse)
|
||||
:hx-swap "outerHTML"
|
||||
:hx-trigger "pasted"}
|
||||
(fc/with-field :table
|
||||
[:div
|
||||
(com/errors {:errors (fc/field-errors)})
|
||||
(com/text-area {:x-model "clipboard" :name (fc/field-name) :value (fc/field-value) :class "hidden"})])
|
||||
(com/button {"@click.prevent" "clipboard = (await getclpboard()); $nextTick(() => $dispatch('pasted'))"
|
||||
"x-on:paste.document" "clipboard = (await getclpboard()); console.log(clipboard); $nextTick(() => $dispatch('pasted'))"}
|
||||
"Load from clipboard")]))
|
||||
|
||||
(fc/start-form
|
||||
(or (:form-params request) {}) (:form-errors request)
|
||||
[:form#parse-form {:x-data (hx/json {"clipboard" nil})
|
||||
:hx-post (bidi.bidi/path-for ssr-routes/only-routes ::route/external-import-parse)
|
||||
:hx-swap "outerHTML"
|
||||
:hx-trigger "pasted"}
|
||||
(fc/with-field :table
|
||||
[:div
|
||||
(com/errors {:errors (fc/field-errors)})
|
||||
(com/text-area {:x-model "clipboard" :name (fc/field-name) :value (fc/field-value) :class "hidden"})])
|
||||
(com/button {"@click.prevent" "clipboard = (await getclpboard()); $nextTick(() => $dispatch('pasted'))"
|
||||
"x-on:paste.document" "clipboard = (await getclpboard()); console.log(clipboard); $nextTick(() => $dispatch('pasted'))"}
|
||||
"Load from clipboard")]))
|
||||
|
||||
(defn external-import-form* [request]
|
||||
[:div#forms {:hx-target "this"
|
||||
:hx-swap "outerHTML"}
|
||||
(when (and (not (:just-parsed? request))
|
||||
(when (and (not (:just-parsed? request))
|
||||
(seq (->> (:form-errors request)
|
||||
:table
|
||||
vals
|
||||
@@ -289,14 +282,14 @@
|
||||
:client (:client request)
|
||||
:identity (:identity request)
|
||||
:request request}
|
||||
(com/breadcrumbs {}
|
||||
(com/breadcrumbs {}
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes ::route/all-page)}
|
||||
"Ledger"]
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes ::route/external-import-page)}
|
||||
"Import"])
|
||||
#_(when (:above-grid grid-spec)
|
||||
( (:above-grid grid-spec) request))
|
||||
|
||||
((:above-grid grid-spec) request))
|
||||
|
||||
[:script
|
||||
(hiccup/raw
|
||||
"async function getclpboard() {
|
||||
@@ -306,7 +299,7 @@
|
||||
console.log(r)
|
||||
return await r.text()
|
||||
}")]
|
||||
|
||||
|
||||
(external-import-form* request)
|
||||
[:div #_{:x-data (hx/json {:selected [] :all_selected false :type (:entity-name grid-spec)})
|
||||
"x-on:copy" "if (selected.length > 0) {$clipboard(JSON.stringify({'type': type, 'selected': selected}))}"
|
||||
@@ -322,23 +315,21 @@
|
||||
#_(if (string? (:title grid-spec))
|
||||
(:title grid-spec)
|
||||
((:title grid-spec) request))))
|
||||
|
||||
|
||||
|
||||
(defn trim-header [t]
|
||||
(if (->> t
|
||||
first
|
||||
(map clojure.string/lower-case)
|
||||
(filter #{"id" "client" "source" "vendor" "date" "account" "location" "debit"})
|
||||
seq)
|
||||
(drop 1 t)
|
||||
t))
|
||||
first
|
||||
(map clojure.string/lower-case)
|
||||
(filter #{"id" "client" "source" "vendor" "date" "account" "location" "debit"})
|
||||
seq)
|
||||
(drop 1 t)
|
||||
t))
|
||||
|
||||
(defn tsv->import-data [data]
|
||||
(if (string? data)
|
||||
(with-open [r (io/reader (char-array data))]
|
||||
(into [] (filter (fn filter-row [r]
|
||||
(seq (filter (comp not-empty #(str/replace % #"\s+" "")) r))))
|
||||
(seq (filter (comp not-empty #(str/replace % #"\s+" "")) r))))
|
||||
(trim-header (csv/read-csv r :separator \tab))))
|
||||
data))
|
||||
|
||||
@@ -347,52 +338,45 @@
|
||||
[:bank-account
|
||||
[:string]]]))
|
||||
|
||||
|
||||
|
||||
(def parse-form-schema (mc/schema
|
||||
[:map
|
||||
[:map
|
||||
[:table {:min 1 :error/message "Clipboard should contain rows to import"
|
||||
:decode/string tsv->import-data}
|
||||
:decode/string tsv->import-data}
|
||||
[:vector {:coerce? true}
|
||||
[:map { :decode/arbitrary (fn [t]
|
||||
[:map {:decode/arbitrary (fn [t]
|
||||
(if (vector? t)
|
||||
(into {} (map vector [:external-id :client-code :source :vendor-name :date :account-code :location :debit :credit] t))
|
||||
t))}
|
||||
[:external-id [:string {:title "external id"
|
||||
:min 1
|
||||
:decode/string strip}]]
|
||||
[:client-code [:string {:title "client code"
|
||||
:min 1
|
||||
:decode/string strip}]]
|
||||
[:source [:string {:title "source"
|
||||
:min 1
|
||||
:decode/string strip}]]
|
||||
[:vendor-name [:string {:min 1 :decode/string strip}]]
|
||||
[:date [:and clj-date-schema
|
||||
[:any {:title "date"}]]]
|
||||
[:account-code account-schema]
|
||||
|
||||
[:location [:string { :min 1
|
||||
:max 2
|
||||
:decode/string strip}]]
|
||||
[:debit [:maybe money]]
|
||||
[:credit [:maybe money]]]]
|
||||
|
||||
[:external-id [:string {:title "external id"
|
||||
:min 1
|
||||
:decode/string strip}]]
|
||||
[:client-code [:string {:title "client code"
|
||||
:min 1
|
||||
:decode/string strip}]]
|
||||
[:source [:string {:title "source"
|
||||
:min 1
|
||||
:decode/string strip}]]
|
||||
[:vendor-name [:string {:min 1 :decode/string strip}]]
|
||||
[:date [:and clj-date-schema
|
||||
[:any {:title "date"}]]]
|
||||
[:account-code account-schema]
|
||||
|
||||
[:location [:string {:min 1
|
||||
:max 2
|
||||
:decode/string strip}]]
|
||||
[:debit [:maybe money]]
|
||||
[:credit [:maybe money]]]]
|
||||
|
||||
#_[:string {:decode/string tsv->import-data
|
||||
:error/message "Clipboard should contain rows to import"}]]]))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(defn external-import-parse [request]
|
||||
(html-response
|
||||
( external-import-form* (assoc request :just-parsed? true))))
|
||||
(html-response
|
||||
(external-import-form* (assoc request :just-parsed? true))))
|
||||
|
||||
(defn line->id [{:keys [source external-id client-code]}]
|
||||
(str client-code "-" source "-" external-id))
|
||||
|
||||
|
||||
(defn add-errors [entry all-vendors all-accounts client-locked-lookup all-client-bank-accounts all-client-locations]
|
||||
(let [vendor (all-vendors (:vendor-name entry))
|
||||
locked-until (client-locked-lookup (:client-code entry))
|
||||
@@ -413,29 +397,29 @@
|
||||
entry (cond
|
||||
(not locked-until)
|
||||
(all-row-error (str "Client '" (:client-code entry) "' not found."))
|
||||
|
||||
|
||||
(not vendor)
|
||||
(all-row-error (str "Vendor '" (:vendor-name entry) "' not found."))
|
||||
|
||||
|
||||
(and locked-until
|
||||
(and (not (t/after? (:date entry)
|
||||
(coerce/to-date-time locked-until)))
|
||||
(not (t/equal? (:date entry)
|
||||
(coerce/to-date-time locked-until)))))
|
||||
(all-row-error (str "Client's data is locked until " locked-until))
|
||||
(not (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line-items entry)))
|
||||
(reduce (fnil + 0.0 0.0) 0.0 (map :credit (:line-items entry)))))
|
||||
(all-row-error (str "Debits '"
|
||||
(reduce (fnil + 0.0 0.0) 0 (map :debit (:line-items entry)))
|
||||
"' and credits '"
|
||||
(reduce (fnil + 0.0 0.0) 0 (map :credit (:line-items entry)))
|
||||
"' do not add up."))
|
||||
(dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line-items entry)))
|
||||
0.0)
|
||||
(all-row-error (str "Cannot have ledger entries that total $0.00") :warn)
|
||||
|
||||
:else
|
||||
entry)]
|
||||
(not (dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line-items entry)))
|
||||
(reduce (fnil + 0.0 0.0) 0.0 (map :credit (:line-items entry)))))
|
||||
(all-row-error (str "Debits '"
|
||||
(reduce (fnil + 0.0 0.0) 0 (map :debit (:line-items entry)))
|
||||
"' and credits '"
|
||||
(reduce (fnil + 0.0 0.0) 0 (map :credit (:line-items entry)))
|
||||
"' do not add up."))
|
||||
(dollars= (reduce (fnil + 0.0 0.0) 0.0 (map :debit (:line-items entry)))
|
||||
0.0)
|
||||
(all-row-error (str "Cannot have ledger entries that total $0.00") :warn)
|
||||
|
||||
:else
|
||||
entry)]
|
||||
(update
|
||||
entry
|
||||
:line-items
|
||||
@@ -466,7 +450,6 @@
|
||||
(:account-code ea))))
|
||||
(row-error ea (str "Bank Account '" (:account-code ea) "' not found."))
|
||||
|
||||
|
||||
(and matching-account
|
||||
(:account/location matching-account)
|
||||
(not= (:account/location matching-account)
|
||||
@@ -494,12 +477,11 @@
|
||||
(let [lines-with-indexes (for [[i l] (map vector (range) table)]
|
||||
(assoc l :index i))]
|
||||
(into []
|
||||
(for [
|
||||
[_ lines] (group-by line->id lines-with-indexes)
|
||||
:let [{:keys [source client-code date vendor-name note cleared-against] :as line} (first lines)]]
|
||||
(for [[_ lines] (group-by line->id lines-with-indexes)
|
||||
:let [{:keys [source client-code date vendor-name note cleared-against] :as line} (first lines)]]
|
||||
(add-errors {:source source
|
||||
:indices (map :index lines)
|
||||
:external-id (line->id line)
|
||||
:external-id (line->id line)
|
||||
:client-code client-code
|
||||
:date date
|
||||
:note note
|
||||
@@ -515,9 +497,9 @@
|
||||
:debit debit
|
||||
:credit credit})
|
||||
lines)}
|
||||
all-vendors
|
||||
all-accounts
|
||||
client-locked-lookup
|
||||
all-vendors
|
||||
all-accounts
|
||||
client-locked-lookup
|
||||
all-client-bank-accounts
|
||||
all-client-locations)))))
|
||||
|
||||
@@ -645,7 +627,7 @@
|
||||
good-entries (filter (fn [e] (and (not (:error (entry-error-types e))) (not (:warn (entry-error-types e))))) entries)
|
||||
bad-entries (filter (fn [e] (:error (entry-error-types e))) entries)
|
||||
form-errors (reduce (fn [acc [path m status]]
|
||||
(update-in acc path conj [ m status]))
|
||||
(update-in acc path conj [m status]))
|
||||
{}
|
||||
errors)
|
||||
_ (when (seq bad-entries)
|
||||
@@ -654,7 +636,7 @@
|
||||
{:type :field-validation
|
||||
:form-errors form-errors
|
||||
:form-params form-params})))
|
||||
|
||||
|
||||
retraction (mapv (fn [x] [:db/retractEntity [:journal-entry/external-id (:external-id x)]])
|
||||
good-entries)
|
||||
ignore-retraction (->> ignored-entries
|
||||
@@ -696,21 +678,20 @@
|
||||
|
||||
(defn external-import-import [request]
|
||||
(let [result (import-ledger request)]
|
||||
(html-response
|
||||
[:div
|
||||
(html-response
|
||||
[:div
|
||||
(external-import-form* (assoc request :form-errors (:form-errors result)))]
|
||||
:headers {"hx-trigger" (hx/json { "notification" (format "%d successful, %d with warnings. Any ledger entries with warnings have been removed." (:successful result) (:ignored result))})})))
|
||||
|
||||
:headers {"hx-trigger" (hx/json {"notification" (format "%d successful, %d with warnings. Any ledger entries with warnings have been removed." (:successful result) (:ignored result))})})))
|
||||
|
||||
(def key->handler
|
||||
(merge
|
||||
(merge
|
||||
(apply-middleware-to-all-handlers
|
||||
(->
|
||||
{::route/all-page (-> (helper/page-route grid-page)
|
||||
(wrap-implied-route-param :external? false))
|
||||
::route/external-page (-> (helper/page-route grid-page)
|
||||
(wrap-implied-route-param :external? true))
|
||||
|
||||
|
||||
::route/table (helper/table-route grid-page)
|
||||
::route/csv (helper/csv-route grid-page)
|
||||
::route/external-import-page external-import-page
|
||||
@@ -739,4 +720,4 @@
|
||||
profit-and-loss/key->handler
|
||||
cash-flows/key->handler
|
||||
investigate/key->handler
|
||||
new/key->handler))
|
||||
new/key->handler))
|
||||
@@ -2,7 +2,7 @@
|
||||
(:require
|
||||
[amazonica.aws.s3 :as s3]
|
||||
[auto-ap.datomic
|
||||
:refer [conn pull-many]]
|
||||
:refer [conn pull-many]]
|
||||
[auto-ap.graphql.utils :refer [assert-can-see-client]]
|
||||
[auto-ap.ledger :refer [build-account-lookup upsert-running-balance]]
|
||||
[auto-ap.ledger.reports :as l-reports]
|
||||
@@ -11,7 +11,7 @@
|
||||
[auto-ap.permissions :refer [wrap-must]]
|
||||
[auto-ap.routes.ledger :as route]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-client-redirect-unauthenticated]]
|
||||
:refer [wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.form-cursor :as fc]
|
||||
@@ -20,9 +20,9 @@
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[auto-ap.ssr.utils
|
||||
:refer [apply-middleware-to-all-handlers clj-date-schema
|
||||
html-response modal-response wrap-form-4xx-2
|
||||
wrap-schema-enforce]]
|
||||
:refer [apply-middleware-to-all-handlers clj-date-schema
|
||||
html-response modal-response wrap-form-4xx-2
|
||||
wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[clj-pdf.core :as pdf]
|
||||
@@ -38,42 +38,38 @@
|
||||
[java.util UUID]
|
||||
[org.apache.commons.io.output ByteArrayOutputStream]))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(def query-schema (mc/schema
|
||||
[:maybe [:map
|
||||
[:client {:unspecified/value :all}
|
||||
[:or
|
||||
[:enum :all]
|
||||
[:vector {:coerce? true :min 1}
|
||||
[:entity-map {:pull [:db/id :client/name]}]]]]
|
||||
[:client {:unspecified/value :all}
|
||||
[:or
|
||||
[:enum :all]
|
||||
[:vector {:coerce? true :min 1}
|
||||
[:entity-map {:pull [:db/id :client/name]}]]]]
|
||||
[:include-deltas {:default false}
|
||||
[:boolean {:decode/string {:enter #(if (= % "on") true
|
||||
|
||||
(boolean %))}}]]
|
||||
[:date {:unspecified/fn (fn [] [(atime/local-now)])}
|
||||
[:vector {:coerce? true
|
||||
:decode/string (fn [s] (if (string? s) (str/split s #", ")
|
||||
s))}
|
||||
clj-date-schema]] ]]))
|
||||
[:boolean {:decode/string {:enter #(if (= % "on") true
|
||||
|
||||
(boolean %))}}]]
|
||||
[:date {:unspecified/fn (fn [] [(atime/local-now)])}
|
||||
[:vector {:coerce? true
|
||||
:decode/string (fn [s] (if (string? s) (str/split s #", ")
|
||||
s))}
|
||||
clj-date-schema]]]]))
|
||||
;; TODO
|
||||
;; 1. Rerender form when running
|
||||
;; 2. Don't throw crazy errors when missing a field
|
||||
;; 3. General cleanup of the patterns in run-balance-sheet
|
||||
;; 4. Review ledger dialog
|
||||
|
||||
(defn get-report [{ {:keys [date client] :as qp} :query-params :as request}]
|
||||
(defn get-report [{{:keys [date client] :as qp} :query-params :as request}]
|
||||
(when (and date client)
|
||||
(let [client (if (= :all client) (take 5 (:clients request)) client)
|
||||
date (reverse (sort date ))
|
||||
date (reverse (sort date))
|
||||
client-ids (map :db/id client)
|
||||
_ (doseq [client-id client-ids]
|
||||
(assert-can-see-client (:identity request) client-id))
|
||||
|
||||
|
||||
_ (upsert-running-balance (into #{} client-ids))
|
||||
|
||||
|
||||
lookup-account (->> client-ids
|
||||
(map (fn build-lookup [client-id]
|
||||
[client-id (build-account-lookup client-id)]))
|
||||
@@ -97,50 +93,50 @@
|
||||
args (assoc (:query-params request)
|
||||
:periods (map coerce/to-date (filter identity date)))
|
||||
clients (pull-many (dc/db conn) [:client/code :client/name :db/id] client-ids)
|
||||
|
||||
|
||||
pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients))
|
||||
report (l-reports/summarize-balance-sheet pnl-data) ]
|
||||
report (l-reports/summarize-balance-sheet pnl-data)]
|
||||
(alog/info ::balance-sheet :params args)
|
||||
{:data report
|
||||
:report report})))
|
||||
|
||||
(defn maybe-trim-clients [request client ]
|
||||
(defn maybe-trim-clients [request client]
|
||||
(if (= :all client)
|
||||
(cond-> {:client (take 20 (:clients request))}
|
||||
(> (count (:clients request)) 20)
|
||||
(assoc :warning "You requested a report with more than 20 clients. This report will only contain the first 20."))
|
||||
{:client client}))
|
||||
|
||||
(defn balance-sheet* [{ {:keys [date client] } :query-params :as request}]
|
||||
[:div#report
|
||||
(defn balance-sheet* [{{:keys [date client]} :query-params :as request}]
|
||||
[:div#report
|
||||
(when (and date client)
|
||||
(let [{:keys [client warning]} (maybe-trim-clients request client)
|
||||
{:keys [data report]} (get-report (assoc-in request [:query-params :client] client))
|
||||
client-count (count (set (map :client-id (:data data)))) ]
|
||||
(list
|
||||
[:div.text-2xl.font-bold.text-gray-600 (str "Balance Sheet - " (str/join ", " (map :client/name client))) ]
|
||||
(rtable/table {:widths (cond-> (into [30 ] (repeat 13 client-count))
|
||||
(> (count date) 1) (into (repeat 13 (* 2 client-count (dec (count date)))))
|
||||
(and (> client-count 1) (= (count date) 1)) (conj 13))
|
||||
:investigate-url (bidi.bidi/path-for ssr-routes/only-routes ::route/investigate)
|
||||
:table report
|
||||
:warning (not-empty (str/join "\n " (filter not-empty [warning (:warning report)])))} ))))])
|
||||
client-count (count (set (map :client-id (:data data))))]
|
||||
(list
|
||||
[:div.text-2xl.font-bold.text-gray-600 (str "Balance Sheet - " (str/join ", " (map :client/name client)))]
|
||||
(rtable/table {:widths (cond-> (into [30] (repeat 13 client-count))
|
||||
(> (count date) 1) (into (repeat 13 (* 2 client-count (dec (count date)))))
|
||||
(and (> client-count 1) (= (count date) 1)) (conj 13))
|
||||
:investigate-url (bidi.bidi/path-for ssr-routes/only-routes ::route/investigate)
|
||||
:table report
|
||||
:warning (not-empty (str/join "\n " (filter not-empty [warning (:warning report)])))}))))])
|
||||
|
||||
(defn form* [request & children]
|
||||
(let [params (or (:query-params request) {})]
|
||||
(fc/start-form
|
||||
params
|
||||
params
|
||||
(:form-errors request)
|
||||
[:div#balance-sheet-form.flex.flex-col.gap-4.mt-4
|
||||
[:div.flex.gap-8
|
||||
[:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::route/run-balance-sheet)
|
||||
[:div.flex.gap-8
|
||||
[:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::route/run-balance-sheet)
|
||||
:hx-target "#balance-sheet-form"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-disabled-elt "find fieldset"}
|
||||
[:fieldset
|
||||
[:fieldset
|
||||
[:div.flex.gap-8 {:x-data (hx/json {})}
|
||||
(fc/with-field :client
|
||||
(com/validated-inline-field
|
||||
(com/validated-inline-field
|
||||
{:label "Customers" :errors (fc/field-errors)}
|
||||
(com/multi-typeahead {:name (fc/field-name)
|
||||
:placeholder "Search for companies..."
|
||||
@@ -152,7 +148,7 @@
|
||||
:content-fn :client/name})))
|
||||
(fc/with-field :date
|
||||
(com/validated-inline-field {:label "Date"
|
||||
:errors (fc/field-errors)}
|
||||
:errors (fc/field-errors)}
|
||||
(com/dates-dropdown {:value (fc/field-value)
|
||||
:name (fc/field-name)})))
|
||||
(fc/with-field :include-deltas
|
||||
@@ -161,7 +157,7 @@
|
||||
"Include Deltas"))
|
||||
(com/button {:color :primary :class "w-32"}
|
||||
"Run")
|
||||
(com/button {:formaction (bidi.bidi/path-for ssr-routes/only-routes ::route/export-balance-sheet) } "Export PDF")]]] ]
|
||||
(com/button {:formaction (bidi.bidi/path-for ssr-routes/only-routes ::route/export-balance-sheet)} "Export PDF")]]]]
|
||||
children])))
|
||||
|
||||
(defn form [request]
|
||||
@@ -174,47 +170,47 @@
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/main-aside-nav
|
||||
|
||||
|
||||
:client-selection (:client-selection request)
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
:identity (:identity request)
|
||||
:request request}
|
||||
(apply com/breadcrumbs {} [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
|
||||
"Ledger"]])
|
||||
"Ledger"]])
|
||||
(form* request))
|
||||
"Balance Sheet"))
|
||||
|
||||
(defn make-balance-sheet-pdf [request report]
|
||||
|
||||
|
||||
(let [output-stream (ByteArrayOutputStream.)
|
||||
client-count (count (or (seq (:client (:query-params request)))
|
||||
(seq (:client (:form-params request)))))
|
||||
date (:date (:query-params request)) ]
|
||||
date (:date (:query-params request))]
|
||||
(pdf/pdf
|
||||
(-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15
|
||||
:size :letter
|
||||
:font {:size 6
|
||||
:ttf-name "fonts/calibri-light.ttf"}}
|
||||
[:heading (str "Balance Sheet - " (str/join ", " (map :client/name (or (seq (:client (:query-params request)))
|
||||
(seq (:client (:form-params request)))))))]]
|
||||
(-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15
|
||||
:size :letter
|
||||
:font {:size 6
|
||||
:ttf-name "fonts/calibri-light.ttf"}}
|
||||
[:heading (str "Balance Sheet - " (str/join ", " (map :client/name (or (seq (:client (:query-params request)))
|
||||
(seq (:client (:form-params request)))))))]]
|
||||
|
||||
(conj [:paragraph {:color [128 0 0] :size 9} (:warning report)])
|
||||
(conj
|
||||
(table->pdf report
|
||||
(cond-> (into [30 ] (repeat client-count 13))
|
||||
(> (count date) 1) (into (repeat (* 2 client-count (dec (count date))) 13 ))
|
||||
(and (> client-count 1) (= (count date) 1)) (conj 13)))))
|
||||
output-stream)
|
||||
(conj [:paragraph {:color [128 0 0] :size 9} (:warning report)])
|
||||
(conj
|
||||
(table->pdf report
|
||||
(cond-> (into [30] (repeat client-count 13))
|
||||
(> (count date) 1) (into (repeat (* 2 client-count (dec (count date))) 13))
|
||||
(and (> client-count 1) (= (count date) 1)) (conj 13)))))
|
||||
output-stream)
|
||||
(.toByteArray output-stream)))
|
||||
|
||||
(defn join-names [client-ids]
|
||||
(str/replace (->> client-ids (pull-many (dc/db conn) [:client/name]) (map :client/name) (str/join "-")) #"[^\w]" "_" ))
|
||||
(str/replace (->> client-ids (pull-many (dc/db conn) [:client/name]) (map :client/name) (str/join "-")) #"[^\w]" "_"))
|
||||
|
||||
(defn balance-sheet-args->name [request]
|
||||
(let [date (atime/unparse-local
|
||||
(:date (:query-params request))
|
||||
atime/iso-date)
|
||||
(:date (:query-params request))
|
||||
atime/iso-date)
|
||||
name (->> request :query-params :client (map :db/id) join-names)]
|
||||
(format "Balance-sheet-%s-for-%s" date name)))
|
||||
|
||||
@@ -258,8 +254,7 @@
|
||||
[:span.text-gray-800
|
||||
"Click "
|
||||
(com/link {:href (:report/url bs)} "here")
|
||||
" to download"]
|
||||
]))
|
||||
" to download"]]))
|
||||
nil))
|
||||
:headers (-> {}
|
||||
(assoc "hx-retarget" ".modal-stack")
|
||||
@@ -269,15 +264,14 @@
|
||||
(apply-middleware-to-all-handlers
|
||||
(->
|
||||
{::route/balance-sheet (-> balance-sheet
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-form-4xx-2 balance-sheet))
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-form-4xx-2 balance-sheet))
|
||||
::route/run-balance-sheet (-> form
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-form-4xx-2 form))
|
||||
::route/export-balance-sheet (-> export
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-form-4xx-2 form))}
|
||||
)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-form-4xx-2 form))})
|
||||
(fn [h]
|
||||
(-> h
|
||||
#_(wrap-merge-prior-hx)
|
||||
|
||||
@@ -38,39 +38,37 @@
|
||||
[java.util UUID]
|
||||
[org.apache.commons.io.output ByteArrayOutputStream]))
|
||||
|
||||
|
||||
(def query-schema (mc/schema
|
||||
[:maybe [:map
|
||||
[:client {:unspecified/value :all}
|
||||
[:or
|
||||
[:enum :all]
|
||||
[:vector {:coerce? true :min 1}
|
||||
[:entity-map {:pull [:db/id :client/name]}]]]]
|
||||
|
||||
[:periods {:unspecified/fn (fn [] (let [now (atime/local-now)]
|
||||
[{:start (atime/as-local-time (time/date-time (time/year now)
|
||||
1
|
||||
1))
|
||||
:end (atime/local-now)}])
|
||||
[:client {:unspecified/value :all}
|
||||
[:or
|
||||
[:enum :all]
|
||||
[:vector {:coerce? true :min 1}
|
||||
[:entity-map {:pull [:db/id :client/name]}]]]]
|
||||
|
||||
)}
|
||||
[:vector {:coerce? true}
|
||||
[:map
|
||||
[:start clj-date-schema]
|
||||
[:end clj-date-schema]]]]]]))
|
||||
[:periods {:unspecified/fn (fn [] (let [now (atime/local-now)]
|
||||
[{:start (atime/as-local-time (time/date-time (time/year now)
|
||||
1
|
||||
1))
|
||||
:end (atime/local-now)}]))}
|
||||
|
||||
[:vector {:coerce? true}
|
||||
[:map
|
||||
[:start clj-date-schema]
|
||||
[:end clj-date-schema]]]]]]))
|
||||
;; TODO
|
||||
;; 1. Rerender form when running
|
||||
;; 2. Don't throw crazy errors when missing a field
|
||||
;; 3. General cleanup of the patterns in run-balance-sheet
|
||||
;; 4. Review ledger dialog
|
||||
|
||||
(defn get-report [{ {:keys [periods client] :as qp} :form-params :as request}]
|
||||
(defn get-report [{{:keys [periods client] :as qp} :form-params :as request}]
|
||||
(when (and (seq periods) client)
|
||||
(let [client (if (= :all client) (take 5 (:clients request)) client)
|
||||
client-ids (map :db/id client)
|
||||
_ (doseq [client-id client-ids]
|
||||
(assert-can-see-client (:identity request) client-id))
|
||||
|
||||
|
||||
lookup-account (->> client-ids
|
||||
(map (fn build-lookup [client-id]
|
||||
[client-id (build-account-lookup client-id)]))
|
||||
@@ -90,11 +88,11 @@
|
||||
:account-type (:account_type account)
|
||||
:numeric-code (:numeric_code account)
|
||||
:name (:name account)
|
||||
:period {:start ( coerce/to-date (:start p)) :end ( coerce/to-date (:end p))}}))
|
||||
:period {:start (coerce/to-date (:start p)) :end (coerce/to-date (:end p))}}))
|
||||
args (assoc (:form-params request)
|
||||
:periods (map (fn [d] {:start ( coerce/to-date (:start d)) :end ( coerce/to-date (:end d))}) periods))
|
||||
:periods (map (fn [d] {:start (coerce/to-date (:start d)) :end (coerce/to-date (:end d))}) periods))
|
||||
clients (pull-many (dc/db conn) [:client/code :client/name :db/id] client-ids)
|
||||
|
||||
|
||||
pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients))
|
||||
report (l-reports/summarize-cash-flows pnl-data)]
|
||||
(alog/info ::cash-flows :params args)
|
||||
@@ -108,14 +106,14 @@
|
||||
(assoc :warning "You requested a report with more than 20 clients. This report will only contain the first 20."))
|
||||
{:client client}))
|
||||
|
||||
(defn cash-flows* [{ {:keys [periods client] } :form-params :as request}]
|
||||
[:div#report
|
||||
(defn cash-flows* [{{:keys [periods client]} :form-params :as request}]
|
||||
[:div#report
|
||||
(when (and periods client)
|
||||
(let [{:keys [client warning]} (maybe-trim-clients request client)
|
||||
{:keys [data report]} (get-report (assoc-in request [:form-params :client] client))
|
||||
client-count (count (set (map :client-id (:data data))))
|
||||
client-count (count (set (map :client-id (:data data))))
|
||||
table-contents (concat-tables (:details report))]
|
||||
(list
|
||||
(list
|
||||
[:div.text-2xl.font-bold.text-gray-600 (str "Cash flows - " (str/join ", " (map :client/name client)))]
|
||||
(table {:widths (into [20] (take (dec (cell-count table-contents))
|
||||
(mapcat identity
|
||||
@@ -128,18 +126,18 @@
|
||||
(defn form* [request & children]
|
||||
(let [params (or (:query-params request) {})]
|
||||
(fc/start-form
|
||||
params
|
||||
params
|
||||
(:form-errors request)
|
||||
[:div#cash-flows-form.flex.flex-col.gap-4.mt-4
|
||||
[:div.flex.gap-8
|
||||
[:form {:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/run-cash-flows)
|
||||
[:div.flex.gap-8
|
||||
[:form {:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/run-cash-flows)
|
||||
:hx-target "#cash-flows-form"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-disabled-elt "find fieldset"}
|
||||
[:fieldset
|
||||
[:fieldset
|
||||
[:div.flex.gap-8 {:x-data (hx/json {})}
|
||||
(fc/with-field :client
|
||||
(com/validated-inline-field
|
||||
(com/validated-inline-field
|
||||
{:label "Customers" :errors (fc/field-errors)}
|
||||
(com/multi-typeahead {:name (fc/field-name)
|
||||
:placeholder "Search for companies..."
|
||||
@@ -151,12 +149,12 @@
|
||||
:content-fn :client/name})))
|
||||
(fc/with-field :periods
|
||||
(com/validated-inline-field {:label "Periods"
|
||||
:errors (fc/field-errors)}
|
||||
:errors (fc/field-errors)}
|
||||
(com/periods-dropdown {:value (fc/field-value)})))
|
||||
|
||||
|
||||
(com/button {:color :primary :class "w-32"}
|
||||
"Run")
|
||||
(com/button {:formaction (bidi.bidi/path-for ssr-routes/only-routes ::route/export-cash-flows) } "Export PDF")]]]]
|
||||
(com/button {:formaction (bidi.bidi/path-for ssr-routes/only-routes ::route/export-cash-flows)} "Export PDF")]]]]
|
||||
children])))
|
||||
|
||||
(defn form [request]
|
||||
@@ -169,7 +167,7 @@
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/main-aside-nav
|
||||
|
||||
|
||||
:client-selection (:client-selection request)
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
@@ -181,20 +179,20 @@
|
||||
"Cash Flows"))
|
||||
|
||||
(defn make-cash-flows-pdf [request report]
|
||||
(let [ output-stream (ByteArrayOutputStream.)
|
||||
(let [output-stream (ByteArrayOutputStream.)
|
||||
date (:periods (:form-params request))]
|
||||
(pdf/pdf
|
||||
(-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15
|
||||
:size :letter
|
||||
:font {:size 6
|
||||
:ttf-name "fonts/calibri-light.ttf"}}
|
||||
[:heading (str "Balance Sheet - " (str/join ", " (map :client/name (seq (:client (:form-params request))))))]]
|
||||
(-> [{:left-margin 10 :right-margin 10 :top-margin 15 :bottom-margin 15
|
||||
:size :letter
|
||||
:font {:size 6
|
||||
:ttf-name "fonts/calibri-light.ttf"}}
|
||||
[:heading (str "Balance Sheet - " (str/join ", " (map :client/name (seq (:client (:form-params request))))))]]
|
||||
|
||||
(conj [:paragraph {:color [128 0 0] :size 9} (:warning report)])
|
||||
(conj
|
||||
(table->pdf (concat-tables (:details report))
|
||||
(into [20 ] (mapcat identity (repeat (count date) [ 13 13 13]))))))
|
||||
output-stream)
|
||||
(conj [:paragraph {:color [128 0 0] :size 9} (:warning report)])
|
||||
(conj
|
||||
(table->pdf (concat-tables (:details report))
|
||||
(into [20] (mapcat identity (repeat (count date) [13 13 13]))))))
|
||||
output-stream)
|
||||
(.toByteArray output-stream)))
|
||||
|
||||
(defn join-names [client-ids]
|
||||
@@ -202,8 +200,8 @@
|
||||
|
||||
(defn cash-flows-args->name [request]
|
||||
(let [date (atime/unparse-local
|
||||
(:date (:query-params request))
|
||||
atime/iso-date)
|
||||
(:date (:query-params request))
|
||||
atime/iso-date)
|
||||
name (->> request :query-params :client (map :db/id) join-names)]
|
||||
(format "Balance-sheet-%s-for-%s" date name)))
|
||||
|
||||
@@ -248,7 +246,7 @@
|
||||
"Click "
|
||||
(com/link {:href (:report/url bs)} "here")
|
||||
" to download"]]))
|
||||
|
||||
|
||||
nil))
|
||||
:headers (-> {}
|
||||
(assoc "hx-retarget" ".modal-stack")
|
||||
@@ -258,15 +256,15 @@
|
||||
(apply-middleware-to-all-handlers
|
||||
(->
|
||||
{::route/cash-flows (-> cash-flows
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-form-4xx-2 cash-flows))
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-form-4xx-2 cash-flows))
|
||||
::route/run-cash-flows (-> form
|
||||
(wrap-schema-enforce :form-schema query-schema)
|
||||
(wrap-form-4xx-2 form))
|
||||
(wrap-schema-enforce :form-schema query-schema)
|
||||
(wrap-form-4xx-2 form))
|
||||
::route/export-cash-flows (-> export
|
||||
(wrap-schema-enforce :form-schema query-schema)
|
||||
(wrap-form-4xx-2 form))})
|
||||
|
||||
|
||||
(fn [h]
|
||||
(-> h
|
||||
#_(wrap-merge-prior-hx)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
(ns auto-ap.ssr.ledger.common
|
||||
(ns auto-ap.ssr.ledger.common
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-4 conn
|
||||
merge-query observable-query pull-many]]
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-4 conn
|
||||
merge-query observable-query pull-many]]
|
||||
[auto-ap.datomic.accounts :as d-accounts]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.permissions :refer [can?]]
|
||||
@@ -17,8 +17,8 @@
|
||||
[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 html-response ref->enum-schema
|
||||
strip]]
|
||||
:refer [clj-date-schema entity-id html-response ref->enum-schema
|
||||
strip]]
|
||||
[auto-ap.time :as atime]
|
||||
[auto-ap.utils :refer [dollars-0?]]
|
||||
[bidi.bidi :as bidi]
|
||||
@@ -68,10 +68,10 @@
|
||||
|
||||
(defn filters [request]
|
||||
[:form#ledger-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"}
|
||||
"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)})
|
||||
@@ -102,7 +102,7 @@
|
||||
:value (:invoice-number (:query-params request))
|
||||
:placeholder "e.g., ABC-456"
|
||||
:size :small}))
|
||||
|
||||
|
||||
(com/field {:label "Account Code"}
|
||||
[:div.flex.space-x-4.items-baseline
|
||||
(com/int-input {:name "numeric-code-gte"
|
||||
@@ -140,10 +140,10 @@
|
||||
:value (:amount-lte (:query-params request))
|
||||
:placeholder "9999.34"
|
||||
:size :small})])
|
||||
[:div.mt-4 {:x-data (hx/json { :onlyUnbalanced (:only-unbalanced (:query-params request))})}
|
||||
[:div.mt-4 {:x-data (hx/json {:onlyUnbalanced (:only-unbalanced (:query-params request))})}
|
||||
(com/hidden {:name "only-unbalanced"
|
||||
":value" "onlyUnbalanced ? 'on' : ''"})
|
||||
(com/checkbox {:value (:only-unbalanced (:query-params request))
|
||||
(com/checkbox {:value (:only-unbalanced (:query-params request))
|
||||
:x-model "onlyUnbalanced"}
|
||||
"Show unbalanced")]
|
||||
(exact-match-id* request)]])
|
||||
@@ -163,8 +163,8 @@
|
||||
(filter (fn [[debits credits]]
|
||||
(not (dollars= debits credits))))
|
||||
(map last)
|
||||
(into #{})) ]
|
||||
(for [ result results
|
||||
(into #{}))]
|
||||
(for [result results
|
||||
:when (get unbalanced-ids (last result))]
|
||||
result))
|
||||
results))
|
||||
@@ -245,20 +245,19 @@
|
||||
:args [(or (:numeric-code-gte args) 0) (or (:numeric-code-lte args) 99999)]})
|
||||
|
||||
(seq (:numeric-code args))
|
||||
(merge-query {:query {:in '[ [ [?from-numeric-code ?to-numeric-code] ...]]
|
||||
(merge-query {:query {:in '[[[?from-numeric-code ?to-numeric-code] ...]]
|
||||
:where ['[?li :journal-entry-line/account ?a]
|
||||
'(or-join [?a ?c]
|
||||
[?a :account/numeric-code ?c]
|
||||
[?a :bank-account/numeric-code ?c])
|
||||
'[(>= ?c ?from-numeric-code)]
|
||||
'[(<= ?c ?to-numeric-code)]]}
|
||||
:args [ (map (juxt :from :to ) (:numeric-code args))]})
|
||||
:args [(map (juxt :from :to) (:numeric-code args))]})
|
||||
(seq (:account args))
|
||||
(merge-query {:query {:in ['?a3]
|
||||
:where ['[?li :journal-entry-line/account ?a3]]}
|
||||
:args [(:db/id (:account args))]})
|
||||
|
||||
|
||||
(:amount-gte args)
|
||||
(merge-query {:query {:in ['?amount-gte]
|
||||
:where ['[?e :journal-entry/amount ?a]
|
||||
@@ -317,17 +316,15 @@
|
||||
(apply-only-unbalanced query-params)
|
||||
(apply-pagination query-params))))
|
||||
|
||||
|
||||
#_(dc/q '{:find [ ?sort-vendor (count ?e)],
|
||||
:in [$ [?clients ?start ?end]],
|
||||
:where [[(iol-ion.query/scan-ledger $ ?clients ?start ?end)
|
||||
[[?e _ ?sort-default] ...]]
|
||||
#_(not [?e :journal-entry/vendor])
|
||||
[(missing? $ ?e :journal-entry/vendor)]
|
||||
[(ground "ih") ?sort-vendor]]}
|
||||
(dc/db conn)
|
||||
args
|
||||
)
|
||||
#_(dc/q '{:find [?sort-vendor (count ?e)],
|
||||
:in [$ [?clients ?start ?end]],
|
||||
:where [[(iol-ion.query/scan-ledger $ ?clients ?start ?end)
|
||||
[[?e _ ?sort-default] ...]]
|
||||
#_(not [?e :journal-entry/vendor])
|
||||
[(missing? $ ?e :journal-entry/vendor)]
|
||||
[(ground "ih") ?sort-vendor]]}
|
||||
(dc/db conn)
|
||||
args)
|
||||
|
||||
(def default-read
|
||||
'[:journal-entry/amount
|
||||
@@ -338,7 +335,7 @@ args
|
||||
:db/id
|
||||
[:journal-entry/date :xform clj-time.coerce/from-date]
|
||||
{:journal-entry/vendor [:vendor/name :db/id]
|
||||
:journal-entry/original-entity [:invoice/invoice-number
|
||||
:journal-entry/original-entity [:invoice/invoice-number
|
||||
:invoice/source-url
|
||||
:transaction/description-original :db/id]
|
||||
:journal-entry/client [:client/name :client/code :db/id]
|
||||
@@ -363,32 +360,30 @@ args
|
||||
refunds))
|
||||
|
||||
(defn sum-outstanding [ids]
|
||||
|
||||
(->>
|
||||
(dc/q {:find ['?id '?o]
|
||||
:in ['$ '[?id ...]]
|
||||
:where ['[?id :invoice/outstanding-balance ?o]]}
|
||||
(dc/db conn)
|
||||
ids)
|
||||
|
||||
(->>
|
||||
(dc/q {:find ['?id '?o]
|
||||
:in ['$ '[?id ...]]
|
||||
:where ['[?id :invoice/outstanding-balance ?o]]}
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(map last)
|
||||
(reduce
|
||||
+
|
||||
0.0)))
|
||||
|
||||
(defn sum-total-amount [ids]
|
||||
|
||||
(->>
|
||||
(dc/q {:find ['?id '?o]
|
||||
:in ['$ '[?id ...]]
|
||||
:where ['[?id :invoice/total ?o]]
|
||||
}
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(map last)
|
||||
(reduce
|
||||
+
|
||||
0.0)))
|
||||
|
||||
(->>
|
||||
(dc/q {:find ['?id '?o]
|
||||
:in ['$ '[?id ...]]
|
||||
:where ['[?id :invoice/total ?o]]}
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(map last)
|
||||
(reduce
|
||||
+
|
||||
0.0)))
|
||||
|
||||
(defn fetch-page [request]
|
||||
(let [db (dc/db conn)
|
||||
@@ -413,12 +408,12 @@ args
|
||||
(list
|
||||
|
||||
(if account-name
|
||||
[:div { :x-tooltip (hx/json (str "Running Balance: " (some->> (:journal-entry-line/running-balance jel)
|
||||
(format "$%,.2f"))))}
|
||||
[:div {:x-tooltip (hx/json (str "Running Balance: " (some->> (:journal-entry-line/running-balance jel)
|
||||
(format "$%,.2f"))))}
|
||||
[:div.text-left.underline.cursor-pointer {:x-ref "source"}
|
||||
(:journal-entry-line/location jel) ": "
|
||||
(or (:account/numeric-code account) (:bank-account/numeric-code account))
|
||||
" - " account-name] ]
|
||||
" - " account-name]]
|
||||
[:div.text-left (com/pill {:color :yellow} "Unassigned")])
|
||||
[:div.text-right.text-underline (format "$%,.2f" (key jel))]))
|
||||
|
||||
@@ -436,12 +431,12 @@ args
|
||||
[:amount-gte {:optional true} [:maybe :double]]
|
||||
[:amount-lte {:optional true} [:maybe :double]]
|
||||
[:client-id {:optional true} [:maybe entity-id]]
|
||||
[:only-unbalanced {:optional true }
|
||||
[:only-unbalanced {:optional true}
|
||||
[:maybe [:boolean {:decode/string {:enter #(cond (= % "on") true
|
||||
(= % "") false
|
||||
:else
|
||||
|
||||
(boolean %))}
|
||||
(= % "") false
|
||||
:else
|
||||
|
||||
(boolean %))}
|
||||
:encode/string {:enter #(if % "on" "")}}]]]
|
||||
[:numeric-code {:optional true :decode/string clojure.edn/read-string}
|
||||
[:maybe [:vector [:map [:from nat-int?]
|
||||
@@ -527,103 +522,101 @@ args
|
||||
jel (:journal-entry/line-items je)]
|
||||
(merge jel je)))
|
||||
: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 :journal-entry/client :client/name)])
|
||||
:render-csv (fn [x] (-> x :journal-entry/client :client/name))}
|
||||
: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 :journal-entry/client :client/name)])
|
||||
:render-csv (fn [x] (-> x :journal-entry/client :client/name))}
|
||||
|
||||
{:key "vendor"
|
||||
:name "Vendor"
|
||||
:sort-key "vendor"
|
||||
:render (fn [e] (or (-> e :journal-entry/vendor :vendor/name)
|
||||
[:span.italic.text-gray-400 (-> e :journal-entry/alternate-description)]))
|
||||
:render-csv (fn [e] (or (-> e :journal-entry/vendor :vendor/name)
|
||||
(-> e :journal-entry/alternate-description)))}
|
||||
{:key "source"
|
||||
:name "Source"
|
||||
:sort-key "source"
|
||||
:hide? (fn [args]
|
||||
(not (:external? (:route-params args))))
|
||||
:render :journal-entry/source
|
||||
:render-csv :journal-entry/source}
|
||||
{:key "external-id"
|
||||
:name "External Id"
|
||||
:sort-key "external-id"
|
||||
:class "max-w-[12rem]"
|
||||
:hide? (fn [args]
|
||||
(not (:external? (:route-params args))))
|
||||
:render (fn [x] [:p.truncate (:journal-entry/external-id x)])
|
||||
:render-csv :journal-entry/external-id}
|
||||
{:key "date"
|
||||
:sort-key "date"
|
||||
:name "Date"
|
||||
:show-starting "lg"
|
||||
:render (fn [{:journal-entry/keys [date]}]
|
||||
(some-> date (atime/unparse-local atime/normal-date)))}
|
||||
{:key "amount"
|
||||
:sort-key "amount"
|
||||
:name "Amount"
|
||||
:show-starting "lg"
|
||||
:render (fn [{:journal-entry/keys [amount]}]
|
||||
(some->> amount
|
||||
(format "$%,.2f")))}
|
||||
{:key "account"
|
||||
:name "Account"
|
||||
:sort-key "account"
|
||||
:class "text-right"
|
||||
:render-csv #(or (-> % :journal-entry-line/account :account/name)
|
||||
(-> % :journal-entry-line/account :bank-account/name))
|
||||
:render-for #{:csv}}
|
||||
{:key "debit"
|
||||
:name "Debit"
|
||||
:class "text-right"
|
||||
:render (partial render-lines :journal-entry-line/debit)
|
||||
:render-csv :journal-entry-line/debit}
|
||||
{:key "vendor"
|
||||
:name "Vendor"
|
||||
:sort-key "vendor"
|
||||
:render (fn [e] (or (-> e :journal-entry/vendor :vendor/name)
|
||||
[:span.italic.text-gray-400 (-> e :journal-entry/alternate-description)]))
|
||||
:render-csv (fn [e] (or (-> e :journal-entry/vendor :vendor/name)
|
||||
(-> e :journal-entry/alternate-description)))}
|
||||
{:key "source"
|
||||
:name "Source"
|
||||
:sort-key "source"
|
||||
:hide? (fn [args]
|
||||
(not (:external? (:route-params args))))
|
||||
:render :journal-entry/source
|
||||
:render-csv :journal-entry/source}
|
||||
{:key "external-id"
|
||||
:name "External Id"
|
||||
:sort-key "external-id"
|
||||
:class "max-w-[12rem]"
|
||||
:hide? (fn [args]
|
||||
(not (:external? (:route-params args))))
|
||||
:render (fn [x] [:p.truncate (:journal-entry/external-id x)])
|
||||
:render-csv :journal-entry/external-id}
|
||||
{:key "date"
|
||||
:sort-key "date"
|
||||
:name "Date"
|
||||
:show-starting "lg"
|
||||
:render (fn [{:journal-entry/keys [date]}]
|
||||
(some-> date (atime/unparse-local atime/normal-date)))}
|
||||
{:key "amount"
|
||||
:sort-key "amount"
|
||||
:name "Amount"
|
||||
:show-starting "lg"
|
||||
:render (fn [{:journal-entry/keys [amount]}]
|
||||
(some->> amount
|
||||
(format "$%,.2f")))}
|
||||
{:key "account"
|
||||
:name "Account"
|
||||
:sort-key "account"
|
||||
:class "text-right"
|
||||
:render-csv #(or (-> % :journal-entry-line/account :account/name)
|
||||
(-> % :journal-entry-line/account :bank-account/name))
|
||||
:render-for #{:csv}}
|
||||
{:key "debit"
|
||||
:name "Debit"
|
||||
:class "text-right"
|
||||
:render (partial render-lines :journal-entry-line/debit)
|
||||
:render-csv :journal-entry-line/debit}
|
||||
|
||||
{:key "credit"
|
||||
:name "Credit"
|
||||
:class "text-right"
|
||||
:render (partial render-lines :journal-entry-line/credit)
|
||||
:render-csv :journal-entry-line/credit}
|
||||
|
||||
{:key "links"
|
||||
:name "Links"
|
||||
:show-starting "lg"
|
||||
:class "w-8"
|
||||
:render (fn [i]
|
||||
(link-dropdown
|
||||
(cond-> []
|
||||
(-> i :journal-entry/original-entity :invoice/invoice-number)
|
||||
(conj
|
||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/all-page)
|
||||
{:exact-match-id (:db/id (:journal-entry/original-entity i))})
|
||||
:color :primary
|
||||
:content (format "Invoice '%s'" (-> i :journal-entry/original-entity :invoice/invoice-number))})
|
||||
(-> i :journal-entry/original-entity :invoice/source-url)
|
||||
{:link (-> i :journal-entry/original-entity :invoice/source-url)
|
||||
:color :secondary
|
||||
:content (str "File")}
|
||||
|
||||
{:key "credit"
|
||||
:name "Credit"
|
||||
:class "text-right"
|
||||
:render (partial render-lines :journal-entry-line/credit)
|
||||
:render-csv :journal-entry-line/credit}
|
||||
|
||||
{:key "links"
|
||||
:name "Links"
|
||||
:show-starting "lg"
|
||||
:class "w-8"
|
||||
:render (fn [i]
|
||||
(link-dropdown
|
||||
(cond-> []
|
||||
(-> i :journal-entry/original-entity :invoice/invoice-number)
|
||||
(conj
|
||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/all-page)
|
||||
{:exact-match-id (:db/id (:journal-entry/original-entity i))})
|
||||
:color :primary
|
||||
:content (format "Invoice '%s'" (-> i :journal-entry/original-entity :invoice/invoice-number))})
|
||||
(-> i :journal-entry/original-entity :invoice/source-url)
|
||||
{:link (-> i :journal-entry/original-entity :invoice/source-url)
|
||||
:color :secondary
|
||||
:content (str "File")}
|
||||
|
||||
(-> i :journal-entry/original-entity :transaction/description-original)
|
||||
(conj
|
||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::transaction-routes/all-page)
|
||||
{:exact-match-id (:db/id (:journal-entry/original-entity i))})
|
||||
:color :primary
|
||||
:content (format "Transaction '%s'" (-> i :journal-entry/original-entity :transaction/description-original))})
|
||||
(-> i :journal-entry/memo)
|
||||
(conj {:color :secondary
|
||||
:content (str "Memo: " (:journal-entry/memo i))}))))
|
||||
:render-for #{:html}}]}))
|
||||
(-> i :journal-entry/original-entity :transaction/description-original)
|
||||
(conj
|
||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::transaction-routes/all-page)
|
||||
{:exact-match-id (:db/id (:journal-entry/original-entity i))})
|
||||
:color :primary
|
||||
:content (format "Transaction '%s'" (-> i :journal-entry/original-entity :transaction/description-original))})
|
||||
(-> i :journal-entry/memo)
|
||||
(conj {:color :secondary
|
||||
:content (str "Memo: " (:journal-entry/memo i))}))))
|
||||
:render-for #{:html}}]}))
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
@@ -1,4 +1,4 @@
|
||||
(ns auto-ap.ssr.ledger.investigate
|
||||
(ns auto-ap.ssr.ledger.investigate
|
||||
(:require
|
||||
[auto-ap.permissions :refer [wrap-must]]
|
||||
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||
@@ -14,37 +14,33 @@
|
||||
wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]))
|
||||
|
||||
|
||||
(def altered-grid-page
|
||||
(assoc grid-page
|
||||
(def altered-grid-page
|
||||
(assoc grid-page
|
||||
:id "yoho"
|
||||
:raw? true
|
||||
:raw? true
|
||||
:check-boxes? false
|
||||
:route ::route/investigate-results))
|
||||
|
||||
|
||||
(defn investigate [request]
|
||||
(modal-response
|
||||
(modal-response
|
||||
(com/modal {:class "max-h-[600px]"}
|
||||
(com/modal-card {:hx-vals (hx/json (cond-> (:query-params request)
|
||||
true (update :numeric-code pr-str)
|
||||
(:start-date (:query-params request)) (update :start-date #(some-> (atime/unparse-local % atime/normal-date)))
|
||||
(:end-date (:query-params request)) (update :end-date #(some-> (atime/unparse-local % atime/normal-date))))) }
|
||||
[:div "Ledger entries"]
|
||||
(table*
|
||||
altered-grid-page
|
||||
identity
|
||||
request
|
||||
#_(assoc-in request [:query-params :sort] [{:sort-key "date" :asc? false :name "Date"}]))
|
||||
nil)
|
||||
)))
|
||||
(com/modal-card {:hx-vals (hx/json (cond-> (:query-params request)
|
||||
true (update :numeric-code pr-str)
|
||||
(:start-date (:query-params request)) (update :start-date #(some-> (atime/unparse-local % atime/normal-date)))
|
||||
(:end-date (:query-params request)) (update :end-date #(some-> (atime/unparse-local % atime/normal-date)))))}
|
||||
[:div "Ledger entries"]
|
||||
(table*
|
||||
altered-grid-page
|
||||
identity
|
||||
request
|
||||
#_(assoc-in request [:query-params :sort] [{:sort-key "date" :asc? false :name "Date"}]))
|
||||
nil))))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(->
|
||||
{::route/investigate investigate
|
||||
::route/investigate-results (helper/table-route altered-grid-page :push-url? false)}
|
||||
)
|
||||
{::route/investigate investigate
|
||||
::route/investigate-results (helper/table-route altered-grid-page :push-url? false)})
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-apply-sort grid-page)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(ns auto-ap.ssr.ledger.new
|
||||
(ns auto-ap.ssr.ledger.new
|
||||
(:require
|
||||
[auto-ap.datomic :refer [audit-transact conn pull-attr]]
|
||||
[auto-ap.datomic.accounts :as d-accounts]
|
||||
@@ -26,25 +26,25 @@
|
||||
[datomic.api :as dc]
|
||||
[iol-ion.query :refer [dollars=]]
|
||||
[iol-ion.utils :refer [remove-nils]])
|
||||
(:import
|
||||
[java.util UUID]))
|
||||
(:import
|
||||
[java.util UUID]))
|
||||
|
||||
(def new-ledger-schema
|
||||
[:and
|
||||
[:map
|
||||
[:db/id {:optional true} [:maybe entity-id]]
|
||||
[:journal-entry/client {:optional false} [:entity-map {:pull [:db/id :client/name :client/locations] }]]
|
||||
[:journal-entry/client {:optional false} [:entity-map {:pull [:db/id :client/name :client/locations]}]]
|
||||
[:journal-entry/date clj-date-schema]
|
||||
[:journal-entry/memo {:optional true} [:maybe [ :string {:decode/string strip}]]]
|
||||
[:journal-entry/memo {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:journal-entry/vendor {:optional false :default nil}
|
||||
[:entity-map {:pull [:db/id :vendor/name] }]]
|
||||
[:entity-map {:pull [:db/id :vendor/name]}]]
|
||||
[:journal-entry/amount {:min 0.01}
|
||||
money]
|
||||
[:journal-entry/line-items
|
||||
[:vector {:coerce? true}
|
||||
[:and
|
||||
[:map
|
||||
[:journal-entry-line/account [:and [:entity-map {:pull a/default-read }]
|
||||
[:journal-entry-line/account [:and [:entity-map {:pull a/default-read}]
|
||||
[:fn {:error/message "Not an allowed account."}
|
||||
(fn check-allow [x]
|
||||
(check-allowance (:db/id x) :account/default-allowance))]]]
|
||||
@@ -81,18 +81,18 @@
|
||||
:value value
|
||||
:content-fn (fn [value]
|
||||
(when value
|
||||
(str
|
||||
(:account/numeric-code value)
|
||||
" - "
|
||||
(:account/name (d-accounts/clientize value
|
||||
client-id)))))})])
|
||||
(str
|
||||
(:account/numeric-code value)
|
||||
" - "
|
||||
(:account/name (d-accounts/clientize value
|
||||
client-id)))))})])
|
||||
|
||||
(defn- location-select*
|
||||
[{:keys [name account-location client-locations value]}]
|
||||
(com/select {:options (into [["" ""]]
|
||||
(cond account-location
|
||||
[[account-location account-location]]
|
||||
|
||||
|
||||
:else
|
||||
(for [c (seq client-locations)]
|
||||
[c c])))
|
||||
@@ -198,19 +198,19 @@
|
||||
(com/hidden {:name (fc/field-name)
|
||||
:value (:db/id (:client request))})
|
||||
[:div.w-96
|
||||
(com/validated-field
|
||||
{:label "Client"
|
||||
:errors (fc/field-errors)}
|
||||
[:div.w-96
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:error? (fc/error?)
|
||||
:class "w-96"
|
||||
:placeholder "Search..."
|
||||
:url (bidi/path-for ssr-routes/only-routes :company-search)
|
||||
:value (fc/field-value)
|
||||
:value-fn :db/id
|
||||
:content-fn :client/name
|
||||
:x-model "clientId"})])]))
|
||||
(com/validated-field
|
||||
{:label "Client"
|
||||
:errors (fc/field-errors)}
|
||||
[:div.w-96
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:error? (fc/error?)
|
||||
:class "w-96"
|
||||
:placeholder "Search..."
|
||||
:url (bidi/path-for ssr-routes/only-routes :company-search)
|
||||
:value (fc/field-value)
|
||||
:value-fn :db/id
|
||||
:content-fn :client/name
|
||||
:x-model "clientId"})])]))
|
||||
(fc/with-field :journal-entry/date
|
||||
(com/validated-field
|
||||
{:label "Date"
|
||||
@@ -245,18 +245,18 @@
|
||||
:class "w-24"
|
||||
:error? (fc/field-errors)
|
||||
:placeholder "212.44"})]))
|
||||
(fc/with-field :journal-entry/memo
|
||||
(fc/with-field :journal-entry/memo
|
||||
[:div.w-96
|
||||
(com/validated-field
|
||||
{:label "Memo"
|
||||
:errors (fc/field-errors)}
|
||||
[:div.w-96
|
||||
(com/text-input {:name (fc/field-name)
|
||||
:error? (fc/error?)
|
||||
:class "w-96"
|
||||
:placeholder "A custom note"
|
||||
:url (bidi/path-for ssr-routes/only-routes :company-search)
|
||||
:value (fc/field-value) })])])
|
||||
:error? (fc/error?)
|
||||
:class "w-96"
|
||||
:placeholder "A custom note"
|
||||
:url (bidi/path-for ssr-routes/only-routes :company-search)
|
||||
:value (fc/field-value)})])])
|
||||
(fc/with-field :journal-entry/line-items
|
||||
(com/validated-field
|
||||
{:errors (fc/field-errors)}
|
||||
@@ -273,7 +273,6 @@
|
||||
:tr-params (hx/bind-alpine-vals {} {"client-id" "clientId"})}
|
||||
"New account"))))])))
|
||||
|
||||
|
||||
(defn new [request]
|
||||
(modal-response
|
||||
(com/modal {:hx-target "this"
|
||||
@@ -282,7 +281,7 @@
|
||||
::route/new-submit)}
|
||||
(com/modal-card {:class "md:h-[800px] md:w-[750px] flex-col relative"
|
||||
:error (when (vector? (:form-errors request))
|
||||
(str/join ", "(:form-errors request) ))}
|
||||
(str/join ", " (:form-errors request)))}
|
||||
[:div "New ledger entry"]
|
||||
[:div.overflow-y-scroll.relative (form* request)]
|
||||
[:div (com/button {:color :primary} "Save")])])))
|
||||
@@ -296,10 +295,10 @@
|
||||
(update :journal-entry/line-items
|
||||
(fn [lis]
|
||||
(mapv
|
||||
#(remove-nils (-> %
|
||||
(update :journal-entry-line/account :db/id)
|
||||
(assoc :journal-entry-line/client (-> request :form-params :journal-entry/client :db/id)
|
||||
:journal-entry-line/date (-> request :form-params :journal-entry/date coerce/to-date))))
|
||||
#(remove-nils (-> %
|
||||
(update :journal-entry-line/account :db/id)
|
||||
(assoc :journal-entry-line/client (-> request :form-params :journal-entry/client :db/id)
|
||||
:journal-entry-line/date (-> request :form-params :journal-entry/date coerce/to-date))))
|
||||
lis)))
|
||||
(assoc :journal-entry/external-id (str "manual-" (UUID/randomUUID))))
|
||||
(= :post (:request-method request)) (assoc :db/id "new"))
|
||||
@@ -308,9 +307,9 @@
|
||||
:client/ledger-last-change (iol-ion.tx.upsert-ledger/current-date (dc/db conn))}]
|
||||
(:identity request))
|
||||
updated-entity (dc/pull (dc/db conn)
|
||||
ledger.common/default-read
|
||||
(or (get tempids (:db/id entity)) (:db/id entity)))]
|
||||
|
||||
ledger.common/default-read
|
||||
(or (get tempids (:db/id entity)) (:db/id entity)))]
|
||||
|
||||
(html-response
|
||||
(ledger.common/row* identity updated-entity
|
||||
{:flash? true
|
||||
@@ -323,7 +322,6 @@
|
||||
(assoc "hx-retarget" "#entity-table tbody"
|
||||
"hx-reswap" "afterbegin")))))
|
||||
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(->
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
[java.util UUID]
|
||||
[org.apache.commons.io.output ByteArrayOutputStream]))
|
||||
|
||||
|
||||
(def query-schema (mc/schema
|
||||
[:maybe [:map
|
||||
[:client {:unspecified/value :all}
|
||||
@@ -71,14 +70,14 @@
|
||||
;; 4. Review ledger dialog
|
||||
;; 5. pagination and filtering within dialog. looks weird with the full screen refresh
|
||||
|
||||
(defn get-report [{ {:keys [periods client] :as qp} :form-params :as request}]
|
||||
(defn get-report [{{:keys [periods client] :as qp} :form-params :as request}]
|
||||
(when (and (seq periods) client)
|
||||
(let [client (if (= :all client) (take 5 (:clients request)) client)
|
||||
client-ids (map :db/id client)
|
||||
_ (upsert-running-balance (into #{} client-ids))
|
||||
_ (doseq [client-id client-ids]
|
||||
(assert-can-see-client (:identity request) client-id))
|
||||
|
||||
|
||||
lookup-account (->> client-ids
|
||||
(map (fn build-lookup [client-id]
|
||||
[client-id (build-account-lookup client-id)]))
|
||||
@@ -103,13 +102,13 @@
|
||||
:numeric-code (:numeric_code account)
|
||||
:name (:name account)
|
||||
:sample sample
|
||||
:period {:start ( coerce/to-date (:start p)) :end (coerce/to-date (:end p))}}))
|
||||
|
||||
:period {:start (coerce/to-date (:start p)) :end (coerce/to-date (:end p))}}))
|
||||
|
||||
args (assoc (:form-params request)
|
||||
:periods (map (fn [d]
|
||||
{:start ( coerce/to-date (:start d)) :end ( coerce/to-date (:end d))}) periods))
|
||||
:periods (map (fn [d]
|
||||
{:start (coerce/to-date (:start d)) :end (coerce/to-date (:end d))}) periods))
|
||||
clients (pull-many (dc/db conn) [:client/code :client/name :db/id :client/feature-flags] client-ids)
|
||||
|
||||
|
||||
pnl-data (l-reports/->PNLData args data (by :db/id :client/code clients))
|
||||
#_#__ (clojure.pprint/pprint pnl-data)
|
||||
report (l-reports/summarize-pnl pnl-data)]
|
||||
@@ -124,14 +123,14 @@
|
||||
(assoc :warning "You requested a report with more than 20 clients. This report will only contain the first 20."))
|
||||
{:client client}))
|
||||
|
||||
(defn profit-and-loss* [{ {:keys [periods client] } :form-params :as request}]
|
||||
[:div#report
|
||||
(defn profit-and-loss* [{{:keys [periods client]} :form-params :as request}]
|
||||
[:div#report
|
||||
(when (and periods client)
|
||||
(let [{:keys [client warning]} (maybe-trim-clients request client)
|
||||
{:keys [data report]} (get-report (assoc-in request [:form-params :client] client))
|
||||
client-count (count (set (map :client-id (:data data))))
|
||||
client-count (count (set (map :client-id (:data data))))
|
||||
table-contents (concat-tables (concat (:summaries report) (:details report)))]
|
||||
(list
|
||||
(list
|
||||
[:div.text-2xl.font-bold.text-gray-600 (str "Profit and loss - " (str/join ", " (map :client/name client)))]
|
||||
(table {:widths (into [20] (take (dec (cell-count table-contents))
|
||||
(mapcat identity
|
||||
@@ -148,14 +147,11 @@
|
||||
{:subject :history
|
||||
:activity :view})
|
||||
(for [n (:invalid-ids report)]
|
||||
[:div
|
||||
[:div
|
||||
(com/link {:href (str (bidi/path-for ssr-routes/only-routes
|
||||
:admin-history)
|
||||
"/" n)}
|
||||
"Sample")]))]
|
||||
}))))])
|
||||
|
||||
|
||||
"Sample")]))]}))))])
|
||||
|
||||
(defn form* [request & children]
|
||||
(let [params (or (:query-params request) {})]
|
||||
@@ -209,7 +205,7 @@
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/main-aside-nav
|
||||
|
||||
|
||||
:client-selection (:client-selection request)
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
@@ -259,8 +255,8 @@
|
||||
|
||||
(defn profit-and-loss-args->name [request]
|
||||
(let [date (atime/unparse-local
|
||||
(:date (:query-params request))
|
||||
atime/iso-date)
|
||||
(:date (:query-params request))
|
||||
atime/iso-date)
|
||||
name (->> request :query-params :client (map :db/id) join-names)]
|
||||
(format "Profit-and-loss-%s-for-%s" date name)))
|
||||
|
||||
@@ -268,7 +264,7 @@
|
||||
(let [uuid (str (UUID/randomUUID))
|
||||
{:keys [client warning]} (maybe-trim-clients request (:client (:form-params request)))
|
||||
request (assoc-in request [:form-params :client] client)
|
||||
pdf-data (binding [*report-pedantic* (boolean ((set (:client/feature-flags (first client)))
|
||||
pdf-data (binding [*report-pedantic* (boolean ((set (:client/feature-flags (first client)))
|
||||
"report-pedantic"))] (make-profit-and-loss-pdf request (:report (get-report request))))
|
||||
name (profit-and-loss-args->name request)
|
||||
key (str "reports/profit-and-loss/" uuid "/" name ".pdf")
|
||||
@@ -306,7 +302,7 @@
|
||||
"Click "
|
||||
(com/link {:href (:report/url bs)} "here")
|
||||
" to download"]]))
|
||||
|
||||
|
||||
nil))
|
||||
:headers (-> {}
|
||||
(assoc "hx-retarget" ".modal-stack")
|
||||
@@ -316,15 +312,15 @@
|
||||
(apply-middleware-to-all-handlers
|
||||
(->
|
||||
{::route/profit-and-loss (-> profit-and-loss
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-form-4xx-2 profit-and-loss))
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-form-4xx-2 profit-and-loss))
|
||||
::route/run-profit-and-loss (-> form
|
||||
(wrap-schema-enforce :form-schema query-schema)
|
||||
(wrap-form-4xx-2 form))
|
||||
(wrap-schema-enforce :form-schema query-schema)
|
||||
(wrap-form-4xx-2 form))
|
||||
::route/export-profit-and-loss (-> export
|
||||
(wrap-schema-enforce :form-schema query-schema)
|
||||
(wrap-form-4xx-2 form))})
|
||||
|
||||
(wrap-schema-enforce :form-schema query-schema)
|
||||
(wrap-form-4xx-2 form))})
|
||||
|
||||
(fn [h]
|
||||
(-> h
|
||||
#_(wrap-merge-prior-hx)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
(ns auto-ap.ssr.ledger.report-table
|
||||
(ns auto-ap.ssr.ledger.report-table
|
||||
(:require
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.time :as atime]
|
||||
@@ -7,22 +7,19 @@
|
||||
[hiccup.util :as hu]
|
||||
[iol-ion.query :as query]))
|
||||
|
||||
|
||||
|
||||
(defn cell [{:keys [width investigate-url other-style]} c]
|
||||
(let [cell-contents (cond
|
||||
|
||||
|
||||
(= :dollar (:format c))
|
||||
(format "$%,.2f" (if (query/dollars-0? (:value c))
|
||||
0.0
|
||||
(:value c)))
|
||||
|
||||
|
||||
|
||||
(= :percent (:format c))
|
||||
(format "%%%.1f" (if (query/dollars-0? (:value c))
|
||||
0.0
|
||||
(* 100.0 (or (:value c) 0.0))))
|
||||
|
||||
|
||||
:else
|
||||
(str (:value c)))
|
||||
cell-contents (if (:filters c)
|
||||
@@ -32,8 +29,7 @@
|
||||
(inst? (:date-range (:filters c))) (assoc :end-date (atime/unparse-local (coerce/to-date-time (:date-range (:filters c))) atime/normal-date))
|
||||
(:end (:date-range (:filters c))) (assoc :end-date (atime/unparse-local (coerce/to-date-time (:end (:date-range (:filters c)))) atime/normal-date))
|
||||
(:start (:date-range (:filters c))) (assoc :start-date (atime/unparse-local (coerce/to-date-time (:start (:date-range (:filters c)))) atime/normal-date))
|
||||
(:client-id (:filters c)) (assoc :client-id (:client-id (:filters c))))
|
||||
)}
|
||||
(:client-id (:filters c)) (assoc :client-id (:client-id (:filters c)))))}
|
||||
cell-contents)
|
||||
cell-contents)]
|
||||
[:td.px-4.py-2
|
||||
@@ -44,10 +40,9 @@
|
||||
(fn [s]
|
||||
(->> (:border c)
|
||||
(map
|
||||
(fn [b]
|
||||
[(keyword (str "border-" (name b))) "1px solid black"])
|
||||
)
|
||||
(into s))))
|
||||
(fn [b]
|
||||
[(keyword (str "border-" (name b))) "1px solid black"]))
|
||||
(into s))))
|
||||
(:colspan c) (assoc :colspan (:colspan c))
|
||||
(:align c) (assoc :align (:align c))
|
||||
(= :dollar (:format c)) (assoc :align :right)
|
||||
@@ -57,10 +52,10 @@
|
||||
(str/join ","
|
||||
(:color c))
|
||||
")"))
|
||||
true (assoc-in [:style :background-color] (str "rgb("
|
||||
(str/join ","
|
||||
(or (:bg-color c) [255 255 255]))
|
||||
")")))
|
||||
true (assoc-in [:style :background-color] (str "rgb("
|
||||
(str/join ","
|
||||
(or (:bg-color c) [255 255 255]))
|
||||
")")))
|
||||
|
||||
cell-contents]))
|
||||
|
||||
@@ -72,47 +67,47 @@
|
||||
|
||||
(defn table [{:keys [table widths investigate-url warning]}]
|
||||
(let [cell-count (cell-count table)]
|
||||
(com/content-card {:class "inline-block overflow-scroll"}
|
||||
[:div {:class "overflow-scroll h-[70vh] m-4 inline-block"}
|
||||
(when warning [:div.rounded.bg-red-50.text-red-800.p-4.m-2
|
||||
warning])
|
||||
(-> [:table {:class "text-sm text-left text-gray-500 dark:text-gray-400"}
|
||||
[:thead {:class "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 font-bold"}
|
||||
(map
|
||||
(fn [header-row header]
|
||||
(into
|
||||
[:tr {:class " dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"}]
|
||||
(map
|
||||
(fn [w header i]
|
||||
(cell {:width w
|
||||
:investigate-url investigate-url
|
||||
:other-style {:position "sticky"
|
||||
:top (* header-row (+ 22 18))}} header))
|
||||
widths
|
||||
header
|
||||
(range))))
|
||||
(range)
|
||||
(:header table))]]
|
||||
|
||||
(conj
|
||||
(-> [:tbody {:style {}}]
|
||||
(into
|
||||
(for [[i row] (map vector (range) (:rows table))]
|
||||
|
||||
[:tr {:class " dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"}
|
||||
(for [[i c] (map vector (range) (take cell-count
|
||||
(reduce
|
||||
(fn [[acc cnt] cur]
|
||||
(if (>= (+ cnt (:colspan cur 1)) cell-count)
|
||||
(reduced (conj acc cur))
|
||||
[(conj acc cur) (+ cnt (:colspan cur 1))]))
|
||||
[[] 0]
|
||||
(concat row (repeat nil)))))]
|
||||
|
||||
(cell {:investigate-url investigate-url} c))]))
|
||||
(conj [:tr (for [i (range cell-count)]
|
||||
|
||||
(cell {:investigate-url investigate-url} {:value " "}))]))))])))
|
||||
(com/content-card {:class "inline-block overflow-scroll"}
|
||||
[:div {:class "overflow-scroll h-[70vh] m-4 inline-block"}
|
||||
(when warning [:div.rounded.bg-red-50.text-red-800.p-4.m-2
|
||||
warning])
|
||||
(-> [:table {:class "text-sm text-left text-gray-500 dark:text-gray-400"}
|
||||
[:thead {:class "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 font-bold"}
|
||||
(map
|
||||
(fn [header-row header]
|
||||
(into
|
||||
[:tr {:class " dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"}]
|
||||
(map
|
||||
(fn [w header i]
|
||||
(cell {:width w
|
||||
:investigate-url investigate-url
|
||||
:other-style {:position "sticky"
|
||||
:top (* header-row (+ 22 18))}} header))
|
||||
widths
|
||||
header
|
||||
(range))))
|
||||
(range)
|
||||
(:header table))]]
|
||||
|
||||
(conj
|
||||
(-> [:tbody {:style {}}]
|
||||
(into
|
||||
(for [[i row] (map vector (range) (:rows table))]
|
||||
|
||||
[:tr {:class " dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700"}
|
||||
(for [[i c] (map vector (range) (take cell-count
|
||||
(reduce
|
||||
(fn [[acc cnt] cur]
|
||||
(if (>= (+ cnt (:colspan cur 1)) cell-count)
|
||||
(reduced (conj acc cur))
|
||||
[(conj acc cur) (+ cnt (:colspan cur 1))]))
|
||||
[[] 0]
|
||||
(concat row (repeat nil)))))]
|
||||
|
||||
(cell {:investigate-url investigate-url} c))]))
|
||||
(conj [:tr (for [i (range cell-count)]
|
||||
|
||||
(cell {:investigate-url investigate-url} {:value " "}))]))))])))
|
||||
|
||||
(defn concat-tables [tables]
|
||||
(let [[first & rest] tables]
|
||||
@@ -120,8 +115,8 @@
|
||||
:rows (concat (:rows first)
|
||||
[[]]
|
||||
(mapcat
|
||||
(fn [table]
|
||||
(-> (:header table)
|
||||
(into (:rows table))
|
||||
(conj [])))
|
||||
rest))}))
|
||||
(fn [table]
|
||||
(-> (:header table)
|
||||
(into (:rows table))
|
||||
(conj [])))
|
||||
rest))}))
|
||||
|
||||
@@ -39,11 +39,11 @@
|
||||
"Return a list of name-value pairs for a parameter map."
|
||||
[params]
|
||||
(mapcat
|
||||
(fn [[name value]]
|
||||
(if (and (sequential? value) (not (coll? (first value))))
|
||||
(for [v value] [name v])
|
||||
[[name value]]))
|
||||
params))
|
||||
(fn [[name value]]
|
||||
(if (and (sequential? value) (not (coll? (first value))))
|
||||
(for [v value] [name v])
|
||||
[[name value]]))
|
||||
params))
|
||||
|
||||
(defn- nest-params
|
||||
"Takes a flat map of parameters and turns it into a nested map of
|
||||
@@ -51,10 +51,10 @@
|
||||
into keys."
|
||||
[params parse]
|
||||
(reduce
|
||||
(fn [m [k v]]
|
||||
(assoc-nested m (parse k) v))
|
||||
{}
|
||||
(param-pairs params)))
|
||||
(fn [m [k v]]
|
||||
(assoc-nested m (parse k) v))
|
||||
{}
|
||||
(param-pairs params)))
|
||||
|
||||
(defn nested-params-request
|
||||
"Converts a request with a flat map of parameters to a nested map.
|
||||
|
||||
@@ -2,21 +2,20 @@
|
||||
(:require [auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.ui :refer [base-page]]))
|
||||
|
||||
|
||||
(defn page [{:keys [identity matched-route] :as request}]
|
||||
(base-page
|
||||
request
|
||||
(com/page { :request request
|
||||
(com/page {:request request
|
||||
:client (:client request)
|
||||
:clients (:clients request)
|
||||
:identity (:identity request)
|
||||
:app-params {}}
|
||||
#_(com/breadcrumbs {}
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company)}
|
||||
"My Company"])
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company)}
|
||||
"My Company"])
|
||||
[:div.flex.items-center.justify-center.flex-col
|
||||
|
||||
|
||||
[:div.text-2xl.font-bold.text-gray-600 "Page not found"]
|
||||
[:p.text-gray-500 "Sorry, we can't find the page you're looking for. Try going " (com/link {:href "/"} "home") " and try again."]])
|
||||
"Not found"))
|
||||
@@ -120,8 +120,6 @@
|
||||
:value (-> (fc/field-value)
|
||||
(atime/unparse-local atime/normal-date))})]))
|
||||
|
||||
|
||||
|
||||
(fc/with-field-default :outgoing-invoice/line-items [{:db/id "first"}]
|
||||
(com/validated-field {:errors (fc/field-errors)
|
||||
:label "Line items"}
|
||||
@@ -280,6 +278,6 @@
|
||||
(add-new-entity-handler [:outgoing-invoice/line-items]
|
||||
(fn render [cursor request]
|
||||
(line-item
|
||||
{:value cursor }))
|
||||
{:value cursor}))
|
||||
(fn build-new-row [base _]
|
||||
base)))})
|
||||
@@ -1,9 +1,9 @@
|
||||
(ns auto-ap.ssr.payments
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact conn merge-query observable-query
|
||||
pull-many]]
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3
|
||||
audit-transact conn merge-query observable-query
|
||||
pull-many]]
|
||||
[auto-ap.graphql.utils :refer [assert-can-see-client
|
||||
exception->notification extract-client-ids
|
||||
notify-if-locked]]
|
||||
@@ -14,7 +14,7 @@
|
||||
[auto-ap.routes.payments :as route]
|
||||
[auto-ap.routes.transactions :as transaction-routes]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.components.bank-account-icon :as bank-account-icon]
|
||||
@@ -24,11 +24,11 @@
|
||||
[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 clj-date-schema
|
||||
dissoc-nil-transformer entity-id html-response
|
||||
main-transformer modal-response ref->enum-schema strip
|
||||
wrap-entity wrap-implied-route-param wrap-merge-prior-hx
|
||||
wrap-schema-enforce]]
|
||||
:refer [apply-middleware-to-all-handlers clj-date-schema
|
||||
dissoc-nil-transformer entity-id html-response
|
||||
main-transformer modal-response ref->enum-schema strip
|
||||
wrap-entity wrap-implied-route-param wrap-merge-prior-hx
|
||||
wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[clj-time.coerce :as coerce]
|
||||
@@ -105,19 +105,18 @@
|
||||
:size :small})])
|
||||
(com/field {:label "Payment Type"}
|
||||
(com/radio-card {:size :small
|
||||
:name "payment-type"
|
||||
:value (:payment-type (:query-params request))
|
||||
:options [{:value ""
|
||||
:content "All"}
|
||||
{:value "cash"
|
||||
:content "Cash"}
|
||||
{:value "check"
|
||||
:content "Check"}
|
||||
{:value "debit"
|
||||
:content "Debit"}]}))
|
||||
:name "payment-type"
|
||||
:value (:payment-type (:query-params request))
|
||||
:options [{:value ""
|
||||
:content "All"}
|
||||
{:value "cash"
|
||||
:content "Cash"}
|
||||
{:value "check"
|
||||
:content "Check"}
|
||||
{:value "debit"
|
||||
:content "Debit"}]}))
|
||||
(exact-match-id* request)]])
|
||||
|
||||
|
||||
(def default-read '[*
|
||||
[:payment/date :xform clj-time.coerce/from-date]
|
||||
{:invoice-payment/_payment [* {:invoice-payment/invoice [*]}]}
|
||||
@@ -213,7 +212,6 @@
|
||||
'[(iol-ion.query/dollars= ?transaction-amount ?amount)]]}
|
||||
:args [(:amount query-params)]})
|
||||
|
||||
|
||||
(:status route-params)
|
||||
(merge-query {:query {:in ['?status]
|
||||
:where ['[?e :payment/status ?status]]}
|
||||
@@ -244,30 +242,30 @@
|
||||
refunds))
|
||||
|
||||
(defn sum-visible-pending [ids]
|
||||
(->>
|
||||
(dc/q {:find ['?id '?o]
|
||||
:in ['$ '[?id ...]]
|
||||
:where ['[?id :payment/amount ?o]
|
||||
'[?id :payment/status :payment-status/pending]]}
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(->>
|
||||
(dc/q {:find ['?id '?o]
|
||||
:in ['$ '[?id ...]]
|
||||
:where ['[?id :payment/amount ?o]
|
||||
'[?id :payment/status :payment-status/pending]]}
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(map last)
|
||||
(reduce
|
||||
+
|
||||
0.0)))
|
||||
|
||||
(defn sum-client-pending [clients]
|
||||
(->>
|
||||
(dc/q {:find '[?e ?a]
|
||||
:in '[$ [?clients ?start ?end]]
|
||||
:where '[[(iol-ion.query/scan-payments $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]
|
||||
[?e :payment/status :payment-status/pending]
|
||||
[?e :payment/amount ?a]]}
|
||||
(dc/db conn)
|
||||
[clients
|
||||
nil
|
||||
nil])
|
||||
|
||||
(->>
|
||||
(dc/q {:find '[?e ?a]
|
||||
:in '[$ [?clients ?start ?end]]
|
||||
:where '[[(iol-ion.query/scan-payments $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]
|
||||
[?e :payment/status :payment-status/pending]
|
||||
[?e :payment/amount ?a]]}
|
||||
(dc/db conn)
|
||||
[clients
|
||||
nil
|
||||
nil])
|
||||
|
||||
(map last)
|
||||
(reduce
|
||||
+
|
||||
@@ -278,16 +276,14 @@
|
||||
{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-visible-pending all-ids)
|
||||
(sum-client-pending (extract-client-ids (:clients request)
|
||||
(:client request)
|
||||
(:client-id (:query-params request))
|
||||
(when (:client-code (:query-params request))
|
||||
[:client/code (:client-code (:query-params request))])))
|
||||
]))
|
||||
(:client request)
|
||||
(:client-id (:query-params request))
|
||||
(when (:client-code (:query-params request))
|
||||
[:client/code (:client-code (:query-params request))])))]))
|
||||
|
||||
(def query-schema (mc/schema
|
||||
[:maybe [:map {:date-range [:date-range :start-date :end-date]}
|
||||
@@ -328,7 +324,7 @@
|
||||
(assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)])
|
||||
:query-schema query-schema
|
||||
:action-buttons (fn [request]
|
||||
(let [[_ _ visible-in-float total-in-float ] (:page-results request)]
|
||||
(let [[_ _ visible-in-float total-in-float] (:page-results request)]
|
||||
[(com/pill {:color :primary} " Visible in float "
|
||||
(format "$%,.2f" visible-in-float))
|
||||
(com/pill {:color :secondary} " Total in float "
|
||||
@@ -355,7 +351,7 @@
|
||||
|
||||
(= (-> request :query-params :sort first :name) "Bank account")
|
||||
(-> entity :payment/bank-account :bank-account/name)
|
||||
|
||||
|
||||
:else nil))
|
||||
:title (fn [r]
|
||||
(str
|
||||
@@ -410,7 +406,7 @@
|
||||
:render (fn [{:payment/keys [date]}]
|
||||
(some-> date (atime/unparse-local atime/normal-date)))}
|
||||
{:key "amount"
|
||||
:sort-key "amount"
|
||||
:sort-key "amount"
|
||||
:name "Amount"
|
||||
:render (fn [{:payment/keys [amount]}]
|
||||
(some->> amount (format "$%.2f")))}
|
||||
@@ -422,10 +418,10 @@
|
||||
(map :invoice-payment/invoice)
|
||||
(filter identity)
|
||||
(map (fn [invoice]
|
||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/all-page)
|
||||
{:exact-match-id (:db/id invoice)})
|
||||
:content (str "Inv. " (:invoice/invoice-number invoice))})))
|
||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/all-page)
|
||||
{:exact-match-id (:db/id invoice)})
|
||||
:content (str "Inv. " (:invoice/invoice-number invoice))})))
|
||||
(some-> p :transaction/_payment ((fn [t]
|
||||
[{:link (hu/url (bidi/path-for ssr-routes/only-routes ::transaction-routes/all-page)
|
||||
{:exact-match-id (:db/id (first t))})
|
||||
@@ -434,8 +430,6 @@
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
|
||||
|
||||
|
||||
(comment
|
||||
(mc/decode query-schema {"exact-match-id" "123"} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
|
||||
(mc/decode query-schema {} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
|
||||
@@ -445,7 +439,6 @@
|
||||
(mc/decode query-schema {"payment-type" "food"} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
|
||||
(mc/decode query-schema {"vendor" "87"} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
|
||||
|
||||
|
||||
(mc/decode query-schema {"start-date" #inst "2023-12-21T08:00:00.000-00:00"} (mt/transformer main-transformer mt/strip-extra-keys-transformer)))
|
||||
|
||||
(defn delete [{check :entity :as request identity :identity}]
|
||||
@@ -459,7 +452,7 @@
|
||||
#(assert-can-see-client identity (:db/id (:payment/client check))))
|
||||
(notify-if-locked (:db/id (:payment/client check))
|
||||
(:payment/date check))
|
||||
(let [ removing-payments (mapcat (fn [x]
|
||||
(let [removing-payments (mapcat (fn [x]
|
||||
(let [invoice (:invoice-payment/invoice x)
|
||||
new-balance (+ (:invoice/outstanding-balance invoice)
|
||||
(:invoice-payment/amount x))]
|
||||
@@ -475,9 +468,9 @@
|
||||
:payment/status :payment-status/voided}]
|
||||
(audit-transact (cond-> removing-payments
|
||||
true (conj updated-payment)
|
||||
(:transaction/_payment check) (conj [:db/retract (:db/id (first (:transaction/_payment check)))
|
||||
(:transaction/_payment check) (conj [:db/retract (:db/id (first (:transaction/_payment check)))
|
||||
:transaction/payment
|
||||
(:db/id check)]))
|
||||
(:db/id check)]))
|
||||
identity)
|
||||
|
||||
(html-response (row* (:identity request) updated-payment {:delete-after-settle? true :class "live-removed"
|
||||
@@ -578,7 +571,6 @@
|
||||
(assoc-in [:query-params :start] 0)
|
||||
(assoc-in [:query-params :per-page] 250))))
|
||||
|
||||
|
||||
:else
|
||||
selected)
|
||||
updated-count (void-payments-internal ids (:identity request))]
|
||||
@@ -591,7 +583,7 @@
|
||||
|
||||
(defn wrap-status-from-source [handler]
|
||||
(fn [{:keys [matched-current-page-route] :as request}]
|
||||
(let [ request (cond-> request
|
||||
(let [request (cond-> request
|
||||
(= ::route/cleared-page matched-current-page-route) (assoc-in [:route-params :status] :payment-status/cleared)
|
||||
(= ::route/pending-page matched-current-page-route) (assoc-in [:route-params :status] :payment-status/pending)
|
||||
(= ::route/voided-page matched-current-page-route) (assoc-in [:route-params :status] :payment-status/voided)
|
||||
@@ -605,7 +597,7 @@
|
||||
::route/pending-page (-> (helper/page-route grid-page)
|
||||
(wrap-implied-route-param :status :payment-status/pending))
|
||||
::route/voided-page (-> (helper/page-route grid-page)
|
||||
(wrap-implied-route-param :status :payment-status/voided))
|
||||
(wrap-implied-route-param :status :payment-status/voided))
|
||||
::route/all-page (-> (helper/page-route grid-page)
|
||||
(wrap-implied-route-param :status nil))
|
||||
|
||||
@@ -618,7 +610,6 @@
|
||||
::route/bulk-delete (-> bulk-delete-dialog
|
||||
(wrap-admin))
|
||||
|
||||
|
||||
::route/table (helper/table-route grid-page)}
|
||||
(fn [h]
|
||||
(-> h
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
[:maybe clj-date-schema]]
|
||||
[:end-date {:optional true}
|
||||
[:maybe clj-date-schema]]
|
||||
[:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]] ]
|
||||
[:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]]]
|
||||
default-grid-fields-schema)]))
|
||||
|
||||
(defn filters [params]
|
||||
@@ -36,7 +36,7 @@
|
||||
"hx-indicator" "#cash-drawer-shift-table"
|
||||
#_#_:hx-disabled-elt "find fieldset"}
|
||||
|
||||
[:fieldset.space-y-6
|
||||
[:fieldset.space-y-6
|
||||
(date-range-field* params)
|
||||
(total-field* params)]])
|
||||
|
||||
@@ -52,15 +52,14 @@
|
||||
:where '[[(iol-ion.query/scan-cash-drawer-shifts $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]]}
|
||||
:args [db [(:trimmed-clients request)
|
||||
(some-> (:start-date query-params) c/to-date)
|
||||
(some-> (:end-date query-params) c/to-date )]]}
|
||||
(some-> (:end-date query-params) c/to-date)]]}
|
||||
(:sort query-params) (add-sorter-fields {"client" ['[?e :cash-drawer-shift/client ?c]
|
||||
'[?c :client/name ?sort-client]]
|
||||
"date" ['[?e :cash-drawer-shift/date ?sort-date]]
|
||||
"paid-in" ['[?e :cash-drawer-shift/paid-in ?sort-paid-in]]
|
||||
"paid-out" ['[?e :cash-drawer-shift/paid-out ?sort-paid-out]]
|
||||
"expected-cash" ['[?e :cash-drawer-shift/expected-cash ?sort-expected-cash]]
|
||||
"opened-cash" ['[?e :cash-drawer-shift/opened-cash ?sort-opened-cash]]
|
||||
}
|
||||
"opened-cash" ['[?e :cash-drawer-shift/opened-cash ?sort-opened-cash]]}
|
||||
query-params)
|
||||
|
||||
(:exact-match-id query-params)
|
||||
@@ -71,7 +70,7 @@
|
||||
true
|
||||
(merge-query {:query {:find ['?sort-default '?e]
|
||||
:where ['[?e :cash-drawer-shift/date ?sort-default]]}}))]
|
||||
|
||||
|
||||
(cond->> (query2 query)
|
||||
true (apply-sort-3 query-params)
|
||||
true (apply-pagination query-params))))
|
||||
@@ -80,8 +79,8 @@
|
||||
(let [results (->> (pull-many db default-read ids)
|
||||
(group-by :db/id))
|
||||
cash-drawer-shifts (->> ids
|
||||
(map results)
|
||||
(map first))]
|
||||
(map results)
|
||||
(map first))]
|
||||
cash-drawer-shifts))
|
||||
|
||||
(defn fetch-page [request]
|
||||
@@ -109,7 +108,7 @@
|
||||
"Cash Drawer Shifts"]]
|
||||
:title "Cash drawer shifts"
|
||||
:entity-name "Cash drawer shift"
|
||||
:query-schema query-schema
|
||||
:query-schema query-schema
|
||||
:route :pos-cash-drawer-shift-table
|
||||
:headers [{:key "client"
|
||||
:name "Client"
|
||||
@@ -138,12 +137,11 @@
|
||||
:sort-key "opened-cash"
|
||||
:render #(some->> % :cash-drawer-shift/opened-cash (format "$%.2f"))}]}))
|
||||
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
(def table* (partial helper/table* grid-page))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(apply-middleware-to-all-handlers
|
||||
{:pos-cash-drawer-shifts (helper/page-route grid-page)
|
||||
:pos-cash-drawer-shift-table (helper/table-route grid-page)}
|
||||
(fn [h]
|
||||
|
||||
@@ -5,30 +5,30 @@
|
||||
[auto-ap.ssr.svg :as svg]))
|
||||
(defn date-range-field* [request]
|
||||
(dr/date-range-field {:value {:start (:start-date (:query-params request))
|
||||
:end (:end-date (:query-params request))}
|
||||
:id "date-range"}))
|
||||
:end (:end-date (:query-params request))}
|
||||
:id "date-range"}))
|
||||
|
||||
(defn processor-field* [request]
|
||||
(com/field {:label "Processor"}
|
||||
(com/radio-card {:size :small
|
||||
:name "processor"
|
||||
:value (:processor (:query-params request))
|
||||
:options [{:value ""
|
||||
:content "All"}
|
||||
{:value "square"
|
||||
:content [:div.flex.space-x-2 [:img.align-center {:src "/img/square.png" :style {:width "16px" :height "16px"}}] [:div "Square"]]}
|
||||
{:value "doordash"
|
||||
:content [:div.flex.space-x-2 [:img.align-center {:src "/img/doordash.png" :style {:width "16px" :height "16px"}}] [:div "Doordash"]]}
|
||||
{:value "uber-eats"
|
||||
:content [:div.flex.space-x-2 [:img.align-center {:src "/img/ubereats.png" :style {:width "16px" :height "16px"}}] [:div "Uber eats"]]}
|
||||
{:value "grubhub"
|
||||
:content [:div.flex.space-x-2 [:img.align-center {:src "/img/grubhub.png" :style {:width "16px" :height "16px"}}] [:div "Grubhub"]]}
|
||||
{:value "koala"
|
||||
:content [:div.flex.space-x-2 [:img.align-center {:src "/img/koala.png" :style {:width "16px" :height "16px"}}] [:div "Koala"]]}
|
||||
{:value "ezcater"
|
||||
:content [:div.flex.space-x-2 [:img.align-center {:src "/img/ezcater.png" :style {:width "16px" :height "16px"}}] [:div "EZCater"]]}
|
||||
{:value "na"
|
||||
:content "No Processor"}]})))
|
||||
:name "processor"
|
||||
:value (:processor (:query-params request))
|
||||
:options [{:value ""
|
||||
:content "All"}
|
||||
{:value "square"
|
||||
:content [:div.flex.space-x-2 [:img.align-center {:src "/img/square.png" :style {:width "16px" :height "16px"}}] [:div "Square"]]}
|
||||
{:value "doordash"
|
||||
:content [:div.flex.space-x-2 [:img.align-center {:src "/img/doordash.png" :style {:width "16px" :height "16px"}}] [:div "Doordash"]]}
|
||||
{:value "uber-eats"
|
||||
:content [:div.flex.space-x-2 [:img.align-center {:src "/img/ubereats.png" :style {:width "16px" :height "16px"}}] [:div "Uber eats"]]}
|
||||
{:value "grubhub"
|
||||
:content [:div.flex.space-x-2 [:img.align-center {:src "/img/grubhub.png" :style {:width "16px" :height "16px"}}] [:div "Grubhub"]]}
|
||||
{:value "koala"
|
||||
:content [:div.flex.space-x-2 [:img.align-center {:src "/img/koala.png" :style {:width "16px" :height "16px"}}] [:div "Koala"]]}
|
||||
{:value "ezcater"
|
||||
:content [:div.flex.space-x-2 [:img.align-center {:src "/img/ezcater.png" :style {:width "16px" :height "16px"}}] [:div "EZCater"]]}
|
||||
{:value "na"
|
||||
:content "No Processor"}]})))
|
||||
|
||||
(defn total-field* [request]
|
||||
(com/field {:label "Total"}
|
||||
@@ -40,7 +40,7 @@
|
||||
:value (:total-gte (:query-params request))
|
||||
:placeholder "0.01"
|
||||
:size :small})
|
||||
[:div.align-baseline
|
||||
[:div.align-baseline
|
||||
"to"]
|
||||
(com/money-input {:name "total-lte"
|
||||
:hx-preserve "true"
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
(defn exact-match-id-field* [request]
|
||||
(when-let [exact-match-id (:exact-match-id (:query-params request))]
|
||||
[:div
|
||||
[:div
|
||||
(com/field {:label "Exact match"}
|
||||
(com/pill {:color :primary}
|
||||
[:span.inline-flex.gap-2
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
[:processor {:optional true} [:maybe (ref->enum-schema "ccp-processor")]]]
|
||||
default-grid-fields-schema)]))
|
||||
|
||||
|
||||
(defn filters [request]
|
||||
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
||||
"hx-get" (bidi/path-for ssr-routes/only-routes
|
||||
@@ -42,7 +41,7 @@
|
||||
"hx-indicator" "#expected-deposit-table"
|
||||
#_#_:hx-disabled-elt "find fieldset"}
|
||||
|
||||
[:fieldset.space-y-6
|
||||
[:fieldset.space-y-6
|
||||
(date-range-field* request)
|
||||
(exact-match-id-field* request)]])
|
||||
|
||||
@@ -70,26 +69,25 @@
|
||||
(some-> (:start-date query-params) c/to-date)
|
||||
(some-> (:end-date query-params) c/to-date)]]}
|
||||
(:sort query-params) (add-sorter-fields {"client" ['[?e :expected-deposit/client ?c]
|
||||
'[?c :client/name ?sort-client]]
|
||||
"location" ['[?e :expected-deposit/location ?sort-location]]
|
||||
"date" ['[?e :expected-deposit/date ?sort-date]]
|
||||
"total" ['[?e :expected-deposit/total ?sort-total]]
|
||||
"fee" ['[?e :expected-deposit/fee ?sort-fee]]}
|
||||
query-params)
|
||||
'[?c :client/name ?sort-client]]
|
||||
"location" ['[?e :expected-deposit/location ?sort-location]]
|
||||
"date" ['[?e :expected-deposit/date ?sort-date]]
|
||||
"total" ['[?e :expected-deposit/total ?sort-total]]
|
||||
"fee" ['[?e :expected-deposit/fee ?sort-fee]]}
|
||||
query-params)
|
||||
|
||||
(:exact-match-id query-params)
|
||||
(merge-query {:query {:in ['?e]
|
||||
:where []}
|
||||
:args [(:exact-match-id query-params)]})
|
||||
|
||||
|
||||
(:total-gte query-params)
|
||||
(:total-gte query-params)
|
||||
(merge-query {:query {:in ['?total-gte]
|
||||
:where ['[?e :expected-deposit/total ?a]
|
||||
'[(>= ?a ?total-gte)]]}
|
||||
:args [(:total-gte query-params)]})
|
||||
|
||||
(:total-lte query-params)
|
||||
(:total-lte query-params)
|
||||
(merge-query {:query {:in ['?total-lte]
|
||||
:where ['[?e :expected-deposit/total ?a]
|
||||
'[(<= ?a ?total-lte)]]}
|
||||
@@ -104,7 +102,7 @@
|
||||
true
|
||||
(merge-query {:query {:find ['?sort-default '?e]
|
||||
:where ['[?e :expected-deposit/date ?sort-default]]}}))]
|
||||
|
||||
|
||||
(cond->> (query2 query)
|
||||
true (apply-sort-3 query-params)
|
||||
true (apply-pagination query-params))))
|
||||
@@ -113,25 +111,25 @@
|
||||
(let [results (->> (pull-many db default-read ids)
|
||||
(group-by :db/id))
|
||||
payments (->> ids
|
||||
(map results)
|
||||
(map first)
|
||||
(map (fn get-totals [ed]
|
||||
(assoc ed :totals
|
||||
(->> (dc/q '[:find ?d4 (count ?c) (sum ?a)
|
||||
:in $ ?ed
|
||||
:where [?ed :expected-deposit/charges ?c]
|
||||
[?c :charge/total ?a]
|
||||
[?o :sales-order/charges ?c]
|
||||
[?o :sales-order/date ?d]
|
||||
[(clj-time.coerce/from-date ?d) ?d2]
|
||||
[(auto-ap.time/localize ?d2) ?d3]
|
||||
[(clj-time.coerce/to-local-date ?d3) ?d4]]
|
||||
(dc/db conn)
|
||||
(:db/id ed))
|
||||
(map (fn [[date count amount]]
|
||||
{:date (c/to-date-time date)
|
||||
:count count
|
||||
:amount amount})))))))]
|
||||
(map results)
|
||||
(map first)
|
||||
(map (fn get-totals [ed]
|
||||
(assoc ed :totals
|
||||
(->> (dc/q '[:find ?d4 (count ?c) (sum ?a)
|
||||
:in $ ?ed
|
||||
:where [?ed :expected-deposit/charges ?c]
|
||||
[?c :charge/total ?a]
|
||||
[?o :sales-order/charges ?c]
|
||||
[?o :sales-order/date ?d]
|
||||
[(clj-time.coerce/from-date ?d) ?d2]
|
||||
[(auto-ap.time/localize ?d2) ?d3]
|
||||
[(clj-time.coerce/to-local-date ?d3) ?d4]]
|
||||
(dc/db conn)
|
||||
(:db/id ed))
|
||||
(map (fn [[date count amount]]
|
||||
{:date (c/to-date-time date)
|
||||
:count count
|
||||
:amount amount})))))))]
|
||||
payments))
|
||||
|
||||
(defn fetch-page [args]
|
||||
@@ -142,66 +140,64 @@
|
||||
matching-count]))
|
||||
|
||||
(def grid-page
|
||||
(helper/build
|
||||
{:id "expected-deposit-table"
|
||||
:nav com/main-aside-nav
|
||||
:page-specific-nav filters
|
||||
:fetch-page fetch-page
|
||||
:oob-render
|
||||
(fn [request]
|
||||
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)])
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company)}
|
||||
"POS"]
|
||||
(helper/build
|
||||
{:id "expected-deposit-table"
|
||||
:nav com/main-aside-nav
|
||||
:page-specific-nav filters
|
||||
:fetch-page fetch-page
|
||||
:oob-render
|
||||
(fn [request]
|
||||
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)])
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company)}
|
||||
"POS"]
|
||||
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:pos-expected-deposits)}
|
||||
"Expected deposits"]]
|
||||
:title "Expected deposits"
|
||||
:entity-name "Expected deposit"
|
||||
:query-schema query-schema
|
||||
:route :pos-expected-deposit-table
|
||||
:row-buttons (fn [_ e]
|
||||
[
|
||||
(when (:expected-deposit/reference-link e)
|
||||
(com/a-icon-button {:href (:expected-deposit/reference-link e)}
|
||||
svg/external-link))
|
||||
(when-let [transaction-id (-> e (:transaction/_expected-deposit) first :db/id)]
|
||||
(com/a-button {:href (str (bidi/path-for ssr-routes/only-routes
|
||||
::transaction-routes/all-page)
|
||||
"?exact-match-id="
|
||||
transaction-id)} "Transaction"))])
|
||||
:headers [{:key "client"
|
||||
:name "Client"
|
||||
:sort-key "client"
|
||||
:hide? (fn [args]
|
||||
(= (count (:clients args)) 1))
|
||||
:render #(-> % :expected-deposit/client :client/code)}
|
||||
{:key "date"
|
||||
:name "Date"
|
||||
:sort-key "date"
|
||||
:render #(atime/unparse-local (:expected-deposit/date %) atime/standard-time)}
|
||||
{:key "sales-date"
|
||||
:name "Sales Date"
|
||||
:sort-key "sales-date"
|
||||
:render #(atime/unparse-local (:expected-deposit/sales-date %) atime/standard-time)}
|
||||
{:key "total"
|
||||
:name "Total"
|
||||
:sort-key "total"
|
||||
:render #(some->> % :expected-deposit/total (format "$%.2f"))}
|
||||
{:key "fee"
|
||||
:name "Fee"
|
||||
:sort-key "fee"
|
||||
:render #(some->> % :expected-deposit/fee (format "$%.2f"))}]}))
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:pos-expected-deposits)}
|
||||
"Expected deposits"]]
|
||||
:title "Expected deposits"
|
||||
:entity-name "Expected deposit"
|
||||
:query-schema query-schema
|
||||
:route :pos-expected-deposit-table
|
||||
:row-buttons (fn [_ e]
|
||||
[(when (:expected-deposit/reference-link e)
|
||||
(com/a-icon-button {:href (:expected-deposit/reference-link e)}
|
||||
svg/external-link))
|
||||
(when-let [transaction-id (-> e (:transaction/_expected-deposit) first :db/id)]
|
||||
(com/a-button {:href (str (bidi/path-for ssr-routes/only-routes
|
||||
::transaction-routes/all-page)
|
||||
"?exact-match-id="
|
||||
transaction-id)} "Transaction"))])
|
||||
:headers [{:key "client"
|
||||
:name "Client"
|
||||
:sort-key "client"
|
||||
:hide? (fn [args]
|
||||
(= (count (:clients args)) 1))
|
||||
:render #(-> % :expected-deposit/client :client/code)}
|
||||
{:key "date"
|
||||
:name "Date"
|
||||
:sort-key "date"
|
||||
:render #(atime/unparse-local (:expected-deposit/date %) atime/standard-time)}
|
||||
{:key "sales-date"
|
||||
:name "Sales Date"
|
||||
:sort-key "sales-date"
|
||||
:render #(atime/unparse-local (:expected-deposit/sales-date %) atime/standard-time)}
|
||||
{:key "total"
|
||||
:name "Total"
|
||||
:sort-key "total"
|
||||
:render #(some->> % :expected-deposit/total (format "$%.2f"))}
|
||||
{:key "fee"
|
||||
:name "Fee"
|
||||
:sort-key "fee"
|
||||
:render #(some->> % :expected-deposit/fee (format "$%.2f"))}]}))
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
(def table* (partial helper/table* grid-page))
|
||||
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{:pos-expected-deposits (helper/page-route grid-page)
|
||||
:pos-expected-deposit-table (helper/table-route grid-page)}
|
||||
:pos-expected-deposit-table (helper/table-route grid-page)}
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"hx-indicator" "#refund-table"
|
||||
#_#_:hx-disabled-elt "find fieldset"}
|
||||
|
||||
[:fieldset.space-y-6
|
||||
[:fieldset.space-y-6
|
||||
(date-range-field* request)
|
||||
(total-field* request)]])
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
:where '[[(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]]}
|
||||
:args [db [(:trimmed-clients request)
|
||||
(some-> query-params :start-date c/to-date)
|
||||
(some-> query-params :end-date c/to-date )]]}
|
||||
(some-> query-params :end-date c/to-date)]]}
|
||||
(:sort query-params) (add-sorter-fields {"client" ['[?e :sales-refund/client ?c]
|
||||
'[?c :client/name ?sort-client]]
|
||||
"date" ['[?e :sales-refund/date ?sort-date]]
|
||||
@@ -68,13 +68,13 @@
|
||||
:where []}
|
||||
:args [(:exact-match-id query-params)]})
|
||||
|
||||
(:total-gte query-params)
|
||||
(:total-gte query-params)
|
||||
(merge-query {:query {:in ['?total-gte]
|
||||
:where ['[?e :sales-refund/total ?a]
|
||||
'[(>= ?a ?total-gte)]]}
|
||||
:args [(:total-gte query-params)]})
|
||||
|
||||
(:total-lte query-params)
|
||||
(:total-lte query-params)
|
||||
(merge-query {:query {:in ['?total-lte]
|
||||
:where ['[?e :sales-refund/total ?a]
|
||||
'[(<= ?a ?total-lte)]]}
|
||||
@@ -83,7 +83,7 @@
|
||||
true
|
||||
(merge-query {:query {:find ['?sort-default '?e]
|
||||
:where ['[?e :sales-refund/date ?sort-default]]}}))]
|
||||
|
||||
|
||||
(cond->> (query2 query)
|
||||
true (apply-sort-3 query-params)
|
||||
true (apply-pagination query-params))))
|
||||
@@ -149,13 +149,13 @@
|
||||
(def table* (partial helper/table* grid-page))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(apply-middleware-to-all-handlers
|
||||
{:pos-refunds (helper/page-route grid-page)
|
||||
:pos-refund-table (helper/table-route grid-page)}
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)))))
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)))))
|
||||
|
||||
@@ -36,29 +36,28 @@
|
||||
(defn filters [request]
|
||||
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
||||
"hx-get" (bidi/path-for ssr-routes/only-routes
|
||||
:pos-sales-table)
|
||||
:pos-sales-table)
|
||||
"hx-target" "#sales-table"
|
||||
"hx-indicator" "#sales-table"
|
||||
#_#_:hx-disabled-elt "find fieldset"}
|
||||
|
||||
[:fieldset.space-y-6
|
||||
[:fieldset.space-y-6
|
||||
(date-range-field* request)
|
||||
(total-field* request)
|
||||
[:div
|
||||
(com/field {:label "Payment Method"}
|
||||
(com/radio-card {:size :small
|
||||
:name "payment-method"
|
||||
:options [{:value ""
|
||||
:content "All"}
|
||||
{:value "CASH"
|
||||
:content "Cash"}
|
||||
{:value "CARD"
|
||||
:content "Card"}
|
||||
{:value "SQUARE_GIFT_CARD"
|
||||
:content "Gift Card"}
|
||||
{:value "OTHER"
|
||||
:content "Other"}
|
||||
]}))]
|
||||
:name "payment-method"
|
||||
:options [{:value ""
|
||||
:content "All"}
|
||||
{:value "CASH"
|
||||
:content "Cash"}
|
||||
{:value "CARD"
|
||||
:content "Card"}
|
||||
{:value "SQUARE_GIFT_CARD"
|
||||
:content "Gift Card"}
|
||||
{:value "OTHER"
|
||||
:content "Other"}]}))]
|
||||
[:div
|
||||
(processor-field* request)]
|
||||
|
||||
@@ -87,8 +86,7 @@
|
||||
:sales-order/source,
|
||||
:sales-order/reference-link,
|
||||
{:sales-order/client [:client/name :db/id :client/code]
|
||||
:sales-order/charges [
|
||||
:charge/type-name,
|
||||
:sales-order/charges [:charge/type-name,
|
||||
:charge/total,
|
||||
:charge/tax,
|
||||
:charge/tip,
|
||||
@@ -125,13 +123,13 @@
|
||||
:where []}
|
||||
:args [(:exact-match-id query-params)]})
|
||||
|
||||
(:total-gte query-params)
|
||||
(:total-gte query-params)
|
||||
(merge-query {:query {:in ['?total-gte]
|
||||
:where ['[?e :sales-order/total ?a]
|
||||
'[(>= ?a ?total-gte)]]}
|
||||
:args [(:total-gte query-params)]})
|
||||
|
||||
(:total-lte query-params)
|
||||
(:total-lte query-params)
|
||||
(merge-query {:query {:in ['?total-lte]
|
||||
:where ['[?e :sales-order/total ?a]
|
||||
'[(<= ?a ?total-lte)]]}
|
||||
@@ -155,7 +153,6 @@
|
||||
'[?chg :charge/processor ?processor]]}
|
||||
:args [(:processor query-params)]})
|
||||
|
||||
|
||||
true
|
||||
(merge-query {:query {:find ['?sort-default '?e]}}))]
|
||||
(clojure.pprint/pprint (update-in query [:args] #(drop 1 %)))
|
||||
@@ -178,7 +175,6 @@
|
||||
[(->> (hydrate-results ids-to-retrieve db request))
|
||||
matching-count]))
|
||||
|
||||
|
||||
(def grid-page
|
||||
(helper/build
|
||||
{:id "sales-table"
|
||||
@@ -255,7 +251,6 @@
|
||||
"OTHER" "other"
|
||||
nil)))])}]}))
|
||||
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
(def table* (partial helper/table* grid-page))
|
||||
|
||||
|
||||
@@ -158,13 +158,13 @@
|
||||
[:span.text-sm account-name]
|
||||
(com/pill {:color :red} "Missing acct"))
|
||||
(com/a-icon-button {:class "p-1"
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-item-account)
|
||||
:hx-target "closest .account-cell"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-vals (hx/json {:item-index (or (:item-index item) 0)
|
||||
:client-id client-id
|
||||
:current-account-id (or account-id "")})}
|
||||
svg/pencil)]))
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-item-account)
|
||||
:hx-target "closest .account-cell"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-vals (hx/json {:item-index (or (:item-index item) 0)
|
||||
:client-id client-id
|
||||
:current-account-id (or account-id "")})}
|
||||
svg/pencil)]))
|
||||
|
||||
(defn account-edit-cell [{:keys [field-name-prefix client-id current-account-id]}]
|
||||
(let [account-input-name (str field-name-prefix "[ledger-mapped/account]")]
|
||||
@@ -172,23 +172,23 @@
|
||||
(account-typeahead* {:name account-input-name
|
||||
:value current-account-id
|
||||
:client-id client-id})
|
||||
[:div.flex.gap-1
|
||||
(com/a-icon-button {:class "p-1"
|
||||
:hx-put (bidi/path-for ssr-routes/only-routes ::route/save-item-account)
|
||||
:hx-target "closest .account-cell"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-include "closest .account-cell"
|
||||
:hx-vals (hx/json {:field-name-prefix field-name-prefix
|
||||
:client-id client-id})}
|
||||
svg/check)
|
||||
(com/a-icon-button {:class "p-1"
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/cancel-item-account)
|
||||
:hx-target "closest .account-cell"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-vals (hx/json {:field-name-prefix field-name-prefix
|
||||
:client-id client-id
|
||||
:current-account-id (or current-account-id "")})}
|
||||
svg/x)]]))
|
||||
[:div.flex.gap-1
|
||||
(com/a-icon-button {:class "p-1"
|
||||
:hx-put (bidi/path-for ssr-routes/only-routes ::route/save-item-account)
|
||||
:hx-target "closest .account-cell"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-include "closest .account-cell"
|
||||
:hx-vals (hx/json {:field-name-prefix field-name-prefix
|
||||
:client-id client-id})}
|
||||
svg/check)
|
||||
(com/a-icon-button {:class "p-1"
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/cancel-item-account)
|
||||
:hx-target "closest .account-cell"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-vals (hx/json {:field-name-prefix field-name-prefix
|
||||
:client-id client-id
|
||||
:current-account-id (or current-account-id "")})}
|
||||
svg/x)]]))
|
||||
|
||||
(def grid-page
|
||||
(helper/build {:id "entity-table"
|
||||
@@ -576,8 +576,8 @@
|
||||
[:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)]
|
||||
(account-display-cell {:item (assoc item :item-index actual-idx)
|
||||
:field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]")
|
||||
:client-id client-id})
|
||||
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
|
||||
:client-id client-id})
|
||||
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
|
||||
[:div.h-6]))]
|
||||
[:div.mt-2.border-t.pt-1
|
||||
(summary-total-display request)
|
||||
@@ -619,13 +619,13 @@
|
||||
[:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)]
|
||||
(account-display-cell {:item (assoc item :item-index actual-idx)
|
||||
:field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]")
|
||||
:client-id client-id})
|
||||
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
|
||||
:client-id client-id})
|
||||
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
|
||||
[:div.h-6]))]
|
||||
[:div.mt-2.border-t.pt-1
|
||||
(summary-total-display request)
|
||||
(unbalanced-display request)]]]
|
||||
[:div.mt-4.border-t.pt-2
|
||||
[:div.mt-4.border-t.pt-2
|
||||
(fc/with-field :sales-summary/items
|
||||
(com/data-grid-new-row {:colspan 2
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item)
|
||||
@@ -761,16 +761,16 @@
|
||||
::route/edit-wizard-navigate (-> mm/next-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/new-summary-item (-> (add-new-entity-handler [:step-params :sales-summary/items]
|
||||
(fn render [cursor request]
|
||||
(sales-summary-item-row*
|
||||
{:value cursor
|
||||
:client-id (:client-id (:query-params request))}))
|
||||
(fn build-new-row [base _]
|
||||
(assoc base :sales-summary-item/manual? true)))
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:client-id {:optional true}
|
||||
[:maybe entity-id]]]))
|
||||
::route/new-summary-item (-> (add-new-entity-handler [:step-params :sales-summary/items]
|
||||
(fn render [cursor request]
|
||||
(sales-summary-item-row*
|
||||
{:value cursor
|
||||
:client-id (:client-id (:query-params request))}))
|
||||
(fn build-new-row [base _]
|
||||
(assoc base :sales-summary-item/manual? true)))
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:client-id {:optional true}
|
||||
[:maybe entity-id]]]))
|
||||
::route/edit-item-account (-> edit-item-account
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:item-index nat-int?]
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"hx-indicator" "#tender-table"
|
||||
#_#_:hx-disabled-elt "find fieldset"}
|
||||
|
||||
[:fieldset.space-y-6
|
||||
[:fieldset.space-y-6
|
||||
(date-range-field* request)
|
||||
(processor-field* request)
|
||||
(total-field* request)]])
|
||||
@@ -79,13 +79,13 @@
|
||||
:where []}
|
||||
:args [(:exact-match-id query-params)]})
|
||||
|
||||
(:total-gte query-params)
|
||||
(:total-gte query-params)
|
||||
(merge-query {:query {:in ['?total-gte]
|
||||
:where ['[?e :charge/total ?a]
|
||||
'[(>= ?a ?total-gte)]]}
|
||||
:args [(:total-gte query-params)]})
|
||||
|
||||
(:total-lte query-params)
|
||||
(:total-lte query-params)
|
||||
(merge-query {:query {:in ['?total-lte]
|
||||
:where ['[?e :charge/total ?a]
|
||||
'[(<= ?a ?total-lte)]]}
|
||||
@@ -96,10 +96,9 @@
|
||||
:where ['[?e :charge/processor ?processor]]}
|
||||
:args [(:processor query-params)]})
|
||||
|
||||
|
||||
true
|
||||
(merge-query {:query {:find ['?sort-default '?e]}}))]
|
||||
|
||||
|
||||
(cond->> (query2 query)
|
||||
true (apply-sort-3 query-params)
|
||||
true (apply-pagination query-params))))
|
||||
@@ -121,63 +120,62 @@
|
||||
|
||||
(def grid-page
|
||||
(helper/build
|
||||
{:id "tender-table"
|
||||
:nav com/main-aside-nav
|
||||
:page-specific-nav filters
|
||||
:fetch-page fetch-page
|
||||
:oob-render
|
||||
(fn [request]
|
||||
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)])
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company)}
|
||||
"POS"]
|
||||
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:pos-tenders)}
|
||||
"Tenders"]]
|
||||
:title "Tenders"
|
||||
:entity-name "Tender"
|
||||
:query-schema query-schema
|
||||
:route :pos-tender-table
|
||||
:row-buttons (fn [request e]
|
||||
(when (:charge/reference-link e)
|
||||
[(com/a-icon-button {:href (:charge/reference-link e)}
|
||||
svg/external-link)]))
|
||||
:headers [{:key "client"
|
||||
:name "Client"
|
||||
:sort-key "client"
|
||||
:hide? (fn [request]
|
||||
(= (count (:clients request)) 1))
|
||||
:render #(-> % :charge/client :client/code)}
|
||||
{:key "date"
|
||||
:name "Date"
|
||||
:sort-key "date"
|
||||
:render #(atime/unparse-local (:charge/date %) atime/standard-time)}
|
||||
{:key "total"
|
||||
:name "Total"
|
||||
:sort-key "total"
|
||||
:render #(some->> % :charge/total (format "$%.2f"))}
|
||||
{:key "processor"
|
||||
:name "Processor"
|
||||
:sort-key "processor"
|
||||
:render (fn [sales-order]
|
||||
(when (:charge/processor sales-order)
|
||||
(com/pill {:color :primary }
|
||||
(name (:charge/processor sales-order)))))}
|
||||
{:key "tip"
|
||||
:name "Tip"
|
||||
:sort-key "tip"
|
||||
:render #(some->> % :charge/tip (format "$%.2f"))}
|
||||
{:key "links"
|
||||
:name "Links"
|
||||
:render (fn [entity]
|
||||
(when-let [expected-deposit-id (some->> entity :expected-deposit/_charges first :db/id)]
|
||||
[:a {:href (str (bidi/path-for ssr-routes/only-routes
|
||||
:pos-expected-deposits)
|
||||
"?exact-match-id=" expected-deposit-id)
|
||||
:hx-boost "true"}
|
||||
(com/pill {:color :secondary} "expected deposit")]))}]}))
|
||||
{:id "tender-table"
|
||||
:nav com/main-aside-nav
|
||||
:page-specific-nav filters
|
||||
:fetch-page fetch-page
|
||||
:oob-render
|
||||
(fn [request]
|
||||
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)])
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:company)}
|
||||
"POS"]
|
||||
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||
:pos-tenders)}
|
||||
"Tenders"]]
|
||||
:title "Tenders"
|
||||
:entity-name "Tender"
|
||||
:query-schema query-schema
|
||||
:route :pos-tender-table
|
||||
:row-buttons (fn [request e]
|
||||
(when (:charge/reference-link e)
|
||||
[(com/a-icon-button {:href (:charge/reference-link e)}
|
||||
svg/external-link)]))
|
||||
:headers [{:key "client"
|
||||
:name "Client"
|
||||
:sort-key "client"
|
||||
:hide? (fn [request]
|
||||
(= (count (:clients request)) 1))
|
||||
:render #(-> % :charge/client :client/code)}
|
||||
{:key "date"
|
||||
:name "Date"
|
||||
:sort-key "date"
|
||||
:render #(atime/unparse-local (:charge/date %) atime/standard-time)}
|
||||
{:key "total"
|
||||
:name "Total"
|
||||
:sort-key "total"
|
||||
:render #(some->> % :charge/total (format "$%.2f"))}
|
||||
{:key "processor"
|
||||
:name "Processor"
|
||||
:sort-key "processor"
|
||||
:render (fn [sales-order]
|
||||
(when (:charge/processor sales-order)
|
||||
(com/pill {:color :primary}
|
||||
(name (:charge/processor sales-order)))))}
|
||||
{:key "tip"
|
||||
:name "Tip"
|
||||
:sort-key "tip"
|
||||
:render #(some->> % :charge/tip (format "$%.2f"))}
|
||||
{:key "links"
|
||||
:name "Links"
|
||||
:render (fn [entity]
|
||||
(when-let [expected-deposit-id (some->> entity :expected-deposit/_charges first :db/id)]
|
||||
[:a {:href (str (bidi/path-for ssr-routes/only-routes
|
||||
:pos-expected-deposits)
|
||||
"?exact-match-id=" expected-deposit-id)
|
||||
:hx-boost "true"}
|
||||
(com/pill {:color :secondary} "expected deposit")]))}]}))
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
(def table* (partial helper/table* grid-page))
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
|
||||
(defn try-cleanse-date [d]
|
||||
(try
|
||||
(or
|
||||
(some-> (atime/parse-utc d atime/normal-date) (atime/unparse atime/solr-date))
|
||||
(some-> (atime/parse-utc d atime/iso-date) (atime/unparse atime/solr-date))
|
||||
d)
|
||||
(or
|
||||
(some-> (atime/parse-utc d atime/normal-date) (atime/unparse atime/solr-date))
|
||||
(some-> (atime/parse-utc d atime/iso-date) (atime/unparse atime/solr-date))
|
||||
d)
|
||||
(catch Exception _
|
||||
d)))
|
||||
|
||||
(defn try-parse-number [n]
|
||||
(if (re-find #"^[\-]?\d+\.\d+$" n )
|
||||
(if (re-find #"^[\-]?\d+\.\d+$" n)
|
||||
(str (with-precision 2
|
||||
(some-> n
|
||||
(Double/parseDouble)
|
||||
@@ -28,7 +28,6 @@
|
||||
(double))))
|
||||
n))
|
||||
|
||||
|
||||
(defn q->solr-q [q]
|
||||
(let [matches (re-seq #"(?:\".*?\"|\S)+" q)]
|
||||
(str/join " AND "
|
||||
@@ -46,10 +45,8 @@
|
||||
(= "journal-entry" m)
|
||||
"type:journal-entry"
|
||||
|
||||
|
||||
:else
|
||||
(str "_text_:\"" (try-parse-number (try-cleanse-date m)) ""\"))))))))
|
||||
|
||||
(str "_text_:\"" (try-parse-number (try-cleanse-date m)) "" \"))))))))
|
||||
|
||||
(defn search-results [q id]
|
||||
(into []
|
||||
@@ -58,96 +55,89 @@
|
||||
(solr/query solr/impl "invoices" {"query" (q->solr-q q)
|
||||
"fields" "id, date, amount, type, description, number, client_code, client_id, vendor_name"})))
|
||||
|
||||
|
||||
(defn search-results* [q id]
|
||||
(let [results (search-results q id)]
|
||||
[:div
|
||||
(let [results (search-results q id)]
|
||||
[:div
|
||||
(if (seq results)
|
||||
[:div.flex.gap-8.flex-col
|
||||
(for [doc results]
|
||||
(com/card {}
|
||||
[:div.flex.flex-col.gap-4
|
||||
[:div.flex.items-center.p-2.gap-4.bg-gray-50.dark:bg-gray-800
|
||||
[:div.h-8.w-8.p-2
|
||||
(cond (= "transaction" (:type doc))
|
||||
svg/bank
|
||||
|
||||
[:div.flex.flex-col.gap-4
|
||||
[:div.flex.items-center.p-2.gap-4.bg-gray-50.dark:bg-gray-800
|
||||
[:div.h-8.w-8.p-2
|
||||
(cond (= "transaction" (:type doc))
|
||||
svg/bank
|
||||
|
||||
(= "invoice" (:type doc))
|
||||
svg/accounting-invoice-mail
|
||||
|
||||
(= "invoice" (:type doc))
|
||||
svg/accounting-invoice-mail
|
||||
|
||||
(= "payment" (:type doc))
|
||||
svg/payments
|
||||
(= "payment" (:type doc))
|
||||
svg/payments
|
||||
|
||||
(= "journal-entry" (:type doc))
|
||||
svg/receipt
|
||||
(= "journal-entry" (:type doc))
|
||||
svg/receipt
|
||||
|
||||
:else
|
||||
nil)]
|
||||
(clojure.string/capitalize (:type doc))
|
||||
(com/pill {:color :primary}
|
||||
"client: " (:client_code doc))
|
||||
(com/pill {:color :secondary}
|
||||
"amount: $" (first (:amount doc)))
|
||||
(when-let [vendor-name (first (:vendor_name doc))]
|
||||
(com/pill {:color :yellow}
|
||||
"vendor: " vendor-name))
|
||||
[:div
|
||||
(com/link {:href (str "/" (cond (= "invoice"
|
||||
(:type doc))
|
||||
"invoices"
|
||||
:else
|
||||
nil)]
|
||||
(clojure.string/capitalize (:type doc))
|
||||
(com/pill {:color :primary}
|
||||
"client: " (:client_code doc))
|
||||
(com/pill {:color :secondary}
|
||||
"amount: $" (first (:amount doc)))
|
||||
(when-let [vendor-name (first (:vendor_name doc))]
|
||||
(com/pill {:color :yellow}
|
||||
"vendor: " vendor-name))
|
||||
[:div
|
||||
(com/link {:href (str "/" (cond (= "invoice"
|
||||
(:type doc))
|
||||
"invoices"
|
||||
|
||||
(= "transaction"
|
||||
(:type doc))
|
||||
"transactions"
|
||||
(= "transaction"
|
||||
(:type doc))
|
||||
"transactions"
|
||||
|
||||
(= "journal-entry"
|
||||
(:type doc))
|
||||
"ledger"
|
||||
(= "journal-entry"
|
||||
(:type doc))
|
||||
"ledger"
|
||||
|
||||
:else
|
||||
"payments") "/?exact-match-id=" (:id doc))
|
||||
:target "_blank"}
|
||||
[:div.h-8.w-8.p-2
|
||||
svg/external-link])]
|
||||
]
|
||||
|
||||
|
||||
[:div.px-4.pb-2
|
||||
[:span
|
||||
|
||||
[:strong (atime/unparse (atime/parse (:date doc) atime/solr-date) atime/normal-date)]
|
||||
": "
|
||||
(str (or (first (:description doc))
|
||||
(first (:number doc))))]]])
|
||||
)]
|
||||
:else
|
||||
"payments") "/?exact-match-id=" (:id doc))
|
||||
:target "_blank"}
|
||||
[:div.h-8.w-8.p-2
|
||||
svg/external-link])]]
|
||||
|
||||
[:div.px-4.pb-2
|
||||
[:span
|
||||
|
||||
[:strong (atime/unparse (atime/parse (:date doc) atime/solr-date) atime/normal-date)]
|
||||
": "
|
||||
(str (or (first (:description doc))
|
||||
(first (:number doc))))]]]))]
|
||||
[:div.block "No results found."])]))
|
||||
|
||||
(defn dialog-contents [request]
|
||||
(if-let [q (get (:form-params request) "q")]
|
||||
(html-response (search-results* q (:identity request)))
|
||||
(modal-response
|
||||
(com/modal {}
|
||||
(com/modal-card {:class "w-full h-full"}
|
||||
[:div.p-2 "Search"]
|
||||
[:div#search.overflow-auto.space-y-6.p-2.w-full
|
||||
|
||||
(com/text-input {:id "search-input"
|
||||
:type "search"
|
||||
:placeholder "5/5/2034 Magheritas"
|
||||
:name "q"
|
||||
:hx-post "/search"
|
||||
:hx-trigger "keyup changed delay:300ms, search"
|
||||
:hx-target "#search-results"
|
||||
:hx-indicator "#search"
|
||||
:value (:q (:params request))
|
||||
:autofocus true})
|
||||
[:i.text-sm.text-gray-600.dark:text-gray-50 "Try dates, numbers, vendors. To filter to specific type, use 'invoice', 'transaction', 'journal-entry', 'payment'."]
|
||||
#_[:style
|
||||
".htmx-request #search-results {display: none} .htmx-request .htmx-indicator { display: block !important; }"]
|
||||
[:div#search-results
|
||||
]
|
||||
[:div.loader.is-loading.big.htmx-indicator ]]
|
||||
nil)))))
|
||||
(com/modal {}
|
||||
(com/modal-card {:class "w-full h-full"}
|
||||
[:div.p-2 "Search"]
|
||||
[:div#search.overflow-auto.space-y-6.p-2.w-full
|
||||
|
||||
(com/text-input {:id "search-input"
|
||||
:type "search"
|
||||
:placeholder "5/5/2034 Magheritas"
|
||||
:name "q"
|
||||
:hx-post "/search"
|
||||
:hx-trigger "keyup changed delay:300ms, search"
|
||||
:hx-target "#search-results"
|
||||
:hx-indicator "#search"
|
||||
:value (:q (:params request))
|
||||
:autofocus true})
|
||||
[:i.text-sm.text-gray-600.dark:text-gray-50 "Try dates, numbers, vendors. To filter to specific type, use 'invoice', 'transaction', 'journal-entry', 'payment'."]
|
||||
#_[:style
|
||||
".htmx-request #search-results {display: none} .htmx-request .htmx-indicator { display: block !important; }"]
|
||||
[:div#search-results]
|
||||
[:div.loader.is-loading.big.htmx-indicator]]
|
||||
nil)))))
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
[:path {:d "M10.5,14.25v-9a9,9,0,1,0,5.561,16.077Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
|
||||
[:path {:d "M22.5,12.75h-9l5.561,7.077A8.986,8.986,0,0,0,22.5,12.75Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]])
|
||||
|
||||
|
||||
(def accounting-invoice-mail
|
||||
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
|
||||
[:defs]
|
||||
@@ -91,7 +90,6 @@
|
||||
[:svg {:id "theme-toggle-light-icon", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
|
||||
[:path {:d "M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z", :fill-rule "evenodd", :clip-rule "evenodd"}]])
|
||||
|
||||
|
||||
(def home
|
||||
[:svg {:fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
|
||||
[:path {:d "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"}]])
|
||||
@@ -112,7 +110,6 @@
|
||||
[:svg {:xmlns "http://www.w3.org/2000/svg", :aria-hidden "true", :fill "none", :viewbox "0 0 24 24", :stroke-width "1.5", :stroke "currentColor"}
|
||||
[:path {:stroke-linecap "round", :stroke-linejoin "round", :d "M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"}]])
|
||||
|
||||
|
||||
(def upload
|
||||
[:svg {:xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 24 24", :stroke-width "2", :stroke "currentColor", :aria-hidden "true"}
|
||||
[:path {:stroke-linecap "round", :stroke-linejoin "round", :d "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"}]])
|
||||
@@ -315,7 +312,6 @@
|
||||
[:line {:x1 "7", :y1 "7", :x2 "17", :y2 "17", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
|
||||
[:line {:x1 "17", :y1 "7", :x2 "7", :y2 "17", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]])
|
||||
|
||||
|
||||
(def filled-x
|
||||
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
|
||||
[:circle {:cx "12", :cy "12", :r "11.5", :fill "#FFF", :stroke-linecap "round", :stroke-linejoin "round"}]
|
||||
@@ -384,7 +380,6 @@
|
||||
[:circle {:cx "12", :cy "6.75", :r "5.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
|
||||
[:path {:d "M7.261,3.958A9.124,9.124,0,0,0,13.833,6.75a9.138,9.138,0,0,0,3.617-.744", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]])
|
||||
|
||||
|
||||
(def accounts
|
||||
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
|
||||
[:defs]
|
||||
@@ -404,7 +399,6 @@
|
||||
[:line {:x1 "15.504", :y1 "5.5", :x2 "15.504", :y2 "12.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
|
||||
[:line {:x1 "15.504", :y1 "0.5", :x2 "15.504", :y2 "3.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]])
|
||||
|
||||
|
||||
(def cog
|
||||
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
|
||||
[:defs]
|
||||
@@ -432,7 +426,6 @@
|
||||
[:path {:stroke "currentColor", :d "M20.458 13.742C20.3206 13.742 20.2092 13.6305 20.2092 13.4931C20.2092 13.3557 20.3206 13.2443 20.458 13.2443"}]
|
||||
[:path {:stroke "currentColor", :d "M20.458 13.742C20.5955 13.742 20.7069 13.6305 20.7069 13.4931C20.7069 13.3557 20.5955 13.2443 20.458 13.2443"}]])
|
||||
|
||||
|
||||
(def arrow-in
|
||||
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
|
||||
[:defs]
|
||||
@@ -528,4 +521,4 @@
|
||||
[:path {:d "M11 15a1 1 0 1 0 2 0 1 1 0 1 0 -2 0", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
|
||||
[:path {:d "m12 16 0 3", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
|
||||
[:path {:d "M4.5 9.5h15s1 0 1 1v12s0 1 -1 1h-15s-1 0 -1 -1v-12s0 -1 1 -1", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
|
||||
[:path {:d "M6.5 6a5.5 5.5 0 0 1 11 0v3.5h-11Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]])
|
||||
[:path {:d "M6.5 6a5.5 5.5 0 0 1 11 0v3.5h-11Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]])
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
(ns auto-ap.ssr.transaction
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [audit-transact audit-transact-batch conn pull-attr
|
||||
pull-many]]
|
||||
: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]]
|
||||
@@ -20,10 +20,10 @@
|
||||
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 entity-id html-response
|
||||
many-entity modal-response percentage ref->enum-schema
|
||||
wrap-implied-route-param wrap-merge-prior-hx
|
||||
wrap-schema-enforce]]
|
||||
: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]
|
||||
[clojure.string :as str]
|
||||
[datomic.api :as dc]
|
||||
@@ -39,8 +39,6 @@
|
||||
|
||||
(def page (helper/page-route grid-page))
|
||||
|
||||
|
||||
|
||||
(def table (helper/table-route grid-page))
|
||||
|
||||
(def csv (helper/csv-route grid-page))
|
||||
@@ -60,29 +58,29 @@
|
||||
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
|
||||
(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))
|
||||
|
||||
all-ids)
|
||||
(:identity request))
|
||||
|
||||
;; Then retract or suppress the transactions
|
||||
(audit-transact
|
||||
(mapcat (fn [i]
|
||||
@@ -94,14 +92,12 @@
|
||||
[:db/retractEntity [:journal-entry/original-entity i]]]))
|
||||
all-ids)
|
||||
(:identity request))
|
||||
|
||||
(html-response
|
||||
|
||||
(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
|
||||
bulk-code/key->handler
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
(ns auto-ap.ssr.transaction.bulk-code
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [audit-transact-batch conn pull-attr pull-many]]
|
||||
: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]]
|
||||
@@ -23,9 +23,9 @@
|
||||
[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]]
|
||||
: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=]]
|
||||
@@ -34,52 +34,52 @@
|
||||
|
||||
(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))})
|
||||
(-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? value))))
|
||||
:accountId (fc/field-value (:account value))})
|
||||
:data-key "show"
|
||||
:x-ref "p"}
|
||||
:x-ref "p"}
|
||||
hx/alpine-mount-then-appear)
|
||||
(fc/with-field :db/id
|
||||
(com/hidden {:name (fc/field-name)
|
||||
(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)
|
||||
(account-typeahead* {:value (fc/field-value)
|
||||
:client-id client-id
|
||||
:name (fc/field-name)
|
||||
:x-model "accountId"}))))
|
||||
: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) }
|
||||
: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-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])))
|
||||
(location-select* {:name (fc/field-name)
|
||||
:account-location (let [account-id (:account @value)]
|
||||
(when (nat-int? account-id)
|
||||
(:account/location (dc/pull (dc/db conn) '[:account/location] account-id))))
|
||||
:client-locations (pull-attr (dc/db conn) :client/locations client-id)
|
||||
:value (fc/field-value)}))))
|
||||
: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)
|
||||
(com/money-input {:name (fc/field-name)
|
||||
:class "w-16"
|
||||
:value (some-> (fc/field-value)
|
||||
(* 100)
|
||||
(long))}))))
|
||||
(* 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))))
|
||||
|
||||
@@ -90,64 +90,58 @@
|
||||
{: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))
|
||||
: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
|
||||
(def bulk-code-schema
|
||||
(mc/schema [:map
|
||||
[:vendor {:optional true} [:maybe entity-id]]
|
||||
[:approval-status {:optional true} [:maybe (ref->enum-schema "transaction-approval-status")] ]
|
||||
[:approval-status {:optional true} [:maybe (ref->enum-schema "transaction-approval-status")]]
|
||||
[:accounts {:optional true}
|
||||
[:maybe
|
||||
[:maybe
|
||||
[:vector {:coerce? true}
|
||||
[:map [:account entity-id]
|
||||
[:location [:string {:min 1 :error/message "required"}]]
|
||||
[:percentage percentage]]]] ]]))
|
||||
|
||||
[: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))
|
||||
(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)
|
||||
(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*))
|
||||
@@ -167,74 +161,76 @@
|
||||
[])
|
||||
|
||||
(step-schema [_]
|
||||
(mm/form-schema linear-wizard))
|
||||
(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#account-entries.space-y-3
|
||||
(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))))
|
||||
(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 {:hx-trigger "change"
|
||||
:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-vendor-changed)
|
||||
:hx-target "#account-entries"
|
||||
:hx-swap "innerHTML"
|
||||
:hx-include "closest form"}
|
||||
(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#account-entries.space-y-3
|
||||
(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))]
|
||||
@@ -263,78 +259,128 @@
|
||||
(steps [_]
|
||||
[:accounts])
|
||||
(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]
|
||||
(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))
|
||||
|
||||
(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))
|
||||
(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 {}))]
|
||||
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 {}))]
|
||||
|
||||
;; Validate account locations
|
||||
(doseq [a accounts
|
||||
:let [{:keys [:account/location :account/name]} (dc/pull db
|
||||
[:account/location :account/name]
|
||||
(:account a))]]
|
||||
(when (and location (not= location (:location a)))
|
||||
(form-validation-error (str "Account " name " uses location " (:location a) ", but is supposed to be " location)))
|
||||
(doseq [[_ locations] client->locations]
|
||||
(when (and (not location)
|
||||
(not (get (into #{"Shared"} locations)
|
||||
(:location a))))
|
||||
(form-validation-error (str "Account " name " uses location " (:location a) ", but doesn't belong to the client.")))))
|
||||
(doseq [a accounts
|
||||
:let [{:keys [:account/location :account/name]} (dc/pull db
|
||||
[:account/location :account/name]
|
||||
(:account a))]]
|
||||
(when (and location (not= location (:location a)))
|
||||
(form-validation-error (str "Account " name " uses location " (:location a) ", but is supposed to be " location)))
|
||||
(doseq [[_ locations] client->locations]
|
||||
(when (and (not location)
|
||||
(not (get (into #{"Shared"} locations)
|
||||
(:location a))))
|
||||
(form-validation-error (str "Account " name " uses location " (:location a) ", but doesn't belong to the client.")))))
|
||||
|
||||
(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)
|
||||
(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)
|
||||
vendor
|
||||
(assoc :transaction/vendor vendor)
|
||||
|
||||
(seq accounts)
|
||||
(assoc :transaction/accounts
|
||||
(maybe-code-accounts t accounts locations)))]))
|
||||
transactions)
|
||||
(:identity request))
|
||||
(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"})))))
|
||||
(html-response
|
||||
(com/success-modal {:title "Transactions Coded"}
|
||||
[:p (str "Successfully coded " (count all-ids) " transactions.")])
|
||||
:headers {"hx-trigger" "refreshTable"})))))
|
||||
|
||||
(defn- get-client-id [request]
|
||||
(-> request :clients first :db/id))
|
||||
|
||||
(defn- vendor-default-account [vendor-id client-id]
|
||||
(when vendor-id
|
||||
(let [vendor (edit/get-vendor vendor-id)
|
||||
clientized (edit/clientize-vendor vendor client-id)]
|
||||
(:vendor/default-account clientized))))
|
||||
|
||||
(defn- build-default-account-row [account]
|
||||
{:db/id (str (java.util.UUID/randomUUID))
|
||||
:account (:db/id account)
|
||||
:location (or (:account/location account) "Shared")
|
||||
:percentage 1.0})
|
||||
|
||||
(defn- render-accounts-section [request]
|
||||
(let [step-params (:step-params (:multi-form-state request))]
|
||||
(html-response
|
||||
[:div
|
||||
(fc/start-form step-params
|
||||
(when (:form-errors request) {:step-params (:form-errors request)})
|
||||
(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")))))])))
|
||||
|
||||
(defn vendor-changed-handler [request]
|
||||
(let [snapshot (:snapshot (:multi-form-state request))
|
||||
step-params (:step-params (:multi-form-state request))
|
||||
client-id (get-client-id request)
|
||||
vendor-id (or (:vendor step-params) (:vendor snapshot))
|
||||
updated-step-params (if (and (empty? (:accounts step-params))
|
||||
vendor-id
|
||||
client-id)
|
||||
(if-let [default-account (vendor-default-account vendor-id client-id)]
|
||||
(assoc step-params :accounts [(build-default-account-row default-account)])
|
||||
step-params)
|
||||
step-params)]
|
||||
(render-accounts-section (assoc-in request [:multi-form-state :step-params] updated-step-params))))
|
||||
|
||||
(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 (-> 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]
|
||||
@@ -345,9 +391,12 @@
|
||||
(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))}
|
||||
::route/bulk-code-vendor-changed (-> vendor-changed-handler
|
||||
(mm/wrap-wizard bulk-code-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::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)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
(ns auto-ap.ssr.transaction.common
|
||||
(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.datomic.accounts :as d-accounts]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids is-admin?]]
|
||||
[auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-4
|
||||
conn merge-query observable-query pull-many]]
|
||||
[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.ledger :as ledger-routes]
|
||||
[auto-ap.routes.payments :as payment-routes]
|
||||
@@ -33,26 +33,26 @@
|
||||
[: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]]
|
||||
[:unresolved {:optional true}
|
||||
[:maybe [:boolean {:decode/string {:enter #(cond (= % "on") true
|
||||
(= % "") false
|
||||
:else
|
||||
(boolean %))}}]]]
|
||||
[: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]}]]]
|
||||
[: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")]]
|
||||
[: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}]]]
|
||||
[: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]}]]]
|
||||
[: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")]]
|
||||
[: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}
|
||||
@@ -66,14 +66,14 @@
|
||||
'[: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/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/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
|
||||
@@ -104,13 +104,13 @@
|
||||
[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))
|
||||
: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}]
|
||||
@@ -161,75 +161,75 @@
|
||||
: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))]})
|
||||
(:vendor args)
|
||||
(merge-query {:query {:in ['?vendor-id]
|
||||
:where ['[?e :transaction/vendor ?vendor-id]]}
|
||||
: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)
|
||||
(merge-query {:query {:in ['?import-batch-id]
|
||||
:where ['[?import-batch-id :import-batch/entry ?e]]}
|
||||
:args [(:import-batch-id 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)
|
||||
(merge-query {:query {:in ['?import-batch-id]
|
||||
:where ['[?import-batch-id :import-batch/entry ?e]]}
|
||||
: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]))) ]}})
|
||||
(: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)]})
|
||||
(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) "payment")
|
||||
(merge-query {:query {:where ['[?e :transaction/payment]]}})
|
||||
|
||||
(= (:linked-to args) "expected-deposit")
|
||||
(merge-query {:query {:where ['[?e :transaction/expected-deposit]]}})
|
||||
(= (: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) "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])]}})
|
||||
(= (: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]}))
|
||||
(: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]
|
||||
:where ['[?e :transaction/approval-status ?status]]}
|
||||
:args [(:status route-params)]})
|
||||
@@ -253,8 +253,8 @@
|
||||
(->> (observable-query query)
|
||||
(apply-sort-4 (assoc query-params :default-asc? true))
|
||||
(apply-pagination query-params))))
|
||||
|
||||
(defn fetch-page [request]
|
||||
|
||||
(defn fetch-page [request]
|
||||
(let [db (dc/db conn)
|
||||
{ids-to-retrieve :ids matching-count :count
|
||||
all-ids :all-ids} (fetch-ids db request)]
|
||||
@@ -263,8 +263,6 @@
|
||||
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"}
|
||||
@@ -290,8 +288,6 @@
|
||||
(com/link {"@click" "import_batch_id=null; $nextTick(() => $dispatch('change'))"}
|
||||
svg/x)]])]))
|
||||
|
||||
|
||||
|
||||
(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)
|
||||
@@ -313,7 +309,6 @@
|
||||
{: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
|
||||
@@ -324,94 +319,93 @@
|
||||
(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}))
|
||||
(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)
|
||||
(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}))
|
||||
(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)
|
||||
|
||||
(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 "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 "Location"}
|
||||
(com/text-input {:name "location"
|
||||
:id "location"
|
||||
:class "hot-filter"
|
||||
:value (:location (:query-params request))
|
||||
:placeholder "SC"
|
||||
: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"}
|
||||
[: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})])
|
||||
(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})])
|
||||
|
||||
(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"}]}))
|
||||
(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 (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)]])
|
||||
(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)]])
|
||||
|
||||
(def grid-page
|
||||
(helper/build {:id "entity-table"
|
||||
@@ -420,19 +414,17 @@
|
||||
: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)
|
||||
(some-> (import-batch-id* request) (assoc-in [1 :hx-swap-oob] true))])
|
||||
: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)
|
||||
(some-> (import-batch-id* request) (assoc-in [1 :hx-swap-oob] true))])
|
||||
:action-buttons (fn [request]
|
||||
[
|
||||
(com/button {:color :primary
|
||||
[(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"
|
||||
}
|
||||
"hx-include" "#transaction-filters"}
|
||||
"Code")
|
||||
(com/button {:color :primary
|
||||
:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-delete)
|
||||
@@ -454,13 +446,13 @@
|
||||
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]]
|
||||
[[: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)])))
|
||||
|
||||
::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]
|
||||
@@ -477,9 +469,7 @@
|
||||
(= (-> request :query-params :sort first :name) "Vendor")
|
||||
(or (-> entity :transaction/vendor :vendor/name)
|
||||
"No vendor")
|
||||
|
||||
|
||||
|
||||
|
||||
:else nil))
|
||||
:page->csv-entities (fn [[transactions]]
|
||||
transactions)
|
||||
@@ -529,20 +519,20 @@
|
||||
: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)))
|
||||
(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))))]
|
||||
(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
|
||||
@@ -553,41 +543,36 @@
|
||||
{: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"}))
|
||||
|
||||
{: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)
|
||||
::invoice-routes/all-page)
|
||||
{:exact-match-id (:db/id inv)})
|
||||
:color :secondary
|
||||
:content (format "Invoice '%s'" (:invoice/invoice-number inv))}))
|
||||
|
||||
))))
|
||||
: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
|
||||
(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)
|
||||
@@ -598,7 +583,6 @@
|
||||
(assoc-in [:query-params :start] 0)
|
||||
(assoc-in [:query-params :per-page] 250))))
|
||||
|
||||
|
||||
:else
|
||||
selected)]
|
||||
ids))
|
||||
@@ -142,6 +142,12 @@
|
||||
true (dissoc :vendor/account-overrides :vendor/terms-overrides))]
|
||||
vendor)))
|
||||
|
||||
(defn vendor-default-account [vendor-id client-id]
|
||||
(when vendor-id
|
||||
(let [vendor (get-vendor vendor-id)
|
||||
clientized (clientize-vendor vendor client-id)]
|
||||
(:vendor/default-account clientized))))
|
||||
|
||||
(defn location-select*
|
||||
[{:keys [name account-location client-locations value]}]
|
||||
(let [options (into (cond account-location
|
||||
@@ -904,18 +910,23 @@
|
||||
(transaction-rules-view request)]
|
||||
[:div {:x-show "activeForm === 'manual'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"}
|
||||
[:div {}
|
||||
(fc/with-field :transaction/vendor
|
||||
(com/validated-field
|
||||
{:label "Vendor"
|
||||
:errors (fc/field-errors)}
|
||||
[:div.w-96
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:error? (fc/error?)
|
||||
:class "w-96"
|
||||
:placeholder "Search..."
|
||||
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
||||
:value (fc/field-value)
|
||||
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})]))
|
||||
[:div {:hx-trigger "change"
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-vendor-changed)
|
||||
:hx-target "#account-grid-body"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-include "closest form"}
|
||||
(fc/with-field :transaction/vendor
|
||||
(com/validated-field
|
||||
{:label "Vendor"
|
||||
:errors (fc/field-errors)}
|
||||
[:div.w-96
|
||||
(com/typeahead {:name (fc/field-name)
|
||||
:error? (fc/error?)
|
||||
:class "w-96"
|
||||
:placeholder "Search..."
|
||||
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
||||
:value (fc/field-value)
|
||||
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})]))]
|
||||
|
||||
;; Memo field
|
||||
|
||||
@@ -1381,6 +1392,35 @@
|
||||
[]
|
||||
entity)))
|
||||
|
||||
(defn edit-vendor-changed-handler [request]
|
||||
(let [snapshot (:snapshot (:multi-form-state request))
|
||||
client-id (or (:transaction/client snapshot)
|
||||
(-> request :entity :transaction/client :db/id))
|
||||
vendor-id (:transaction/vendor snapshot)
|
||||
total (Math/abs (or (:transaction/amount snapshot) 0.0))
|
||||
amount-mode (or (:amount-mode snapshot) "$")]
|
||||
(if (and (empty? (:transaction/accounts snapshot))
|
||||
vendor-id
|
||||
client-id)
|
||||
(if-let [default-account (vendor-default-account vendor-id client-id)]
|
||||
(let [new-account {:db/id (str (java.util.UUID/randomUUID))
|
||||
:transaction-account/account (:db/id default-account)
|
||||
:transaction-account/location (or (:account/location default-account) "Shared")}
|
||||
new-account (if (= amount-mode "%")
|
||||
(assoc new-account :transaction-account/amount 100.0)
|
||||
(assoc new-account :transaction-account/amount total))
|
||||
updated-snapshot (assoc snapshot :transaction/accounts [new-account])
|
||||
updated-request (assoc-in request [:multi-form-state :snapshot] updated-snapshot)]
|
||||
(html-response
|
||||
[:div#account-grid-body
|
||||
(account-grid-body* updated-request)]))
|
||||
(html-response
|
||||
[:div#account-grid-body
|
||||
(account-grid-body* request)]))
|
||||
(html-response
|
||||
[:div#account-grid-body
|
||||
(account-grid-body* request)]))))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{::route/edit-wizard (-> mm/open-wizard-handler
|
||||
@@ -1399,6 +1439,9 @@
|
||||
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/edit-vendor-changed (-> edit-vendor-changed-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/location-select (-> location-select
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:name :string]
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
:args [(dc/db conn)
|
||||
(iol-ion.query/recent-date 300)
|
||||
(map :db/id clients)
|
||||
|
||||
|
||||
pull-expr]})
|
||||
(map first)
|
||||
(drop-while (fn [x]
|
||||
@@ -71,44 +71,42 @@
|
||||
(take 50)
|
||||
(into []))))
|
||||
|
||||
|
||||
(defn get-pinecone [transaction-id]
|
||||
(->
|
||||
(http/get (-> "https://transactions-a8257ba.svc.us-west4-gcp-free.pinecone.io/vectors/fetch"
|
||||
url/url
|
||||
(assoc :query {:ids transaction-id})
|
||||
str)
|
||||
{:headers {"Api-Key" "f2d3a78e-bcea-4fcd-88b6-2527b8423607"}
|
||||
:as :json
|
||||
:keywordize? false})
|
||||
:body
|
||||
:vectors
|
||||
((keyword (str transaction-id)))
|
||||
:values))
|
||||
(->
|
||||
(http/get (-> "https://transactions-a8257ba.svc.us-west4-gcp-free.pinecone.io/vectors/fetch"
|
||||
url/url
|
||||
(assoc :query {:ids transaction-id})
|
||||
str)
|
||||
{:headers {"Api-Key" "f2d3a78e-bcea-4fcd-88b6-2527b8423607"}
|
||||
:as :json
|
||||
:keywordize? false})
|
||||
:body
|
||||
:vectors
|
||||
((keyword (str transaction-id)))
|
||||
:values))
|
||||
|
||||
(defn get-pinecone-similarities [transaction-id]
|
||||
(filter
|
||||
(fn [{:keys [score]}]
|
||||
(> score 0.95)
|
||||
)
|
||||
(->
|
||||
(http/post (-> "https://transactions-a8257ba.svc.us-west4-gcp-free.pinecone.io/query"
|
||||
url/url
|
||||
str)
|
||||
{:headers {"Api-Key" "f2d3a78e-bcea-4fcd-88b6-2527b8423607"}
|
||||
:form-params {"vector" (get-pinecone transaction-id)
|
||||
"topK" 100,
|
||||
"includeMetadata" true
|
||||
"namespace" ""}
|
||||
:content-type :json
|
||||
:as :json})
|
||||
:body
|
||||
:matches)))
|
||||
(filter
|
||||
(fn [{:keys [score]}]
|
||||
(> score 0.95))
|
||||
(->
|
||||
(http/post (-> "https://transactions-a8257ba.svc.us-west4-gcp-free.pinecone.io/query"
|
||||
url/url
|
||||
str)
|
||||
{:headers {"Api-Key" "f2d3a78e-bcea-4fcd-88b6-2527b8423607"}
|
||||
:form-params {"vector" (get-pinecone transaction-id)
|
||||
"topK" 100,
|
||||
"includeMetadata" true
|
||||
"namespace" ""}
|
||||
:content-type :json
|
||||
:as :json})
|
||||
:body
|
||||
:matches)))
|
||||
|
||||
(defn pinecone-similarity-list [transaction-id]
|
||||
(for [{{:keys [amount date description vendor]} :metadata score :score id :id} (get-pinecone-similarities transaction-id)
|
||||
:let [vendor-name (:vendor/name (:transaction/vendor (dc/pull (dc/db conn) [{:transaction/vendor [:vendor/name]} ] (Long/parseLong id))))
|
||||
account-code (-> (dc/pull (dc/db conn) [{:transaction/accounts [{:transaction-account/account [:account/numeric-code]}]} ] (Long/parseLong id))
|
||||
:let [vendor-name (:vendor/name (:transaction/vendor (dc/pull (dc/db conn) [{:transaction/vendor [:vendor/name]}] (Long/parseLong id))))
|
||||
account-code (-> (dc/pull (dc/db conn) [{:transaction/accounts [{:transaction-account/account [:account/numeric-code]}]}] (Long/parseLong id))
|
||||
:transaction/accounts
|
||||
first
|
||||
:transaction-account/account
|
||||
@@ -121,7 +119,6 @@
|
||||
:description description
|
||||
:score score}))
|
||||
|
||||
|
||||
(defn transaction-row [r & {:keys [hide-actions? class last? other-params]}]
|
||||
(com/data-grid-row
|
||||
(cond-> {:class class}
|
||||
@@ -219,8 +216,8 @@
|
||||
@(dc/transact conn [updated-transaction])
|
||||
(html-response (transaction-row
|
||||
(parse-outcome (dc/pull db-before
|
||||
pull-expr
|
||||
(Long/parseLong transaction-id)))
|
||||
pull-expr
|
||||
(Long/parseLong transaction-id)))
|
||||
:hide-actions? true
|
||||
:class "live-added"
|
||||
:other-params (hx/alpine-mount-then-disappear {})))))
|
||||
@@ -237,48 +234,48 @@
|
||||
|
||||
(defn explain [{:keys [identity session] {:keys [transaction-id]} :route-params}]
|
||||
(let [r (dc/pull (dc/db conn)
|
||||
pull-expr
|
||||
(Long/parseLong transaction-id))
|
||||
pull-expr
|
||||
(Long/parseLong transaction-id))
|
||||
similar (pinecone-similarity-list transaction-id)]
|
||||
(modal-response
|
||||
(com/modal {}
|
||||
(com/modal-card {:style {:width "900px"}}
|
||||
[:div.flex [:div.p-2 "Similar Transactions"]]
|
||||
(com/data-grid {:headers [(com/data-grid-header {:name "Date"
|
||||
:key "date"})
|
||||
(com/data-grid-header {:name "Description"
|
||||
:key "description"})
|
||||
(com/data-grid-header {:name "Amount"
|
||||
:key "amount"})
|
||||
(com/data-grid-header {:name "Vendor"
|
||||
:key "vendor"})
|
||||
(com/data-grid-header {:name "Account"
|
||||
:key "account"})
|
||||
(com/data-grid-header {:name "Score"
|
||||
:key "score"})]}
|
||||
(com/modal {}
|
||||
(com/modal-card {:style {:width "900px"}}
|
||||
[:div.flex [:div.p-2 "Similar Transactions"]]
|
||||
(com/data-grid {:headers [(com/data-grid-header {:name "Date"
|
||||
:key "date"})
|
||||
(com/data-grid-header {:name "Description"
|
||||
:key "description"})
|
||||
(com/data-grid-header {:name "Amount"
|
||||
:key "amount"})
|
||||
(com/data-grid-header {:name "Vendor"
|
||||
:key "vendor"})
|
||||
(com/data-grid-header {:name "Account"
|
||||
:key "account"})
|
||||
(com/data-grid-header {:name "Score"
|
||||
:key "score"})]}
|
||||
|
||||
(com/data-grid-row {:class "bg-primary-200"}
|
||||
(com/data-grid-cell {:class "text-left font-bold"} (some-> r :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date)))
|
||||
(com/data-grid-cell {:class "text-left font-bold"} (-> r :transaction/description-original) )
|
||||
(com/data-grid-cell {:class "font-bold"} (if (> (-> r :transaction/amount) 0.0)
|
||||
[:div.tag.is-success.is-light (str "$" (Math/round (:transaction/amount r)))]
|
||||
[:div.tag.is-danger.is-light (str "$" (Math/round (:transaction/amount r)))]))
|
||||
(com/data-grid-cell {})
|
||||
(com/data-grid-cell {})
|
||||
(com/data-grid-cell {}))
|
||||
(com/data-grid-row {:class "bg-primary-200"}
|
||||
(com/data-grid-cell {:class "text-left font-bold"} (some-> r :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date)))
|
||||
(com/data-grid-cell {:class "text-left font-bold"} (-> r :transaction/description-original))
|
||||
(com/data-grid-cell {:class "font-bold"} (if (> (-> r :transaction/amount) 0.0)
|
||||
[:div.tag.is-success.is-light (str "$" (Math/round (:transaction/amount r)))]
|
||||
[:div.tag.is-danger.is-light (str "$" (Math/round (:transaction/amount r)))]))
|
||||
(com/data-grid-cell {})
|
||||
(com/data-grid-cell {})
|
||||
(com/data-grid-cell {}))
|
||||
|
||||
(com/data-grid-row {}
|
||||
(take 10
|
||||
(for [{:keys [amount date description vendor-name numeric-code score]} similar]
|
||||
(com/data-grid-row
|
||||
{}
|
||||
(com/data-grid-cell {:class "text-left"} (subs date 0 10))
|
||||
(com/data-grid-cell {:class "text-left"} description )
|
||||
(com/data-grid-cell {} (some->> amount double (format "$%.2f")))
|
||||
(com/data-grid-cell {} vendor-name)
|
||||
(com/data-grid-cell {} numeric-code)
|
||||
(com/data-grid-cell {} (format "%.1f%%" (* 100 (double score)))))))))
|
||||
[:div])))))
|
||||
(com/data-grid-row {}
|
||||
(take 10
|
||||
(for [{:keys [amount date description vendor-name numeric-code score]} similar]
|
||||
(com/data-grid-row
|
||||
{}
|
||||
(com/data-grid-cell {:class "text-left"} (subs date 0 10))
|
||||
(com/data-grid-cell {:class "text-left"} description)
|
||||
(com/data-grid-cell {} (some->> amount double (format "$%.2f")))
|
||||
(com/data-grid-cell {} vendor-name)
|
||||
(com/data-grid-cell {} numeric-code)
|
||||
(com/data-grid-cell {} (format "%.1f%%" (* 100 (double score)))))))))
|
||||
[:div])))))
|
||||
|
||||
(defn transaction-rows* [{:keys [clients identity after]}]
|
||||
(let [recommendations (transaction-recommendations identity clients :after after)]
|
||||
@@ -286,7 +283,7 @@
|
||||
(for [r recommendations
|
||||
:let [last? (= r (last recommendations))]]
|
||||
(transaction-row r :last? last?))
|
||||
[:tr [:td.has-text-centered.has-text-gray {:colspan 7 }
|
||||
[:tr [:td.has-text-centered.has-text-gray {:colspan 7}
|
||||
[:i "That's the last of 'em!"]]])))
|
||||
|
||||
(defn transaction-rows [{:keys [session identity route-params clients]}]
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
{}
|
||||
hiccup))})
|
||||
|
||||
|
||||
(defn base-page [request contents page-name]
|
||||
(html-page
|
||||
[:html
|
||||
@@ -28,7 +27,7 @@
|
||||
[:script {:src "//cdn.jsdelivr.net/chartist.js/latest/chartist.min.js"}]
|
||||
[:link {:rel "stylesheet", :href "/output.css"}]
|
||||
[:script {:src "https://cdn.plaid.com/link/v2/stable/link-initialize.js"}]
|
||||
[:script { :src "https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-tooltip@1.x.x/dist/cdn.min.js" :defer true}]
|
||||
[:script {:src "https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-tooltip@1.x.x/dist/cdn.min.js" :defer true}]
|
||||
[:link {:rel "stylesheet" :href "/css/tippy/tippy.css"}]
|
||||
[:link {:rel "stylesheet" :href "/css/tippy/light.css"}]
|
||||
[:script {:src "/js/htmx.min.js"
|
||||
@@ -71,7 +70,6 @@ input[type=number] {
|
||||
contents
|
||||
[:script {:src "/js/flowbite.min.js"}]
|
||||
|
||||
|
||||
[:div#modal-holder
|
||||
{:class "fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen"
|
||||
"x-show" "open"
|
||||
@@ -106,4 +104,4 @@ input[type=number] {
|
||||
"x-transition:leave-start" "!opacity-100 !translate-y-0"
|
||||
"x-transition:leave-end" "!opacity-0 !translate-y-32"}
|
||||
|
||||
[:div#modal-content.flex.items-center.justify-center {:class "md:p-12"}]]]]]]))
|
||||
[:div#modal-content.flex.items-center.justify-center {:class "md:p-12"}]]]]]]))
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
(defn filters [request]
|
||||
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
||||
"hx-get" (bidi/path-for ssr-routes/only-routes
|
||||
:user-table)
|
||||
:user-table)
|
||||
"hx-target" "#user-table"
|
||||
"hx-indicator" "#user-table"}
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
(com/field {:label "Role"}
|
||||
(com/radio-card {:size :small
|
||||
:name "role"
|
||||
:value (:role (:query-params request))
|
||||
:value (:role (:query-params request))
|
||||
:options [{:value ""
|
||||
:content "All"}
|
||||
{:value "admin"
|
||||
@@ -84,7 +84,7 @@
|
||||
[:maybe
|
||||
(into [:map {}
|
||||
[:role {:optional true} [:maybe (ref->enum-schema "user-role")]]
|
||||
[:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]] ]
|
||||
[:client {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :client/name]}]]]]
|
||||
default-grid-fields-schema)]))
|
||||
|
||||
(def default-read '[:db/id
|
||||
@@ -93,19 +93,19 @@
|
||||
:user/profile-image-url
|
||||
[:user/last-login :xform clj-time.coerce/from-date]
|
||||
{[:user/role :xform iol-ion.query/ident] [:db/ident]
|
||||
|
||||
|
||||
:user/clients [:client/code :db/id :client/locations :client/name]}])
|
||||
|
||||
(defn fetch-ids [db request]
|
||||
(let [query-params (:query-params request)
|
||||
query (cond-> {:query {:find []
|
||||
:in '[$ ]
|
||||
:in '[$]
|
||||
:where '[]}
|
||||
:args [db ]}
|
||||
:args [db]}
|
||||
(:sort query-params) (add-sorter-fields {"name" ['[?e :user/name ?un]
|
||||
'[(clojure.string/upper-case ?un) ?sort-name]]
|
||||
"email" ['[(get-else $ ?e :user/email "") ?sort-email]]
|
||||
|
||||
|
||||
"role" ['[?e :user/role ?r]
|
||||
'[?r :db/ident ?ri]
|
||||
'[(name ?ri) ?sort-role]]
|
||||
@@ -136,16 +136,14 @@
|
||||
(some->> query-params :role)
|
||||
(merge-query {:query {:find []
|
||||
:in ['?r]
|
||||
:where ['[?e :user/role ?r] ]}
|
||||
:where ['[?e :user/role ?r]]}
|
||||
:args [(some->> query-params :role)]})
|
||||
|
||||
|
||||
|
||||
true
|
||||
(merge-query {:query {:find ['?sort-default '?e]
|
||||
:where ['[?e :user/name ?un]
|
||||
'[(clojure.string/upper-case ?un) ?sort-default]]}}))]
|
||||
|
||||
|
||||
(cond->> (query2 query)
|
||||
true (apply-sort-3 query-params)
|
||||
true (apply-pagination query-params))))
|
||||
@@ -186,14 +184,12 @@
|
||||
[:div.flex.space-x-2
|
||||
(for [{:client/keys [code]} (take 3 (:user/clients user))]
|
||||
(com/pill {:color :primary}
|
||||
code)
|
||||
)
|
||||
code))
|
||||
(let [remainder (- (count (:user/clients user)) 3)]
|
||||
(when (> remainder 0)
|
||||
(com/pill {:color :white}
|
||||
(format "%d more" remainder))))])
|
||||
|
||||
|
||||
(def grid-page
|
||||
(helper/build {:id "user-table"
|
||||
:nav com/admin-aside-nav
|
||||
@@ -223,10 +219,10 @@
|
||||
:sort-key "name"
|
||||
:render (fn [user]
|
||||
[:div.flex.space-x-2.place-items-center
|
||||
(when-let [profile-image (:user/profile-image-url user) ]
|
||||
(when-let [profile-image (:user/profile-image-url user)]
|
||||
[:div.rounded-full.overflow-hidden.w-8.h-8.display-inline
|
||||
[:img {:src profile-image }]])
|
||||
[:span.inline-block ] (:user/name user)])}
|
||||
[:img {:src profile-image}]])
|
||||
[:span.inline-block] (:user/name user)])}
|
||||
|
||||
{:key "email"
|
||||
:name "Email"
|
||||
@@ -242,8 +238,7 @@
|
||||
:render #(some-> % (:user/last-login) (atime/unparse-local atime/standard-time))}
|
||||
{:key "clients"
|
||||
:name "Clients"
|
||||
:render user->client-pills}
|
||||
]}))
|
||||
:render user->client-pills}]}))
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
(def table* (partial helper/table* grid-page))
|
||||
@@ -266,19 +261,17 @@
|
||||
(com/data-grid-cell {}
|
||||
(com/validated-field {:errors (fc/field-errors (:db/id fc/*current*))}
|
||||
(com/typeahead {:name (fc/field-name (:db/id fc/*current*))
|
||||
:class "w-full"
|
||||
:url (bidi/path-for ssr-routes/only-routes
|
||||
:company-search)
|
||||
:value (fc/field-value)
|
||||
:value-fn :db/id
|
||||
:class "w-full"
|
||||
:url (bidi/path-for ssr-routes/only-routes
|
||||
:company-search)
|
||||
:value (fc/field-value)
|
||||
:value-fn :db/id
|
||||
|
||||
|
||||
:content-fn #(pull-attr (dc/db conn) :client/name (:db/id %))
|
||||
:size :small})))
|
||||
:content-fn #(pull-attr (dc/db conn) :client/name (:db/id %))
|
||||
:size :small})))
|
||||
(com/data-grid-cell {:class "align-top"}
|
||||
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
|
||||
|
||||
|
||||
(defn dialog* [{:keys [form-params form-errors entity]}]
|
||||
(println "FORM PARMS" form-params)
|
||||
(fc/start-form
|
||||
@@ -328,65 +321,64 @@
|
||||
(defn user-edit-save [{:keys [form-params identity] :as request}]
|
||||
(let [_ @(dc/transact conn [[:upsert-entity form-params]])
|
||||
user (some-> form-params :db/id (#(dc/pull (dc/db conn) default-read %)))]
|
||||
|
||||
(html-response
|
||||
(row* identity user {:flash? true})
|
||||
:headers {"hx-trigger" "modalclose"
|
||||
"hx-retarget" (format "#user-table tr[data-id=\"%d\"]" (:db/id user))})))
|
||||
|
||||
(html-response
|
||||
(row* identity user {:flash? true})
|
||||
:headers {"hx-trigger" "modalclose"
|
||||
"hx-retarget" (format "#user-table tr[data-id=\"%d\"]" (:db/id user))})))
|
||||
|
||||
(def form-schema
|
||||
(mc/schema
|
||||
[:map
|
||||
[:db/id entity-id]
|
||||
[:user/clients {:optional true}
|
||||
[:maybe
|
||||
(many-entity {} [:db/id entity-id])]]
|
||||
[:user/role (ref->enum-schema "user-role")]]))
|
||||
[:map
|
||||
[:db/id entity-id]
|
||||
[:user/clients {:optional true}
|
||||
[:maybe
|
||||
(many-entity {} [:db/id entity-id])]]
|
||||
[:user/role (ref->enum-schema "user-role")]]))
|
||||
|
||||
(defn user-dialog [{:keys [form-params entity form-errors]}]
|
||||
(modal-response
|
||||
(dialog* {:form-params (or (when (seq form-params)
|
||||
form-params)
|
||||
(when entity
|
||||
(mc/decode form-schema entity main-transformer))
|
||||
{})
|
||||
:entity entity
|
||||
:form-errors form-errors})))
|
||||
(dialog* {:form-params (or (when (seq form-params)
|
||||
form-params)
|
||||
(when entity
|
||||
(mc/decode form-schema entity main-transformer))
|
||||
{})
|
||||
:entity entity
|
||||
:form-errors form-errors})))
|
||||
|
||||
(defn new-client [{ {:keys [index]} :query-params}]
|
||||
(html-response
|
||||
(fc/start-form-with-prefix [:user/clients (or index 0)] {:db/id nil
|
||||
:new? true} []
|
||||
(client-row* fc/*current*))))
|
||||
(defn new-client [{{:keys [index]} :query-params}]
|
||||
(html-response
|
||||
(fc/start-form-with-prefix [:user/clients (or index 0)] {:db/id nil
|
||||
:new? true} []
|
||||
(client-row* fc/*current*))))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{:users (helper/page-route grid-page)
|
||||
:user-table (helper/table-route grid-page)
|
||||
:user-edit-save (-> user-edit-save
|
||||
(wrap-entity [:form-params :db/id] default-read)
|
||||
(wrap-schema-enforce :form-schema form-schema)
|
||||
(wrap-nested-form-params)
|
||||
(wrap-form-4xx-2 (wrap-entity user-dialog [:form-params :db/id] default-read)))
|
||||
:user-client-new (-> new-client
|
||||
{:users (helper/page-route grid-page)
|
||||
:user-table (helper/table-route grid-page)
|
||||
:user-edit-save (-> user-edit-save
|
||||
(wrap-entity [:form-params :db/id] default-read)
|
||||
(wrap-schema-enforce :form-schema form-schema)
|
||||
(wrap-nested-form-params)
|
||||
(wrap-form-4xx-2 (wrap-entity user-dialog [:form-params :db/id] default-read)))
|
||||
:user-client-new (-> new-client
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:index {:optional true
|
||||
:default 0} [nat-int? {:default 0}]]]))
|
||||
:user-edit-dialog (-> user-dialog
|
||||
(wrap-entity [:route-params :db/id] default-read)
|
||||
(wrap-schema-enforce
|
||||
:route-schema (mc/schema [:map [:db/id entity-id]])))
|
||||
:user-impersonate (-> impersonate
|
||||
(wrap-entity [:params :db/id] default-read)
|
||||
(wrap-schema-enforce
|
||||
:params-schema (mc/schema [:map [:db/id entity-id]])))}
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)
|
||||
(wrap-admin)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
[:index {:optional true
|
||||
:default 0} [nat-int? {:default 0}]]]))
|
||||
:user-edit-dialog (-> user-dialog
|
||||
(wrap-entity [:route-params :db/id] default-read)
|
||||
(wrap-schema-enforce
|
||||
:route-schema (mc/schema [:map [:db/id entity-id]])))
|
||||
:user-impersonate (-> impersonate
|
||||
(wrap-entity [:params :db/id] default-read)
|
||||
(wrap-schema-enforce
|
||||
:params-schema (mc/schema [:map [:db/id entity-id]])))}
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)
|
||||
(wrap-admin)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
|
||||
@@ -67,8 +67,6 @@
|
||||
(assoc-in [:headers "hx-retarget"] "#modal-content")
|
||||
(assoc-in [:headers "hx-reswap"] "innerHTML"))))))
|
||||
|
||||
|
||||
|
||||
(defn form-data->map [form-data]
|
||||
(reduce-kv
|
||||
(fn [acc k v]
|
||||
@@ -91,7 +89,6 @@
|
||||
(str/join "_" (map path->name k))
|
||||
:else k))
|
||||
|
||||
|
||||
(defn forced-vector [x]
|
||||
[:vector {:decode/json {:enter (fn [x]
|
||||
(if (sequential? x)
|
||||
@@ -172,7 +169,6 @@
|
||||
[x]))})
|
||||
schema]))
|
||||
|
||||
|
||||
(defn str->keyword [s]
|
||||
(if (string? s)
|
||||
(let [[ns k] (str/split s #"/")]
|
||||
@@ -186,10 +182,9 @@
|
||||
(subs (str k) 1)
|
||||
(string? k)
|
||||
k
|
||||
:else
|
||||
:else
|
||||
k))
|
||||
|
||||
|
||||
;; TODO make this bubble the form data automatically
|
||||
(defn field-validation-error [m path & {:as data}]
|
||||
(throw+ (ex-info m (merge data {:type :field-validation
|
||||
@@ -201,19 +196,18 @@
|
||||
:form-validation-errors [m]}))))
|
||||
|
||||
(def clj-date-schema
|
||||
(mc/schema [:and [inst? {:date-format atime/normal-date
|
||||
}]
|
||||
(mc/schema [:and [inst? {:date-format atime/normal-date}]
|
||||
[:fn
|
||||
{:error/message "Invalid date"}
|
||||
(fn [d]
|
||||
(if d
|
||||
(if d
|
||||
(time/after? (coerce/to-date-time d)
|
||||
(coerce/to-date-time #inst "2000-01-01"))
|
||||
true))]
|
||||
[:fn
|
||||
{:error/message "Can not look more than four years into the future."}
|
||||
(fn [d]
|
||||
(if d
|
||||
(if d
|
||||
(time/before? (coerce/to-date-time d)
|
||||
(time/plus (time/now) (time/years 4)))
|
||||
true))]]))
|
||||
@@ -270,7 +264,7 @@
|
||||
"year"
|
||||
(assoc m
|
||||
start-date-key (atime/as-local-time (time/date-time (time/year (atime/local-today))
|
||||
1
|
||||
1
|
||||
1))
|
||||
end-date-key nil)
|
||||
|
||||
@@ -333,7 +327,7 @@
|
||||
(when (:coerce? (m/properties schema))
|
||||
(fn [data]
|
||||
(cond
|
||||
(vector? data)
|
||||
(vector? data)
|
||||
data
|
||||
(sequential? data)
|
||||
data
|
||||
@@ -360,7 +354,6 @@
|
||||
(into {})))))
|
||||
(handler request)))))
|
||||
|
||||
|
||||
(def dissoc-nil-transformer
|
||||
(let [e {:map {:compile (fn [schema _]
|
||||
(fn [data]
|
||||
@@ -374,26 +367,25 @@
|
||||
:decoders e})))
|
||||
|
||||
(def unspecified-transformer
|
||||
(mt2/transformer
|
||||
{:decoders {:map {:compile (fn [x g]
|
||||
(fn [value]
|
||||
(if (or (nil? value)
|
||||
(map? value))
|
||||
(let [ specified-keys (set (keys value))]
|
||||
(reduce
|
||||
(fn [value [k params]]
|
||||
(cond (and (:unspecified/fn params)
|
||||
(not (get specified-keys k)))
|
||||
(assoc value k ((:unspecified/fn params)))
|
||||
(and (:unspecified/value params)
|
||||
(not (get specified-keys k)))
|
||||
(assoc value k (:unspecified/value params))
|
||||
:else
|
||||
value
|
||||
))
|
||||
value
|
||||
(m/children x)))
|
||||
value)))}}}))
|
||||
(mt2/transformer
|
||||
{:decoders {:map {:compile (fn [x g]
|
||||
(fn [value]
|
||||
(if (or (nil? value)
|
||||
(map? value))
|
||||
(let [specified-keys (set (keys value))]
|
||||
(reduce
|
||||
(fn [value [k params]]
|
||||
(cond (and (:unspecified/fn params)
|
||||
(not (get specified-keys k)))
|
||||
(assoc value k ((:unspecified/fn params)))
|
||||
(and (:unspecified/value params)
|
||||
(not (get specified-keys k)))
|
||||
(assoc value k (:unspecified/value params))
|
||||
:else
|
||||
value))
|
||||
value
|
||||
(m/children x)))
|
||||
value)))}}}))
|
||||
|
||||
(def main-transformer
|
||||
(mt2/transformer
|
||||
@@ -407,8 +399,7 @@
|
||||
coerce-vector
|
||||
date-range-transformer
|
||||
pull-transformer
|
||||
mt2/default-value-transformer
|
||||
))
|
||||
mt2/default-value-transformer))
|
||||
|
||||
(defn strip [s]
|
||||
(cond (and (string? s) (str/blank? s))
|
||||
@@ -434,7 +425,6 @@
|
||||
:decoded entity
|
||||
:error {:explain (mc/explain schema entity)}}))))
|
||||
|
||||
|
||||
(defn schema-enforce-request [{:keys [form-params query-params hx-query-params multipart-params params] :as request} & {:keys [form-schema multipart-schema hx-schema query-schema route-schema params-schema]}]
|
||||
(let [request (try
|
||||
(cond-> request
|
||||
@@ -451,7 +441,7 @@
|
||||
route-schema
|
||||
(:route-params request)
|
||||
main-transformer))
|
||||
|
||||
|
||||
(and (:multipart-params request) multipart-schema)
|
||||
(assoc :multipart-params
|
||||
(mc/coerce
|
||||
@@ -473,23 +463,22 @@
|
||||
hx-query-params
|
||||
main-transformer))
|
||||
|
||||
|
||||
(and query-schema query-params)
|
||||
(assoc :query-params
|
||||
(mc/coerce
|
||||
query-schema
|
||||
query-params
|
||||
main-transformer)))
|
||||
query-schema
|
||||
query-params
|
||||
main-transformer)))
|
||||
|
||||
(catch Exception e
|
||||
(alog/warn ::validation-error
|
||||
(alog/warn ::validation-error
|
||||
:error e
|
||||
::errors (-> e
|
||||
(ex-data)
|
||||
:data
|
||||
:explain
|
||||
(me/humanize {:errors (assoc me/default-errors
|
||||
::mc/missing-key {:error/message {:en "required"}})})))
|
||||
(ex-data)
|
||||
:data
|
||||
:explain
|
||||
(me/humanize {:errors (assoc me/default-errors
|
||||
::mc/missing-key {:error/message {:en "required"}})})))
|
||||
(throw (ex-info (->> (-> e
|
||||
(ex-data)
|
||||
:data
|
||||
@@ -520,7 +509,6 @@
|
||||
:route-schema route-schema
|
||||
:params-schema params-schema))))
|
||||
|
||||
|
||||
(defn schema-decode-request [{:keys [form-params query-params params] :as request} & {:keys [form-schema query-schema route-schema params-schema]}]
|
||||
(let [request (cond-> request
|
||||
(and (:params request) params-schema)
|
||||
@@ -588,7 +576,6 @@
|
||||
:when (= n (namespace ident))]
|
||||
{:value (name ident) :content (str/replace (str/capitalize (name ident)) "-" " ")})))
|
||||
|
||||
|
||||
(defn wrap-form-4xx-2 [handler form-handler]
|
||||
(fn [request]
|
||||
(try+
|
||||
@@ -613,8 +600,7 @@
|
||||
(form-handler (assoc request
|
||||
:form-params (or (:form e) ;; TODO is :form actually used?
|
||||
(:form-params e)
|
||||
(:form-params request)
|
||||
)
|
||||
(:form-params request))
|
||||
:form-errors (:form-errors e))))
|
||||
(catch [:type :form-validation] e
|
||||
(form-handler (assoc request
|
||||
@@ -624,7 +610,6 @@
|
||||
:form-validation-errors (:form-validation-errors e)
|
||||
:form-errors {:errors (:form-validation-errors e)}))))))
|
||||
|
||||
|
||||
(defn apply-middleware-to-all-handlers [key->handler f]
|
||||
(->> key->handler
|
||||
(reduce
|
||||
@@ -645,7 +630,6 @@
|
||||
(str "[" (k->n k) "]"))
|
||||
rest)))))
|
||||
|
||||
|
||||
(defn wrap-entity [handler path read]
|
||||
(fn wrap-entity-request [request]
|
||||
(let [entity (some->>
|
||||
@@ -664,7 +648,7 @@
|
||||
:entity-map
|
||||
(mc/-simple-schema {:type :entity-map
|
||||
:pred map?
|
||||
:type-properties { :error/message "required"}})
|
||||
:type-properties {:error/message "required"}})
|
||||
#_[:map {:name :entity-map} [:db/id nat-int?]]}))
|
||||
|
||||
(comment
|
||||
@@ -681,8 +665,6 @@
|
||||
(with-precision 2
|
||||
(double (.setScale (bigdec d) 2 java.math.RoundingMode/HALF_UP))))
|
||||
|
||||
|
||||
|
||||
(defn wrap-implied-route-param [handler & {:as route-params}]
|
||||
(fn [request]
|
||||
(handler (update-in request [:route-params] merge route-params))))
|
||||
@@ -694,7 +676,7 @@
|
||||
allowance (allowance-key (dc/pull (dc/db conn) '[{[:account/invoice-allowance :xform iol-ion.query/ident] [:db/ident]
|
||||
[:account/vendor-allowance :xform iol-ion.query/ident] [:db/ident]
|
||||
[:account/default-allowance :xform iol-ion.query/ident] [:db/ident]}]
|
||||
account-id))]
|
||||
account-id))]
|
||||
(not= :allowance/denied
|
||||
allowance)))
|
||||
|
||||
@@ -713,9 +695,8 @@
|
||||
(throw (ex-info "Exception." {:type "'A' not allowed"})))
|
||||
true))
|
||||
|
||||
(def default-grid-fields-schema
|
||||
[
|
||||
[:sort {:optional true} [:maybe [:any]]]
|
||||
(def default-grid-fields-schema
|
||||
[[:sort {:optional true} [:maybe [:any]]]
|
||||
[:per-page {:optional true :default 25} [:maybe :int]]
|
||||
[:start {:optional true :default 0} [:maybe :int]]
|
||||
[:exact-match-id {:optional true} [:maybe entity-id]]])
|
||||
@@ -8,7 +8,7 @@
|
||||
[ring.middleware.json :refer [wrap-json-response]]))
|
||||
|
||||
(defn best-match [q]
|
||||
|
||||
|
||||
(let [name-like-ids (when (not-empty q)
|
||||
(map (comp #(Long/parseLong %) :id)
|
||||
(solr/query solr/impl "vendors"
|
||||
@@ -21,7 +21,7 @@
|
||||
(first valid-clients)))
|
||||
|
||||
(defn search [{:keys [clients query-params identity]}]
|
||||
|
||||
|
||||
(let [name-like-ids (when (not-empty (get query-params "q"))
|
||||
(map (comp #(Long/parseLong %) :id)
|
||||
(solr/query solr/impl "vendors"
|
||||
@@ -30,7 +30,7 @@
|
||||
"fields" "id"
|
||||
"limit" 300})))
|
||||
valid-clients (for [n name-like-ids]
|
||||
{"value" n "label" (pull-attr (dc/db conn) :vendor/name n)} )]
|
||||
{"value" n "label" (pull-attr (dc/db conn) :vendor/name n)})]
|
||||
{:body (take 10 valid-clients)}))
|
||||
|
||||
(def search (wrap-json-response search))
|
||||
@@ -38,40 +38,39 @@
|
||||
#_(comment
|
||||
(solr/delete solr/impl "vendors")
|
||||
|
||||
(count (let [valid-ids (->> (dc/q '[:find ?v
|
||||
:in $
|
||||
:where [?v :vendor/name]]
|
||||
(dc/db conn))
|
||||
(map first)
|
||||
(into #{}))]
|
||||
(for [v (solr/query solr/impl "vendors"
|
||||
{"query" "*"
|
||||
"limit" 10000})
|
||||
:when (not (valid-ids (Long/parseLong (:id v))))]
|
||||
v)))
|
||||
(count (let [valid-ids (->> (dc/q '[:find ?v
|
||||
:in $
|
||||
:where [?v :vendor/name]]
|
||||
(dc/db conn))
|
||||
(map first)
|
||||
(into #{}))]
|
||||
(for [v (solr/query solr/impl "vendors"
|
||||
{"query" "*"
|
||||
"limit" 10000})
|
||||
:when (not (valid-ids (Long/parseLong (:id v))))]
|
||||
v)))
|
||||
|
||||
(let [name-like-ids (when (not-empty "A&J")
|
||||
(map (comp (juxt identity #(Long/parseLong %)) :id)
|
||||
(solr/query solr/impl "vendors"
|
||||
{"query" (cond-> (format "name:(%s*)" (str/upper-case "A&J"))
|
||||
(not (is-admin? identity)) (str " hidden:false"))
|
||||
"fields" "id,name"
|
||||
"limit" 300})))
|
||||
valid-clients (for [[z n] name-like-ids]
|
||||
{"value" n "internal-label" z "label" (dc/pull (dc/db conn) '[*] n)})]
|
||||
(take 5 valid-clients))
|
||||
(let [name-like-ids (when (not-empty "A&J")
|
||||
(map (comp (juxt identity #(Long/parseLong %)) :id)
|
||||
(solr/query solr/impl "vendors"
|
||||
{"query" (cond-> (format "name:(%s*)" (str/upper-case "A&J"))
|
||||
(not (is-admin? identity)) (str " hidden:false"))
|
||||
"fields" "id,name"
|
||||
"limit" 300})))
|
||||
valid-clients (for [[z n] name-like-ids]
|
||||
{"value" n "internal-label" z "label" (dc/pull (dc/db conn) '[*] n)})]
|
||||
(take 5 valid-clients))
|
||||
|
||||
(solr/query solr/impl "vendors"
|
||||
{"query" (cond-> (format "name:(%s*)" (str/upper-case (solr/escape "A&J Pr")))
|
||||
(not true) (str " hidden:false"))
|
||||
"fields" "id, name"
|
||||
"limit" 300})
|
||||
|
||||
(solr/query solr/impl "vendors"
|
||||
{"query" (cond-> (format "name:(%s*)" (str/upper-case (solr/escape "A&J Pr")))
|
||||
(not true) (str " hidden:false"))
|
||||
"fields" "id, name"
|
||||
"limit" 300})
|
||||
(solr/escape "A&J")
|
||||
|
||||
(solr/escape "A&J")
|
||||
|
||||
(first (solr/query solr/impl "vendors"
|
||||
{"query" (cond-> (format "name:(A\\&J PRO*)")
|
||||
(not true) (str " hidden:false"))
|
||||
"fields" "id, name"
|
||||
"limit" 300})))
|
||||
(first (solr/query solr/impl "vendors"
|
||||
{"query" (cond-> (format "name:(A\\&J PRO*)")
|
||||
(not true) (str " hidden:false"))
|
||||
"fields" "id, name"
|
||||
"limit" 300})))
|
||||
Reference in New Issue
Block a user