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:
2026-05-21 14:45:19 -07:00
parent 8bd0cee1b1
commit ba87805d4c
210 changed files with 8694 additions and 9627 deletions

View File

@@ -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))

View File

@@ -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))})

View File

@@ -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

View File

@@ -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)))))

View File

@@ -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)

View File

@@ -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)))))

View File

@@ -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")))

View File

@@ -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)))))

View File

@@ -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)))))

View File

@@ -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)))))

View File

@@ -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"))

View File

@@ -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

View File

@@ -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))

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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]

View File

@@ -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]]])))))

View File

@@ -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)))))

View File

@@ -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

View File

@@ -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")

View File

@@ -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]))

View File

@@ -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"]]]]])

View File

@@ -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 "")

View File

@@ -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)]])

View File

@@ -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)])))

View File

@@ -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]

View File

@@ -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)]

View File

@@ -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"}

View File

@@ -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]

View File

@@ -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)]]])

View File

@@ -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"}

View File

@@ -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"})]]]])

View File

@@ -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)])])

View File

@@ -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])

View File

@@ -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"

View File

@@ -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"]]]]])

View File

@@ -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)

View File

@@ -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)))))

View File

@@ -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]

View File

@@ -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"

View File

@@ -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 "

View File

@@ -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"))))))

View File

@@ -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]])})

View File

@@ -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

View File

@@ -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)))

View File

@@ -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})))))

View File

@@ -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)))

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)

View File

@@ -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)

View File

@@ -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))

View File

@@ -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)

View File

@@ -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
(->

View File

@@ -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)

View File

@@ -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))}))

View File

@@ -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.

View File

@@ -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"))

View File

@@ -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)))})

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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)

View File

@@ -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)))))

View File

@@ -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))

View File

@@ -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?]

View File

@@ -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))

View File

@@ -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)))))

View File

@@ -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"}]])

View File

@@ -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

View File

@@ -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)

View File

@@ -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))

View File

@@ -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]

View File

@@ -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]}]

View File

@@ -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"}]]]]]]))

View File

@@ -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)))))

View File

@@ -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]]])

View File

@@ -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})))