- 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
1914 lines
103 KiB
Clojure
1914 lines
103 KiB
Clojure
(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]]
|
|
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
|
[auto-ap.logging :as alog]
|
|
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
|
[auto-ap.routes.admin.clients :as route]
|
|
[auto-ap.routes.indicators :as indicators]
|
|
[auto-ap.routes.queries :as q]
|
|
[auto-ap.routes.utils
|
|
: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]
|
|
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler
|
|
add-new-primitive-handler]]
|
|
[auto-ap.ssr.components :as com]
|
|
[auto-ap.ssr.components.multi-modal :as mm]
|
|
[auto-ap.ssr.form-cursor :as fc]
|
|
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
|
|
[auto-ap.ssr.hiccup-helper :as hh]
|
|
[auto-ap.ssr.hx :as hx]
|
|
[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]]
|
|
[auto-ap.time :as atime]
|
|
[bidi.bidi :as bidi]
|
|
[cheshire.core :as cheshire]
|
|
[clj-time.coerce :as coerce]
|
|
[clj-time.core :as time]
|
|
[clojure.java.io :as io]
|
|
[clojure.string :as str]
|
|
[datomic.api :as dc]
|
|
[hiccup.util :as hu]
|
|
[malli.core :as mc]
|
|
[malli.transform :as mt]
|
|
[malli.util :as mut]
|
|
[manifold.deferred :as de])
|
|
(: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
|
|
;; TODO a few bug fixes from slack
|
|
;; TOOD check pinecone
|
|
|
|
(def query-schema (mc/schema
|
|
[:maybe [:map
|
|
[:sort {:optional true} [:maybe [:any]]]
|
|
[:per-page {:optional true :default 25} [:maybe :int]]
|
|
[:start {:optional true :default 0} [:maybe :int]]
|
|
[:code {:optional true} [:maybe {:decode/string strip} :string]]
|
|
[:name {:optional true} [:maybe {:decode/string strip} :string]]
|
|
[:group {:optional true} [:maybe {:decode/string strip} :string]]
|
|
[:select {:optional true :default "all"}
|
|
[:maybe
|
|
[: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
|
|
::route/table)
|
|
"hx-target" "#entity-table"
|
|
"hx-indicator" "#entity-table"}
|
|
|
|
[:fieldset.space-y-6
|
|
(com/field {:label "Name"}
|
|
(com/text-input {:name "name"
|
|
:id "name"
|
|
:class "hot-filter"
|
|
:value (:name (:query-params request))
|
|
:placeholder "Best Restaurant LLC"
|
|
:size :small}))
|
|
|
|
(com/field {:label "Code"}
|
|
(com/text-input {:name "code"
|
|
:id "code"
|
|
:class "hot-filter"
|
|
:value (:code (:query-params request))
|
|
:placeholder "BRLC"
|
|
:size :small}))
|
|
(com/field {:label "Group"}
|
|
(com/text-input {:name "group"
|
|
:id "group"
|
|
:class "hot-filter"
|
|
:value (:group (:query-params request))
|
|
:placeholder "NTG"
|
|
:size :small}))
|
|
(com/field {:label "Select"}
|
|
(com/radio-card {:size :small
|
|
:name "select"
|
|
:value (:select (:query-params request))
|
|
:options [{:value ""
|
|
:content "All"}
|
|
{:value "only-mine"
|
|
:content "Only mine"}]}))]])
|
|
|
|
(def default-read '[:db/id
|
|
:client/name
|
|
:client/code
|
|
:client/locations
|
|
:client/groups
|
|
:client/matches
|
|
:client/week-a-credits
|
|
:client/week-b-credits
|
|
:client/week-a-debits
|
|
:client/week-b-debits
|
|
:client/square-auth-token
|
|
|
|
:client/feature-flags
|
|
|
|
{:client/bank-accounts [:bank-account/code
|
|
:db/id
|
|
:bank-account/bank-name
|
|
:bank-account/numeric-code
|
|
:bank-account/name
|
|
:bank-account/include-in-reports
|
|
:bank-account/visible
|
|
:bank-account/number
|
|
:bank-account/bank-code
|
|
:bank-account/sort-order
|
|
:bank-account/routing
|
|
:bank-account/check-number
|
|
:bank-account/use-date-instead-of-post-date?
|
|
{:bank-account/yodlee-account [:db/id :yodlee-account/name]}
|
|
{:bank-account/plaid-account [:db/id :plaid-account/name]}
|
|
{:bank-account/intuit-bank-account [:db/id :intuit-bank-account/name]}
|
|
|
|
[:bank-account/start-date :xform clj-time.coerce/from-date]
|
|
{[:bank-account/type :xform iol-ion.query/ident] [:db/ident]}
|
|
{:bank-account/integration-status
|
|
[[:integration-status/last-updated :xform clj-time.coerce/from-date]
|
|
{[:integration-status/state :xform iol-ion.query/ident] [:db/ident]}]}]
|
|
:client/address [:address/street1 :address/street2 :address/city :address/state :address/zip :db/id]
|
|
:client/square-locations [:db/id
|
|
:square-location/name
|
|
:square-location/client-location
|
|
:square-location/square-id]}
|
|
[:client/locked-until :xform clj-time.coerce/from-date]
|
|
{:client/emails [:email-contact/description :email-contact/email :db/id]
|
|
:client/location-matches [:location-match/matches :location-match/location :db/id]}])
|
|
|
|
(defn fetch-ids [db request]
|
|
(let [query-params (:query-params request)
|
|
valid-clients (extract-client-ids #_(:clients request)
|
|
(map first (dc/q '[:find ?c :where [?c :client/code]] (dc/db conn)))
|
|
(:client-id query-params)
|
|
(when (:client-code query-params)
|
|
[:client/code (:client-code query-params)]))
|
|
query (cond-> {:query {:find []
|
|
:in ['$ '[?e ...]]
|
|
:where []}
|
|
:args [db valid-clients]}
|
|
(:sort query-params) (add-sorter-fields {"name" ['[?e :client/name ?sort-name]]
|
|
|
|
"code" ['[?e :client/code ?sort-code]]}
|
|
query-params)
|
|
(= "only-mine" (:select query-params))
|
|
(merge-query {:query {:in ['?uid]
|
|
:where ['[?uid :user/clients ?e]]}
|
|
:args [(:db/id (:identity request))]})
|
|
|
|
(:group query-params)
|
|
(merge-query {:query {:in ['?g]
|
|
: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]]}
|
|
:args [(clojure.string/upper-case (:code query-params))]})
|
|
|
|
(not (str/blank? (:name query-params)))
|
|
(merge-query {:query {:in ['?description]
|
|
:where ['[?e :client/name ?d]
|
|
'[(clojure.string/lower-case ?d) ?d2]
|
|
'[(clojure.string/includes? ?d2 ?description)]]}
|
|
:args [(clojure.string/lower-case (:name query-params))]})
|
|
|
|
true
|
|
(merge-query {:query {:find ['?e]
|
|
:where ['[?e :client/name]]}}))]
|
|
|
|
(cond->> (query2 query)
|
|
true (apply-sort-3 query-params)
|
|
true (apply-pagination query-params))))
|
|
|
|
(defn hydrate-results [ids db _]
|
|
(let [results (->> (pull-many db default-read ids)
|
|
(group-by :db/id))
|
|
refunds (->> ids
|
|
(map results)
|
|
(map first))]
|
|
refunds))
|
|
|
|
(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]))
|
|
|
|
(def grid-page
|
|
(helper/build {:id "entity-table"
|
|
:nav com/admin-aside-nav
|
|
:page-specific-nav filters
|
|
:fetch-page fetch-page
|
|
:action-buttons (fn [_]
|
|
[(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/new-dialog))
|
|
:color :primary}
|
|
"New Client")])
|
|
:row-buttons (fn [_ entity]
|
|
[(com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes
|
|
::route/biweekly-sales-powerquery
|
|
:db/id (:db/id entity))}
|
|
svg/dollar-tag)
|
|
(com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes
|
|
::route/edit-dialog
|
|
:db/id (:db/id entity))}
|
|
svg/pencil)])
|
|
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :admin)}
|
|
"Admin"]
|
|
[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
|
|
"Clients"]]
|
|
:title "Clients"
|
|
:entity-name "Client"
|
|
:query-schema query-schema
|
|
:route ::route/table
|
|
:headers [{:key "name"
|
|
:name "Name"
|
|
:sort-key "name"
|
|
:render :client/name}
|
|
{:key "code"
|
|
:name "Code"
|
|
:sort-key "code"
|
|
:render :client/code}
|
|
{:key "locations"
|
|
:name "Locations"
|
|
:render (fn [{:client/keys [locations]}]
|
|
[:div.flex.gap-2
|
|
(for [l locations]
|
|
(com/pill {:color :primary} l))])}
|
|
{:key "emails"
|
|
:name "Emails"
|
|
:show-starting "lg"
|
|
:render (fn [{:client/keys [emails]}]
|
|
[:div.flex.gap-2.flex-wrap
|
|
(for [{:email-contact/keys [email description]} emails]
|
|
(com/pill {:color :secondary} (format "%s - %s" description email)))])}
|
|
{:key "status"
|
|
:name "Status"
|
|
:show-starting "lg"
|
|
:render (fn [{:client/keys [locked-until bank-accounts]}]
|
|
[:div.flex.gap-2.flex-wrap
|
|
(if locked-until
|
|
(let [days-since-locked (try (time/in-days (time/interval locked-until (time/now)))
|
|
(catch Exception _
|
|
0))]
|
|
(cond
|
|
(< days-since-locked 90)
|
|
(com/pill {:color :primary} (format "Locked %s" (atime/unparse-local locked-until atime/normal-date)))
|
|
|
|
(< days-since-locked 365)
|
|
(com/pill {:color :yellow} (format "Locked %s" (atime/unparse-local locked-until atime/normal-date)))
|
|
|
|
:else
|
|
(com/pill {:color :red} (format "Locked %s" (atime/unparse-local locked-until atime/normal-date)))))
|
|
(com/pill {:color :red} (format "Not locked")))
|
|
(for [ba bank-accounts]
|
|
(com/pill {:color (cond (#{:integration-state/failed :integration-state/unauthorized} (-> ba :bank-account/integration-status :integration-status/state))
|
|
:red
|
|
|
|
:else
|
|
:secondary)}
|
|
(:bank-account/code ba)))]
|
|
#_[:div.flex.gap-2.flex-wrap
|
|
(for [{:email-contact/keys [email description]} emails]
|
|
(com/pill {:color :secondary} (format "%s - %s" description email)))])}]}))
|
|
|
|
(def row* (partial helper/row* grid-page))
|
|
|
|
(def bank-account-schema [: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]]
|
|
[:bank-account/check-number {:optional true} [:maybe :int]]
|
|
[:bank-account/bank-name {:optional true} [:maybe :string]]
|
|
[:bank-account/number {:optional true} [:maybe :string]]
|
|
[:bank-account/routing {:optional true} [:maybe :string]]
|
|
[:bank-account/bank-code {:optional true} [:maybe :string]]
|
|
[:bank-account/sort-order {:default 0} [:maybe :int]]
|
|
[:bank-account/yodlee-account {:optional true} [:maybe entity-id]]
|
|
[:bank-account/plaid-account {:optional true} [:maybe entity-id]]
|
|
[: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)
|
|
(clj-time.coerce/to-date (auto-ap.time/parse m atime/normal-date))
|
|
m))}
|
|
inst?]]]
|
|
[:fn {:error/message "Bank account financial code is required if the 'Include in Reports?' flag is true."
|
|
:error/path [:bank-account/numeric-code]}
|
|
(fn [{:bank-account/keys [include-in-reports numeric-code] :as g}]
|
|
(cond
|
|
(and include-in-reports (not numeric-code))
|
|
false
|
|
:else
|
|
true))]])
|
|
|
|
(def form-schema-2 (mc/schema
|
|
[:map
|
|
[:db/id {:optional true} [:maybe entity-id]]
|
|
[:client/name :string]
|
|
[:client/code :string]
|
|
[:client/feature-flags {:optional true}
|
|
[:maybe [:vector {:decode/arbitrary (fn [m] (if (map? m)
|
|
(vals m)
|
|
m))}
|
|
:string]]]
|
|
|
|
[:client/locations [:vector {:decode/arbitrary (fn [m] (if (map? m)
|
|
(vals m)
|
|
m))} :string]]
|
|
[:client/groups {:optional true :default []} [:maybe [:vector {:decode/arbitrary (fn [m] (if (map? m)
|
|
(vals m)
|
|
m))} :string]]]
|
|
[:client/emails {:optional true} [:maybe (many-entity {}
|
|
[:db/id [:or entity-id temp-id]]
|
|
[:email-contact/description :string]
|
|
[:email-contact/email :string])]]
|
|
[:client/locked-until {:optional true} [:maybe {:decode/arbitrary (fn [m]
|
|
(if (string? m)
|
|
(auto-ap.time/parse m atime/normal-date)
|
|
m))}
|
|
inst?]]
|
|
[:client/location-matches {:optional true}
|
|
[:maybe (many-entity {}
|
|
[:db/id [:or entity-id temp-id]]
|
|
[:location-match/matches [:vector {:decode/arbitrary (fn [m] (if (map? m)
|
|
(vals m)
|
|
m))}
|
|
:string]]
|
|
[:location-match/location :string])]]
|
|
[:client/square-auth-token {:optional true} [:maybe :string]]
|
|
[:client/square-locations {:optional true}
|
|
[:maybe (many-entity {}
|
|
[:db/id [:or entity-id temp-id]]
|
|
[:square-location/name :string]
|
|
[:square-location/square-id :string]
|
|
[:square-location/client-location :string])]]
|
|
|
|
[:client/bank-accounts {:default []}
|
|
[: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]]
|
|
[:bank-account/sort-order {:default 0} [:maybe :int]]
|
|
[:bank-account/routing {:optional true} [:maybe :string]]
|
|
[:bank-account/bank-code {:optional true} [:maybe :string]]
|
|
[:bank-account/bank-name {:optional true} [:maybe :string]]
|
|
[:bank-account/number {:optional true} [:maybe :string]]
|
|
[:bank-account/check-number {:optional true} [:maybe :int]]
|
|
[:bank-account/yodlee-account {:optional true} [:maybe entity-id]]
|
|
[:bank-account/plaid-account {:optional true} [:maybe entity-id]]
|
|
[: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)
|
|
(clj-time.coerce/to-date (auto-ap.time/parse m atime/normal-date))
|
|
m))}
|
|
inst?]]]
|
|
[:fn {:error/message "Bank account financial code is required if the 'Include in Reports?' flag is true."
|
|
:error/path [:bank-account/numeric-code]}
|
|
(fn [{:bank-account/keys [include-in-reports numeric-code] :as g}]
|
|
(cond
|
|
(and include-in-reports (not numeric-code))
|
|
false
|
|
:else
|
|
true))]])]]
|
|
[:client/matches {:optional true :default []} [:vector {:decode/arbitrary (fn [m] (if (map? m)
|
|
(vals m)
|
|
m))}
|
|
:string]]
|
|
[:client/address {:optional true}
|
|
[:maybe
|
|
[:map
|
|
[:db/id {:optional true} [:maybe [:or entity-id temp-id]]]
|
|
[:address/street1 {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]]
|
|
;; I don't like using a default because otherwise it wouldn't allow creating an empty address
|
|
[:address/street2 {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]]
|
|
[:address/city {:optional true} [:maybe [:string {:min 2 :decode/string strip}]]]
|
|
[:address/state {:optional true} [:maybe [:string {:min 2 :max 2 :decode/string strip}]]]
|
|
[:address/zip {:optional true} [:maybe [:string {:min 5 :max 5 :decode/string strip}]]]]]]
|
|
[:client/week-a-credits {:optional true} [:maybe :double]]
|
|
[:client/week-a-debits {:optional true} [:maybe :double]]
|
|
[: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))))})
|
|
:data-key "show"
|
|
:x-ref "p"}
|
|
hx/alpine-mount-then-appear)
|
|
(list
|
|
(fc/with-field :db/id
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)}))
|
|
(com/data-grid-cell {}
|
|
(fc/with-field :email-contact/description
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:class "shrink-0"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "Suite 300"
|
|
:class "w-full"
|
|
:value (fc/field-value)}))))
|
|
(com/data-grid-cell {}
|
|
(fc/with-field :email-contact/email
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:class "shrink-0"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "Suite 300"
|
|
:class "w-full"
|
|
: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 location-row [_]
|
|
(com/data-grid-row
|
|
{:x-ref "p"
|
|
:x-data (hx/json {})}
|
|
(com/data-grid-cell
|
|
{}
|
|
(com/validated-field {}
|
|
(com/text-input {:name (fc/field-name)
|
|
:value (fc/field-value)
|
|
:class "w-24"})))
|
|
(com/data-grid-cell {:class "align-top"}
|
|
(com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x))))
|
|
|
|
(defn group-row [_]
|
|
(com/data-grid-row
|
|
{:x-ref "p"
|
|
:x-data (hx/json {})}
|
|
(com/data-grid-cell
|
|
{}
|
|
(com/validated-field {}
|
|
(com/text-input {:name (fc/field-name)
|
|
:value (fc/field-value)
|
|
:class "w-24"})))
|
|
(com/data-grid-cell {:class "align-top"}
|
|
(com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x))))
|
|
|
|
(defn feature-flag-row [_]
|
|
(com/data-grid-row
|
|
{:x-ref "p"
|
|
:x-data (hx/json {})}
|
|
(com/data-grid-cell
|
|
{}
|
|
(com/validated-field {}
|
|
(com/select {:name (fc/field-name)
|
|
:allow-blank? true
|
|
:error? (fc/error?)
|
|
:class "w-full"
|
|
:value (fc/field-value)
|
|
:options [["new-square" "New Square+Ezcater (no effect)"]
|
|
["manually-pay-cintas" "Manually Pay Cintas"]
|
|
["include-in-ntg-corp-reports" "Include in NTG Corporate reports"]
|
|
["import-custom-amount" "Import Custom Amount Line Items from Square"]
|
|
["code-sysco-items" "Code individual sysco line items"]
|
|
["report-pedantic" "Show two decimals in reports"]]})))
|
|
|
|
(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 [_]
|
|
"Info")
|
|
(step-key [_]
|
|
:info)
|
|
|
|
(edit-path [_ _]
|
|
[])
|
|
|
|
(step-schema [_]
|
|
(mut/select-keys (mm/form-schema linear-wizard) #{:client/name :client/code :client/locations}))
|
|
|
|
(render-step [this _]
|
|
(mm/default-render-step
|
|
linear-wizard this
|
|
:head (dialog-header this)
|
|
:body (mm/default-step-body
|
|
{}
|
|
[:div.flex.space-x-2
|
|
(fc/with-field :client/name
|
|
(com/validated-field {:label "Name"
|
|
:errors (fc/field-errors)}
|
|
(com/text-input {:name (fc/field-name)
|
|
:value (fc/field-value)
|
|
:x-model "clientName"
|
|
:autofocus true
|
|
:class "w-96"})))
|
|
|
|
(fc/with-field :client/code
|
|
(com/validated-field {:label "Code"
|
|
:errors (fc/field-errors)}
|
|
(list
|
|
(com/text-input {:name (fc/field-name)
|
|
:disabled (if (:db/id (:entity linear-wizard)) true false)
|
|
:value (fc/field-value)
|
|
:class "w-24"})
|
|
(when (:db/id (:entity linear-wizard))
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)})))))]
|
|
(fc/with-field :client/locked-until
|
|
(com/validated-field {:label "Locked Until"
|
|
:errors (fc/field-errors)}
|
|
(com/date-input {:name (fc/field-name)
|
|
:placeholder "Disallow changes before this date"
|
|
:value
|
|
(some-> (fc/field-value)
|
|
(atime/unparse-local atime/normal-date))
|
|
|
|
:class "w-24"})))
|
|
|
|
(fc/with-field :client/locations
|
|
(com/validated-field
|
|
{:errors (fc/field-errors)
|
|
:label "Locations"}
|
|
(com/data-grid {:headers [(com/data-grid-header {} "Location")
|
|
(com/data-grid-header {:class "w-16"})]}
|
|
(fc/cursor-map #(location-row %))
|
|
(com/data-grid-new-row {:colspan 2
|
|
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-location)
|
|
:index (count (fc/field-value))}
|
|
"New location")))))
|
|
:footer
|
|
(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"
|
|
:x-data (hx/json {})}
|
|
(com/data-grid-cell
|
|
{}
|
|
(com/validated-field {}
|
|
(com/text-input {:name (fc/field-name)
|
|
:value (fc/field-value)
|
|
:class "w-full"})))
|
|
(com/data-grid-cell {:class "align-top"}
|
|
(com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x))))
|
|
|
|
(defn location-match-row [location-match-cursor]
|
|
(com/data-grid-row
|
|
(-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? location-match-cursor))))})
|
|
:data-key "show"
|
|
:x-ref "p"}
|
|
hx/alpine-mount-then-appear)
|
|
|
|
(fc/with-field :db/id
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)}))
|
|
(com/data-grid-cell {}
|
|
(fc/with-field-default :location-match/matches [""]
|
|
(fc/with-field 0
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:class "shrink-0"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "Suite 300"
|
|
:class "w-full"
|
|
:value (fc/field-value)})))))
|
|
(com/data-grid-cell {}
|
|
(fc/with-field :location-match/location
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:class "shrink-0"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "Suite 300"
|
|
:class "w-24"
|
|
: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))))
|
|
|
|
(defrecord MatchesModal [linear-wizard]
|
|
mm/ModalWizardStep
|
|
(step-name [_]
|
|
"Matches")
|
|
(step-key [_]
|
|
:matches)
|
|
(edit-path [_ _]
|
|
[])
|
|
|
|
(step-schema [_]
|
|
(mut/select-keys (mm/form-schema linear-wizard) #{:client/matches :client/location-matches}))
|
|
|
|
(render-step [this _]
|
|
(mm/default-render-step
|
|
linear-wizard this
|
|
:head (dialog-header this)
|
|
:body (mm/default-step-body {}
|
|
(fc/with-field :client/matches
|
|
(com/validated-field
|
|
{:errors (fc/field-errors)
|
|
:label "Matches"}
|
|
(com/data-grid {:headers [(com/data-grid-header {} "Match")
|
|
(com/data-grid-header {:class "w-16"})]}
|
|
(fc/cursor-map #(match-row %))
|
|
(com/data-grid-new-row {:colspan 2
|
|
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-match)
|
|
:index (count (fc/field-value))}
|
|
"New Match"))))
|
|
(fc/with-field :client/location-matches
|
|
(com/validated-field
|
|
{:errors (fc/field-errors)
|
|
:label "Location Matches"}
|
|
(com/data-grid {:headers [(com/data-grid-header {} "Match")
|
|
(com/data-grid-header {} "location")
|
|
(com/data-grid-header {:class "w-16"})]}
|
|
(fc/cursor-map #(location-match-row %))
|
|
(com/data-grid-new-row {:colspan 3
|
|
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-location-match)
|
|
:index (count (fc/field-value))}
|
|
"New Match"))))
|
|
[:div])
|
|
:footer
|
|
(mm/default-step-footer linear-wizard this :validation-route ::route/navigate)
|
|
:validation-route ::route/navigate)))
|
|
|
|
(defrecord ContactModal [linear-wizard]
|
|
mm/ModalWizardStep
|
|
(step-name [_]
|
|
"Contact")
|
|
|
|
(step-key [_]
|
|
:contact)
|
|
|
|
(edit-path [_ _]
|
|
[])
|
|
|
|
(step-schema [_]
|
|
(mut/select-keys (mm/form-schema linear-wizard) #{:client/address}))
|
|
|
|
(render-step [this _]
|
|
(mm/default-render-step
|
|
linear-wizard this
|
|
:head (dialog-header this)
|
|
:body (mm/default-step-body {}
|
|
|
|
(fc/with-field-default :client/address {}
|
|
[:div.flex.flex-col.w-full
|
|
(when (:db/id @fc/*current*)
|
|
(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"
|
|
:errors (fc/field-errors)}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:autofocus true
|
|
:class "w-full"
|
|
:placeholder "1200 Pennsylvania Avenue"
|
|
:value (fc/field-value)})))
|
|
(fc/with-field :address/street2
|
|
(com/validated-field {:errors (fc/field-errors)}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:class "w-full"
|
|
:placeholder "Suite 300"
|
|
:value (fc/field-value)})))
|
|
[:div.flex.w-full.space-x-4
|
|
(fc/with-field :address/city
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:class "w-full grow shrink"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:class "w-full"
|
|
:placeholder "Suite 300"
|
|
:value (fc/field-value)})))
|
|
(fc/with-field :address/state
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:class "w-16 shrink-0"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:class "w-full"
|
|
:placeholder "Suite 300"
|
|
:value (fc/field-value)})))
|
|
(fc/with-field :address/zip
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:class "w-24 shrink-0"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "Suite 300"
|
|
:class "w-full"
|
|
:value (fc/field-value)})))]])
|
|
|
|
(fc/with-field :client/emails
|
|
(com/validated-field
|
|
{:errors (fc/field-errors)
|
|
:label "Email Contacts"}
|
|
(com/data-grid {:headers [(com/data-grid-header {} "Name")
|
|
(com/data-grid-header {} "Email")
|
|
(com/data-grid-header {:class "w-16"})]}
|
|
(fc/cursor-map #(email-contact-row %))
|
|
(com/data-grid-new-row {:colspan 3
|
|
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-email-contact)
|
|
:index (count (fc/field-value))}
|
|
"New email contact")))))
|
|
:footer (mm/default-step-footer linear-wizard this :validation-route ::route/navigate)
|
|
:validation-route ::route/navigate)))
|
|
|
|
(defn bank-account-card-base [{:keys [bg-color text-color icon bank-account]}]
|
|
[:div {:class "w-[30em] cursor-move"}
|
|
(com/card {:class "w-full"}
|
|
[:div.flex.items-stretch
|
|
(com/hidden {:name "item"
|
|
:value (fc/field-value (:db/id bank-account))})
|
|
[:div.grow-0.flex.flex-col.justify-center
|
|
[:div.p-1.m-2.rounded-full
|
|
{:class
|
|
bg-color}
|
|
[:div {:class
|
|
(hh/add-class "p-1.5 w-8 h-8" text-color)}
|
|
icon]]]
|
|
[:div.flex.flex-col.grow.m-2
|
|
[:div.font-medium.text-gray-700 (fc/field-value (:bank-account/name bank-account))]
|
|
[:div.font-light.text-gray-600 (fc/field-value (:bank-account/bank-name bank-account))]]
|
|
[:div.grow-0.p-4
|
|
(com/a-icon-button {:hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/navigate)
|
|
{:from (mm/encode-step-key :bank-accounts)
|
|
: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"
|
|
:text-color "text-green-600"
|
|
:icon svg/dollar
|
|
:bank-account bank-account}))
|
|
|
|
(defmethod bank-account-card
|
|
:bank-account-type/credit
|
|
[bank-account]
|
|
(bank-account-card-base {:bg-color "bg-purple-50"
|
|
:text-color "text-purple-600"
|
|
:icon svg/credit-card
|
|
:bank-account bank-account}))
|
|
|
|
(defmethod bank-account-card
|
|
:bank-account-type/check [bank-account]
|
|
(bank-account-card-base {:bg-color "bg-blue-50"
|
|
:text-color "text-blue-600"
|
|
: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
|
|
[:h2.text-lg
|
|
(if (:new @bank-account)
|
|
"New Cash Account"
|
|
(str "Edit Cash Account: " (:bank-account/name @bank-account)))]
|
|
(fc/with-field :db/id
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)}))
|
|
(fc/with-field :bank-account/type
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (name (fc/field-value))}))
|
|
[:div.flex.space-x-2
|
|
(fc/with-field :bank-account/name
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Nickname"
|
|
:class "w-[20em]"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:autofocus true
|
|
:placeholder "BofA Checking"
|
|
:class "w-full"
|
|
:value (fc/field-value)})))
|
|
(fc/with-field :bank-account/code
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Code"
|
|
:class "w-20"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:disabled (not (:new? @bank-account))
|
|
:placeholder "NGOM-CASH"
|
|
:class "w-full"
|
|
:value (fc/field-value)})
|
|
(when-not (:new? @bank-account)
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)}))))]
|
|
|
|
(fc/with-field :bank-account/numeric-code
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Financial code"}
|
|
[:div {:class "w-[5em]"}
|
|
(com/int-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "11101"
|
|
:class "w-[5em]"
|
|
:value (fc/field-value)})]))
|
|
|
|
(fc/with-field :bank-account/start-date
|
|
[:div.flex.space-x-2.items-center
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Start Date"}
|
|
[:div {:class "w-[7em]"}
|
|
(com/date-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "12/01/2023"
|
|
:hx-get (bidi/path-for ssr-routes/only-routes
|
|
::indicators/days-ago)
|
|
:hx-trigger "change"
|
|
:hx-target "#days-indicator"
|
|
:hx-vals "js:{date: event.target.value}"
|
|
:hx-swap "innerHTML"
|
|
:class "w-[5em]"
|
|
:value (some-> (fc/field-value)
|
|
(clj-time.coerce/to-date-time)
|
|
;; todo do date coercion in the input
|
|
(atime/unparse-local atime/normal-date))})])
|
|
[: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))
|
|
:checked (fc/field-value)}
|
|
"Include in reports"))
|
|
[:div
|
|
(fc/with-field :bank-account/visible
|
|
(com/checkbox {:name (fc/field-name)
|
|
:value (boolean (fc/field-value))
|
|
: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)
|
|
:label "Plaid account"
|
|
:class "w-[20em]"
|
|
:x-data (hx/json {:plaidAccount (fc/field-value)})}
|
|
[:div.flex.gap-2.items-center
|
|
(com/select {:name (fc/field-name)
|
|
:allow-blank? true
|
|
:error? (fc/error?)
|
|
:class "w-full"
|
|
:value (fc/field-value)
|
|
:x-model "plaidAccount"
|
|
:options
|
|
(when client-id
|
|
(dc/q '[:find ?pa ?pn
|
|
:in $ ?client
|
|
:where [?pi :plaid-item/client ?client]
|
|
[?pi :plaid-item/accounts ?pa]
|
|
[?pa :plaid-account/name ?pn]]
|
|
(dc/db conn)
|
|
client-id))})
|
|
[:svg {":data-jdenticon-value" "plaidAccount" :width "24" :height "24"
|
|
:x-init "$watch('plaidAccount', () => jdenticon())"}]])))
|
|
|
|
(defn- yodlee-account-select [client-id]
|
|
(list
|
|
(fc/with-field :bank-account/yodlee-account
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Yodlee account"
|
|
:class "w-[20em]"}
|
|
(com/select {:name (fc/field-name)
|
|
:allow-blank? true
|
|
:error? (fc/error?)
|
|
:class "w-full"
|
|
:value (fc/field-value)
|
|
:options
|
|
(when client-id
|
|
(dc/q '[:find ?pa ?pn2
|
|
:in $ ?client
|
|
:where [?pi :yodlee-provider-account/client ?client]
|
|
[?pi :yodlee-provider-account/accounts ?pa]
|
|
[?pa :yodlee-account/name ?pn]
|
|
[?pa :yodlee-account/number ?num]
|
|
[(str ?pn " - " ?num) ?pn2]]
|
|
(dc/db conn)
|
|
client-id))})))
|
|
(fc/with-field :bank-account/use-date-instead-of-post-date?
|
|
(com/checkbox {:name (fc/field-name)
|
|
:checked (fc/field-value)}
|
|
"(Yodlee only) use date instead of post date"))))
|
|
|
|
(defn- intuit-account-select [client-id]
|
|
(list
|
|
(fc/with-field :bank-account/intuit-bank-account
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Intuit account"
|
|
:class "w-[20em]"}
|
|
(com/select {:name (fc/field-name)
|
|
:allow-blank? true
|
|
:error? (fc/error?)
|
|
:class "w-full"
|
|
:value (fc/field-value)
|
|
:options
|
|
(sort-by second (dc/q '[:find ?ia ?inn
|
|
:in $
|
|
:where [?ia :intuit-bank-account/name ?inn]]
|
|
(dc/db conn)))})))
|
|
(fc/with-field :bank-account/use-date-instead-of-post-date?
|
|
(com/checkbox {:name (fc/field-name)
|
|
:checked (fc/field-value)}
|
|
"(Yodlee only) use date instead of post date"))))
|
|
(defmethod bank-account-form
|
|
:bank-account-type/credit [bank-account]
|
|
[:div
|
|
[:h2.text-lg
|
|
(if (:new @bank-account)
|
|
"New Credit Card Account"
|
|
(str "Edit Credit Card: " (:bank-account/name @bank-account)))]
|
|
|
|
(fc/with-field :db/id
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)}))
|
|
(fc/with-field :bank-account/type
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (name (fc/field-value))}))
|
|
[:div.flex.space-x-2
|
|
(fc/with-field :bank-account/name
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Nickname"
|
|
:class "w-[20em]"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:autofocus true
|
|
:placeholder "BofA Checking"
|
|
:class "w-full"
|
|
:value (fc/field-value)})))
|
|
(fc/with-field :bank-account/code
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Code"
|
|
:class "w-20"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:disabled (not (:new? @bank-account))
|
|
:placeholder "NGOM-CASH"
|
|
:class "w-full"
|
|
:value (fc/field-value)})
|
|
(when-not (:new? @bank-account)
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)}))))]
|
|
(fc/with-field :bank-account/numeric-code
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Financial code"}
|
|
[:div {:class "w-[5em]"}
|
|
(com/int-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "11101"
|
|
:class "w-[5em]"
|
|
:value (fc/field-value)})]))
|
|
|
|
(fc/with-field :bank-account/start-date
|
|
[:div.flex.space-x-2.items-center
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Start Date"}
|
|
[:div {:class "w-[7em]"}
|
|
(com/date-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "12/01/2023"
|
|
:hx-get (bidi/path-for ssr-routes/only-routes
|
|
::indicators/days-ago)
|
|
:hx-trigger "change"
|
|
:hx-target "#days-indicator"
|
|
:hx-vals "js:{date: event.target.value}"
|
|
:hx-swap "innerHTML"
|
|
:class "w-[5em]"
|
|
:value (some-> (fc/field-value)
|
|
(clj-time.coerce/to-date-time)
|
|
;; todo do date coercion in the input
|
|
(atime/unparse-local atime/normal-date))})])
|
|
[: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))
|
|
:checked (fc/field-value)}
|
|
"Include in reports"))
|
|
|
|
[:div
|
|
(fc/with-field :bank-account/visible
|
|
(com/checkbox {:name (fc/field-name)
|
|
:value (boolean (fc/field-value))
|
|
:checked (fc/field-value)}
|
|
"Visible for payment"))]
|
|
|
|
[:h2.text-lg "Bank details"]
|
|
(fc/with-field :bank-account/bank-name
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Bank Name"
|
|
:class "w-[20em]"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "Bank of America"
|
|
:value (fc/field-value)})))
|
|
|
|
[:div.flex.gap-2
|
|
(fc/with-field :bank-account/number
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Account #"
|
|
:class "w-[10em]"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:class "w-full"
|
|
:placeholder "1820190122"
|
|
:value (fc/field-value)})))]
|
|
|
|
[:h2.text-lg "Integration details"]
|
|
(plaid-account-select (:db/id (:snapshot fc/*form-data*)))
|
|
(yodlee-account-select (:db/id (:snapshot fc/*form-data*)))
|
|
(intuit-account-select (:db/id (:snapshot fc/*form-data*)))])
|
|
|
|
(defmethod bank-account-form
|
|
:bank-account-type/check [bank-account]
|
|
[:div
|
|
[:h2.text-lg
|
|
(if (:new @bank-account)
|
|
"New Checking Account"
|
|
|
|
(str "Edit Checking Account: " (:bank-account/name @bank-account)))]
|
|
(fc/with-field :db/id
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)}))
|
|
(fc/with-field :bank-account/type
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (name (fc/field-value))}))
|
|
[:div.flex.space-x-2
|
|
(fc/with-field :bank-account/name
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Nickname"
|
|
:class "w-[20em]"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:autofocus true
|
|
:placeholder "BofA Checking"
|
|
:class "w-full"
|
|
:value (fc/field-value)})))
|
|
(fc/with-field :bank-account/code
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Code"
|
|
:class "w-20"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:disabled (not (:new? @bank-account))
|
|
:placeholder "NGOM-CASH"
|
|
:class "w-full"
|
|
:value (fc/field-value)})
|
|
(when-not (:new? @bank-account)
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)}))))]
|
|
(fc/with-field :bank-account/numeric-code
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Financial code"}
|
|
[:div {:class "w-[5em]"}
|
|
(com/int-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "11101"
|
|
:class "w-[5em]"
|
|
:value (fc/field-value)})]))
|
|
|
|
(fc/with-field :bank-account/start-date
|
|
[:div.flex.space-x-2.items-center
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Start Date"}
|
|
[:div {:class "w-[7em]"}
|
|
(com/date-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "12/01/2023"
|
|
:hx-get (bidi/path-for ssr-routes/only-routes
|
|
::indicators/days-ago)
|
|
:hx-trigger "change"
|
|
:hx-target "#days-indicator"
|
|
:hx-vals "js:{date: event.target.value}"
|
|
:hx-swap "innerHTML"
|
|
:class "w-[5em]"
|
|
:value (some-> (fc/field-value)
|
|
(clj-time.coerce/to-date-time)
|
|
;; todo do date coercion in the input
|
|
(atime/unparse-local atime/normal-date))})])
|
|
[: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))
|
|
:checked (fc/field-value)}
|
|
"Include in reports"))
|
|
|
|
[:div
|
|
(fc/with-field :bank-account/visible
|
|
(com/checkbox {:name (fc/field-name)
|
|
:value (boolean (fc/field-value))
|
|
:checked (fc/field-value)}
|
|
"Visible for payment"))]
|
|
|
|
[:h2.text-lg "Bank details"]
|
|
(fc/with-field :bank-account/bank-name
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Bank Name"
|
|
:class "w-[20em]"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "Bank of America"
|
|
:value (fc/field-value)})))
|
|
|
|
[:div.flex.gap-2
|
|
(fc/with-field :bank-account/number
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Account #"
|
|
:class "w-[10em]"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:class "w-full"
|
|
:placeholder "1820190122"
|
|
:value (fc/field-value)})))
|
|
(fc/with-field :bank-account/routing
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Routing #"
|
|
:class "w-[8em]"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:class "w-full"
|
|
:placeholder "1238912"
|
|
:value (fc/field-value)})))
|
|
(fc/with-field :bank-account/bank-code
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Bank Code"
|
|
:class "w-[8em]"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:class "w-full"
|
|
:placeholder "12/10123"
|
|
:value (fc/field-value)})))]
|
|
|
|
(fc/with-field :bank-account/check-number
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Check Number"
|
|
:class "w-[8em]"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:class "w-full"
|
|
:placeholder "50000"
|
|
:value (fc/field-value)})))
|
|
[:h2.text-lg "Integration details"]
|
|
(plaid-account-select (:db/id (:snapshot fc/*form-data*)))
|
|
(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"}
|
|
[:div.flex.justify-center.items-center.h-16
|
|
[:div [:span "Add a new "
|
|
(com/link {:hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/navigate)
|
|
{:to (mm/encode-step-key :new-bank-account)
|
|
:from (mm/encode-step-key :bank-accounts)
|
|
:bank-account-type "cash"})} "cash account")
|
|
", "
|
|
(com/link {:hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/navigate)
|
|
{:to (mm/encode-step-key :new-bank-account)
|
|
:from (mm/encode-step-key :bank-accounts)
|
|
:bank-account-type "credit"})} "credit card")
|
|
", "
|
|
(com/link {:hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/navigate)
|
|
{:to (mm/encode-step-key :new-bank-account)
|
|
:from (mm/encode-step-key :bank-accounts)
|
|
:bank-account-type "check"})} "checking account")]]])])
|
|
|
|
(defrecord BankAccountsModal [linear-wizard]
|
|
mm/ModalWizardStep
|
|
(step-name [_]
|
|
"Bank Accounts")
|
|
(step-key [_]
|
|
:bank-accounts)
|
|
|
|
(edit-path [_ _] [])
|
|
|
|
(step-schema [_]
|
|
(mut/select-keys (mm/form-schema linear-wizard) #{}))
|
|
|
|
(render-step
|
|
[this _]
|
|
(mm/default-render-step
|
|
linear-wizard this
|
|
:head (dialog-header this)
|
|
:body (mm/default-step-body {}
|
|
|
|
[:div#bank-account-list {:hx-put (bidi/path-for ssr-routes/only-routes ::route/sort-bank-accounts)
|
|
:hx-trigger "end"}
|
|
(fc/with-field :client/bank-accounts
|
|
(com/validated-field
|
|
{:errors (fc/field-errors)
|
|
:label "Bank Accounts"}
|
|
[:div.flex.flex-col.space-y-4.sortable
|
|
(fc/cursor-map (fn [ba-cursor]
|
|
(when (:bank-account/type @ba-cursor)
|
|
(bank-account-card ba-cursor))))
|
|
|
|
(new-bank-account-card)]))])
|
|
:footer
|
|
[:fieldset {}
|
|
(mm/default-step-footer linear-wizard this
|
|
:validation-route ::route/navigate)]
|
|
:validation-route ::route/navigate)))
|
|
|
|
(defn square-location-table []
|
|
[:div#square-locations
|
|
[:div.htmx-indicator
|
|
"Loading..."]
|
|
[:div.htmx-indicator-hidden
|
|
[:table
|
|
[:thead
|
|
[:tr
|
|
[:td "Square location"]
|
|
[:td "Client location"]]]
|
|
[:tbody
|
|
(fc/cursor-map (fn [square-location]
|
|
[:tr
|
|
(fc/with-field :db/id
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)}))
|
|
(fc/with-field :square-location/name
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)}))
|
|
(fc/with-field :square-location/square-id
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)}))
|
|
[:td (:square-location/name @square-location)]
|
|
[:td (fc/with-field :square-location/client-location
|
|
(com/text-input {:name (fc/field-name)
|
|
:value (fc/field-value)}))]]))]]]])
|
|
|
|
(defn refresh-square-locations [request]
|
|
(let [locations @(de/timeout!
|
|
(de/chain (square/client-locations {:client/square-auth-token (get-in request [:query-params (keyword "step-params[client/square-auth-token]")])})
|
|
(fn [client-locations]
|
|
(into []
|
|
(for [square-location client-locations]
|
|
{:db/id (str (java.util.UUID/randomUUID))
|
|
:square-location/name (:name square-location)
|
|
:square-location/square-id (:id square-location)}))))
|
|
2000
|
|
:not-found)]
|
|
(html-response
|
|
(if (= locations :not-found)
|
|
[:div#square-locations
|
|
"No locations found."]
|
|
(fc/start-form-with-prefix
|
|
[:step-params :client/square-locations]
|
|
locations
|
|
[]
|
|
(square-location-table))))))
|
|
|
|
(defrecord IntegrationsModal [linear-wizard]
|
|
mm/ModalWizardStep
|
|
(step-name [_]
|
|
"Integrations")
|
|
(step-key [_]
|
|
:integrations)
|
|
|
|
(edit-path [_ _] [])
|
|
|
|
(step-schema [_]
|
|
(mut/select-keys (mm/form-schema linear-wizard) #{}))
|
|
|
|
(render-step [this _]
|
|
(mm/default-render-step
|
|
linear-wizard this
|
|
:head (dialog-header this)
|
|
:body (mm/default-step-body
|
|
{}
|
|
[:div
|
|
[:div.flex.gap-2.items-center
|
|
(fc/with-field :client/square-auth-token
|
|
(com/validated-field
|
|
{:errors (fc/field-errors)
|
|
:label "Square Auth Token"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:id "square-token"
|
|
:error? (fc/error?)
|
|
:placeholder "Token from square"
|
|
:class "w-64"
|
|
:value (fc/field-value)})))
|
|
(com/button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/refresh-square-locations)
|
|
:hx-include "#square-token"
|
|
:hx-trigger "click"
|
|
:hx-indicator "#square-locations"
|
|
:hx-target "#square-locations"}
|
|
"Refresh")]
|
|
|
|
(fc/with-field :client/square-locations
|
|
(square-location-table))])
|
|
:footer
|
|
(mm/default-step-footer linear-wizard this :validation-route ::route/navigate)
|
|
:validation-route ::route/navigate)))
|
|
|
|
(defrecord BankAccountModal [linear-wizard which]
|
|
mm/ModalWizardStep
|
|
(step-name [_]
|
|
"Bank Accounts")
|
|
(step-key [_]
|
|
[:bank-account which])
|
|
|
|
(edit-path [_ request]
|
|
(let [account-index (->> (:client/bank-accounts (:snapshot (:multi-form-state request)))
|
|
(map vector (range))
|
|
(filter (fn [[_ ba]]
|
|
(= (:db/id ba)
|
|
which)))
|
|
ffirst)]
|
|
|
|
[:client/bank-accounts (or account-index
|
|
(count (:client/bank-accounts (:snapshot (:multi-form-state request)))))]))
|
|
|
|
(step-schema [_]
|
|
bank-account-schema)
|
|
|
|
(render-step [this _]
|
|
(mm/default-render-step
|
|
linear-wizard this
|
|
:head (dialog-header this)
|
|
:body (mm/default-step-body {}
|
|
[:div {:class "htmx-added:opacity-0 opacity-100 transition-opacity duration-300"}
|
|
(fc/with-field :new?
|
|
(when (fc/field-value)
|
|
(com/hidden {:name (fc/field-name)
|
|
:value "true"})))
|
|
(bank-account-form fc/*current*)])
|
|
|
|
:footer
|
|
(mm/default-step-footer linear-wizard this
|
|
:next-button
|
|
(com/button {:hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/navigate)
|
|
{:from (mm/encode-step-key [:bank-account which])
|
|
:to (mm/encode-step-key :bank-accounts)})}
|
|
;; todo maybe make a helper for progress urls
|
|
"Accept")
|
|
:discard-button
|
|
(com/a-button {:hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/discard)
|
|
{:from (mm/encode-step-key [:bank-account which])
|
|
:to (mm/encode-step-key :bank-accounts)})}
|
|
;; todo maybe make a helper for progress urls
|
|
"discard"))
|
|
:validation-route ::route/navigate))
|
|
mm/Initializable
|
|
(init-step-params
|
|
[_ multi-form-state request]
|
|
(let [bank-account-type (get-in request [:query-params :bank-account-type])]
|
|
(if (= {} (:step-params multi-form-state))
|
|
(cond->
|
|
{:db/id (str (java.util.UUID/randomUUID))
|
|
:new? true}
|
|
|
|
bank-account-type (assoc :bank-account/type (keyword "bank-account-type" bank-account-type)
|
|
:bank-account/visible true))
|
|
(:step-params multi-form-state))))
|
|
|
|
mm/Discardable
|
|
(can-discard? [_ step-params]
|
|
(:new? step-params))
|
|
(discard-changes [_ multi-form-state]
|
|
(-> multi-form-state
|
|
(update-in [:snapshot :client/bank-accounts]
|
|
(fn [bank-accounts]
|
|
(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 [_]
|
|
"Cash Flow")
|
|
|
|
(step-key [_]
|
|
:cash-flow)
|
|
|
|
(edit-path [_ _] [])
|
|
|
|
(step-schema [_]
|
|
(mut/select-keys (mm/form-schema linear-wizard) #{:client/week-a-credits :client/week-a-debits :client/week-b-credits :client/week-b-debits}))
|
|
|
|
(render-step [this _]
|
|
(mm/default-render-step
|
|
linear-wizard this
|
|
:head (dialog-header this)
|
|
:body (mm/default-step-body {}
|
|
[:div.flex.space-x-4
|
|
(fc/with-field :client/week-a-credits
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Week A Credits"
|
|
:class "w-32"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "123.33"
|
|
:class "w-32"
|
|
:value (fc/field-value)})))
|
|
(fc/with-field :client/week-a-debits
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Week A Debits"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "123.33"
|
|
:class "w-32"
|
|
:value (fc/field-value)})))]
|
|
[:div.flex.space-x-4
|
|
(fc/with-field :client/week-b-credits
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Week B Credits"
|
|
:class "w-32"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "123.33"
|
|
:class "w-32"
|
|
:value (fc/field-value)})))
|
|
(fc/with-field :client/week-b-debits
|
|
(com/validated-field {:errors (fc/field-errors)
|
|
:label "Week B Debits"}
|
|
(com/text-input {:name (fc/field-name)
|
|
:error? (fc/error?)
|
|
:placeholder "123.33"
|
|
:class "w-32"
|
|
:value (fc/field-value)})))]
|
|
[:div])
|
|
:footer
|
|
(mm/default-step-footer linear-wizard this :validation-route ::route/navigate)
|
|
:validation-route ::route/navigate)))
|
|
|
|
(defrecord OtherSettingsModal [linear-wizard]
|
|
mm/ModalWizardStep
|
|
(step-name [_]
|
|
"Other Settings")
|
|
|
|
(step-key [_]
|
|
:other-settings)
|
|
|
|
(edit-path [_ _] [])
|
|
|
|
(step-schema [_]
|
|
(mut/select-keys (mm/form-schema linear-wizard) #{:client/feature-flags :client/groups}))
|
|
|
|
(render-step [this _]
|
|
(mm/default-render-step
|
|
linear-wizard this
|
|
:head (dialog-header this)
|
|
:body (mm/default-step-body {}
|
|
(fc/with-field :client/feature-flags
|
|
(com/validated-field
|
|
{:errors (fc/field-errors)
|
|
:label "Feature Flags"}
|
|
(com/data-grid {:headers [(com/data-grid-header {} "Flag")
|
|
(com/data-grid-header {:class "w-16"})]}
|
|
(fc/cursor-map #(feature-flag-row %))
|
|
(com/data-grid-new-row {:colspan 2
|
|
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-feature-flag)
|
|
:index (count (fc/field-value))}
|
|
"New flag"))))
|
|
(fc/with-field :client/groups
|
|
(com/validated-field
|
|
{:errors (fc/field-errors)
|
|
:label "Groups"}
|
|
(com/data-grid {:headers [(com/data-grid-header {} "Group")
|
|
(com/data-grid-header {:class "w-16"})]}
|
|
(fc/cursor-map #(group-row %))
|
|
(com/data-grid-new-row {:colspan 2
|
|
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-group)
|
|
:index (count (fc/field-value))}
|
|
|
|
"New group")))))
|
|
:footer
|
|
(mm/default-step-footer linear-wizard this :validation-route ::route/navigate)
|
|
:validation-route ::route/navigate)))
|
|
|
|
(defrecord ClientWizard [form-params current-step entity]
|
|
mm/LinearModalWizard
|
|
(hydrate-from-request [this request]
|
|
(assoc this :entity (:entity request)))
|
|
(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 :info)))
|
|
(render-wizard [this {:keys [multi-form-state] :as request}]
|
|
(mm/default-render-wizard
|
|
this request
|
|
:form-params
|
|
(-> mm/default-form-props
|
|
(assoc (if (get-in multi-form-state [:snapshot :db/id])
|
|
:hx-put
|
|
:hx-post)
|
|
(str (bidi/path-for ssr-routes/only-routes ::route/save))
|
|
:x-data (hx/json {"clientName" (:client/name (:step-params multi-form-state))})))))
|
|
(steps [_]
|
|
[:info
|
|
:matches
|
|
:contact
|
|
:bank-accounts
|
|
:integrations
|
|
:cash-flow
|
|
:other-settings])
|
|
|
|
(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]
|
|
(if (= :step step-key-type)
|
|
(get {:info (->InfoModal this)
|
|
:matches (->MatchesModal this)
|
|
:contact (->ContactModal this)
|
|
:bank-accounts (->BankAccountsModal this)
|
|
:integrations (->IntegrationsModal this)
|
|
:cash-flow (->CashFlowModal this)
|
|
:other-settings (->OtherSettingsModal this)
|
|
:new-bank-account (->BankAccountModal this "new")}
|
|
step-key)
|
|
|
|
(get {:bank-account (->BankAccountModal this (second step-key))}
|
|
(first step-key)))))
|
|
(form-schema [_] form-schema-2)
|
|
(submit [_ {:keys [multi-form-state request-method identity] :as request}]
|
|
(let [snapshot (mc/decode
|
|
form-schema-2
|
|
(:snapshot multi-form-state)
|
|
mt/strip-extra-keys-transformer)
|
|
entity (cond-> snapshot
|
|
(= :post request-method) (assoc :db/id "new")
|
|
(= :put request-method) (dissoc :client/code)
|
|
|
|
(:client/locked-until snapshot) (update :client/locked-until clj-time.coerce/to-date)
|
|
(seq (:client/groups snapshot)) (update :client/groups #(mapv str/upper-case %))
|
|
(seq (:client/bank-accounts snapshot)) (update :client/bank-accounts
|
|
(fn [bank-accounts]
|
|
(mapv
|
|
(fn [bank-account]
|
|
(-> bank-account
|
|
(update :bank-account/start-date #(when % (clj-time.coerce/to-date %)))))
|
|
bank-accounts))))
|
|
_ (alog/info ::peeker :entity (:client/bank-accounts entity))
|
|
_ (when (and (:client/code entity) (pull-id (dc/db conn) [:client/code (:client/code entity)]))
|
|
(form-validation-error (format "The code '%s' is already in use" (:client/code entity))
|
|
:code (:client/code entity)))
|
|
|
|
{:keys [tempids]} (audit-transact [[:upsert-entity entity]]
|
|
(:identity request))
|
|
|
|
updated-client (dc/pull (dc/db conn)
|
|
default-read
|
|
(or (get tempids (:db/id entity)) (:db/id entity)))]
|
|
(when (:client/name updated-client)
|
|
(solr/index-documents-raw solr/impl "clients"
|
|
[{"id" (:db/id updated-client)
|
|
"name" (conj (or (:client/matches updated-client) [])
|
|
(:client/name updated-client))
|
|
"code" (:client/code updated-client)
|
|
|
|
"exact" (map str/upper-case (conj (or (:client/matches updated-client) [])
|
|
(:client/name updated-client)))}]))
|
|
(html-response
|
|
(row* identity updated-client {: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-client))
|
|
"hx-reswap" "outerHTML"))))))
|
|
|
|
(def client-wizard
|
|
(->ClientWizard nil nil nil))
|
|
|
|
(defn sort-bank-accounts [{:keys [multi-form-state wizard] :as request}]
|
|
(let [sort-index (into {} (map vector (:item (:form-params request)) (range)))
|
|
new-bank-accounts (->> multi-form-state
|
|
:snapshot
|
|
:client/bank-accounts
|
|
(map (fn [bank-account]
|
|
(assoc bank-account :bank-account/sort-order (sort-index (:db/id bank-account)))))
|
|
(sort-by :bank-account/sort-order)
|
|
(into []))]
|
|
(html-response
|
|
(mm/render-wizard wizard (update request :multi-form-state
|
|
(comp
|
|
#(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
|
|
:in $
|
|
:where
|
|
[(ground (iol-ion.query/recent-date 120)) ?min-d]
|
|
[(ground #inst \"2040-01-01\") ?max-d]
|
|
[?c :client/code \"%s\"]
|
|
[(iol-ion.query/sales-orders-in-range $ ?c ?min-d ?max-d) [?s ...]]
|
|
[?s :sales-order/date ?d]
|
|
[?s :sales-order/total ?total]
|
|
[?s :sales-order/tax ?tax]
|
|
[?s :sales-order/tip ?tip]
|
|
[?s :sales-order/service-charge ?service-charge]
|
|
[?s :sales-order/returns ?returns]
|
|
[?s :sales-order/discount ?discount]
|
|
[(iol-ion.query/excel-date ?d) ?d4]
|
|
]")
|
|
|
|
(def sales-category-query
|
|
"[:find ?d4 ?n ?n2 (sum ?total) (sum ?tax) (sum ?discount)
|
|
:with ?s ?li
|
|
:in $
|
|
:where
|
|
[(ground (iol-ion.query/recent-date 120)) ?min-d]
|
|
[(ground #inst \"2040-01-01\") ?max-d]
|
|
[?c :client/code \"%s\"]
|
|
[(iol-ion.query/sales-orders-in-range $ ?c ?min-d ?max-d) [?s ...]]
|
|
[?s :sales-order/date ?d]
|
|
[?s :sales-order/line-items ?li]
|
|
[?li :order-line-item/category ?n]
|
|
[(get-else $ ?li :order-line-item/item-name \"\") ?n2]
|
|
[?li :order-line-item/total ?total]
|
|
[?li :order-line-item/tax ?tax]
|
|
[?li :order-line-item/discount ?discount]
|
|
[(iol-ion.query/excel-date ?d) ?d4]]")
|
|
|
|
(def expected-deposits-query
|
|
"[:find ?d4 ?t ?f
|
|
:in $
|
|
:where
|
|
[(ground (iol-ion.query/recent-date 120)) ?min-d]
|
|
[?c :client/code \"%s\"]
|
|
[?s :expected-deposit/client ?c]
|
|
[?s :expected-deposit/sales-date ?date]
|
|
[(>= ?date ?min-d)]
|
|
[?s :expected-deposit/total ?t]
|
|
[?s :expected-deposit/fee ?f]
|
|
[(iol-ion.query/excel-date ?date) ?d4]
|
|
]")
|
|
|
|
(def tenders-query
|
|
"[:find ?d4 ?type ?p2 (sum ?total) (sum ?tip)
|
|
:with ?charge
|
|
:in $
|
|
:where
|
|
[(ground (iol-ion.query/recent-date 120)) ?min-d]
|
|
[?c :client/code \"%s\"]
|
|
[?s :sales-order/client ?c]
|
|
[?s :sales-order/date ?date]
|
|
[(>= ?date ?min-d)]
|
|
[?s :sales-order/charges ?charge]
|
|
[?charge :charge/type-name ?type]
|
|
[?charge :charge/total ?total]
|
|
[?charge :charge/tip ?tip]
|
|
[(get-else $ ?charge :charge/processor :na) ?ccp]
|
|
[(get-else $ ?ccp :db/ident :na) ?p]
|
|
[(name ?p) ?p2]
|
|
[(iol-ion.query/excel-date ?date) ?d4]
|
|
]")
|
|
|
|
(def tenders2-query
|
|
"[:find ?d4 ?type ?p2 (sum ?total) (sum ?tip)
|
|
:with ?charge
|
|
:in $
|
|
:where
|
|
[(ground (iol-ion.query/recent-date 120)) ?min-d]
|
|
[?charge :charge/date ?date]
|
|
[(>= ?date ?min-d)]
|
|
[?charge :charge/client ?c]
|
|
[?c :client/code \"%s\"]
|
|
[?charge :charge/type-name ?type]
|
|
[?charge :charge/total ?total]
|
|
[?charge :charge/tip ?tip]
|
|
(or
|
|
|
|
(and [_ :expected-deposit/charges ?charge ]
|
|
[(ground :settlement) ?ccp]
|
|
[(ground :settlement) ?p])
|
|
(and
|
|
(not [_ :expected-deposit/charges ?charge])
|
|
[(get-else $ ?charge :charge/processor :na) ?ccp]
|
|
[(get-else $ ?ccp :db/ident :na) ?p]
|
|
))
|
|
[(name ?p) ?p2]
|
|
[(iol-ion.query/excel-date ?date) ?d4]]
|
|
")
|
|
|
|
(def refunds-query
|
|
"[:find ?d4 ?t (sum ?total) (sum ?fee)
|
|
:with ?r
|
|
:in $
|
|
:where
|
|
[(ground (iol-ion.query/recent-date 120)) ?min-d]
|
|
[?r :sales-refund/client [:client/code \"%s\"]]
|
|
[?r :sales-refund/date ?date]
|
|
[(>= ?date ?min-d)]
|
|
[?r :sales-refund/total ?total]
|
|
[?r :sales-refund/fee ?fee]
|
|
[?r :sales-refund/type ?t]
|
|
[(iol-ion.query/excel-date ?date) ?d4]
|
|
]")
|
|
|
|
(def cash-drawer-shift-query
|
|
"[:find ?d4 (sum ?paid-in) (sum ?paid-out) (sum ?expected-cash) (sum ?opened-cash)
|
|
:with ?cds
|
|
:in $
|
|
:where
|
|
[?cds :cash-drawer-shift/date ?date]
|
|
[(ground (iol-ion.query/recent-date 120)) ?min-d]
|
|
[(>= ?date ?min-d)]
|
|
[?cds :cash-drawer-shift/client [:client/code \"%s\"]]
|
|
[?cds :cash-drawer-shift/paid-in ?paid-in]
|
|
[?cds :cash-drawer-shift/paid-out ?paid-out]
|
|
[?cds :cash-drawer-shift/expected-cash ?expected-cash]
|
|
[?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")]
|
|
(q/put-query (str (UUID/randomUUID))
|
|
(format sales-summary-query client-code)
|
|
(str "sales query for " client-code)
|
|
(str client-code "-sales-summary")
|
|
[:client/code client-code])
|
|
(q/put-query (str (UUID/randomUUID))
|
|
(format sales-category-query client-code)
|
|
(str "sales category query for " client-code)
|
|
(str client-code "-sales-category")
|
|
[:client/code client-code])
|
|
(q/put-query (str (UUID/randomUUID))
|
|
(format expected-deposits-query client-code)
|
|
(str "expected deposit query for " client-code)
|
|
(str client-code "-expected-deposit")
|
|
[:client/code client-code])
|
|
(q/put-query (str (UUID/randomUUID))
|
|
(format (if is-new-square? tenders2-query tenders-query) client-code)
|
|
(str "tender query for " client-code)
|
|
(str client-code "-tender")
|
|
[:client/code client-code])
|
|
|
|
(q/put-query (str (UUID/randomUUID))
|
|
(format refunds-query client-code)
|
|
(str "refunds query for " client-code)
|
|
(str client-code "-refund")
|
|
[:client/code client-code])
|
|
|
|
(q/put-query (str (UUID/randomUUID))
|
|
(format cash-drawer-shift-query client-code)
|
|
(str "cash drawer shift query for " client-code)
|
|
(str client-code "-cash-drawer-shift")
|
|
[:client/code client-code])))
|
|
|
|
(defn reset-all-queries []
|
|
(doseq [[c] (dc/q '[:find ?c :where [?c :client/code]] (dc/db conn))]
|
|
(setup-sales-queries-impl c)))
|
|
|
|
(defn copy-button [{:keys [which url]} & children]
|
|
(com/button {"@click" (format "copyToClipboard(%s)"
|
|
(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
|
|
(com/modal {}
|
|
(com/modal-card-advanced
|
|
{}
|
|
(com/modal-header {} [:div.m-2 "Sales exports"])
|
|
(com/modal-body {}
|
|
(let [client-code (pull-attr (dc/db conn) :client/code (:db/id (:route-params request)))
|
|
sales-summary-id (:saved-query/guid (dc/pull (dc/db conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-sales-summary")]))
|
|
sales-category-id (:saved-query/guid (dc/pull (dc/db conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-sales-category")]))
|
|
expected-deposit-id (:saved-query/guid (dc/pull (dc/db conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-expected-deposit")]))
|
|
tender-id (:saved-query/guid (dc/pull (dc/db conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-tender")]))
|
|
refund-id (:saved-query/guid (dc/pull (dc/db conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-refund")]))
|
|
cash-drawer-shift-id (:saved-query/guid (dc/pull (dc/db conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-cash-drawer-shift")]))]
|
|
[:div.flex.flex-col.gap-2
|
|
(copy-button {:which "powerqueries/sales_summary.txt" :url sales-summary-id}
|
|
"Copy Sales Order Summary Power Query")
|
|
(copy-button {:which "powerqueries/sales_category.txt" :url sales-category-id}
|
|
"Copy Sales Category Power Query")
|
|
(copy-button {:which "powerqueries/tenders.txt" :url tender-id}
|
|
"Copy Tenders Power Query")
|
|
(copy-button {:which "powerqueries/expected_deposits.txt" :url expected-deposit-id}
|
|
"Copy Expected Deposits Power Query")
|
|
(copy-button {:which "powerqueries/refunds.txt" :url refund-id}
|
|
"Copy Refunds Power Query")
|
|
#_(copy-button {:which "powerqueries/cash_drawer_shift.txt" :url cash-drawer-shift-id}
|
|
"Copy Cash Drawer Shift Power Query")]))
|
|
|
|
(com/modal-footer {} [:div])))))
|
|
|
|
(def key->handler
|
|
(apply-middleware-to-all-handlers
|
|
{::route/page (helper/page-route grid-page)
|
|
::route/table (helper/table-route grid-page)
|
|
::route/new-location (add-new-primitive-handler [:step-params :client/locations]
|
|
""
|
|
location-row)
|
|
::route/new-feature-flag (add-new-primitive-handler [:step-params :client/feature-flags]
|
|
""
|
|
feature-flag-row)
|
|
::route/new-match (add-new-primitive-handler [:step-params :client/matches]
|
|
""
|
|
match-row)
|
|
::route/new-group (add-new-primitive-handler [:step-params :client/groups]
|
|
""
|
|
group-row)
|
|
::route/new-location-match (add-new-entity-handler [:step-params :client/location-matches]
|
|
(fn [cursor _] (location-match-row cursor)))
|
|
::route/new-email-contact (add-new-entity-handler [:step-params :client/emails]
|
|
(fn [cursor _] (email-contact-row cursor)))
|
|
::route/save (-> mm/submit-handler
|
|
(mm/wrap-wizard client-wizard)
|
|
(mm/wrap-decode-multi-form-state)
|
|
(wrap-entity [:form-params :db/id] default-read))
|
|
::route/biweekly-sales-powerquery
|
|
(-> biweekly-sales-powerquery
|
|
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
|
|
::route/refresh-square-locations
|
|
refresh-square-locations
|
|
::route/navigate
|
|
(-> mm/next-handler
|
|
(mm/wrap-wizard client-wizard)
|
|
(mm/wrap-decode-multi-form-state)
|
|
(wrap-entity [:route-params :db/id] default-read)
|
|
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
|
|
::route/sort-bank-accounts
|
|
(-> sort-bank-accounts
|
|
(wrap-schema-enforce :form-schema [:map [:item [:vector entity-id]]])
|
|
(mm/wrap-wizard client-wizard)
|
|
(mm/wrap-decode-multi-form-state))
|
|
::route/discard
|
|
(-> mm/discard-handler
|
|
(mm/wrap-wizard client-wizard)
|
|
(mm/wrap-decode-multi-form-state))
|
|
::route/edit-dialog (-> mm/open-wizard-handler
|
|
(mm/wrap-wizard client-wizard)
|
|
(mm/wrap-init-multi-form-state (fn [request]
|
|
(let [sorted (-> (:entity request)
|
|
(update :client/bank-accounts
|
|
(fn [bas]
|
|
(into [] (sort-by :bank-account/sort-order bas)))))]
|
|
(mm/->MultiStepFormState sorted
|
|
[]
|
|
sorted))))
|
|
(wrap-entity [:route-params :db/id] default-read)
|
|
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
|
|
::route/new-dialog (-> mm/open-wizard-handler
|
|
(mm/wrap-init-multi-form-state (fn [_]
|
|
(mm/->MultiStepFormState {}
|
|
[]
|
|
{})))
|
|
(mm/wrap-wizard client-wizard))}
|
|
(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)))))
|