From 8063a8fcbdb3ef786949cfc540ad428cfb9dc558 Mon Sep 17 00:00:00 2001 From: Bryce Date: Tue, 9 Jan 2024 21:40:43 -0800 Subject: [PATCH] Builds client SSR approach, sunsets old cljs. --- .gitignore | 1 + config/dev.edn | 1 + figwheel-main.edn | 2 - iol_ion/src/iol_ion/tx/upsert_entity.clj | 122 +- project.clj | 16 +- resources/functions.edn | 2 +- resources/powerqueries/expected_deposits.txt | 8 + resources/powerqueries/refunds.txt | 8 + resources/powerqueries/sales_category.txt | 8 + resources/powerqueries/sales_summary.txt | 8 + resources/powerqueries/tenders.txt | 8 + resources/public/js/htmx-disable.js | 46 +- resources/public/output.css | 2 +- src/clj/auto_ap/cursor.clj | 7 + src/clj/auto_ap/graphql/clients.clj | 453 +---- src/clj/auto_ap/handler.clj | 6 +- src/clj/auto_ap/jobs/bulk_journal_import.clj | 2 +- src/clj/auto_ap/jobs/close_auto_invoices.clj | 2 +- .../auto_ap/jobs/current_balance_cache.clj | 2 +- src/clj/auto_ap/jobs/ezcater_upsert.clj | 2 +- src/clj/auto_ap/jobs/intuit.clj | 2 +- src/clj/auto_ap/jobs/ledger_reconcile.clj | 2 +- .../auto_ap/jobs/load_historical_sales.clj | 2 +- src/clj/auto_ap/jobs/plaid.clj | 2 +- .../auto_ap/jobs/register_invoice_import.clj | 2 +- src/clj/auto_ap/jobs/square.clj | 2 +- src/clj/auto_ap/jobs/vendor_usages.clj | 2 +- src/clj/auto_ap/jobs/yodlee2.clj | 2 +- src/clj/auto_ap/server.clj | 2 +- src/clj/auto_ap/ssr/admin/background_jobs.clj | 10 +- src/clj/auto_ap/ssr/admin/clients.clj | 1721 +++++++++++++++++ .../auto_ap/ssr/admin/sales_powerqueries.clj | 0 .../auto_ap/ssr/admin/transaction_rules.clj | 1116 ++++++----- src/clj/auto_ap/ssr/admin/vendors.clj | 1473 +++++++------- src/clj/auto_ap/ssr/common_handlers.clj | 32 + src/clj/auto_ap/ssr/company.clj | 217 ++- src/clj/auto_ap/ssr/components.clj | 11 +- src/clj/auto_ap/ssr/components/aside.clj | 6 +- src/clj/auto_ap/ssr/components/buttons.clj | 2 +- src/clj/auto_ap/ssr/components/card.clj | 10 +- src/clj/auto_ap/ssr/components/dialog.clj | 83 +- .../auto_ap/ssr/components/multi_modal.clj | 349 ++++ src/clj/auto_ap/ssr/components/timeline.clj | 25 +- src/clj/auto_ap/ssr/core.clj | 3 + src/clj/auto_ap/ssr/form_cursor.clj | 32 +- src/clj/auto_ap/ssr/hiccup_helper.clj | 46 +- .../auto_ap/ssr/pos/cash_drawer_shifts.clj | 88 +- src/clj/auto_ap/ssr/svg.clj | 282 +-- src/clj/auto_ap/ssr/ui.clj | 46 +- src/clj/auto_ap/ssr/utils.clj | 287 +-- src/clj/user.clj | 344 ++-- src/clj/user.fiddle | 84 + src/cljc/auto_ap/client_routes.cljc | 7 +- src/cljc/auto_ap/permissions.cljc | 21 +- src/cljc/auto_ap/routes/admin/clients.cljc | 19 + .../routes/admin/transaction_rules.cljc | 3 +- src/cljc/auto_ap/routes/admin/vendors.cljc | 1 + .../auto_ap/shared_views/company/sidebar.cljc | 10 - src/cljc/auto_ap/ssr_routes.cljc | 4 + src/cljs/auto_ap/forms/builder.cljs | 2 +- .../views/components/vendor_dialog.cljs | 221 ++- src/cljs/auto_ap/views/main.cljs | 12 +- .../auto_ap/views/pages/admin/clients.cljs | 106 - .../views/pages/admin/clients/form.cljs | 761 -------- .../views/pages/admin/clients/side_bar.cljs | 23 - .../views/pages/admin/clients/table.cljs | 123 -- .../auto_ap/views/pages/admin/vendors.cljs | 94 - .../views/pages/admin/vendors/common.cljs | 17 - .../pages/admin/vendors/merge_dialog.cljs | 80 - .../views/pages/admin/vendors/side_bar.cljs | 16 - .../views/pages/admin/vendors/table.cljs | 36 - tailwind.config.js | 10 +- .../auto_ap/integration/graphql/accounts.clj | 2 +- .../auto_ap/integration/graphql/clients.clj | 91 - 74 files changed, 4603 insertions(+), 4047 deletions(-) create mode 100644 resources/powerqueries/expected_deposits.txt create mode 100644 resources/powerqueries/refunds.txt create mode 100644 resources/powerqueries/sales_category.txt create mode 100644 resources/powerqueries/sales_summary.txt create mode 100644 resources/powerqueries/tenders.txt create mode 100644 src/clj/auto_ap/ssr/admin/clients.clj create mode 100644 src/clj/auto_ap/ssr/admin/sales_powerqueries.clj create mode 100644 src/clj/auto_ap/ssr/common_handlers.clj create mode 100644 src/clj/auto_ap/ssr/components/multi_modal.clj create mode 100644 src/cljc/auto_ap/routes/admin/clients.cljc delete mode 100644 src/cljs/auto_ap/views/pages/admin/clients.cljs delete mode 100644 src/cljs/auto_ap/views/pages/admin/clients/form.cljs delete mode 100644 src/cljs/auto_ap/views/pages/admin/clients/side_bar.cljs delete mode 100644 src/cljs/auto_ap/views/pages/admin/clients/table.cljs delete mode 100644 src/cljs/auto_ap/views/pages/admin/vendors.cljs delete mode 100644 src/cljs/auto_ap/views/pages/admin/vendors/common.cljs delete mode 100644 src/cljs/auto_ap/views/pages/admin/vendors/merge_dialog.cljs delete mode 100644 src/cljs/auto_ap/views/pages/admin/vendors/side_bar.cljs delete mode 100644 src/cljs/auto_ap/views/pages/admin/vendors/table.cljs delete mode 100644 test/clj/auto_ap/integration/graphql/clients.clj diff --git a/.gitignore b/.gitignore index 01da1623..7255b380 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ data/solr/data/invoices/index/ data/solr/data/plaid_merchants/data/ data/solr/data/logs data/solr/logs +.vscode/** diff --git a/config/dev.edn b/config/dev.edn index e0b68e0c..b96917f0 100644 --- a/config/dev.edn +++ b/config/dev.edn @@ -1,4 +1,5 @@ {:db {:server "localhost"} + :port "3449" :scheme "http" :base-url "http://localhost:3449" :solr-uri "http://localhost:8983" diff --git a/figwheel-main.edn b/figwheel-main.edn index 1936ae31..9961007c 100644 --- a/figwheel-main.edn +++ b/figwheel-main.edn @@ -1,5 +1,3 @@ {:watch-dirs ["src/cljs", "src/cljc"] :css-dirs ["resources/public/css"] - :ring-server-options {:port 3449} - :ring-handler auto-ap.handler/app :open-url false} diff --git a/iol_ion/src/iol_ion/tx/upsert_entity.clj b/iol_ion/src/iol_ion/tx/upsert_entity.clj index a6175e16..e3b2f8f3 100644 --- a/iol_ion/src/iol_ion/tx/upsert_entity.clj +++ b/iol_ion/src/iol_ion/tx/upsert_entity.clj @@ -1,5 +1,8 @@ (ns iol-ion.tx.upsert-entity - (:require [datomic.api :as dc]) + (:require [datomic.api :as dc] + ;; [clj-time.core :as time] + ;; [clj-time.coerce :as coerce] + ) (:import [java.util UUID])) @@ -9,11 +12,11 @@ (defn -by [f fv xs] (reduce - #(assoc %1 (f %2) (fv %2)) - {} - xs)) + #(assoc %1 (f %2) (fv %2)) + {} + xs)) -(defn -pull-many [db read ids ] +(defn -pull-many [db read ids] (->> (dc/q '[:find (pull ?e r) :in $ [?e ...] r] db @@ -21,82 +24,93 @@ read) (map first))) +;; TODO add DATOMIC_EXT_CLASSPATH ala https://docs.datomic.com/pro/reference/database-functions.html#transaction-functions +;; (defn transform-common [v] +;; (cond +;; (nil? v) +;; v + +;; (satisfies? clj-time.core/DateTimeProtocol v) +;; (clj-time.coerce/to-date v) + +;; :else +;; v)) + (defn upsert-entity [db entity] (when-not (or (:db/id entity) - (:db/ident entity)) - (datomic.api/cancel {:cognitect.anomalies/category :cognitect.anomalies/incorrect - :cognitect.anomalies/message - (str "Cannot upsert without :db/id or :db/ident, " entity)})) + (:db/ident entity)) + (datomic.api/cancel {:cognitect.anomalies/category :cognitect.anomalies/incorrect + :cognitect.anomalies/message + (str "Cannot upsert without :db/id or :db/ident, " entity)})) (let [e (or (:db/id entity) (:db/ident entity)) is-new? (string? e) extant-entity (when-not is-new? (dc/pull db (keys entity) (or (:db/id entity) (:db/ident entity)))) ident->value-type (-by :db/ident (comp :db/ident - :db/valueType) - (-pull-many + :db/valueType) + (-pull-many db [{:db/valueType [:db/ident]} :db/ident] (keys entity))) ident->cardinality (-by :db/ident (comp :db/ident - :db/cardinality) - (-pull-many + :db/cardinality) + (-pull-many db [{:db/cardinality [:db/ident]} :db/ident] (keys entity))) ops (->> entity (reduce - (fn [ops [a v]] - (cond - (= :db/id a) - ops + (fn [ops [a v]] + (cond + (= :db/id a) + ops - (= :db/ident a) - ops + (= :db/ident a) + ops - (or (= v (a extant-entity)) - (= v (:db/ident (a extant-entity) :nope)) - (= v (:db/id (a extant-entity)) :nope)) - ops - - (and (nil? v) - (not (nil? (a extant-entity)))) - (if (= :db.cardinality/many (ident->cardinality a)) - (into ops (map (fn [v] - [:db/retract e a (cond-> v - (:db/id v) :db/id)]) - (a extant-entity))) + (or (= v (a extant-entity)) + (= v (:db/ident (a extant-entity) :nope)) + (= v (:db/id (a extant-entity)) :nope)) + ops - (conj ops [:db/retract e a (cond-> (a extant-entity) - (:db/id (a extant-entity)) :db/id)])) + (and (nil? v) + (not (nil? (a extant-entity)))) + (if (= :db.cardinality/many (ident->cardinality a)) + (into ops (map (fn [v] + [:db/retract e a (cond-> v + (:db/id v) :db/id)]) + (a extant-entity))) - (nil? v) - ops + (conj ops [:db/retract e a (cond-> (a extant-entity) + (:db/id (a extant-entity)) :db/id)])) + + (nil? v) + ops ;; reset relationships if it's refs, and not a lookup (i.e., seq of maps, or empty seq) + + (and (sequential? v) (= :db.type/tuple (ident->value-type a)) (not (= :db.cardinality/many (ident->cardinality a)))) + (conj ops [:db/add e a v]) - (and (sequential? v) (= :db.type/tuple (ident->value-type a)) (not (= :db.cardinality/many (ident->cardinality a)))) - (conj ops [:db/add e a v]) + (and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v)) + (into ops [[:reset-rels e a v]]) - (and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v)) - (into ops [[:reset-rels e a v]]) + (= :db.cardinality/many (ident->cardinality a)) + (into ops [[:reset-scalars e a v]]) - (= :db.cardinality/many (ident->cardinality a)) - (into ops [[:reset-scalars e a v]]) + (and (sequential? v) (not= :db.type/ref (ident->value-type a))) + (into ops [[:reset-scalars e a v]]) - (and (sequential? v) (not= :db.type/ref (ident->value-type a))) - (into ops [[:reset-scalars e a v]]) + (and (map? v) + (= :db.type/ref (ident->value-type a))) + (let [id (or (:db/id v) (-random-tempid))] + (-> ops + (conj [:db/add e a id]) + (into [[:upsert-entity (assoc v :db/id id)]]))) - (and (map? v) - (= :db.type/ref (ident->value-type a))) - (let [id (or (:db/id v) (-random-tempid))] - (-> ops - (conj [:db/add e a id]) - (into [[:upsert-entity (assoc v :db/id id)]]))) - - :else - (conj ops [:db/add e a v]) - )) - []))] + :else + (conj ops [:db/add e a v]))) + []))] ops)) diff --git a/project.clj b/project.clj index c730fb5a..3cc85652 100644 --- a/project.clj +++ b/project.clj @@ -23,7 +23,7 @@ #_org.eclipse.jetty/jetty-http #_org.eclipse.jetty/jetty-util #_org.eclipse.jetty/jetty-server]] - + [ring/ring-jetty-adapter "1.9.6" :exclusions [org.eclipse.jetty/jetty-server]] [yogthos/config "1.1.7"] @@ -39,9 +39,10 @@ [buddy/buddy-auth "2.2.0" :exclusions [com.fasterxml.jackson.dataformat/jackson-dataformat-cbor com.fasterxml.jackson.core/jackson-core]] - + [nrepl "0.8.3" :exclusions [org.clojure/tools.logging]] [cheshire "5.9.0"] + [hawk "0.2.11"] [clj-time "0.15.2"] [ring/ring-json "0.5.0" :exclusions [cheshire]] [com.cemerick/url "0.1.1"] @@ -91,7 +92,7 @@ [org.clojure/core.async]] [hiccup "2.0.0-alpha2"] - + ;; needed for java 11 [javax.xml.bind/jaxb-api "2.4.0-b180830.0359"] [io.forward/clojure-mail "1.0.8"] @@ -107,19 +108,22 @@ [lein-cljsbuild "1.1.5"] [lein-ancient "0.6.15"]] :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] - :ring {:handler auto-ap.handler/app} + #_#_:ring {:handler auto-ap.handler/app} :source-paths ["src/clj" "src/cljc" "src/cljs" "iol_ion/src" ] :resource-paths ["resources"] :aliases {"build" ["do" ["uberjar"]] "fig:dev" ["run" "-m" "figwheel.main" "-b" "dev" "-r"] + "build-dev" ["trampoline" "run" "-m" "figwheel.main" "-b" "dev" "-r"] "fig:min" ["run" "-m" "figwheel.main" "-O" "whitespace" "-bo" "min"]} :profiles { :dev - {:resource-paths ["resources" "target"] + { + :resource-paths ["resources" "target"] :dependencies [#_[binaryage/devteols "1.0.2"] [postgresql/postgresql "9.3-1102.jdbc41"] + [org.clojure/tools.namespace "1.4.5"] [org.clojure/java.jdbc "0.7.11"] #_[com.datomic/dev-local "1.0.243"] [etaoin "0.4.1"] @@ -177,7 +181,7 @@ :main auto-ap.server - :aot [auto-ap.server auto-ap.time clj-time.core clj-time.coerce clj-time.format clojure.tools.logging.impl] + #_#_:aot [auto-ap.server auto-ap.time clj-time.core clj-time.coerce clj-time.format clojure.tools.logging.impl] :uberjar-name "auto-ap.jar" :test-paths ["test/clj"] diff --git a/resources/functions.edn b/resources/functions.edn index 3d7425eb..38e2b5c1 100644 --- a/resources/functions.edn +++ b/resources/functions.edn @@ -1 +1 @@ -[#:db{:ident :pay, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) amount (genfn-coerce-arg amount)] (do (let [current-outstanding-balance (-> (dc/pull db [:invoice/outstanding-balance] e) :invoice/outstanding-balance) new-outstanding-balance (- current-outstanding-balance amount)] [[:upsert-invoice {:invoice/status (if (> new-outstanding-balance 0) :invoice-status/unpaid :invoice-status/paid), :db/id e, :invoice/outstanding-balance new-outstanding-balance}]]))))))"}} #:db{:ident :plus, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) amount (genfn-coerce-arg amount)] (do [[:db/add e a (-> (dc/pull db [a] e) a (+ amount))]])))))"}} #:db{:ident :propose-invoice, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [existing? (boolean (seq (dc/q (quote [:find ?i :in $ ?invoice-number ?client ?vendor :where [?i :invoice/invoice-number ?invoice-number] [?i :invoice/client ?client] [?i :invoice/vendor ?vendor] (not [?i :invoice/status :invoice-status/voided])]) db (:invoice/invoice-number invoice) (:invoice/client invoice) (:invoice/vendor invoice))))] (if existing? [] [[:upsert-invoice invoice]])))))))"}} #:db{:ident :reset-rels, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (assert (every? :db/id vs) (format \"In order to reset attribute %s, every value must have :db/id\" a)) (let [ids (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) new-id-set (set (map :db/id vs)) retract-ids (filter (complement new-id-set) ids) {is-component? :db/isComponent} (dc/pull db [:db/isComponent] a) new-rels (filter (complement (set ids)) (map :db/id vs))] (-> [] (into (map (fn [i] (if is-component? [:db/retractEntity i] [:db/retract e a i])) retract-ids)) (into (map (fn [i] [:db/add e a i]) new-rels)) (into (map (fn [i] [:upsert-entity i]) vs)))))))))"}} #:db{:ident :reset-scalars, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (let [extant (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) retracts (filter (complement (set vs)) extant) new (filter (complement (set extant)) vs)] (-> [] (into (map (fn [i] [:db/retract e a i]) retracts)) (into (map (fn [i] [:db/add e a i]) new)))))))))"}} #:db{:ident :upsert-entity, :fn #db/fn{:lang :clojure, :imports [[java.util UUID]], :requires [[datomic.api :as dc]], :params [db entity], :code "(let [] (letfn [(-random-tempid [] (do (str (UUID/randomUUID)))) (-by [f fv xs] (do (reduce (fn* [p1__164280# p2__164281#] (assoc p1__164280# (f p2__164281#) (fv p2__164281#))) {} xs))) (-pull-many [db read ids] (do (->> (dc/q (quote [:find (pull ?e r) :in $ [?e ...] r]) db ids read) (map first))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [entity (genfn-coerce-arg entity)] (do (when-not (or (:db/id entity) (:db/ident entity)) (datomic.api/cancel #:cognitect.anomalies{:message (str \"Cannot upsert without :db/id or :db/ident, \" entity), :category :cognitect.anomalies/incorrect})) (let [e (or (:db/id entity) (:db/ident entity)) is-new? (string? e) extant-entity (when-not is-new? (dc/pull db (keys entity) (or (:db/id entity) (:db/ident entity)))) ident->value-type (-by :db/ident (comp :db/ident :db/valueType) (-pull-many db [#:db{:valueType [:db/ident]} :db/ident] (keys entity))) ident->cardinality (-by :db/ident (comp :db/ident :db/cardinality) (-pull-many db [#:db{:cardinality [:db/ident]} :db/ident] (keys entity))) ops (->> entity (reduce (fn [ops [a v]] (cond (= :db/id a) ops (= :db/ident a) ops (or (= v (a extant-entity)) (= v (:db/ident (a extant-entity) :nope)) (= v (:db/id (a extant-entity)) :nope)) ops (and (nil? v) (not (nil? (a extant-entity)))) (if (= :db.cardinality/many (ident->cardinality a)) (into ops (map (fn [v] [:db/retract e a (cond-> v (:db/id v) :db/id)]) (a extant-entity))) (conj ops [:db/retract e a (cond-> (a extant-entity) (:db/id (a extant-entity)) :db/id)])) (nil? v) ops (and (sequential? v) (= :db.type/tuple (ident->value-type a)) (not (= :db.cardinality/many (ident->cardinality a)))) (conj ops [:db/add e a v]) (and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v)) (into ops [[:reset-rels e a v]]) (= :db.cardinality/many (ident->cardinality a)) (into ops [[:reset-scalars e a v]]) (and (sequential? v) (not= :db.type/ref (ident->value-type a))) (into ops [[:reset-scalars e a v]]) (and (map? v) (= :db.type/ref (ident->value-type a))) (let [id (or (:db/id v) (-random-tempid))] (-> ops (conj [:db/add e a id]) (into [[:upsert-entity (assoc v :db/id id)]]))) :else (conj ops [:db/add e a v]))) []))] ops))))))"}} #:db{:ident :upsert-invoice, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (invoice->journal-entry [db invoice-id raw-invoice-id] (do (let [entity (dc/pull db (quote [:invoice/total :invoice/exclude-from-ledger :invoice/outstanding-balance :invoice/date #:invoice{:client [:db/id :client/code], :status [:db/ident], :import-status [:db/ident], :vendor [:db/id :vendor/name], :payment [:db/id #:payment{:status [:db/ident]}], :expense-accounts [:invoice-expense-account/account :invoice-expense-account/amount :invoice-expense-account/location]}]) invoice-id) credit-invoice? (< (:invoice/total entity 0.0) 0.0)] (when-not (or (not (:invoice/total entity)) (= true (:invoice/exclude-from-ledger entity)) (= :import-status/pending (:db/ident (:invoice/import-status entity))) (= :invoice-status/voided (:db/ident (:invoice/status entity))) (< -0.001 (:invoice/total entity) 0.001)) (-remove-nils #:journal-entry{:date (:invoice/date entity), :original-entity raw-invoice-id, :client (:db/id (:invoice/client entity)), :line-items (into [(cond-> {:journal-entry-line/account :account/accounts-payable, :db/id (str raw-invoice-id \"-\" 0), :journal-entry-line/location \"A\"} credit-invoice? (assoc :journal-entry-line/debit (Math/abs (:invoice/total entity))) (not credit-invoice?) (assoc :journal-entry-line/credit (Math/abs (:invoice/total entity))))] (map-indexed (fn [i ea] (cond-> {:journal-entry-line/account (:db/id (:invoice-expense-account/account ea)), :db/id (str raw-invoice-id \"-\" (inc i)), :journal-entry-line/location (or (:invoice-expense-account/location ea) \"HQ\")} credit-invoice? (assoc :journal-entry-line/credit (Math/abs (:invoice-expense-account/amount ea))) (not credit-invoice?) (assoc :journal-entry-line/debit (Math/abs (:invoice-expense-account/amount ea))))) (:invoice/expense-accounts entity))), :source \"invoice\", :cleared (and (< (:invoice/outstanding-balance entity) 0.01) (every? (fn* [p1__164283#] (= :payment-status/cleared (:payment/status p1__164283#))) (:invoice/payments entity))), :amount (Math/abs (:invoice/total entity)), :vendor (:db/id (:invoice/vendor entity))})))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [upserted-entity [[:upsert-entity invoice]] with-invoice (dc/with db upserted-entity) invoice-id (or (-> with-invoice :tempids (get (:db/id invoice))) (:db/id invoice)) journal-entry (invoice->journal-entry (:db-after with-invoice) invoice-id (:db/id invoice))] (into upserted-entity (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id invoice)]]]))))))))"}} #:db{:ident :upsert-ledger, :fn #db/fn{:lang :clojure, :imports [[java.util UUID]], :requires [[datomic.api :as dc]], :params [db ledger-entry], :code "(let [extant-read (quote [:db/id :journal-entry/date :journal-entry/client #:journal-entry{:line-items [:journal-entry-line/account :journal-entry-line/location :db/id :journal-entry-line/client+account+location+date]}])] (letfn [(-random-tempid [] (do (str (UUID/randomUUID)))) (get-line-items-after [db journal-entry] (do (for [jel (:journal-entry/line-items journal-entry) next-jel (->> (dc/index-pull db {:selector [:db/id :journal-entry-line/client+account+location+date], :index :avet, :start [:journal-entry-line/client+account+location+date (:journal-entry-line/client+account+location+date jel) (:db/id jel)]}) (take-while (fn line-must-match-client-account-location [result] (and (= (take 3 (:journal-entry-line/client+account+location+date result)) (take 3 (:journal-entry-line/client+account+location+date jel))) (not= (:db/id jel) (:db/id result))))) (take 2)) :when next-jel] (:db/id next-jel)))) (calc-client+account+location+date [je jel] (do [(or (:db/id (:journal-entry/client je)) (:journal-entry/client je)) (or (:db/id (:journal-entry-line/account jel)) (:journal-entry-line/account jel)) (-> jel :journal-entry-line/location) (-> je :journal-entry/date)]))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [ledger-entry (genfn-coerce-arg ledger-entry)] (do (assert (:journal-entry/date ledger-entry) (format \"Must at least provide date when updating ledger: %s\" (pr-str ledger-entry))) (assert (:journal-entry/client ledger-entry) \"Must at least provide client when updating ledger\") (let [extant-entry (or (when-let [original-entity (:journal-entry/original-entity ledger-entry)] (dc/pull db extant-read [:journal-entry/original-entity original-entity])) (when-let [external-id (:journal-entry/external-id ledger-entry)] (dc/pull db extant-read [:journal-entry/external-id external-id]))) extant-entry-exists? (:db/id extant-entry)] (cond-> [[:upsert-entity (into (-> ledger-entry (assoc :db/id (or (:db/id ledger-entry) (:db/id extant-entry) (-random-tempid))) (update :journal-entry/line-items (fn [lis] (mapv (fn* [p1__164287#] (-> p1__164287# (assoc :journal-entry-line/dirty true) (assoc :journal-entry-line/client+account+location+date (calc-client+account+location+date ledger-entry p1__164287#)))) lis)))))]] extant-entry-exists? (into (map (fn [li] {:db/id li, :journal-entry-line/dirty true}) (get-line-items-after db extant-entry))))))))))"}} #:db{:ident :upsert-transaction, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db transaction], :code "(let [] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (transaction->journal-entry [db transaction-id raw-transaction-id] (do (let [entity (dc/pull db [:transaction/client :transaction/date :transaction/description-original :db/id :transaction/vendor :transaction/amount :transaction/cleared-against #:transaction{:bank-account [:db/id #:bank-account{:type [:db/ident]}], :approval-status [:db/ident], :accounts [:transaction-account/account :transaction-account/location :transaction-account/amount]}] transaction-id) decreasing? (< (or (:transaction/amount entity) 0.0) 0.0) credit-from-bank? decreasing? debit-from-bank? (not decreasing?)] (when (and (not (= :transaction-approval-status/excluded (:db/ident (:transaction/approval-status entity)))) (not (= :transaction-approval-status/suppressed (:db/ident (:transaction/approval-status entity)))) (:transaction/amount entity) (not (< -0.001 (:transaction/amount entity) 0.001))) (-remove-nils #:journal-entry{:vendor (:db/id (:transaction/vendor entity)), :amount (Math/abs (:transaction/amount entity)), :date (:transaction/date entity), :alternate-description (:transaction/description-original entity), :original-entity raw-transaction-id, :client (:db/id (:transaction/client entity)), :cleared-against (:transaction/cleared-against entity), :source \"transaction\", :line-items (into [(-remove-nils {:journal-entry-line/credit (when credit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/account (:db/id (:transaction/bank-account entity)), :db/id (str raw-transaction-id \"-\" 0), :journal-entry-line/debit (when debit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/location \"A\"})] (map-indexed (fn [i a] (-remove-nils {:journal-entry-line/credit (when debit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/account (:db/id (:transaction-account/account a)), :db/id (str raw-transaction-id \"-\" (inc i)), :journal-entry-line/debit (when credit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/location (:transaction-account/location a)})) (if (seq (:transaction/accounts entity)) (:transaction/accounts entity) [#:transaction-account{:amount (:transaction/amount entity)}]))), :cleared true})))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [transaction (genfn-coerce-arg transaction)] (do (let [upserted-entity [[:upsert-entity (dissoc transaction :transaction/payment :import-batch/_entry)]] with-transaction (dc/with db upserted-entity) transaction-id (or (-> with-transaction :tempids (get (:db/id transaction))) (:db/id transaction)) journal-entry (transaction->journal-entry (:db-after with-transaction) transaction-id (:db/id transaction))] (into [[:upsert-entity transaction]] (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id transaction)]]]))))))))"}}] \ No newline at end of file +[#:db{:ident :pay, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) amount (genfn-coerce-arg amount)] (do (let [current-outstanding-balance (-> (dc/pull db [:invoice/outstanding-balance] e) :invoice/outstanding-balance) new-outstanding-balance (- current-outstanding-balance amount)] [[:upsert-invoice {:invoice/status (if (> new-outstanding-balance 0) :invoice-status/unpaid :invoice-status/paid), :db/id e, :invoice/outstanding-balance new-outstanding-balance}]]))))))"}} #:db{:ident :plus, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a amount], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) amount (genfn-coerce-arg amount)] (do [[:db/add e a (-> (dc/pull db [a] e) a (+ amount))]])))))"}} #:db{:ident :propose-invoice, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [existing? (boolean (seq (dc/q (quote [:find ?i :in $ ?invoice-number ?client ?vendor :where [?i :invoice/invoice-number ?invoice-number] [?i :invoice/client ?client] [?i :invoice/vendor ?vendor] (not [?i :invoice/status :invoice-status/voided])]) db (:invoice/invoice-number invoice) (:invoice/client invoice) (:invoice/vendor invoice))))] (if existing? [] [[:upsert-invoice invoice]])))))))"}} #:db{:ident :reset-rels, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (assert (every? :db/id vs) (format \"In order to reset attribute %s, every value must have :db/id\" a)) (let [ids (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) new-id-set (set (map :db/id vs)) retract-ids (filter (complement new-id-set) ids) {is-component? :db/isComponent} (dc/pull db [:db/isComponent] a) new-rels (filter (complement (set ids)) (map :db/id vs))] (-> [] (into (map (fn [i] (if is-component? [:db/retractEntity i] [:db/retract e a i])) retract-ids)) (into (map (fn [i] [:db/add e a i]) new-rels)) (into (map (fn [i] [:upsert-entity i]) vs)))))))))"}} #:db{:ident :reset-scalars, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db e a vs], :code "(let [] (letfn [] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [e (genfn-coerce-arg e) a (genfn-coerce-arg a) vs (genfn-coerce-arg vs)] (do (let [extant (when-not (string? e) (->> (dc/q (quote [:find ?z :in $ ?e ?a :where [?e ?a ?z]]) db e a) (map first))) retracts (filter (complement (set vs)) extant) new (filter (complement (set extant)) vs)] (-> [] (into (map (fn [i] [:db/retract e a i]) retracts)) (into (map (fn [i] [:db/add e a i]) new)))))))))"}} #:db{:ident :upsert-entity, :fn #db/fn{:lang :clojure, :imports [[java.util UUID]], :requires [[datomic.api :as dc]], :params [db entity], :code "(let [] (letfn [(-random-tempid [] (do (str (UUID/randomUUID)))) (-by [f fv xs] (do (reduce (fn* [p1__185866# p2__185867#] (assoc p1__185866# (f p2__185867#) (fv p2__185867#))) {} xs))) (-pull-many [db read ids] (do (->> (dc/q (quote [:find (pull ?e r) :in $ [?e ...] r]) db ids read) (map first))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [entity (genfn-coerce-arg entity)] (do (when-not (or (:db/id entity) (:db/ident entity)) (datomic.api/cancel #:cognitect.anomalies{:message (str \"Cannot upsert without :db/id or :db/ident, \" entity), :category :cognitect.anomalies/incorrect})) (let [e (or (:db/id entity) (:db/ident entity)) is-new? (string? e) extant-entity (when-not is-new? (dc/pull db (keys entity) (or (:db/id entity) (:db/ident entity)))) ident->value-type (-by :db/ident (comp :db/ident :db/valueType) (-pull-many db [#:db{:valueType [:db/ident]} :db/ident] (keys entity))) ident->cardinality (-by :db/ident (comp :db/ident :db/cardinality) (-pull-many db [#:db{:cardinality [:db/ident]} :db/ident] (keys entity))) ops (->> entity (reduce (fn [ops [a v]] (cond (= :db/id a) ops (= :db/ident a) ops (or (= v (a extant-entity)) (= v (:db/ident (a extant-entity) :nope)) (= v (:db/id (a extant-entity)) :nope)) ops (and (nil? v) (not (nil? (a extant-entity)))) (if (= :db.cardinality/many (ident->cardinality a)) (into ops (map (fn [v] [:db/retract e a (cond-> v (:db/id v) :db/id)]) (a extant-entity))) (conj ops [:db/retract e a (cond-> (a extant-entity) (:db/id (a extant-entity)) :db/id)])) (nil? v) ops (and (sequential? v) (= :db.type/tuple (ident->value-type a)) (not (= :db.cardinality/many (ident->cardinality a)))) (conj ops [:db/add e a v]) (and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v)) (into ops [[:reset-rels e a v]]) (= :db.cardinality/many (ident->cardinality a)) (into ops [[:reset-scalars e a v]]) (and (sequential? v) (not= :db.type/ref (ident->value-type a))) (into ops [[:reset-scalars e a v]]) (and (map? v) (= :db.type/ref (ident->value-type a))) (let [id (or (:db/id v) (-random-tempid))] (-> ops (conj [:db/add e a id]) (into [[:upsert-entity (assoc v :db/id id)]]))) :else (conj ops [:db/add e a v]))) []))] ops))))))"}} #:db{:ident :upsert-invoice, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db invoice], :code "(let [] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (invoice->journal-entry [db invoice-id raw-invoice-id] (do (let [entity (dc/pull db (quote [:invoice/total :invoice/exclude-from-ledger :invoice/outstanding-balance :invoice/date #:invoice{:client [:db/id :client/code], :status [:db/ident], :import-status [:db/ident], :vendor [:db/id :vendor/name], :payment [:db/id #:payment{:status [:db/ident]}], :expense-accounts [:invoice-expense-account/account :invoice-expense-account/amount :invoice-expense-account/location]}]) invoice-id) credit-invoice? (< (:invoice/total entity 0.0) 0.0)] (when-not (or (not (:invoice/total entity)) (= true (:invoice/exclude-from-ledger entity)) (= :import-status/pending (:db/ident (:invoice/import-status entity))) (= :invoice-status/voided (:db/ident (:invoice/status entity))) (< -0.001 (:invoice/total entity) 0.001)) (-remove-nils #:journal-entry{:date (:invoice/date entity), :original-entity raw-invoice-id, :client (:db/id (:invoice/client entity)), :line-items (into [(cond-> {:journal-entry-line/account :account/accounts-payable, :db/id (str raw-invoice-id \"-\" 0), :journal-entry-line/location \"A\"} credit-invoice? (assoc :journal-entry-line/debit (Math/abs (:invoice/total entity))) (not credit-invoice?) (assoc :journal-entry-line/credit (Math/abs (:invoice/total entity))))] (map-indexed (fn [i ea] (cond-> {:journal-entry-line/account (:db/id (:invoice-expense-account/account ea)), :db/id (str raw-invoice-id \"-\" (inc i)), :journal-entry-line/location (or (:invoice-expense-account/location ea) \"HQ\")} credit-invoice? (assoc :journal-entry-line/credit (Math/abs (:invoice-expense-account/amount ea))) (not credit-invoice?) (assoc :journal-entry-line/debit (Math/abs (:invoice-expense-account/amount ea))))) (:invoice/expense-accounts entity))), :source \"invoice\", :cleared (and (< (:invoice/outstanding-balance entity) 0.01) (every? (fn* [p1__185869#] (= :payment-status/cleared (:payment/status p1__185869#))) (:invoice/payments entity))), :amount (Math/abs (:invoice/total entity)), :vendor (:db/id (:invoice/vendor entity))})))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [invoice (genfn-coerce-arg invoice)] (do (let [upserted-entity [[:upsert-entity invoice]] with-invoice (dc/with db upserted-entity) invoice-id (or (-> with-invoice :tempids (get (:db/id invoice))) (:db/id invoice)) journal-entry (invoice->journal-entry (:db-after with-invoice) invoice-id (:db/id invoice))] (into upserted-entity (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id invoice)]]]))))))))"}} #:db{:ident :upsert-ledger, :fn #db/fn{:lang :clojure, :imports [[java.util UUID]], :requires [[datomic.api :as dc]], :params [db ledger-entry], :code "(let [extant-read (quote [:db/id :journal-entry/date :journal-entry/client #:journal-entry{:line-items [:journal-entry-line/account :journal-entry-line/location :db/id :journal-entry-line/client+account+location+date]}])] (letfn [(-random-tempid [] (do (str (UUID/randomUUID)))) (get-line-items-after [db journal-entry] (do (for [jel (:journal-entry/line-items journal-entry) next-jel (->> (dc/index-pull db {:selector [:db/id :journal-entry-line/client+account+location+date], :index :avet, :start [:journal-entry-line/client+account+location+date (:journal-entry-line/client+account+location+date jel) (:db/id jel)]}) (take-while (fn line-must-match-client-account-location [result] (and (= (take 3 (:journal-entry-line/client+account+location+date result)) (take 3 (:journal-entry-line/client+account+location+date jel))) (not= (:db/id jel) (:db/id result))))) (take 2)) :when next-jel] (:db/id next-jel)))) (calc-client+account+location+date [je jel] (do [(or (:db/id (:journal-entry/client je)) (:journal-entry/client je)) (or (:db/id (:journal-entry-line/account jel)) (:journal-entry-line/account jel)) (-> jel :journal-entry-line/location) (-> je :journal-entry/date)]))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [ledger-entry (genfn-coerce-arg ledger-entry)] (do (assert (:journal-entry/date ledger-entry) (format \"Must at least provide date when updating ledger: %s\" (pr-str ledger-entry))) (assert (:journal-entry/client ledger-entry) \"Must at least provide client when updating ledger\") (let [extant-entry (or (when-let [original-entity (:journal-entry/original-entity ledger-entry)] (dc/pull db extant-read [:journal-entry/original-entity original-entity])) (when-let [external-id (:journal-entry/external-id ledger-entry)] (dc/pull db extant-read [:journal-entry/external-id external-id]))) extant-entry-exists? (:db/id extant-entry)] (cond-> [[:upsert-entity (into (-> ledger-entry (assoc :db/id (or (:db/id ledger-entry) (:db/id extant-entry) (-random-tempid))) (update :journal-entry/line-items (fn [lis] (mapv (fn* [p1__185873#] (-> p1__185873# (assoc :journal-entry-line/dirty true) (assoc :journal-entry-line/client+account+location+date (calc-client+account+location+date ledger-entry p1__185873#)))) lis)))))]] extant-entry-exists? (into (map (fn [li] {:db/id li, :journal-entry-line/dirty true}) (get-line-items-after db extant-entry))))))))))"}} #:db{:ident :upsert-transaction, :fn #db/fn{:lang :clojure, :imports [], :requires [[datomic.api :as dc]], :params [db transaction], :code "(let [] (letfn [(-remove-nils [m] (do (let [result (reduce-kv (fn [m k v] (if (not (nil? v)) (assoc m k v) m)) {} m)] (if (seq result) result nil)))) (transaction->journal-entry [db transaction-id raw-transaction-id] (do (let [entity (dc/pull db [:transaction/client :transaction/date :transaction/description-original :db/id :transaction/vendor :transaction/amount :transaction/cleared-against #:transaction{:bank-account [:db/id #:bank-account{:type [:db/ident]}], :approval-status [:db/ident], :accounts [:transaction-account/account :transaction-account/location :transaction-account/amount]}] transaction-id) decreasing? (< (or (:transaction/amount entity) 0.0) 0.0) credit-from-bank? decreasing? debit-from-bank? (not decreasing?)] (when (and (not (= :transaction-approval-status/excluded (:db/ident (:transaction/approval-status entity)))) (not (= :transaction-approval-status/suppressed (:db/ident (:transaction/approval-status entity)))) (:transaction/amount entity) (not (< -0.001 (:transaction/amount entity) 0.001))) (-remove-nils #:journal-entry{:vendor (:db/id (:transaction/vendor entity)), :amount (Math/abs (:transaction/amount entity)), :date (:transaction/date entity), :alternate-description (:transaction/description-original entity), :original-entity raw-transaction-id, :client (:db/id (:transaction/client entity)), :cleared-against (:transaction/cleared-against entity), :source \"transaction\", :line-items (into [(-remove-nils {:journal-entry-line/credit (when credit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/account (:db/id (:transaction/bank-account entity)), :db/id (str raw-transaction-id \"-\" 0), :journal-entry-line/debit (when debit-from-bank? (Math/abs (:transaction/amount entity))), :journal-entry-line/location \"A\"})] (map-indexed (fn [i a] (-remove-nils {:journal-entry-line/credit (when debit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/account (:db/id (:transaction-account/account a)), :db/id (str raw-transaction-id \"-\" (inc i)), :journal-entry-line/debit (when credit-from-bank? (Math/abs (:transaction-account/amount a))), :journal-entry-line/location (:transaction-account/location a)})) (if (seq (:transaction/accounts entity)) (:transaction/accounts entity) [#:transaction-account{:amount (:transaction/amount entity)}]))), :cleared true})))))] (let [genfn-coerce-arg (clojure.core/fn [x] (clojure.walk/prewalk (clojure.core/fn [e] (if (clojure.core/some? e) (do (clojure.core/when (clojure.core/instance? clojure.lang.PersistentTreeMap e) (throw (clojure.core/ex-info \"Using sorted-map will cause different types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/var? e) (throw (clojure.core/ex-info \"Using var does not work for remote transactor\" {:val e}))) (clojure.core/when (clojure.core/or (clojure.core/= clojure.lang.PersistentList$EmptyList (.getClass e)) (clojure.core/instance? clojure.lang.PersistentList e)) (throw (clojure.core/ex-info \"Using list will cause indistinguishable types in transactor for in-mem and remote\" {:val e}))) (clojure.core/when (clojure.core/instance? clojure.lang.PersistentQueue e) (throw (clojure.core/ex-info \"Using clojure.lang.PersistentQueue does not work for remote transactor\" {:val e}))) (clojure.core/cond (clojure.core/instance? java.util.HashSet e) (clojure.core/into #{} e) (clojure.core/and (clojure.core/instance? java.util.List e) (clojure.core/not (clojure.core/vector? e))) (clojure.core/vec e) :else e)) e)) x))] (let [transaction (genfn-coerce-arg transaction)] (do (let [upserted-entity [[:upsert-entity (dissoc transaction :transaction/payment :import-batch/_entry)]] with-transaction (dc/with db upserted-entity) transaction-id (or (-> with-transaction :tempids (get (:db/id transaction))) (:db/id transaction)) journal-entry (transaction->journal-entry (:db-after with-transaction) transaction-id (:db/id transaction))] (into [[:upsert-entity transaction]] (if journal-entry [[:upsert-ledger journal-entry]] [[:db/retractEntity [:journal-entry/original-entity (:db/id transaction)]]]))))))))"}}] \ No newline at end of file diff --git a/resources/powerqueries/expected_deposits.txt b/resources/powerqueries/expected_deposits.txt new file mode 100644 index 00000000..94ce768e --- /dev/null +++ b/resources/powerqueries/expected_deposits.txt @@ -0,0 +1,8 @@ +let + Source = Json.Document(Web.Contents("https://app.integreatconsult.com/api/queries/%s/results/json", [Headers=[#"Accept-Encoding"="gzip"]])), + rawtable = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error), + mytable = Table.FromRecords(Table.TransformRows(rawtable, (x) as record => Record.FromList(x[Column1], {"Date", "Total", "Fee"}))), + #"Changed Type" = Table.TransformColumnTypes(mytable,{{"Total", Currency.Type}, {"Fee", Currency.Type}, {"Date", type date}}), + #"Sorted Rows" = Table.Sort(#"Changed Type",{{"Date", Order.Ascending}}) +in + #"Sorted Rows" diff --git a/resources/powerqueries/refunds.txt b/resources/powerqueries/refunds.txt new file mode 100644 index 00000000..d3508644 --- /dev/null +++ b/resources/powerqueries/refunds.txt @@ -0,0 +1,8 @@ +let + Source = Json.Document(Web.Contents("https://app.integreatconsult.com/api/queries/%s/results/json", [Headers=[#"Accept-Encoding"="gzip"]])), + rawtable = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error), + mytable = Table.FromRecords(Table.TransformRows(rawtable, (x) as record => Record.FromList(x[Column1], {"Date", "Type", "Total", "Fee"}))), + #"Changed Type" = Table.TransformColumnTypes(mytable,{{"Total", Currency.Type}, {"Fee", Currency.Type}, {"Date", type date}}), + #"Sorted Rows" = Table.Sort(#"Changed Type",{{"Date", Order.Ascending}}) +in + #"Sorted Rows" diff --git a/resources/powerqueries/sales_category.txt b/resources/powerqueries/sales_category.txt new file mode 100644 index 00000000..1abe7f6c --- /dev/null +++ b/resources/powerqueries/sales_category.txt @@ -0,0 +1,8 @@ +let + Source = Json.Document(Web.Contents("https://app.integreatconsult.com/api/queries/%s/results/json", [Headers=[#"Accept-Encoding"="gzip"]])), + rawtable = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error), +mytable = Table.FromRecords(Table.TransformRows(rawtable, (x) as record => Record.FromList(x[Column1], {"Date", "Category", "Name", "Total", "Tax", "Discount"}))), + #"Changed Type" = Table.TransformColumnTypes(mytable,{{"Total", Currency.Type}, {"Tax", Currency.Type}, {"Discount", Currency.Type}, {"Date", type date}}), + #"Sorted Rows" = Table.Sort(#"Changed Type",{{"Date", Order.Ascending}, {"Category", Order.Ascending}}) +in + #"Sorted Rows" diff --git a/resources/powerqueries/sales_summary.txt b/resources/powerqueries/sales_summary.txt new file mode 100644 index 00000000..57ca1347 --- /dev/null +++ b/resources/powerqueries/sales_summary.txt @@ -0,0 +1,8 @@ +let + Source = Json.Document(Web.Contents("https://app.integreatconsult.com/api/queries/%s/results/json", [Headers=[#"Accept-Encoding"="gzip"]])), + rawtable = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error), + mytable = Table.FromRecords(Table.TransformRows(rawtable, (x) as record => Record.FromList(x[Column1], {"Date", "Total", "Tax", "Tip", "Service Charge", "Discount", "Returns"}))), + #"Changed Type" = Table.TransformColumnTypes(mytable,{{"Total", Currency.Type}, {"Tax", Currency.Type}, {"Discount", Currency.Type}, {"Service Charge", Currency.Type}, {"Returns", Currency.Type}, {"Tip", Currency.Type}, {"Date", type date}}), + #"Sorted Rows" = Table.Sort(#"Changed Type",{{"Date", Order.Ascending}}) +in + #"Sorted Rows" diff --git a/resources/powerqueries/tenders.txt b/resources/powerqueries/tenders.txt new file mode 100644 index 00000000..490b0745 --- /dev/null +++ b/resources/powerqueries/tenders.txt @@ -0,0 +1,8 @@ +let + Source = Json.Document(Web.Contents("https://app.integreatconsult.com/api/queries/%s/results/json", [Headers=[#"Accept-Encoding"="gzip"]])), + rawtable = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error), + mytable = Table.FromRecords(Table.TransformRows(rawtable, (x) as record => Record.FromList(x[Column1], {"Date", "Type", "Processor", "Total", "Tip"}))), + #"Changed Type" = Table.TransformColumnTypes(mytable,{{"Total", Currency.Type}, {"Tip", Currency.Type}, {"Date", type date}}), + #"Sorted Rows" = Table.Sort(#"Changed Type",{{"Date", Order.Ascending}}) +in + #"Sorted Rows" diff --git a/resources/public/js/htmx-disable.js b/resources/public/js/htmx-disable.js index 4a63b0d0..fa4f86f9 100644 --- a/resources/public/js/htmx-disable.js +++ b/resources/public/js/htmx-disable.js @@ -120,12 +120,54 @@ htmx.defineExtension('trigger-filter', { initDatepicker = function(elem) { -elem.dp = new Datepicker(elem, {format: "mm/dd/yyyy", autohide: true}); + const modalParent = elem.closest('#modal-content'); + if (modalParent) { + elem.dp = new Datepicker(elem, {format: "mm/dd/yyyy", autohide: true, container: ".modal-stack"}); + } else { + elem.dp = new Datepicker(elem, {format: "mm/dd/yyyy", autohide: true}); + } + } countRows = function(id) { var table = document.querySelector(id); var rows = table.querySelectorAll("tbody tr"); - console.log("ROWS", rows.length); return rows.length; } + +htmx.onLoad(function(content) { + var sortables = content.querySelectorAll(".sortable"); + for (var i = 0; i < sortables.length; i++) { + var sortable = sortables[i]; + var sortableInstance = new Sortable(sortable, { + animation: 150, + ghostClass: 'bg-blue-100', + + // Make the `.htmx-indicator` unsortable + filter: ".htmx-indicator", + onMove: function (evt) { + return evt.related.className.indexOf('htmx-indicator') === -1; + }, + + // Disable sorting on the `end` event + onEnd: function (evt) { + this.option("disabled", true); + } + }); + + // Re-enable sorting on the `htmx:afterSwap` event + sortable.addEventListener("htmx:afterSwap", function() { + sortableInstance.option("disabled", false); + }); + } +}) + + async function copyToClipboard(text) { + try { + // Write the text to the clipboard + await navigator.clipboard.writeText(text); + console.log('Text copied to clipboard:', text); + } catch (err) { + console.error('Failed to copy text to clipboard:', err); + } + } diff --git a/resources/public/output.css b/resources/public/output.css index 66f0e4c2..08e300fe 100644 --- a/resources/public/output.css +++ b/resources/public/output.css @@ -1 +1 @@ -/*! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Calibri,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#007dbb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#007dbb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}select:not([size]){background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#007dbb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#007dbb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark [type=checkbox]:checked,.dark [type=radio]:checked,[type=checkbox]:checked,[type=radio]:checked{border-color:#0000;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:indeterminate,[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:#0000;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px auto inherit}input[type=file]::file-selector-button{color:#fff;background:#1f2937;border:0;font-weight:500;font-size:.875rem;cursor:pointer;padding:.625rem 1rem .625rem 2rem;-webkit-margin-start:-1rem;margin-inline-start:-1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}input[type=file]::file-selector-button:hover{background:#374151}.dark input[type=file]::file-selector-button{color:#fff;background:#4b5563}.dark input[type=file]::file-selector-button:hover{background:#6b7280}input[type=range]::-webkit-slider-thumb{height:1.25rem;width:1.25rem;background:#007dbb;border-radius:9999px;border:0;appearance:none;-moz-appearance:none;-webkit-appearance:none;cursor:pointer}input[type=range]:disabled::-webkit-slider-thumb{background:#9ca3af}.dark input[type=range]:disabled::-webkit-slider-thumb{background:#6b7280}input[type=range]:focus::-webkit-slider-thumb{outline:2px solid #0000;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1px;--tw-ring-color:rgb(164 202 254/var(--tw-ring-opacity))}input[type=range]::-moz-range-thumb{height:1.25rem;width:1.25rem;background:#007dbb;border-radius:9999px;border:0;appearance:none;-moz-appearance:none;-webkit-appearance:none;cursor:pointer}input[type=range]:disabled::-moz-range-thumb{background:#9ca3af}.dark input[type=range]:disabled::-moz-range-thumb{background:#6b7280}input[type=range]::-moz-range-progress{background:#009cea}input[type=range]::-ms-fill-lower{background:#009cea}.toggle-bg:after{content:"";position:absolute;top:.125rem;left:.125rem;background:#fff;border-color:#d1d5db;border-width:1px;border-radius:9999px;height:1.25rem;width:1.25rem;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-duration:.15s;box-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}input:checked+.toggle-bg:after{transform:translateX(100%);;border-color:#fff}input:checked+.toggle-bg{background:#007dbb;border-color:#007dbb}.tooltip-arrow,.tooltip-arrow:before{position:absolute;width:8px;height:8px;background:inherit}.tooltip-arrow{visibility:hidden}.tooltip-arrow:before{content:"";visibility:visible;transform:rotate(45deg)}[data-tooltip-style^=light]+.tooltip>.tooltip-arrow:before{border-style:solid;border-color:#e5e7eb}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=top]>.tooltip-arrow:before{border-bottom-width:1px;border-right-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=right]>.tooltip-arrow:before{border-bottom-width:1px;border-left-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=bottom]>.tooltip-arrow:before{border-top-width:1px;border-left-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=left]>.tooltip-arrow:before{border-top-width:1px;border-right-width:1px}.tooltip[data-popper-placement^=top]>.tooltip-arrow{bottom:-4px}.tooltip[data-popper-placement^=bottom]>.tooltip-arrow{top:-4px}.tooltip[data-popper-placement^=left]>.tooltip-arrow{right:-4px}.tooltip[data-popper-placement^=right]>.tooltip-arrow{left:-4px}.tooltip.invisible>.tooltip-arrow:before{visibility:hidden}[data-popper-arrow],[data-popper-arrow]:before{position:absolute;width:8px;height:8px;background:inherit}[data-popper-arrow]{visibility:hidden}[data-popper-arrow]:after,[data-popper-arrow]:before{content:"";visibility:visible;transform:rotate(45deg)}[data-popper-arrow]:after{position:absolute;width:9px;height:9px;background:inherit}[role=tooltip]>[data-popper-arrow]:before{border-style:solid;border-color:#e5e7eb}.dark [role=tooltip]>[data-popper-arrow]:before{border-style:solid;border-color:#4b5563}[role=tooltip]>[data-popper-arrow]:after{border-style:solid;border-color:#e5e7eb}.dark [role=tooltip]>[data-popper-arrow]:after{border-style:solid;border-color:#4b5563}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:before{border-bottom-width:1px;border-right-width:1px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:before{border-bottom-width:1px;border-left-width:1px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:before{border-top-width:1px;border-left-width:1px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:before{border-top-width:1px;border-right-width:1px}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]{bottom:-5px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]{top:-5px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]{right:-5px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]{left:-5px}[role=tooltip].invisible>[data-popper-arrow]:after,[role=tooltip].invisible>[data-popper-arrow]:before{visibility:hidden}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#009cea80;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.-right-2{right:-.5rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-\[60px\]{bottom:60px}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.right-2{right:.5rem}.top-0{top:0}.top-2{top:.5rem}.top-2\/4{top:50%}.top-5{top:1.25rem}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[99\]{z-index:99}.col-span-1{grid-column:span 1/span 1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-6{grid-column:span 6/span 6}.col-start-1{grid-column-start:1}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-4{margin:1rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0{margin-top:0;margin-bottom:0}.my-4{margin-top:1rem;margin-bottom:1rem}.-mb-1{margin-bottom:-.25rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-10{margin-right:2.5rem}.mr-16{margin-right:4rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[350px\]{height:350px}.h-full{height:100%}.h-screen{height:100vh}.max-h-96{max-height:24rem}.max-h-full{max-height:100%}.w-1\/2{width:50%}.w-1\/4{width:25%}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\/4{width:75%}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-96{width:24rem}.w-\[600px\]{width:600px}.w-auto{width:auto}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.w-screen{width:100vw}.w-20{width:5rem}.min-w-\[700px\]{min-width:700px}.max-w-2xl{max-width:42rem}.max-w-6xl{max-width:72rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-screen-2xl{max-width:1536px}.max-w-screen-lg{max-width:1024px}.flex-1{flex:1 1 0%}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.basis-1\/4{flex-basis:25%}.\!translate-y-0{--tw-translate-y:0px!important}.\!translate-y-0,.\!translate-y-32{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.\!translate-y-32{--tw-translate-y:8rem!important}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.-translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-full{--tw-translate-y:-100%}.translate-x-0{--tw-translate-x:0px}.translate-x-0,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x:100%}.translate-y-full{--tw-translate-y:100%}.rotate-180,.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.scale-0{--tw-scale-x:0;--tw-scale-y:0}.scale-0,.scale-100{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform-none{transform:none}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes shake{0%{transform:translateX(0)}12.5%{transform:translateX(-5px)}25%{transform:translateX(0)}37.5%{transform:translateX(5px)}50%{transform:translateX(0)}62.5%{transform:translateX(-5px)}75%{transform:translateX(5px)}87.5%{transform:translateX(5px)}to{transform:translateX(0)}}.animate-shake{animation:shake .5s ease-out 1}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-stretch{justify-content:stretch}.justify-items-stretch{justify-items:stretch}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-2{row-gap:.5rem}.-space-x-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(-1px*var(--tw-space-x-reverse));margin-left:calc(-1px*(1 - var(--tw-space-x-reverse)))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity))}.place-self-end{place-self:end}.self-center{align-self:center}.self-stretch{align-self:stretch}.justify-self-end{justify-self:end}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-b-lg{border-bottom-right-radius:.5rem}.rounded-b-lg,.rounded-l-lg{border-bottom-left-radius:.5rem}.rounded-l-lg{border-top-left-radius:.5rem}.rounded-r-lg{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-0{border-width:0}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-blue-300{--tw-border-opacity:1;border-color:rgb(102 196 242/var(--tw-border-opacity))}.border-blue-600{--tw-border-opacity:1;border-color:rgb(0 125 187/var(--tw-border-opacity))}.border-blue-700{--tw-border-opacity:1;border-color:rgb(0 94 140/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}.border-primary-300{--tw-border-opacity:1;border-color:rgb(175 211 130/var(--tw-border-opacity))}.border-primary-600{--tw-border-opacity:1;border-color:rgb(97 145 37/var(--tw-border-opacity))}.border-red-300{--tw-border-opacity:1;border-color:rgb(255 104 104/var(--tw-border-opacity))}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(204 235 251/var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity:1;background-color:rgb(153 215 247/var(--tw-bg-opacity))}.bg-blue-300{--tw-bg-opacity:1;background-color:rgb(102 196 242/var(--tw-bg-opacity))}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(51 176 238/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(230 245 253/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(0 156 234/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.bg-blue-700{--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}.bg-blue-800{--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.bg-green-300{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(148 196 88/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(121 181 46/var(--tw-bg-opacity))}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}.bg-green-700{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.bg-green-800{--tw-bg-opacity:1;background-color:rgb(48 72 18/var(--tw-bg-opacity))}.bg-primary-200{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity:1;background-color:rgb(242 248 234/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(255 205 205/var(--tw-bg-opacity))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(255 154 154/var(--tw-bg-opacity))}.bg-red-300{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(255 230 230/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(255 3 3/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-white\/50{background-color:#ffffff80}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(253 246 178/var(--tw-bg-opacity))}.\!bg-opacity-0{--tw-bg-opacity:0!important}.\!bg-opacity-100{--tw-bg-opacity:1!important}.\!bg-opacity-50{--tw-bg-opacity:0.5!important}.bg-opacity-50{--tw-bg-opacity:0.5}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-10{padding-left:2.5rem}.pl-11{padding-left:2.75rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pr-2{padding-right:.5rem}.pr-2\.5{padding-right:.625rem}.pt-16{padding-top:4rem}.pt-2{padding-top:.5rem}.pt-5{padding-top:1.25rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-baseline{vertical-align:initial}.align-top{vertical-align:top}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[0\.6rem\]{font-size:.6rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-6{line-height:1.5rem}.leading-9{line-height:2.25rem}.leading-none{line-height:1}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-blue-400{--tw-text-opacity:1;color:rgb(51 176 238/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgb(0 62 94/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgb(48 72 18/var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity:1;color:rgb(97 145 37/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(204 2 2/var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgb(102 1 1/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-yellow-800{--tw-text-opacity:1;color:rgb(114 59 19/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.\!opacity-0{opacity:0!important}.\!opacity-100{opacity:1!important}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px #00000040;--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.outline-0{outline-width:0}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.duration-75{transition-duration:75ms}.ease-\[cubic-bezier\(\.3\2c 2\.3\2c \.6\2c 1\)\]{transition-timing-function:cubic-bezier(.3,2.3,.6,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.htmx-added .fade-in,.htmx-added.fade-in{opacity:0!important}.fade-in{opacity:1}.htmx-settling .fade-in-settle,.htmx-settling.fade-in-settle{opacity:0!important}.fade-in-settle{opacity:1}.htmx-added .swipe-left-swap,.htmx-added.swipe-left-swap{opacity:1!important;--tw-scale-x:1!important;--tw-scale-y:1!important;--tw-translate-x:-50%!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.swipe-left-swap{opacity:1;--tw-scale-x:1;--tw-scale-y:1;--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-settling.htmx-added .swipe-left-swap,.htmx-settling.htmx-added.swipe-left-swap{opacity:0!important;--tw-scale-x:.75!important;--tw-scale-y:.75!important;--tw-translate-x:50%!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.htmx-settling .slide-up-settle,.htmx-settling.slide-up-settle{--tw-translate-y:1.25rem!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.slide-up-settle{--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hidden .slide-up,.htmx-added .slide-up{--tw-translate-y:1.25rem!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.slide-up{--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.live-added{animation:pulse-green .3s 2;animation-direction:alternate;animation-timing-function:ease-in-out}.dark .live-added{animation:pulse-dark-green .3s 2!important;animation-direction:alternate;animation-timing-function:ease-in-out}.live-removed{animation:pulse-red .3s 2;animation-direction:alternate;animation-timing-function:ease-in-out}.dark .live-removed{animation:pulse-dark-red .3s 2!important;animation-direction:alternate;animation-timing-function:ease-in-out}@keyframes pulse-green{0%{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}:is(.dark to){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}@keyframes pulse-dark-green{:is(.dark 0%){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}}@keyframes pulse-red{0%{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}:is(.dark to){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}@keyframes pulse-dark-red{:is(.dark 0%){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}.htmx-request .htmx-indicator,.htmx-request.htmx-indicator{display:inherit!important}.htmx-indicator{display:none}.htmx-request .htmx-indicator-hidden{display:none!important}.htmx-indicator-hidden{display:inherit}.htmx-swapping .fade-out{opacity:0!important}.fade-out{opacity:1}.min-h-content{min-height:calc(100vh - 4em)}.choices{margin-bottom:0!important;border-width:0!important}.choices__inner{display:block!important;width:100%!important;border-radius:.5rem!important;border-width:1px!important;--tw-border-opacity:1!important;border-color:rgb(209 213 219/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(249 250 251/var(--tw-bg-opacity))!important;padding:.25rem!important;font-size:.875rem!important;line-height:1.25rem!important;--tw-text-opacity:1!important;color:rgb(17 24 39/var(--tw-text-opacity))!important}.choices__inner:focus{--tw-border-opacity:1!important;border-color:rgb(0 156 234/var(--tw-border-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))!important}.group.has-error .choices__inner{--tw-border-opacity:1!important;border-color:rgb(255 3 3/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(255 230 230/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(51 1 1/var(--tw-text-opacity))!important}.group.has-error .choices__inner::-moz-placeholder{--tw-placeholder-opacity:1!important;color:rgb(153 2 2/var(--tw-placeholder-opacity))!important}.group.has-error .choices__inner::placeholder{--tw-placeholder-opacity:1!important;color:rgb(153 2 2/var(--tw-placeholder-opacity))!important}.group.has-error .choices__inner:focus{--tw-border-opacity:1!important;border-color:rgb(255 3 3/var(--tw-border-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(255 3 3/var(--tw-ring-opacity))!important}:is(.dark .choices__inner){--tw-border-opacity:1!important;border-color:rgb(75 85 99/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(255 255 255/var(--tw-text-opacity))!important}:is(.dark .choices__inner)::-moz-placeholder{--tw-placeholder-opacity:1!important;color:rgb(156 163 175/var(--tw-placeholder-opacity))!important}:is(.dark .choices__inner)::placeholder{--tw-placeholder-opacity:1!important;color:rgb(156 163 175/var(--tw-placeholder-opacity))!important}:is(.dark .choices__inner:focus){--tw-border-opacity:1!important;border-color:rgb(0 156 234/var(--tw-border-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))!important}.group.has-error :is(.dark .choices__inner){--tw-border-opacity:1!important;border-color:rgb(255 3 3/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(255 3 3/var(--tw-text-opacity))!important}.group.has-error :is(.dark .choices__inner)::-moz-placeholder{--tw-placeholder-opacity:1!important;color:rgb(255 3 3/var(--tw-placeholder-opacity))!important}.group.has-error :is(.dark .choices__inner)::placeholder{--tw-placeholder-opacity:1!important;color:rgb(255 3 3/var(--tw-placeholder-opacity))!important}.choices:focus-within .choices__inner,:is(.dark .choices:focus-within .choices__inner){--tw-border-opacity:1!important;border-color:rgb(0 156 234/var(--tw-border-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))!important}.choices:focus-within .choices__inner{outline:2px solid #0000!important;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#007dbb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#007dbb}.choices__inner .choices__input{margin:0!important;--tw-bg-opacity:1!important;background-color:rgb(249 250 251/var(--tw-bg-opacity))!important}:is(.dark .choices__inner .choices__input){--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(255 255 255/var(--tw-text-opacity))!important}.choices__inner .choices__item{white-space:nowrap!important;border-radius:.25rem!important;--tw-border-opacity:1!important;border-color:rgb(156 163 175/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(175 211 130/var(--tw-bg-opacity))!important;padding:.125rem .5rem!important;font-size:.75rem!important;line-height:1rem!important;font-weight:500!important;--tw-text-opacity:1!important;color:rgb(48 72 18/var(--tw-text-opacity))!important}:is(.dark .choices__inner .choices__item){--tw-bg-opacity:1!important;background-color:rgb(24 36 9/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(175 211 130/var(--tw-text-opacity))!important}.choices__list--dropdown{border-radius:.5rem!important;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a!important;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)!important;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)!important}:is(.dark .choices__list--dropdown){--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important}.choices__list--dropdown .choices__item--selectable.is-highlighted{--tw-bg-opacity:1!important;background-color:rgb(175 211 130/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(48 72 18/var(--tw-text-opacity))!important}:is(.dark .choices__list--dropdown .choices__item--selectable.is-highlighted){--tw-bg-opacity:1!important;background-color:rgb(24 36 9/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(175 211 130/var(--tw-text-opacity))!important}.choices[data-type*=select-multiple] .choices__button{--tw-border-opacity:1!important;border-color:rgb(107 114 128/var(--tw-border-opacity))!important}.choices[data-type*=select-multiple] .choices__button:focus{--tw-border-opacity:1!important;border-color:rgb(0 156 234/var(--tw-border-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))!important}.choices__inner .choices__item:focus-within{--tw-border-opacity:1!important;border-color:rgb(0 156 234/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(121 181 46/var(--tw-bg-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))!important}.choices__list--single .choices__item{display:flex!important;width:auto!important}.choices__list--single{width:auto!important}.choices__list--single button{position:relative!important;margin:0!important;display:block!important;height:auto!important}.choices[data-type*=select-one] .choices__button{right:auto!important}.arrow,.arrow:before{position:absolute;width:24px;height:24px;background:inherit}.arrow{visibility:hidden}.arrow:before{visibility:visible;content:"";transform:rotate(45deg)}.arrow{bottom:-4px}.ct-series-a .ct-bar{stroke:#79b52e;fill:#79b52e;stroke-width:20px}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05}.hover\:scale-105:hover,.hover\:scale-110:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.hover\:bg-blue-300:hover{--tw-bg-opacity:1;background-color:rgb(102 196 242/var(--tw-bg-opacity))}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.hover\:bg-blue-800:hover{--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-green-100:hover{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.hover\:bg-green-300:hover{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.hover\:bg-neutral-100:hover{--tw-bg-opacity:1;background-color:rgb(245 245 245/var(--tw-bg-opacity))}.hover\:bg-primary-100:hover{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.hover\:bg-red-300:hover{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}.hover\:bg-white:hover{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:text-gray-800:hover{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.hover\:text-primary-700:hover{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:z-10:focus{z-index:10}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}.focus\:border-primary-500:focus{--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}.focus\:bg-neutral-100:focus{--tw-bg-opacity:1;background-color:rgb(245 245 245/var(--tw-bg-opacity))}.focus\:text-green-700:focus{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-2:focus,.focus\:ring-4:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-4:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-blue-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(153 215 247/var(--tw-ring-opacity))}.focus\:ring-blue-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(102 196 242/var(--tw-ring-opacity))}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))}.focus\:ring-gray-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity))}.focus\:ring-gray-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.focus\:ring-green-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(201 225 171/var(--tw-ring-opacity))}.focus\:ring-green-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(175 211 130/var(--tw-ring-opacity))}.focus\:ring-green-700:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(73 109 28/var(--tw-ring-opacity))}.focus\:ring-primary-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}.focus\:ring-red-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 154 154/var(--tw-ring-opacity))}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:text-blue-500{--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}.group.has-error .group-\[\.has-error\]\:border-red-500{--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error .group-\[\.has-error\]\:bg-red-50{--tw-bg-opacity:1;background-color:rgb(255 230 230/var(--tw-bg-opacity))}.group.has-error .group-\[\.has-error\]\:text-red-900{--tw-text-opacity:1;color:rgb(51 1 1/var(--tw-text-opacity))}.group.has-error .group-\[\.has-error\]\:placeholder-red-700::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(153 2 2/var(--tw-placeholder-opacity))}.group.has-error .group-\[\.has-error\]\:placeholder-red-700::placeholder{--tw-placeholder-opacity:1;color:rgb(153 2 2/var(--tw-placeholder-opacity))}.group.has-error .group-\[\.has-error\]\:focus\:border-red-500:focus{--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error .group-\[\.has-error\]\:focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 3 3/var(--tw-ring-opacity))}:is(.dark .dark\:block){display:block}:is(.dark .dark\:hidden){display:none}:is(.dark .dark\:divide-gray-600)>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(75 85 99/var(--tw-divide-opacity))}:is(.dark .dark\:border-blue-500){--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-400){--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-500){--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-600){--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-700){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-900){--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity))}:is(.dark .dark\:border-primary-500){--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}:is(.dark .dark\:border-transparent){border-color:#0000}:is(.dark .dark\:bg-blue-600){--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-700){--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-900){--tw-bg-opacity:1;background-color:rgb(0 31 47/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-600){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800\/50){background-color:#1f293780}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-600){--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-700){--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-900){--tw-bg-opacity:1;background-color:rgb(24 36 9/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-700){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-900){--tw-bg-opacity:1;background-color:rgb(51 1 1/var(--tw-bg-opacity))}:is(.dark .dark\:bg-yellow-900){--tw-bg-opacity:1;background-color:rgb(99 49 18/var(--tw-bg-opacity))}:is(.dark .dark\:bg-opacity-80){--tw-bg-opacity:0.8}:is(.dark .dark\:text-blue-200){--tw-text-opacity:1;color:rgb(153 215 247/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-300){--tw-text-opacity:1;color:rgb(102 196 242/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-400){--tw-text-opacity:1;color:rgb(51 176 238/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-500){--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-100){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-300){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-400){--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-50){--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-500){--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}:is(.dark .dark\:text-green-300){--tw-text-opacity:1;color:rgb(175 211 130/var(--tw-text-opacity))}:is(.dark .dark\:text-primary-500){--tw-text-opacity:1;color:rgb(121 181 46/var(--tw-text-opacity))}:is(.dark .dark\:text-red-300){--tw-text-opacity:1;color:rgb(255 104 104/var(--tw-text-opacity))}:is(.dark .dark\:text-red-400){--tw-text-opacity:1;color:rgb(255 53 53/var(--tw-text-opacity))}:is(.dark .dark\:text-red-500){--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}:is(.dark .dark\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-300){--tw-text-opacity:1;color:rgb(250 202 21/var(--tw-text-opacity))}:is(.dark .dark\:placeholder-gray-400)::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}:is(.dark .dark\:placeholder-gray-400)::placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}:is(.dark .dark\:ring-offset-gray-700){--tw-ring-offset-color:#374151}:is(.dark .dark\:ring-offset-gray-800){--tw-ring-offset-color:#1f2937}:is(.dark .dark\:hover\:bg-blue-600:hover){--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-blue-700:hover){--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-600:hover){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-700:hover){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-800:hover){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-green-600:hover){--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-green-700:hover){--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-red-600:hover){--tw-bg-opacity:1;background-color:rgb(204 2 2/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:text-blue-500:hover){--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-100:hover){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-300:hover){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-white:hover){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:focus\:border-blue-500:focus){--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-primary-500:focus){--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}:is(.dark .dark\:focus\:text-white:focus){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:focus\:ring-blue-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-blue-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 125 187/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-blue-800:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 62 94/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-gray-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(75 85 99/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-green-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-green-800:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(48 72 18/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(97 145 37/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-offset-gray-700:focus){--tw-ring-offset-color:#374151}:is(.dark .group:hover .dark\:group-hover\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:border-red-500){--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:text-red-500){--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:placeholder-red-500)::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(255 3 3/var(--tw-placeholder-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:placeholder-red-500)::placeholder{--tw-placeholder-opacity:1;color:rgb(255 3 3/var(--tw-placeholder-opacity))}@media (min-width:640px){.sm\:ml-4{margin-left:1rem}.sm\:block{display:block}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:.5rem}.sm\:p-6{padding:1.5rem}.sm\:py-5{padding-top:1.25rem;padding-bottom:1.25rem}.sm\:text-base{font-size:1rem;line-height:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:ml-2{margin-left:.5rem}.md\:mr-24{margin-right:6rem}.md\:table-cell{display:table-cell}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:justify-center{justify-content:center}.md\:space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:p-12{padding:3rem}}@media (min-width:1024px){.lg\:block{display:block}.lg\:table-cell{display:table-cell}.lg\:hidden{display:none}.lg\:w-96{width:24rem}.lg\:-translate-x-full{--tw-translate-x:-100%}.lg\:-translate-x-full,.lg\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.lg\:translate-x-0{--tw-translate-x:0px}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:justify-end{justify-content:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.lg\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.lg\:px-5{padding-left:1.25rem;padding-right:1.25rem}.lg\:pl-3{padding-left:.75rem}.lg\:pl-64{padding-left:16rem}}.\[\&\.active\]\:bg-primary-300.active{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.\[\&\.active\]\:bg-primary-500.active{--tw-bg-opacity:1;background-color:rgb(121 181 46/var(--tw-bg-opacity))}:is(.dark .\[\&\.active\]\:dark\:bg-primary-700).active{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))} \ No newline at end of file +/*! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Calibri,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#007dbb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#007dbb}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}select:not([size]){background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#007dbb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#007dbb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark [type=checkbox]:checked,.dark [type=radio]:checked,[type=checkbox]:checked,[type=radio]:checked{border-color:#0000;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:indeterminate,[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:#0000;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px auto inherit}input[type=file]::file-selector-button{color:#fff;background:#1f2937;border:0;font-weight:500;font-size:.875rem;cursor:pointer;padding:.625rem 1rem .625rem 2rem;-webkit-margin-start:-1rem;margin-inline-start:-1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}input[type=file]::file-selector-button:hover{background:#374151}.dark input[type=file]::file-selector-button{color:#fff;background:#4b5563}.dark input[type=file]::file-selector-button:hover{background:#6b7280}input[type=range]::-webkit-slider-thumb{height:1.25rem;width:1.25rem;background:#007dbb;border-radius:9999px;border:0;appearance:none;-moz-appearance:none;-webkit-appearance:none;cursor:pointer}input[type=range]:disabled::-webkit-slider-thumb{background:#9ca3af}.dark input[type=range]:disabled::-webkit-slider-thumb{background:#6b7280}input[type=range]:focus::-webkit-slider-thumb{outline:2px solid #0000;outline-offset:2px;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1px;--tw-ring-color:rgb(164 202 254/var(--tw-ring-opacity))}input[type=range]::-moz-range-thumb{height:1.25rem;width:1.25rem;background:#007dbb;border-radius:9999px;border:0;appearance:none;-moz-appearance:none;-webkit-appearance:none;cursor:pointer}input[type=range]:disabled::-moz-range-thumb{background:#9ca3af}.dark input[type=range]:disabled::-moz-range-thumb{background:#6b7280}input[type=range]::-moz-range-progress{background:#009cea}input[type=range]::-ms-fill-lower{background:#009cea}.toggle-bg:after{content:"";position:absolute;top:.125rem;left:.125rem;background:#fff;border-color:#d1d5db;border-width:1px;border-radius:9999px;height:1.25rem;width:1.25rem;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-duration:.15s;box-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}input:checked+.toggle-bg:after{transform:translateX(100%);;border-color:#fff}input:checked+.toggle-bg{background:#007dbb;border-color:#007dbb}.tooltip-arrow,.tooltip-arrow:before{position:absolute;width:8px;height:8px;background:inherit}.tooltip-arrow{visibility:hidden}.tooltip-arrow:before{content:"";visibility:visible;transform:rotate(45deg)}[data-tooltip-style^=light]+.tooltip>.tooltip-arrow:before{border-style:solid;border-color:#e5e7eb}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=top]>.tooltip-arrow:before{border-bottom-width:1px;border-right-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=right]>.tooltip-arrow:before{border-bottom-width:1px;border-left-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=bottom]>.tooltip-arrow:before{border-top-width:1px;border-left-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=left]>.tooltip-arrow:before{border-top-width:1px;border-right-width:1px}.tooltip[data-popper-placement^=top]>.tooltip-arrow{bottom:-4px}.tooltip[data-popper-placement^=bottom]>.tooltip-arrow{top:-4px}.tooltip[data-popper-placement^=left]>.tooltip-arrow{right:-4px}.tooltip[data-popper-placement^=right]>.tooltip-arrow{left:-4px}.tooltip.invisible>.tooltip-arrow:before{visibility:hidden}[data-popper-arrow],[data-popper-arrow]:before{position:absolute;width:8px;height:8px;background:inherit}[data-popper-arrow]{visibility:hidden}[data-popper-arrow]:after,[data-popper-arrow]:before{content:"";visibility:visible;transform:rotate(45deg)}[data-popper-arrow]:after{position:absolute;width:9px;height:9px;background:inherit}[role=tooltip]>[data-popper-arrow]:before{border-style:solid;border-color:#e5e7eb}.dark [role=tooltip]>[data-popper-arrow]:before{border-style:solid;border-color:#4b5563}[role=tooltip]>[data-popper-arrow]:after{border-style:solid;border-color:#e5e7eb}.dark [role=tooltip]>[data-popper-arrow]:after{border-style:solid;border-color:#4b5563}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:before{border-bottom-width:1px;border-right-width:1px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:before{border-bottom-width:1px;border-left-width:1px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:before{border-top-width:1px;border-left-width:1px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:before{border-top-width:1px;border-right-width:1px}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]{bottom:-5px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]{top:-5px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]{right:-5px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]{left:-5px}[role=tooltip].invisible>[data-popper-arrow]:after,[role=tooltip].invisible>[data-popper-arrow]:before{visibility:hidden}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#009cea80;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.-right-2{right:-.5rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-\[60px\]{bottom:60px}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.right-2{right:.5rem}.top-0{top:0}.top-2{top:.5rem}.top-2\/4{top:50%}.top-5{top:1.25rem}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[99\]{z-index:99}.col-span-1{grid-column:span 1/span 1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-6{grid-column:span 6/span 6}.col-start-1{grid-column-start:1}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-4{margin:1rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0{margin-top:0;margin-bottom:0}.my-4{margin-top:1rem;margin-bottom:1rem}.-mb-1{margin-bottom:-.25rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-10{margin-right:2.5rem}.mr-16{margin-right:4rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[350px\]{height:350px}.h-\[600px\]{height:600px}.h-\[700px\]{height:700px}.h-full{height:100%}.h-screen{height:100vh}.max-h-96{max-height:24rem}.max-h-full{max-height:100%}.w-1\/2{width:50%}.w-1\/4{width:25%}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\/4{width:75%}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-96{width:24rem}.w-\[10em\]{width:10em}.w-\[20em\]{width:20em}.w-\[30em\]{width:30em}.w-\[5em\]{width:5em}.w-\[600px\]{width:600px}.w-\[748px\]{width:748px}.w-\[7em\]{width:7em}.w-\[850px\]{width:850px}.w-\[8em\]{width:8em}.w-auto{width:auto}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.w-screen{width:100vw}.min-w-\[700px\]{min-width:700px}.max-w-2xl{max-width:42rem}.max-w-6xl{max-width:72rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-screen-2xl{max-width:1536px}.max-w-screen-lg{max-width:1024px}.flex-1{flex:1 1 0%}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.grow-0{flex-grow:0}.basis-1\/4{flex-basis:25%}.\!translate-y-0{--tw-translate-y:0px!important}.\!translate-y-0,.\!translate-y-32{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.\!translate-y-32{--tw-translate-y:8rem!important}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.-translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-full{--tw-translate-y:-100%}.translate-x-0{--tw-translate-x:0px}.translate-x-0,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x:100%}.translate-y-full{--tw-translate-y:100%}.rotate-180,.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.scale-100,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform-none{transform:none}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes shake{0%{transform:translateX(0)}12.5%{transform:translateX(-5px)}25%{transform:translateX(0)}37.5%{transform:translateX(5px)}50%{transform:translateX(0)}62.5%{transform:translateX(-5px)}75%{transform:translateX(5px)}87.5%{transform:translateX(5px)}to{transform:translateX(0)}}.animate-shake{animation:shake .5s ease-out 1}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-move{cursor:move}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-stretch{justify-content:stretch}.justify-items-stretch{justify-items:stretch}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-2{row-gap:.5rem}.-space-x-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(-1px*var(--tw-space-x-reverse));margin-left:calc(-1px*(1 - var(--tw-space-x-reverse)))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity))}.place-self-end{place-self:end}.self-center{align-self:center}.self-stretch{align-self:stretch}.justify-self-end{justify-self:end}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-visible{overflow:visible}.overflow-scroll{overflow:scroll}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-b-lg{border-bottom-right-radius:.5rem}.rounded-b-lg,.rounded-l-lg{border-bottom-left-radius:.5rem}.rounded-l-lg{border-top-left-radius:.5rem}.rounded-r-lg{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-0{border-width:0}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-dotted{border-style:dotted}.border-blue-300{--tw-border-opacity:1;border-color:rgb(102 196 242/var(--tw-border-opacity))}.border-blue-600{--tw-border-opacity:1;border-color:rgb(0 125 187/var(--tw-border-opacity))}.border-blue-700{--tw-border-opacity:1;border-color:rgb(0 94 140/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}.border-primary-300{--tw-border-opacity:1;border-color:rgb(175 211 130/var(--tw-border-opacity))}.border-primary-600{--tw-border-opacity:1;border-color:rgb(97 145 37/var(--tw-border-opacity))}.border-red-300{--tw-border-opacity:1;border-color:rgb(255 104 104/var(--tw-border-opacity))}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(204 235 251/var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity:1;background-color:rgb(153 215 247/var(--tw-bg-opacity))}.bg-blue-300{--tw-bg-opacity:1;background-color:rgb(102 196 242/var(--tw-bg-opacity))}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(51 176 238/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(230 245 253/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(0 156 234/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.bg-blue-700{--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}.bg-blue-800{--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.bg-green-300{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(148 196 88/var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgb(242 248 234/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(121 181 46/var(--tw-bg-opacity))}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}.bg-green-700{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.bg-green-800{--tw-bg-opacity:1;background-color:rgb(48 72 18/var(--tw-bg-opacity))}.bg-primary-200{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity:1;background-color:rgb(242 248 234/var(--tw-bg-opacity))}.bg-purple-50{--tw-bg-opacity:1;background-color:rgb(246 245 255/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(255 205 205/var(--tw-bg-opacity))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(255 154 154/var(--tw-bg-opacity))}.bg-red-300{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(255 230 230/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(255 3 3/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-white\/50{background-color:#ffffff80}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(253 246 178/var(--tw-bg-opacity))}.\!bg-opacity-0{--tw-bg-opacity:0!important}.\!bg-opacity-100{--tw-bg-opacity:1!important}.\!bg-opacity-50{--tw-bg-opacity:0.5!important}.bg-opacity-50{--tw-bg-opacity:0.5}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-10{padding-left:2.5rem}.pl-11{padding-left:2.75rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pr-2{padding-right:.5rem}.pr-2\.5{padding-right:.625rem}.pr-6{padding-right:1.5rem}.pt-16{padding-top:4rem}.pt-2{padding-top:.5rem}.pt-5{padding-top:1.25rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-baseline{vertical-align:initial}.align-top{vertical-align:top}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[0\.6rem\]{font-size:.6rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-light{font-weight:300}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-6{line-height:1.5rem}.leading-9{line-height:2.25rem}.leading-none{line-height:1}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-blue-400{--tw-text-opacity:1;color:rgb(51 176 238/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgb(0 62 94/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(121 181 46/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(97 145 37/var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgb(48 72 18/var(--tw-text-opacity))}.text-primary-300{--tw-text-opacity:1;color:rgb(175 211 130/var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity:1;color:rgb(97 145 37/var(--tw-text-opacity))}.text-purple-600{--tw-text-opacity:1;color:rgb(126 58 242/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(204 2 2/var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgb(102 1 1/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-yellow-800{--tw-text-opacity:1;color:rgb(114 59 19/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.\!opacity-0{opacity:0!important}.\!opacity-100{opacity:1!important}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px #00000040;--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.outline-0{outline-width:0}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.duration-75{transition-duration:75ms}.ease-\[cubic-bezier\(\.3\2c 2\.3\2c \.6\2c 1\)\]{transition-timing-function:cubic-bezier(.3,2.3,.6,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.htmx-added .fade-in,.htmx-added.fade-in{opacity:0!important}.fade-in{opacity:1}.htmx-settling .fade-in-settle,.htmx-settling.fade-in-settle{opacity:0!important}.fade-in-settle{opacity:1}.htmx-added .swipe-left-swap,.htmx-added.swipe-left-swap{opacity:1!important;--tw-scale-x:1!important;--tw-scale-y:1!important;--tw-translate-x:-50%!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.swipe-left-swap{opacity:1;--tw-scale-x:1;--tw-scale-y:1;--tw-translate-x:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-settling.htmx-added .swipe-left-swap,.htmx-settling.htmx-added.swipe-left-swap{opacity:0!important;--tw-scale-x:.75!important;--tw-scale-y:.75!important;--tw-translate-x:50%!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.htmx-settling .slide-up-settle,.htmx-settling.slide-up-settle{--tw-translate-y:1.25rem!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.slide-up-settle{--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hidden .slide-up,.htmx-added .slide-up{--tw-translate-y:1.25rem!important;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))!important}.slide-up{--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.live-added{animation:pulse-green .3s 2;animation-direction:alternate;animation-timing-function:ease-in-out}.dark .live-added{animation:pulse-dark-green .3s 2!important;animation-direction:alternate;animation-timing-function:ease-in-out}.live-removed{animation:pulse-red .3s 2;animation-direction:alternate;animation-timing-function:ease-in-out}.dark .live-removed{animation:pulse-dark-red .3s 2!important;animation-direction:alternate;animation-timing-function:ease-in-out}@keyframes pulse-green{0%{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}:is(.dark to){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}@keyframes pulse-dark-green{:is(.dark 0%){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}}@keyframes pulse-red{0%{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}:is(.dark to){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}@keyframes pulse-dark-red{:is(.dark 0%){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}.htmx-request .htmx-indicator,.htmx-request.htmx-indicator{display:inherit!important}.htmx-indicator{display:none}.htmx-request .htmx-indicator-hidden{display:none!important}.htmx-indicator-hidden{display:inherit}.htmx-swapping .fade-out{opacity:0!important}.fade-out{opacity:1}.min-h-content{min-height:calc(100vh - 4em)}.choices{margin-bottom:0!important;border-width:0!important}.choices__inner{display:block!important;width:100%!important;border-radius:.5rem!important;border-width:1px!important;--tw-border-opacity:1!important;border-color:rgb(209 213 219/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(249 250 251/var(--tw-bg-opacity))!important;padding:.25rem!important;font-size:.875rem!important;line-height:1.25rem!important;--tw-text-opacity:1!important;color:rgb(17 24 39/var(--tw-text-opacity))!important}.choices__inner:focus{--tw-border-opacity:1!important;border-color:rgb(0 156 234/var(--tw-border-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))!important}.group.has-error .choices__inner{--tw-border-opacity:1!important;border-color:rgb(255 3 3/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(255 230 230/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(51 1 1/var(--tw-text-opacity))!important}.group.has-error .choices__inner::-moz-placeholder{--tw-placeholder-opacity:1!important;color:rgb(153 2 2/var(--tw-placeholder-opacity))!important}.group.has-error .choices__inner::placeholder{--tw-placeholder-opacity:1!important;color:rgb(153 2 2/var(--tw-placeholder-opacity))!important}.group.has-error .choices__inner:focus{--tw-border-opacity:1!important;border-color:rgb(255 3 3/var(--tw-border-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(255 3 3/var(--tw-ring-opacity))!important}:is(.dark .choices__inner){--tw-border-opacity:1!important;border-color:rgb(75 85 99/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(255 255 255/var(--tw-text-opacity))!important}:is(.dark .choices__inner)::-moz-placeholder{--tw-placeholder-opacity:1!important;color:rgb(156 163 175/var(--tw-placeholder-opacity))!important}:is(.dark .choices__inner)::placeholder{--tw-placeholder-opacity:1!important;color:rgb(156 163 175/var(--tw-placeholder-opacity))!important}:is(.dark .choices__inner:focus){--tw-border-opacity:1!important;border-color:rgb(0 156 234/var(--tw-border-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))!important}.group.has-error :is(.dark .choices__inner){--tw-border-opacity:1!important;border-color:rgb(255 3 3/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(255 3 3/var(--tw-text-opacity))!important}.group.has-error :is(.dark .choices__inner)::-moz-placeholder{--tw-placeholder-opacity:1!important;color:rgb(255 3 3/var(--tw-placeholder-opacity))!important}.group.has-error :is(.dark .choices__inner)::placeholder{--tw-placeholder-opacity:1!important;color:rgb(255 3 3/var(--tw-placeholder-opacity))!important}.choices:focus-within .choices__inner,:is(.dark .choices:focus-within .choices__inner){--tw-border-opacity:1!important;border-color:rgb(0 156 234/var(--tw-border-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))!important}.choices:focus-within .choices__inner{outline:2px solid #0000!important;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#007dbb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#007dbb}.choices__inner .choices__input{margin:0!important;--tw-bg-opacity:1!important;background-color:rgb(249 250 251/var(--tw-bg-opacity))!important}:is(.dark .choices__inner .choices__input){--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(255 255 255/var(--tw-text-opacity))!important}.choices__inner .choices__item{white-space:nowrap!important;border-radius:.25rem!important;--tw-border-opacity:1!important;border-color:rgb(156 163 175/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(175 211 130/var(--tw-bg-opacity))!important;padding:.125rem .5rem!important;font-size:.75rem!important;line-height:1rem!important;font-weight:500!important;--tw-text-opacity:1!important;color:rgb(48 72 18/var(--tw-text-opacity))!important}:is(.dark .choices__inner .choices__item){--tw-bg-opacity:1!important;background-color:rgb(24 36 9/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(175 211 130/var(--tw-text-opacity))!important}.choices__list--dropdown{border-radius:.5rem!important;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a!important;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)!important;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)!important}:is(.dark .choices__list--dropdown){--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important}.choices__list--dropdown .choices__item--selectable.is-highlighted{--tw-bg-opacity:1!important;background-color:rgb(175 211 130/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(48 72 18/var(--tw-text-opacity))!important}:is(.dark .choices__list--dropdown .choices__item--selectable.is-highlighted){--tw-bg-opacity:1!important;background-color:rgb(24 36 9/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(175 211 130/var(--tw-text-opacity))!important}.choices[data-type*=select-multiple] .choices__button{--tw-border-opacity:1!important;border-color:rgb(107 114 128/var(--tw-border-opacity))!important}.choices[data-type*=select-multiple] .choices__button:focus{--tw-border-opacity:1!important;border-color:rgb(0 156 234/var(--tw-border-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))!important}.choices__inner .choices__item:focus-within{--tw-border-opacity:1!important;border-color:rgb(0 156 234/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(121 181 46/var(--tw-bg-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))!important}.choices__list--single .choices__item{display:flex!important;width:auto!important}.choices__list--single{width:auto!important}.choices__list--single button{position:relative!important;margin:0!important;display:block!important;height:auto!important}.choices[data-type*=select-one] .choices__button{right:auto!important}.arrow,.arrow:before{position:absolute;width:24px;height:24px;background:inherit}.arrow{visibility:hidden}.arrow:before{visibility:visible;content:"";transform:rotate(45deg)}.arrow{bottom:-4px}.ct-series-a .ct-bar{stroke:#79b52e;fill:#79b52e;stroke-width:20px}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05}.hover\:scale-105:hover,.hover\:scale-110:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.hover\:bg-blue-300:hover{--tw-bg-opacity:1;background-color:rgb(102 196 242/var(--tw-bg-opacity))}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.hover\:bg-blue-800:hover{--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-green-100:hover{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.hover\:bg-green-300:hover{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.hover\:bg-neutral-100:hover{--tw-bg-opacity:1;background-color:rgb(245 245 245/var(--tw-bg-opacity))}.hover\:bg-primary-100:hover{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.hover\:bg-red-300:hover{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}.hover\:bg-white:hover{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:text-gray-800:hover{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.hover\:text-primary-700:hover{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:z-10:focus{z-index:10}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}.focus\:border-primary-500:focus{--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}.focus\:bg-neutral-100:focus{--tw-bg-opacity:1;background-color:rgb(245 245 245/var(--tw-bg-opacity))}.focus\:text-green-700:focus{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-2:focus,.focus\:ring-4:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-4:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-blue-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(153 215 247/var(--tw-ring-opacity))}.focus\:ring-blue-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(102 196 242/var(--tw-ring-opacity))}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))}.focus\:ring-gray-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity))}.focus\:ring-gray-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.focus\:ring-green-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(201 225 171/var(--tw-ring-opacity))}.focus\:ring-green-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(175 211 130/var(--tw-ring-opacity))}.focus\:ring-green-700:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(73 109 28/var(--tw-ring-opacity))}.focus\:ring-primary-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}.focus\:ring-red-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 154 154/var(--tw-ring-opacity))}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:text-blue-500{--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}.group.has-error .group-\[\.has-error\]\:border-red-500{--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error .group-\[\.has-error\]\:bg-red-50{--tw-bg-opacity:1;background-color:rgb(255 230 230/var(--tw-bg-opacity))}.group.has-error .group-\[\.has-error\]\:text-red-900{--tw-text-opacity:1;color:rgb(51 1 1/var(--tw-text-opacity))}.group.has-error .group-\[\.has-error\]\:placeholder-red-700::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(153 2 2/var(--tw-placeholder-opacity))}.group.has-error .group-\[\.has-error\]\:placeholder-red-700::placeholder{--tw-placeholder-opacity:1;color:rgb(153 2 2/var(--tw-placeholder-opacity))}.group.has-error .group-\[\.has-error\]\:focus\:border-red-500:focus{--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error .group-\[\.has-error\]\:focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 3 3/var(--tw-ring-opacity))}.htmx-swapping\:-translate-x-2\/3.htmx-swapping{--tw-translate-x:-66.666667%}.htmx-swapping\:-translate-x-2\/3.htmx-swapping,.htmx-swapping\:translate-x-2\/3.htmx-swapping{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-swapping\:translate-x-2\/3.htmx-swapping{--tw-translate-x:66.666667%}.htmx-swapping\:scale-0.htmx-swapping{--tw-scale-x:0;--tw-scale-y:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-swapping\:opacity-0.htmx-swapping{opacity:0}.htmx-swapping .htmx-swapping\:-translate-x-2\/3{--tw-translate-x:-66.666667%}.htmx-swapping .htmx-swapping\:-translate-x-2\/3,.htmx-swapping .htmx-swapping\:translate-x-2\/3{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-swapping .htmx-swapping\:translate-x-2\/3{--tw-translate-x:66.666667%}.htmx-swapping .htmx-swapping\:scale-0{--tw-scale-x:0;--tw-scale-y:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-swapping .htmx-swapping\:opacity-0{opacity:0}.htmx-added\:-translate-x-2\/3.htmx-added{--tw-translate-x:-66.666667%}.htmx-added\:-translate-x-2\/3.htmx-added,.htmx-added\:translate-x-2\/3.htmx-added{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-added\:translate-x-2\/3.htmx-added{--tw-translate-x:66.666667%}.htmx-added\:scale-0.htmx-added{--tw-scale-x:0;--tw-scale-y:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-added\:opacity-0.htmx-added{opacity:0}.htmx-added .htmx-added\:-translate-x-2\/3{--tw-translate-x:-66.666667%}.htmx-added .htmx-added\:-translate-x-2\/3,.htmx-added .htmx-added\:translate-x-2\/3{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-added .htmx-added\:translate-x-2\/3{--tw-translate-x:66.666667%}.htmx-added .htmx-added\:scale-0{--tw-scale-x:0;--tw-scale-y:0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.htmx-added .htmx-added\:opacity-0{opacity:0}:is(.dark .dark\:block){display:block}:is(.dark .dark\:hidden){display:none}:is(.dark .dark\:divide-gray-600)>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(75 85 99/var(--tw-divide-opacity))}:is(.dark .dark\:border-blue-500){--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-400){--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-500){--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-600){--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-700){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-900){--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity))}:is(.dark .dark\:border-primary-500){--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}:is(.dark .dark\:border-transparent){border-color:#0000}:is(.dark .dark\:bg-blue-600){--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-700){--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-900){--tw-bg-opacity:1;background-color:rgb(0 31 47/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-600){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800\/50){background-color:#1f293780}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-600){--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-700){--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-900){--tw-bg-opacity:1;background-color:rgb(24 36 9/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-700){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-900){--tw-bg-opacity:1;background-color:rgb(51 1 1/var(--tw-bg-opacity))}:is(.dark .dark\:bg-yellow-900){--tw-bg-opacity:1;background-color:rgb(99 49 18/var(--tw-bg-opacity))}:is(.dark .dark\:bg-opacity-80){--tw-bg-opacity:0.8}:is(.dark .dark\:text-blue-200){--tw-text-opacity:1;color:rgb(153 215 247/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-300){--tw-text-opacity:1;color:rgb(102 196 242/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-400){--tw-text-opacity:1;color:rgb(51 176 238/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-500){--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-100){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-300){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-400){--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-50){--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-500){--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}:is(.dark .dark\:text-green-300){--tw-text-opacity:1;color:rgb(175 211 130/var(--tw-text-opacity))}:is(.dark .dark\:text-primary-500){--tw-text-opacity:1;color:rgb(121 181 46/var(--tw-text-opacity))}:is(.dark .dark\:text-red-300){--tw-text-opacity:1;color:rgb(255 104 104/var(--tw-text-opacity))}:is(.dark .dark\:text-red-400){--tw-text-opacity:1;color:rgb(255 53 53/var(--tw-text-opacity))}:is(.dark .dark\:text-red-500){--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}:is(.dark .dark\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-300){--tw-text-opacity:1;color:rgb(250 202 21/var(--tw-text-opacity))}:is(.dark .dark\:placeholder-gray-400)::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}:is(.dark .dark\:placeholder-gray-400)::placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}:is(.dark .dark\:ring-offset-gray-700){--tw-ring-offset-color:#374151}:is(.dark .dark\:ring-offset-gray-800){--tw-ring-offset-color:#1f2937}:is(.dark .dark\:hover\:bg-blue-600:hover){--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-blue-700:hover){--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-600:hover){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-700:hover){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-800:hover){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-green-600:hover){--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-green-700:hover){--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-red-600:hover){--tw-bg-opacity:1;background-color:rgb(204 2 2/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:text-blue-500:hover){--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-100:hover){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-300:hover){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-white:hover){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:focus\:border-blue-500:focus){--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-primary-500:focus){--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}:is(.dark .dark\:focus\:text-white:focus){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:focus\:ring-blue-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-blue-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 125 187/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-blue-800:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 62 94/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-gray-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(75 85 99/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-green-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-green-800:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(48 72 18/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(97 145 37/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-offset-gray-700:focus){--tw-ring-offset-color:#374151}:is(.dark .group:hover .dark\:group-hover\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:border-red-500){--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:text-red-500){--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:placeholder-red-500)::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(255 3 3/var(--tw-placeholder-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:placeholder-red-500)::placeholder{--tw-placeholder-opacity:1;color:rgb(255 3 3/var(--tw-placeholder-opacity))}@media (min-width:640px){.sm\:ml-4{margin-left:1rem}.sm\:block{display:block}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.sm\:rounded-lg{border-radius:.5rem}.sm\:p-6{padding:1.5rem}.sm\:py-5{padding-top:1.25rem;padding-bottom:1.25rem}.sm\:text-base{font-size:1rem;line-height:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:ml-2{margin-left:.5rem}.md\:mr-24{margin-right:6rem}.md\:table-cell{display:table-cell}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:justify-center{justify-content:center}.md\:space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:p-12{padding:3rem}}@media (min-width:1024px){.lg\:block{display:block}.lg\:table-cell{display:table-cell}.lg\:hidden{display:none}.lg\:w-96{width:24rem}.lg\:-translate-x-full{--tw-translate-x:-100%}.lg\:-translate-x-full,.lg\:translate-x-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.lg\:translate-x-0{--tw-translate-x:0px}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:justify-end{justify-content:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.lg\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.lg\:px-5{padding-left:1.25rem;padding-right:1.25rem}.lg\:pl-3{padding-left:.75rem}.lg\:pl-64{padding-left:16rem}}.\[\&\.active\]\:bg-primary-300.active{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.\[\&\.active\]\:bg-primary-500.active{--tw-bg-opacity:1;background-color:rgb(121 181 46/var(--tw-bg-opacity))}:is(.dark .\[\&\.active\]\:dark\:bg-primary-700).active{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))} \ No newline at end of file diff --git a/src/clj/auto_ap/cursor.clj b/src/clj/auto_ap/cursor.clj index 4247b9f5..28508791 100644 --- a/src/clj/auto_ap/cursor.clj +++ b/src/clj/auto_ap/cursor.clj @@ -143,3 +143,10 @@ (defn update! [cursor v] "Replaces value supplied by cursor with value v." (-transact! cursor (constantly v))) + +(defn ensure-path! [cursor p default] + (let [next-to-last (get-in cursor (butlast p)) + next-to-last-v @next-to-last] + (when (not (get next-to-last-v (last p))) + (transact! next-to-last #(assoc % (last p) default)))) + cursor) diff --git a/src/clj/auto_ap/graphql/clients.clj b/src/clj/auto_ap/graphql/clients.clj index 4fb95ec1..4d004881 100644 --- a/src/clj/auto_ap/graphql/clients.clj +++ b/src/clj/auto_ap/graphql/clients.clj @@ -1,182 +1,12 @@ (ns auto-ap.graphql.clients - (:require - [amazonica.aws.s3 :as s3] - [auto-ap.datomic :refer [audit-transact conn]] - [auto-ap.datomic.clients :as d-clients] - [auto-ap.graphql.utils - :refer [->graphql - assert-admin - attach-tracing-resolvers - can-see-client? - <-graphql - result->page - is-admin?]] - [auto-ap.routes.queries :as q] - [auto-ap.square.core3 :as square] - [auto-ap.utils :refer [heartbeat]] - [clj-time.coerce :as coerce] - [clojure.java.io :as io] - [clojure.set :as set] - [clojure.string :as str] - [com.brunobonacci.mulog :as mu] - [datomic.api :as dc] - [iol-ion.tx :refer [random-tempid]] - [mount.core :as mount] - [yang.scheduler :as scheduler] - [auto-ap.solr :as solr]) - (:import - (java.util UUID) - (org.apache.commons.codec.binary Base64))) - -(defn assert-client-code-is-unique [code] - (when (seq (dc/q {:find '[?id] - :in ['$ '?code] - :where ['[?id :client/code ?code]]} - (dc/db conn) code)) - (throw (ex-info "Client is not unique" {:validation-error (str "Client code '" code "' is not unique.")})))) - -(defn upload-signature-data [signature-data] - (let [prefix "data:image/jpeg;base64,"] - (when signature-data - (when-not (str/starts-with? signature-data prefix) - (throw (ex-info "Invalid signature image" {:validation-error (str "Invalid signature image.")}))) - (let [signature-id (str (UUID/randomUUID)) - raw-bytes (Base64/decodeBase64 (subs signature-data (count prefix)))] - (s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env) - :key (str signature-id ".jpg") - :input-stream (io/make-input-stream raw-bytes {}) - :metadata {:content-type "image/jpeg" - :content-length (count raw-bytes)} - :canned-acl "public-read") - (str "https://integreat-signature-images.s3.amazonaws.com/" signature-id ".jpg"))))) - -(defn assert-no-shared-transaction-sources [client-code txes] - (let [new-db (:db-after (dc/with (dc/db conn) - txes))] - (when (seq (->> (dc/q '[:find ?src (count ?ba) - :in $ ?c - :where [?c :client/bank-accounts ?ba] - (or - [?ba :bank-account/intuit-bank-account ?src] - [?ba :bank-account/plaid-account ?src] - [?ba :bank-account/yodlee-account-id ?src])] - new-db [:client/code client-code]) - (filter (fn [[_ cnt]] - (> cnt 1))))) - (throw (ex-info "Cannot reuse yodlee/plaid/intuit account" {:validation-error (str "Cannot reuse yodlee/plaid/intuit account")}))))) - -(defn edit-client [context {:keys [edit_client]} _] - (assert-admin (:id context)) - (when-not (:id edit_client) - (assert-client-code-is-unique (:code edit_client))) - - (let [client (when (:id edit_client) (d-clients/get-by-id (:id edit_client))) - id (or (:db/id client) "new-client") - signature-file (upload-signature-data (:signature_data edit_client)) - client-code (if (str/blank? (:client/code client)) - (:code edit_client) - (:client/code client)) - updated-entity (cond-> {:db/id id - :client/code client-code - :client/name (:name edit_client) - :client/matches (:matches edit_client) - :client/email (:email edit_client) - :client/locked-until (some-> (:locked_until edit_client) (coerce/to-date)) - :client/locations (filter identity (:locations edit_client)) - :client/week-a-debits (:week_a_debits edit_client) - :client/week-a-credits (:week_a_credits edit_client) - :client/week-b-debits (:week_b_debits edit_client) - :client/square-auth-token (:square_auth_token edit_client) - :client/square-locations (map - (fn [sl] - {:db/id (or (:id sl) (random-tempid)) - :square-location/client-location (:client_location sl)}) - (:square_locations edit_client)) - - :client/emails (map (fn [e] - {:db/id (or (:id e) - (random-tempid)) - :email-contact/email (:email e) - :email-contact/description (:description e)}) - (:emails edit_client)) - - :client/feature-flags (:feature_flags edit_client) - :client/ezcater-locations (map - (fn [el] - {:db/id (or (:id el) (random-tempid)) - :ezcater-location/location (:location el) - :ezcater-location/caterer (:caterer el)}) - (:ezcater_locations edit_client)) - :client/week-b-credits (:week_b_credits edit_client) - :client/location-matches (->> (:location_matches edit_client) - (filter (fn [lm] (and (:location lm) (:match lm)))) - (map (fn [lm] {:db/id (or (:id lm) (random-tempid)) - :location-match/location (:location lm) - :location-match/matches [(:match lm)]}))) - :client/address (when (seq (filter identity (vals (:address edit_client)))) - {:db/id (or (:id (:address edit_client)) (random-tempid)) - :address/street1 (:street1 (:address edit_client)) - :address/street2 (:street2 (:address edit_client)) - :address/city (:city (:address edit_client)) - :address/state (:state (:address edit_client)) - :address/zip (:zip (:address edit_client))}) - :client/bank-accounts (map (fn [ba] - {:db/id (or (:id ba) (random-tempid)) - :bank-account/code (:code ba) - :bank-account/bank-name (:bank_name ba) - :bank-account/bank-code (:bank_code ba) - :bank-account/start-date (-> (:start_date ba) (coerce/to-date)) - :bank-account/routing (:routing ba) - :bank-account/include-in-reports (:include_in_reports ba) - - :bank-account/name (:name ba) - :bank-account/visible (:visible ba) - :bank-account/number (:number ba) - :bank-account/check-number (:check_number ba) - :bank-account/numeric-code (:numeric_code ba) - :bank-account/sort-order (:sort_order ba) - :bank-account/locations (:locations ba) - :bank-account/use-date-instead-of-post-date? (boolean (:use_date_instead_of_post_date ba)) - - :bank-account/yodlee-account-id (:yodlee_account_id ba) - :bank-account/type (keyword "bank-account-type" (name (:type ba))) - :bank-account/yodlee-account (when (:yodlee_account ba) - [:yodlee-account/id (:yodlee_account ba)]) - :bank-account/plaid-account (:plaid_account ba) - :bank-account/intuit-bank-account (:intuit_bank_account ba)}) - (:bank_accounts edit_client))} - signature-file (assoc :client/signature-file signature-file)) - - _ (mu/log ::upserting :up updated-entity) - _ (assert-no-shared-transaction-sources client-code [[:upsert-entity updated-entity]]) - - result (audit-transact [[:upsert-entity updated-entity]] (:id context))] - (when (:square_auth_token edit_client) - @(square/upsert-locations (-> result :tempids (get id) (or id) d-clients/get-by-id))) - (let [updated-client (-> result :tempids (get id) (or id) d-clients/get-by-id)] - (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)))}])) - (-> updated-client - - (update :client/bank-accounts - (fn [bas] - (map #(set/rename-keys % {:bank-account/use-date-instead-of-post-date? :use-date-instead-of-post-date}) bas))) - (update :client/location-matches - (fn [lms] - (mapcat (fn [lm] - (map (fn [m] - {:location-match/match m - :location-match/location (:location-match/location lm)}) - (:location-match/matches lm))) - lms))) - ->graphql)))) - + (:require [auto-ap.datomic :refer [conn]] + [auto-ap.datomic.clients :as d-clients] + [auto-ap.graphql.utils + :refer [->graphql <-graphql assert-admin attach-tracing-resolvers + can-see-client? is-admin? result->page]] + [clojure.set :as set] + [com.brunobonacci.mulog :as mu] + [datomic.api :as dc])) (defn refresh-all-current-balance [] (mu/with-context {:source "current-balance-refresh"} @@ -238,201 +68,11 @@ bank-accounts))))))] (result->page clients clients-count :clients (:filters args)))) -(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]) - (let [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")]))] - {:message (str/join "\n" - [ - (str "For " client-code ":") - (str "Sales: " "https://app.integreatconsult.com/api/queries/" sales-summary-id "/results/json") - (str "Sales Category: " "https://app.integreatconsult.com/api/queries/" sales-category-id "/results/json") - (str "Expected Deposits: " "https://app.integreatconsult.com/api/queries/" expected-deposit-id "/results/json") - (str "Tenders: " "https://app.integreatconsult.com/api/queries/" tender-id "/results/json") - (str "Refund: " "https://app.integreatconsult.com/api/queries/" refund-id "/results/json") - (str "Cash Drawer Shift: " "https://app.integreatconsult.com/api/queries/" cash-drawer-shift-id "/results/json")])}))) - - -(defn setup-sales-queries [context args _] - (assert-admin (:id context)) - (setup-sales-queries-impl (:client_id args))) - - -(defn reset-all-queries [] - (doseq [[c] (dc/q '[:find ?c :where [?c :client/code]] (dc/db conn))] - (setup-sales-queries-impl c))) (def objects @@ -535,82 +175,15 @@ :resolve :get-client-page}}) (def mutations - {:edit_client {:type :client - :args {:edit_client {:type :edit_client}} - :resolve :mutation/edit-client} - :setup_sales_queries {:type :message - :args {:client_id {:type :id}} - :resolve :mutation/setup-sales-queries}}) + {}) (def input-objects - {:edit_location_match {:fields {:location {:type 'String} - :match {:type 'String} - :id {:type :id}}} - - :client_filters + { :client_filters {:fields {:code {:type 'String} :name_like {:type 'String} :start {:type 'Int} :per_page {:type 'Int} - :sort {:type '(list :sort_item)}}} - - :edit_square_location {:fields {:client_location {:type 'String} - :id {:type :id}}} - - :edit_ezcater_location {:fields {:location {:type 'String} - :caterer {:type :id} - :id {:type :id}}} - - :edit_forecasted_transaction {:fields {:identifier {:type 'String} - :id {:type :id} - :day_of_month {:type 'Int} - :amount {:type :money}}} - :edit_email_contact {:fields {:id {:type :id} - :email {:type 'String} - :description {:type 'String}}} - :edit_client {:fields {:id {:type :id} - :name {:type 'String} - :locked_until {:type :iso_date} - :code {:type 'String} - :square_auth_token {:type 'String} - :feature_flags {:type '(list String)} - :signature_data {:type 'String} - :email {:type 'String} - :emails {:type '(list :edit_email_contact)} - :week_a_credits {:type :money} - :week_a_debits {:type :money} - :week_b_credits {:type :money} - :week_b_debits {:type :money} - :address {:type :add_address} - :locations {:type '(list String)} - :matches {:type '(list String)} - :location_matches {:type '(list :edit_location_match)} - :square_locations {:type '(list :edit_square_location)} - :ezcater_locations {:type '(list :edit_ezcater_location)} - :bank_accounts {:type '(list :edit_bank_account)} - :forecasted_transactions {:type '(list :edit_forecasted_transaction)}}} - - :edit_bank_account - {:fields {:id {:type :id} - :code {:type 'String} - :type {:type :bank_account_type} - :start_date {:type :iso_date} - :number {:type 'String} - :check_number {:type 'Int} - :numeric_code {:type 'Int} - :visible {:type 'Boolean} - :include_in_reports {:type 'Boolean} - :sort_order {:type 'Int} - :name {:type 'String} - :bank_code {:type 'String} - :routing {:type 'String} - :bank_name {:type 'String} - :locations {:type '(list String)} - :yodlee_account_id {:type 'Int} - :use_date_instead_of_post_date {:type 'Boolean} - :intuit_bank_account {:type :id} - :plaid_account {:type :id} - :yodlee_account {:type 'Int}}}}) + :sort {:type '(list :sort_item)}}} }) (def enums {:bank_account_type {:values [{:enum-value :check} @@ -620,9 +193,7 @@ (def resolvers {:get-client get-client :get-admin-client get-admin-client - :get-client-page get-client-page - :mutation/edit-client edit-client - :mutation/setup-sales-queries setup-sales-queries}) + :get-client-page get-client-page }) (defn attach [schema] diff --git a/src/clj/auto_ap/handler.clj b/src/clj/auto_ap/handler.clj index 8c584f5b..ffc61e4f 100644 --- a/src/clj/auto_ap/handler.clj +++ b/src/clj/auto_ap/handler.clj @@ -217,7 +217,7 @@ (pull-many (dc/db conn) d-clients/full-read))] - (mu/with-context {:clients (map :client/code clients)} + (mu/with-context {:clients (take 10 (map :client/code clients))} (handler (assoc request :clients clients :client (when (= 1 (count clients)) @@ -273,7 +273,7 @@ (handler request))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} -(def app +(defonce app (-> route-handler (wrap-hx-current-url-params) (wrap-guess-route) @@ -293,7 +293,7 @@ (byte-array [42, 52, -31, 105, -126, -33, -118, -69, -82, -59, -15, -69, -38, 103, -102, -1])} )}) - (wrap-reload) + #_(wrap-reload) (wrap-params) (mp/wrap-multipart-params) (wrap-edn-params) diff --git a/src/clj/auto_ap/jobs/bulk_journal_import.clj b/src/clj/auto_ap/jobs/bulk_journal_import.clj index 600bdf72..ddd9f377 100644 --- a/src/clj/auto_ap/jobs/bulk_journal_import.clj +++ b/src/clj/auto_ap/jobs/bulk_journal_import.clj @@ -1,5 +1,5 @@ (ns auto-ap.jobs.bulk-journal-import - (:gen-class) + #_(:gen-class) (:require [amazonica.aws.s3 :as s3] [auto-ap.graphql.ledger :refer [import-ledger]] diff --git a/src/clj/auto_ap/jobs/close_auto_invoices.clj b/src/clj/auto_ap/jobs/close_auto_invoices.clj index 13ec8e95..e81c2e2f 100644 --- a/src/clj/auto_ap/jobs/close_auto_invoices.clj +++ b/src/clj/auto_ap/jobs/close_auto_invoices.clj @@ -1,5 +1,5 @@ (ns auto-ap.jobs.close-auto-invoices - (:gen-class) + #_(:gen-class) (:require [auto-ap.datomic :refer [conn]] [auto-ap.jobs.core :refer [execute]] diff --git a/src/clj/auto_ap/jobs/current_balance_cache.clj b/src/clj/auto_ap/jobs/current_balance_cache.clj index 8f9d447a..22636d82 100644 --- a/src/clj/auto_ap/jobs/current_balance_cache.clj +++ b/src/clj/auto_ap/jobs/current_balance_cache.clj @@ -1,5 +1,5 @@ (ns auto-ap.jobs.current-balance-cache - (:gen-class) + #_(:gen-class) (:require [auto-ap.graphql.clients :as clients] [auto-ap.jobs.core :refer [execute]])) diff --git a/src/clj/auto_ap/jobs/ezcater_upsert.clj b/src/clj/auto_ap/jobs/ezcater_upsert.clj index f28c3a34..1a4deb35 100644 --- a/src/clj/auto_ap/jobs/ezcater_upsert.clj +++ b/src/clj/auto_ap/jobs/ezcater_upsert.clj @@ -1,5 +1,5 @@ (ns auto-ap.jobs.ezcater-upsert - (:gen-class) + #_(:gen-class) (:require [auto-ap.jobs.core :refer [execute]] [auto-ap.ezcater.core :as ezcater])) diff --git a/src/clj/auto_ap/jobs/intuit.clj b/src/clj/auto_ap/jobs/intuit.clj index 787e1dfa..0947a6be 100644 --- a/src/clj/auto_ap/jobs/intuit.clj +++ b/src/clj/auto_ap/jobs/intuit.clj @@ -1,5 +1,5 @@ (ns auto-ap.jobs.intuit - (:gen-class) + #_(:gen-class) (:require [auto-ap.import.intuit :as intuit] [auto-ap.jobs.core :refer [execute]])) diff --git a/src/clj/auto_ap/jobs/ledger_reconcile.clj b/src/clj/auto_ap/jobs/ledger_reconcile.clj index 76dcc2b7..81a0a363 100644 --- a/src/clj/auto_ap/jobs/ledger_reconcile.clj +++ b/src/clj/auto_ap/jobs/ledger_reconcile.clj @@ -1,5 +1,5 @@ (ns auto-ap.jobs.ledger-reconcile - (:gen-class) + #_(:gen-class) (:require [auto-ap.jobs.core :refer [execute]] [auto-ap.ledger :as ledger])) diff --git a/src/clj/auto_ap/jobs/load_historical_sales.clj b/src/clj/auto_ap/jobs/load_historical_sales.clj index 230a063c..6b035169 100644 --- a/src/clj/auto_ap/jobs/load_historical_sales.clj +++ b/src/clj/auto_ap/jobs/load_historical_sales.clj @@ -1,5 +1,5 @@ (ns auto-ap.jobs.load-historical-sales - (:gen-class) + #_(:gen-class) (:require [auto-ap.datomic :refer [conn]] [auto-ap.jobs.core :refer [execute]] diff --git a/src/clj/auto_ap/jobs/plaid.clj b/src/clj/auto_ap/jobs/plaid.clj index ae552e59..99029233 100644 --- a/src/clj/auto_ap/jobs/plaid.clj +++ b/src/clj/auto_ap/jobs/plaid.clj @@ -1,5 +1,5 @@ (ns auto-ap.jobs.plaid - (:gen-class) + #_(:gen-class) (:require [auto-ap.import.plaid :as plaid] [auto-ap.jobs.core :refer [execute]])) diff --git a/src/clj/auto_ap/jobs/register_invoice_import.clj b/src/clj/auto_ap/jobs/register_invoice_import.clj index 48862d59..8979bf96 100644 --- a/src/clj/auto_ap/jobs/register_invoice_import.clj +++ b/src/clj/auto_ap/jobs/register_invoice_import.clj @@ -1,5 +1,5 @@ (ns auto-ap.jobs.register-invoice-import - (:gen-class) + #_(:gen-class) (:require [amazonica.aws.s3 :as s3] [auto-ap.datomic :refer [audit-transact conn pull-attr]] diff --git a/src/clj/auto_ap/jobs/square.clj b/src/clj/auto_ap/jobs/square.clj index db22c9b5..49f1d57f 100644 --- a/src/clj/auto_ap/jobs/square.clj +++ b/src/clj/auto_ap/jobs/square.clj @@ -1,5 +1,5 @@ (ns auto-ap.jobs.square - (:gen-class) + #_(:gen-class) (:require [auto-ap.jobs.core :refer [execute]] [auto-ap.square.core3 :as square3])) diff --git a/src/clj/auto_ap/jobs/vendor_usages.clj b/src/clj/auto_ap/jobs/vendor_usages.clj index 1dfe76da..93e7eb9c 100644 --- a/src/clj/auto_ap/jobs/vendor_usages.clj +++ b/src/clj/auto_ap/jobs/vendor_usages.clj @@ -1,5 +1,5 @@ (ns auto-ap.jobs.vendor-usages - (:gen-class) + #_(:gen-class) (:require [auto-ap.datomic :refer [conn]] [auto-ap.jobs.core :refer [execute]] diff --git a/src/clj/auto_ap/jobs/yodlee2.clj b/src/clj/auto_ap/jobs/yodlee2.clj index aec0e65b..7c6a9f1e 100644 --- a/src/clj/auto_ap/jobs/yodlee2.clj +++ b/src/clj/auto_ap/jobs/yodlee2.clj @@ -1,5 +1,5 @@ (ns auto-ap.jobs.yodlee2 - (:gen-class) + #_(:gen-class) (:require [auto-ap.import.yodlee2 :as yodlee2] [auto-ap.jobs.core :refer [execute]] diff --git a/src/clj/auto_ap/server.clj b/src/clj/auto_ap/server.clj index 0f53c817..8f19b565 100644 --- a/src/clj/auto_ap/server.clj +++ b/src/clj/auto_ap/server.clj @@ -1,5 +1,5 @@ (ns auto-ap.server - (:gen-class) + #_(:gen-class) (:require [auto-ap.handler :refer [app]] [auto-ap.jobs.restore-from-backup :as job-restore-from-backup] diff --git a/src/clj/auto_ap/ssr/admin/background_jobs.clj b/src/clj/auto_ap/ssr/admin/background_jobs.clj index c0896ebc..6922232a 100644 --- a/src/clj/auto_ap/ssr/admin/background_jobs.clj +++ b/src/clj/auto_ap/ssr/admin/background_jobs.clj @@ -36,14 +36,20 @@ (sort-by :created-at) reverse)) -(defn is-background-job? [task] + +(defn is-background-job? + "This function checks whether a given task is a background job. + It does this by checking the environment of the task's container definitions for an environment variable + with the name 'INTEGREAT_JOB'. If such a variable exists, the function returns true, otherwise, it returns false. + Parameters: task - a map representing the task to be checked. + Returns: true if the task is a background job, false otherwise." + [task] (->> task :task-definition :container-definitions (mapcat :environment) (filter (comp #{"INTEGREAT_JOB"} :name)) seq)) - (defn task-definition->job-name [task-definition] (->> (:container-definitions task-definition) (mapcat :environment) diff --git a/src/clj/auto_ap/ssr/admin/clients.clj b/src/clj/auto_ap/ssr/admin/clients.clj new file mode 100644 index 00000000..c466330a --- /dev/null +++ b/src/clj/auto_ap/ssr/admin/clients.clj @@ -0,0 +1,1721 @@ +(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.routes.admin.clients :as route] + [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] + [auto-ap.ssr.hiccup-helper :as hh] + [auto-ap.ssr.hx :as hx] + [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 + modal-response ref->enum-schema strip temp-id wrap-entity + 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 + +(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 (:parsed-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 (:parsed-query-params request)) + :placeholder "BRLC" + :size :small}))]]) + +(def default-read '[:db/id + :client/name + :client/code + :client/locations + :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/number + :bank-account/bank-code + :bank-account/sort-order + :bank-account/routing + :bank-account/check-number + [: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 (:parsed-query-params request) + valid-clients (extract-client-ids (:clients request) + (:client request) + (: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) + + (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 + :parse-query-params (helper/default-parse-query-params grid-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" + :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 (time/in-days (time/interval locked-until (time/now)))] + (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 [: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/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/include-in-reports {: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?]]]) + +(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/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 {} + [: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/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/include-in-reports {: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?]])]] + [:client/matches [: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 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"]]}))) + + (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 + (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" + :class "w-[5em]" + :value (some-> (fc/field-value) + + (clj-time.coerce/to-date-time) + (atime/unparse-local atime/normal-date))})]))]) + + + +(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]"} + (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 ?pn + :in $ ?client + :where [?pi :plaid-item/client ?client] + [?pi :plaid-item/accounts ?pa] + [?pa :plaid-account/name ?pn]] + (dc/db conn) + client-id))})))) + +(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")))) +(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 + (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" + :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))})])) + + (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")) + + [: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*)))]) + +(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 + (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" + :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))})])) + + (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")) + + [: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*)))]) + + + +(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] + #_(alog/peek (keys (:query-params request))) + + (let [locations @(de/timeout! + (de/chain (square/client-locations {:client/square-auth-token (get-in request [:query-params "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 + (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) + :error? (fc/error?) + :hx-get (bidi/path-for ssr-routes/only-routes ::route/refresh-square-locations) + :hx-trigger "keyup changed delay:1s queue:none" + :hx-indicator "#square-locations" + :hx-target "#square-locations" + :placeholder "Token from square" + :class "w-64" + :value (fc/field-value)}))) + + (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 + [_ request] + (let [bank-account-type (get-in request [:query-params :bank-account-type])] + (cond-> + {:db/id (str (java.util.UUID/randomUUID)) + :new? true} + + bank-account-type (assoc :bank-account/type (keyword "bank-account-type" bank-account-type))))) + + 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})) + + (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"))))) + :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/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)))) + _ (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-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-admin) + (wrap-client-redirect-unauthenticated))))) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/admin/sales_powerqueries.clj b/src/clj/auto_ap/ssr/admin/sales_powerqueries.clj new file mode 100644 index 00000000..e69de29b diff --git a/src/clj/auto_ap/ssr/admin/transaction_rules.clj b/src/clj/auto_ap/ssr/admin/transaction_rules.clj index 81f4b989..3dec9d8f 100644 --- a/src/clj/auto_ap/ssr/admin/transaction_rules.clj +++ b/src/clj/auto_ap/ssr/admin/transaction_rules.clj @@ -1,60 +1,43 @@ (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]] - [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] - [auto-ap.routes.admin.transaction-rules :as route] - [auto-ap.routes.utils - :refer [wrap-admin wrap-client-redirect-unauthenticated]] - [auto-ap.rule-matching :as rm] - [auto-ap.solr :as solr] - [auto-ap.ssr-routes :as ssr-routes] - [auto-ap.ssr.company :refer [bank-account-typeahead*]] - [auto-ap.ssr.components :as com] - [auto-ap.ssr.form-cursor :as fc] - [auto-ap.ssr.grid-page-helper :as helper] - [auto-ap.ssr.hx :as hx] - [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] - [auto-ap.ssr.svg :as svg] - [auto-ap.ssr.utils - :refer [apply-middleware-to-all-handlers - entity-id - field-validation-error - form-validation-error - html-response - main-transformer - many-entity - modal-response - money - percentage - ref->enum-schema - ref->radio-options - regex - temp-id - wrap-entity - wrap-form-4xx-2 - wrap-schema-enforce]] - [auto-ap.time :as atime] - [auto-ap.utils :refer [dollars=]] - [bidi.bidi :as bidi] - [clj-time.coerce :as coerce] - [clojure.set :as set] - [clojure.string :as str] - [datomic.api :as dc] - [malli.core :as mc] - [auto-ap.logging :as alog])) + (: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]] + [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] + [auto-ap.routes.admin.transaction-rules :as route] + [auto-ap.routes.utils + :refer [wrap-admin wrap-client-redirect-unauthenticated]] + [auto-ap.rule-matching :as rm] + [auto-ap.solr :as solr] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.common-handlers :refer [add-new-entity-handler]] + [auto-ap.ssr.company :refer [bank-account-typeahead*]] + [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] + [auto-ap.ssr.hx :as hx] + [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] + [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.utils + :refer [apply-middleware-to-all-handlers entity-id + field-validation-error form-validation-error + html-response main-transformer many-entity modal-response + money percentage ref->enum-schema ref->radio-options + regex temp-id wrap-entity wrap-form-4xx-2 + wrap-schema-enforce]] + [auto-ap.time :as atime] + [auto-ap.utils :refer [dollars=]] + [bidi.bidi :as bidi] + [clj-time.coerce :as coerce] + [clojure.set :as set] + [clojure.string :as str] + [datomic.api :as dc] + [malli.core :as mc] + [malli.util :as mut])) (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" @@ -66,13 +49,13 @@ [:fieldset.space-y-6 (com/field {:label "Vendor"} (com/typeahead {:name "vendor" - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes - :vendor-search) - :id (str "vendor-search") - :value (:vendor (:parsed-query-params request)) - :value-fn :db/id - :content-fn :vendor/name})) + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes + :vendor-search) + :id (str "vendor-search") + :value (:vendor (:parsed-query-params request)) + :value-fn :db/id + :content-fn :vendor/name})) (com/field {:label "Note"} (com/text-input {:name "note" :id "note" @@ -121,17 +104,17 @@ :where []} :args [db]} (:sort query-params) (add-sorter-fields {"client" ['[?e :transaction-rule/client ?c] - '[?c :client/name ?sort-client]] + '[?c :client/name ?sort-client]] - "yodlee-merchant" ['[?e :transaction-rule/yodlee-merchant ?ym] - '[?ym :yodlee-merchant/name ?sort-yodlee-merchant]] - "bank-account" ['[?e :transaction-rule/bank-account ?ba] - '[?ba :bank-account/name ?sort-bank-account]] - "description" ['[?e :transaction-rule/description ?sort-description]] - "note" ['[?e :transaction-rule/note ?sort-note]] - "amount-lte" ['[?e :transaction-rule/amount-lte ?sort-amount-lte]] - "amount-gte" ['[?e :transaction-rule/amount-gte ?sort-amount-gte]]} - query-params) + "yodlee-merchant" ['[?e :transaction-rule/yodlee-merchant ?ym] + '[?ym :yodlee-merchant/name ?sort-yodlee-merchant]] + "bank-account" ['[?e :transaction-rule/bank-account ?ba] + '[?ba :bank-account/name ?sort-bank-account]] + "description" ['[?e :transaction-rule/description ?sort-description]] + "note" ['[?e :transaction-rule/note ?sort-note]] + "amount-lte" ['[?e :transaction-rule/amount-lte ?sort-amount-lte]] + "amount-gte" ['[?e :transaction-rule/amount-gte ?sort-amount-gte]]} + query-params) (= 1 (count valid-clients)) (merge-query {:query {:in '[?x] @@ -159,7 +142,7 @@ true (merge-query {:query {:find ['?e] :where ['[?e :transaction-rule/transaction-approval-status]]}}))] - + (cond->> (query2 query) true (apply-sort-3 query-params) true (apply-pagination query-params)))) @@ -185,16 +168,16 @@ :page-specific-nav filters :fetch-page fetch-page :parse-query-params (comp - (query-params/parse-key :vendor #(dc/pull (dc/db conn) '[:vendor/name :db/id] (Long/parseLong %))) - (helper/default-parse-query-params grid-page)) + (query-params/parse-key :vendor #(dc/pull (dc/db conn) '[:vendor/name :db/id] (Long/parseLong %))) + (helper/default-parse-query-params grid-page)) :action-buttons (fn [request] [(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/new-dialog)) :color :primary} "New Transaction Rule")]) :row-buttons (fn [request entity] [(com/icon-button {:hx-delete (bidi/path-for ssr-routes/only-routes - ::route/delete - :db/id (:db/id entity)) + ::route/delete + :db/id (:db/id entity)) :hx-confirm "Are you sure you want to delete?"} svg/trash) (com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes @@ -230,7 +213,7 @@ :sort-key "amount" :render (fn [{:transaction-rule/keys [amount-gte amount-lte]}] [:div.flex.gap-2 (when amount-gte - (com/pill {:color :red} (format "more than $%.2f" amount-gte))) + (com/pill {:color :red} (format "more than $%.2f" amount-gte))) (when amount-lte (com/pill {:color :primary} (format "less than $%.2f" amount-lte)))]) @@ -238,8 +221,7 @@ {:key "note" :name "Note" :sort-key "note" - :render :transaction-rule/note} - ]})) + :render :transaction-rule/note}]})) (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) @@ -264,9 +246,9 @@ (defn bank-account-belongs-to-client? [bank-account-id client-id] (get (->> (dc/pull (dc/db conn) [{:client/bank-accounts [:db/id]}] client-id) - :client/bank-accounts - (map :db/id) - (set)) + :client/bank-accounts + (map :db/id) + (set)) bank-account-id)) (defn validate-transaction-rule [form-params] @@ -289,23 +271,7 @@ [:transaction-rule/bank-account] :form form-params))) -(defn save [{:keys [form-params request-method identity] :as request}] - (validate-transaction-rule form-params) - (let [entity (cond-> form-params - (= :post request-method) (assoc :db/id "new") - true (assoc :transaction-rule/note (entity->note form-params))) - {: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"))))) + @@ -313,7 +279,7 @@ :transaction/bank-account [:bank-account/name]} :transaction/description-original :db/id - [:transaction/date :xform clj-time.coerce/from-date]]) + [:transaction/date :xform clj-time.coerce/from-date]]) (defn transactions-matching-rule [{{:transaction-rule/keys [description client bank-account amount-lte amount-gte dom-lte dom-gte]} :entity @@ -326,7 +292,7 @@ query (cond-> {:query {:find ['(pull ?e read)] :in ['$ 'read] :where []} - :args [(dc/db conn) transaction-read]} + :args [(dc/db conn) transaction-read]} description (merge-query {:query {:in ['?descr] :where ['[(iol-ion.query/->pattern ?descr) ?description-regex]]} @@ -378,74 +344,43 @@ true (merge-query {:query {:where ['[?e :transaction/id]]}})) - results (->> - (query2 query) - (map first))] + results (->> + (query2 query) + (map first))] results)) (defn transaction-rule-test-table* [{:keys [entity clients checkboxes? only-uncoded?]}] (let [results (transactions-matching-rule - {:entity entity - :clients clients - :only-uncoded? only-uncoded?})] - + {:entity entity + :clients clients + :only-uncoded? only-uncoded?})] + [:div#transaction-test-results - [:h2.my-4.text-lg.flex {:x-data (hx/json {:resultCount (count results)})} "Matching transactions" + [:h2.my-4.text-lg.flex {:x-data (hx/json {:resultCount (count results)})} "Matching transactions" [:div.ml-4.relative (com/badge {:class "text-[0.6rem]"} (let [cnt (count results)] (if (>= cnt 99) "99+" cnt)))] [:div.flex.justify-end.flex-1 [:div.gutter]]] (com/data-grid - {:headers [(when checkboxes? - (com/data-grid-checkbox-header {:name "all"})) - (com/data-grid-header {} "Client") - (com/data-grid-header {} "Bank") - (com/data-grid-header {} "Date") - (com/data-grid-header {} "Description")]} - (for [r (take 15 results)] - (com/data-grid-row - {} - (when checkboxes? - (com/data-grid-cell {} (com/checkbox {:name "transaction-id" :value (:db/id r)}))) - (com/data-grid-cell {} (-> r :transaction/client :client/name)) - (com/data-grid-cell {} (-> r :transaction/bank-account :bank-account/name)) - (com/data-grid-cell {} (some-> r :transaction/date (atime/unparse-local atime/normal-date))) - (com/data-grid-cell {} (some-> r :transaction/description-original )))))])) - -(defn test [{:keys [form-params request-method identity] :as request - entity :form-params}] - (validate-transaction-rule form-params) - (html-response - (com/stacked-modal-card - 1 - {} - [:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"] ] - (transaction-rule-test-table* {:entity entity - :clients (:clients request)}) - [:div.flex.justify-between - - (com/button {"@click" "$dispatch('modalpop')" - :class "w-32"} - "Back") - (com/button (cond-> {:color :primary - :hx-include "#my-form" - :class "w-32" - } - (:db/id form-params) (assoc :hx-put (bidi/path-for ssr-routes/only-routes ::route/save)) - (not (:db/id form-params)) (assoc :hx-post (bidi/path-for ssr-routes/only-routes ::route/save))) - "Save rule")]) - :headers (-> {} - (assoc "hx-trigger-after-settle" "modalnext") - (assoc "hx-retarget" ".modal-stack") - (assoc "hx-reswap" "beforeend")))) - - -;; TODO only uncoded - + {:headers [(when checkboxes? + (com/data-grid-checkbox-header {:name "all"})) + (com/data-grid-header {} "Client") + (com/data-grid-header {} "Bank") + (com/data-grid-header {} "Date") + (com/data-grid-header {} "Description")]} + (for [r (take 15 results)] + (com/data-grid-row + {} + (when checkboxes? + (com/data-grid-cell {} (com/checkbox {:name "transaction-id" :value (:db/id r)}))) + (com/data-grid-cell {} (-> r :transaction/client :client/name)) + (com/data-grid-cell {} (-> r :transaction/bank-account :bank-account/name)) + (com/data-grid-cell {} (some-> r :transaction/date (atime/unparse-local atime/normal-date))) + (com/data-grid-cell {} (some-> r :transaction/description-original)))))])) (defn- location-select* - [{:keys [ name account-location client-locations value]}] + [{:keys [name account-location client-locations value]}] (com/select {:options (into [["" ""]] (cond account-location [[account-location account-location]] @@ -464,266 +399,77 @@ [{:keys [name value client-id x-model]}] [:div.flex.flex-col (com/typeahead {:name name - :placeholder "Search..." - :url (str (bidi/path-for ssr-routes/only-routes :account-search) "?client-id=" client-id) - :id name - :x-model x-model - :value value - :content-fn (fn [value] - (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) - client-id)))})]) + :placeholder "Search..." + :url (str (bidi/path-for ssr-routes/only-routes :account-search) "?client-id=" client-id) + :id name + :x-model x-model + :value value + :content-fn (fn [value] + (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) + client-id)))})]) (defn- transaction-rule-account-row* [account client-id client-locations] (com/data-grid-row - (-> {:x-data (hx/json {:accountId (or (:db/id (fc/field-value (:transaction-rule-account/account account))) - (fc/field-value (:transaction-rule-account/account account))) - :location (fc/field-value (:transaction-rule-account/location account)) - :show (boolean (not (fc/field-value (:new? account))))}) - :data-key "show" - :x-ref "p"} - hx/alpine-mount-then-appear) - (let [account-name (fc/field-name (:transaction-rule-account/account account))] - (list - - (fc/with-field :db/id - (com/hidden {:name (fc/field-name) - :value (fc/field-value)})) - (fc/with-field :transaction-rule-account/account - (com/data-grid-cell - {} - (com/validated-field - {:errors (fc/field-errors)} - [:div {:hx-trigger "changed" - :hx-target "next div" - :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', value: event.detail.accountId || ''}" account-name) - :hx-get (str (bidi/path-for ssr-routes/only-routes ::route/account-typeahead)) - :x-init "$watch('clientId', cid => $dispatch('changed', $data));"}] - (account-typeahead* {:value (fc/field-value) - :client-id client-id - :name (fc/field-name) - :x-model "accountId"})))) - (fc/with-field :transaction-rule-account/location - (com/data-grid-cell - {} - (com/validated-field - {:errors (fc/field-errors) - :x-data (hx/json {:location (fc/field-value)})} - [:div {:hx-trigger "changed" - :hx-target "next *" - :hx-swap "outerHTML" - :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || '', value: event.detail.location || ''}" (fc/field-name) ) - :hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select) - :x-init "$watch('clientId', cid => $dispatch('changed', $data)); $watch('accountId', cid => $dispatch('changed', $data) )"}] - (location-select* {:name (fc/field-name) - :account-location (:account/location (cond->> (:transaction-rule-account/account @account) - (nat-int? (:transaction-rule-account/account @account)) (dc/pull (dc/db conn) - '[:account/location]))) - :client-locations client-locations - :x-model "location" - :value (fc/field-value)})))) - (fc/with-field :transaction-rule-account/percentage - (com/data-grid-cell - {} - (com/validated-field - {:errors (fc/field-errors)} - (com/money-input {:name (fc/field-name) - :class "w-16" - :value (some-> (fc/field-value) - (* 100 ) - (long ))})))))) - (com/data-grid-cell {:class "align-top"} - (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) + (-> {:x-data (hx/json {:accountId (or (:db/id (fc/field-value (:transaction-rule-account/account account))) + (fc/field-value (:transaction-rule-account/account account))) + :location (fc/field-value (:transaction-rule-account/location account)) + :show (boolean (not (fc/field-value (:new? account))))}) + :data-key "show" + :x-ref "p"} + hx/alpine-mount-then-appear) + (let [account-name (fc/field-name (:transaction-rule-account/account account))] + (list -(defn dialog* [{:keys [entity form-params form-errors]}] - (fc/start-form form-params form-errors - (com/modal - {:modal-class "max-w-2xl" - :hx-target "this" - } + (fc/with-field :db/id + (com/hidden {:name (fc/field-name) + :value (fc/field-value)})) + (fc/with-field :transaction-rule-account/account + (com/data-grid-cell + {} + (com/validated-field + {:errors (fc/field-errors)} + [:div {:hx-trigger "changed" + :hx-target "next div" + :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', value: event.detail.accountId || ''}" account-name) + :hx-get (str (bidi/path-for ssr-routes/only-routes ::route/account-typeahead)) + :x-init "$watch('clientId', cid => $dispatch('changed', $data));"}] + (account-typeahead* {:value (fc/field-value) + :client-id client-id + :name (fc/field-name) + :x-model "accountId"})))) + (fc/with-field :transaction-rule-account/location + (com/data-grid-cell + {} + (com/validated-field + {:errors (fc/field-errors) + :x-data (hx/json {:location (fc/field-value)})} + [:div {:hx-trigger "changed" + :hx-target "next *" + :hx-swap "outerHTML" + :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || '', value: event.detail.location || ''}" (fc/field-name)) + :hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select) + :x-init "$watch('clientId', cid => $dispatch('changed', $data)); $watch('accountId', cid => $dispatch('changed', $data) )"}] + (location-select* {:name (fc/field-name) + :account-location (:account/location (cond->> (:transaction-rule-account/account @account) + (nat-int? (:transaction-rule-account/account @account)) (dc/pull (dc/db conn) + '[:account/location]))) + :client-locations client-locations + :x-model "location" + :value (fc/field-value)})))) + (fc/with-field :transaction-rule-account/percentage + (com/data-grid-cell + {} + (com/validated-field + {:errors (fc/field-errors)} + (com/money-input {:name (fc/field-name) + :class "w-16" + :value (some-> (fc/field-value) + (* 100) + (long))})))))) + (com/data-grid-cell {:class "align-top"} + (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) - (com/stacked-modal-card - 0 - {} - [:div.flex [:div.p-2 "Transaction Rule"]] - [:form#my-form {:hx-ext "response-targets" - :hx-target-400 "#form-errors .error-content" - :hx-indicator "#submit" - :x-trap "true" - (if (:db/id entity) - :hx-put - :hx-post) (str (bidi/path-for ssr-routes/only-routes ::route/save))} - [:fieldset {:class "hx-disable" - :x-data (hx/json {:clientId (or (:db/id (:transaction-rule/client form-params)) - (:transaction-rule/client form-params) - (:db/id (:transaction-rule/client entity)))})} - - [:div.space-y-1 - (when-let [id (:db/id entity)] - (com/hidden {:name "db/id" - :value id})) - (fc/with-field :transaction-rule/description - (com/validated-field {:label "Description" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :error? (fc/error?) - :x-init "$el.focus()" - :placeholder "HOME DEPOT" - :class "w-96" - :value (fc/field-value)}))) - [:div.filters {:x-data (hx/json {:clientFilter (boolean (fc/field-value (:transaction-rule/client fc/*current*))) - :bankAccountFilter (boolean (fc/field-value (:transaction-rule/bank-account fc/*current*))) - :amountFilter (boolean (or (fc/field-value (:transaction-rule/amount-gte fc/*current*)) - (fc/field-value (:transaction-rule/amount-lte fc/*current*)))) - :domFilter (boolean (or (fc/field-value (:transaction-rule/dom-gte fc/*current*)) - (fc/field-value (:transaction-rule/dom-lte fc/*current*))))})} - - [:div.flex.gap-2.mb-2 - (com/a-button {"@click" "clientFilter=true" - "x-show" "!clientFilter"} "Filter client") - (com/a-button {"@click" "bankAccountFilter=true" - "x-show" "clientFilter && !bankAccountFilter"} "Filter bank account") - (com/a-button {"@click" "amountFilter=true" - "x-show" "!amountFilter"} "Filter amount") - (com/a-button {"@click" "domFilter=true" - "x-show" "!domFilter"} "Filter day of month")] - (fc/with-field :transaction-rule/client - - (com/validated-field - (-> {:label "Client" - :errors (fc/field-errors) - :x-show "clientFilter"} - (hx/alpine-appear)) - [: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) - :x-model "clientId" - :value (fc/field-value) - :content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})])) - - (fc/with-field :transaction-rule/bank-account - (com/validated-field - (-> {:label "Bank Account" - :errors (fc/field-errors) - :x-show "bankAccountFilter"} - hx/alpine-appear) - [:div.w-96 - [:div#bank-account-changer {:hx-get (bidi/path-for ssr-routes/only-routes :bank-account-typeahead) - :hx-trigger "changed" - :hx-target "next *" - :hx-include "#bank-account-changer" - :hx-swap "innerHTML" - - :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" (fc/field-name)) - :x-init "$watch('clientId', cid => $dispatch('changed', $data))"}] - - (bank-account-typeahead* {:client-id (:transaction-rule/client form-params) - :name (fc/field-name) - :value (fc/field-value)})])) - - (com/field (-> {:label "Amount" - :x-show "amountFilter"} - hx/alpine-appear) - [:div.flex.gap-2 - (fc/with-field :transaction-rule/amount-gte - [:div.flex.flex-col - (com/money-input {:name (fc/field-name) - :placeholder ">=" - :class "w-24" - :value (fc/field-value)}) - (com/errors {:errors (fc/field-errors)})]) - (fc/with-field :transaction-rule/amount-lte - [:div.flex.flex-col - (com/money-input {:name (fc/field-name) - :placeholder "<=" - :class "w-24" - :value (fc/field-value)}) - (com/errors {:errors (fc/field-errors)})])]) - - (com/field (-> {:label "Day of month" - :x-show "domFilter"} - hx/alpine-appear) - [:div.flex.gap-2 - (fc/with-field :transaction-rule/dom-gte - (com/validated-field - {:errors (fc/field-errors)} - (com/int-input {:name (fc/field-name) - :placeholder ">=" - :class "w-24" - :value (fc/field-value)}))) - (fc/with-field :transaction-rule/dom-lte - (com/validated-field - {:errors (fc/field-errors)} - (com/int-input {:name (fc/field-name) - :placeholder ">=" - :class "w-24" - :value (fc/field-value)})))])] - - [:h2.text-lg "Outcomes"] - (fc/with-field :transaction-rule/vendor - (com/validated-field {:label "Assign Vendor" - :errors (fc/field-errors)} - [:div.w-96 - (com/typeahead {:name (fc/field-name) - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes :vendor-search) - :class "w-96" - :value (fc/field-value) - :content-fn #(pull-attr (dc/db conn) :vendor/name %)})])) - - (fc/with-field :transaction-rule/accounts - (com/validated-field - {:errors (fc/field-errors)} - (let [client-locations (some->> form-params :transaction-rule/client (pull-attr (dc/db conn) :client/locations))] - (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-rule-account-row* % (:transaction-rule/client form-params) client-locations)) - (com/data-grid-new-row {:colspan 4 - :hx-get (bidi/path-for ssr-routes/only-routes - ::route/new-account) - :index (count (fc/field-value)) - :tr-params (hx/bind-alpine-vals {} {"client-id" "clientId"})} - "New account"))))) - - (fc/with-field :transaction-rule/transaction-approval-status - (com/validated-field {:label "Approval status" - :errors (fc/field-errors)} - (com/radio {:options (ref->radio-options "transaction-approval-status") - :value (fc/field-value) - :name (fc/field-name) - :size :small - :orientation :horizontal})))]]] - [:div - (com/form-errors {:errors (:errors fc/*form-errors*)}) - [:div.flex.justify-end - - (com/validated-save-button {:errors form-errors :color :secondary - :hx-post (bidi/path-for ssr-routes/only-routes ::route/test) - :hx-include "#my-form"} - - "Test rule") - (com/validated-save-button {:errors form-errors - :id "submit" - :form "my-form"} "Save rule")]])))) - - -(defn new-account [{{:keys [client-id index]} :query-params}] - (html-response - (fc/start-form-with-prefix - [:transaction-rule/accounts (or index 0)] - {:db/id (str (java.util.UUID/randomUUID)) - :transaction-rule-account/location "Shared" - :new? true} - [] - (transaction-rule-account-row* - fc/*current* - client-id - (some->> client-id (pull-attr (dc/db conn) :client/locations)))))) (defn all-ids-not-locked [all-ids] (->> all-ids @@ -734,7 +480,7 @@ [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu] [?t :transaction/date ?d] [(>= ?d ?lu)]] - (dc/db conn)) + (dc/db conn)) (map first))) (defn execute [{:keys [form-params clients entity identity]}] @@ -743,7 +489,7 @@ :only-uncoded? true}) (map :db/id) (into #{})) - + ids (if (not-empty (:all form-params)) all-results (set/intersection (into #{} (:transaction-id form-params)) @@ -751,25 +497,24 @@ ids (all-ids-not-locked ids) transactions (transduce - (comp - (map d-transactions/get-by-id) - (map #(update % :transaction/date coerce/to-date))) - conj - [] - ids) + (comp + (map d-transactions/get-by-id) + (map #(update % :transaction/date coerce/to-date))) + conj + [] + ids) entity (update entity :transaction-rule/description #(some-> % iol-ion.query/->pattern)) ;; TODO #_#_x (doseq [transaction transactions] - (when (not (rm/rule-applies? transaction entity)) - (throw (ex-info "Transaction rule does not apply" {:validation-error "Transaction rule does not apply" - :transaction-rule entity - :transaction transaction}))) + (when (not (rm/rule-applies? transaction entity)) + (throw (ex-info "Transaction rule does not apply" {:validation-error "Transaction rule does not apply" + :transaction-rule entity + :transaction transaction}))) - (when (:transaction/payment transaction) + (when (:transaction/payment transaction) - (throw (ex-info "Transaction already associated with a payment" {:validation-error "Transaction already associated with a payment"})))) - ] + (throw (ex-info "Transaction already associated with a payment" {:validation-error "Transaction already associated with a payment"}))))] (audit-transact (mapv (fn [t] [:upsert-transaction @@ -805,69 +550,58 @@ :x-model "accountId"}))) (def form-schema (mc/schema - [:map - [:db/id {:optional true} [:maybe entity-id]] - [:transaction-rule/client {:optional true} [:maybe entity-id]] - [:transaction-rule/description [:and regex - [:string {:min 3}]]] - [:transaction-rule/bank-account [:maybe entity-id]] - [:transaction-rule/amount-gte {:optional true} [:maybe money]] - [:transaction-rule/amount-lte {:optional true} [:maybe money]] - [:transaction-rule/dom-gte {:optional true} [:maybe :int]] - [:transaction-rule/dom-lte {:optional true} [:maybe :int]] - [:transaction-rule/vendor {:optional true} [:maybe entity-id]] - [:transaction-rule/transaction-approval-status (ref->enum-schema "transaction-approval-status")] - [:transaction-rule/accounts - (many-entity {:min 1} - [:db/id [:or entity-id temp-id]] - [:transaction-rule-account/account entity-id] - [:transaction-rule-account/location [:string {:min 1 :error/message "required"}]] - [:transaction-rule-account/percentage percentage])]])) - -(defn edit-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}))) - + [:map + [:db/id {:optional true} [:maybe entity-id]] + [:transaction-rule/client {:optional true} [:maybe entity-id]] + [:transaction-rule/description [:and regex + [:string {:min 3}]]] + [:transaction-rule/bank-account [:maybe entity-id]] + [:transaction-rule/amount-gte {:optional true} [:maybe money]] + [:transaction-rule/amount-lte {:optional true} [:maybe money]] + [:transaction-rule/dom-gte {:optional true} [:maybe :int]] + [:transaction-rule/dom-lte {:optional true} [:maybe :int]] + [:transaction-rule/vendor {:optional true} [:maybe entity-id]] + [:transaction-rule/transaction-approval-status (ref->enum-schema "transaction-approval-status")] + [:transaction-rule/accounts + (many-entity {:min 1} + [:db/id [:or entity-id temp-id]] + [:transaction-rule-account/account entity-id] + [:transaction-rule-account/location [:string {:min 1 :error/message "required"}]] + [:transaction-rule-account/percentage percentage])]])) (defn check-badges [{query-params :query-params}] - (html-response - [:div (if (not-empty (:all query-params)) - (com/pill {:color :secondary} - - [:span "All " [:span {:x-text "resultCount" :x-data "{}"}] " transactions"]) - (com/pill {:color :primary} - (str (count (:transaction-id query-params)) " transactions")))])) + (html-response + [:div (if (not-empty (:all query-params)) + (com/pill {:color :secondary} + + [:span "All " [:span {:x-text "resultCount" :x-data "{}"}] " transactions"]) + (com/pill {:color :primary} + (str (count (:transaction-id query-params)) " transactions")))])) (defn execute-dialog [{:keys [entity clients]}] (modal-response - (com/modal{} - (com/stacked-modal-card - 0 - {} - [:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"]] - [: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})]] - [:div.flex.justify-end (com/validated-save-button {:form "my-form" :id "code"} "Code transactions")])) - :headers (-> {} - (assoc "hx-trigger-after-settle" "modalnext") - (assoc "hx-retarget" ".modal-stack") - (assoc "hx-reswap" "beforeend")))) + (com/modal {} + (com/modal-card-advanced + {} + (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})]]) + (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") + (assoc "hx-retarget" ".modal-stack") + (assoc "hx-reswap" "beforeend")))) (defn delete [{:keys [entity] :as request}] @(dc/transact conn [[:db/retractEntity (:db/id entity)]]) @@ -875,83 +609,347 @@ (html-response (row* (:identity request) entity {:delete-after-settle? true :class "live-removed"}) :headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id entity))})) +(defrecord EditModal [linear-wizard] + mm/ModalWizardStep + (step-name [_] + "Edit") + + (step-key [_] + :edit) + + (edit-path [_ _] []) + + (step-schema [_] + (mm/form-schema linear-wizard)) + + (render-step [this request] + (mm/default-render-step + linear-wizard this + :head "Transaction rule" + :body (mm/default-step-body {} + [:form#my-form {:hx-ext "response-targets" + :hx-target-400 "#form-errors .error-content" + :hx-indicator "#submit" + :x-trap "true" + (if (:db/id (fc/field-value)) + :hx-put + :hx-post) (str (bidi/path-for ssr-routes/only-routes ::route/save))} + [:fieldset {:class "hx-disable" + :x-data (hx/json {:clientId (or (:db/id (:transaction-rule/client (fc/field-value))) + (:transaction-rule/client (fc/field-value)))})} + + [:div.space-y-1 + (when-let [id (:db/id (fc/field-value))] + (com/hidden {:name "db/id" + :value id})) + (fc/with-field :transaction-rule/description + (com/validated-field {:label "Description" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :error? (fc/error?) + :x-init "$el.focus()" + :placeholder "HOME DEPOT" + :class "w-96" + :value (fc/field-value)}))) + [:div.filters {:x-data (hx/json {:clientFilter (boolean (fc/field-value (:transaction-rule/client fc/*current*))) + :bankAccountFilter (boolean (fc/field-value (:transaction-rule/bank-account fc/*current*))) + :amountFilter (boolean (or (fc/field-value (:transaction-rule/amount-gte fc/*current*)) + (fc/field-value (:transaction-rule/amount-lte fc/*current*)))) + :domFilter (boolean (or (fc/field-value (:transaction-rule/dom-gte fc/*current*)) + (fc/field-value (:transaction-rule/dom-lte fc/*current*))))})} + + [:div.flex.gap-2.mb-2 + (com/a-button {"@click" "clientFilter=true" + "x-show" "!clientFilter"} "Filter client") + (com/a-button {"@click" "bankAccountFilter=true" + "x-show" "clientFilter && !bankAccountFilter"} "Filter bank account") + (com/a-button {"@click" "amountFilter=true" + "x-show" "!amountFilter"} "Filter amount") + (com/a-button {"@click" "domFilter=true" + "x-show" "!domFilter"} "Filter day of month")] + (fc/with-field :transaction-rule/client + + (com/validated-field + (-> {:label "Client" + :errors (fc/field-errors) + :x-show "clientFilter"} + (hx/alpine-appear)) + [: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) + :x-model "clientId" + :value (fc/field-value) + :content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})])) + + (fc/with-field :transaction-rule/bank-account + (com/validated-field + (-> {:label "Bank Account" + :errors (fc/field-errors) + :x-show "bankAccountFilter"} + hx/alpine-appear) + [:div.w-96 + [:div#bank-account-changer {:hx-get (bidi/path-for ssr-routes/only-routes :bank-account-typeahead) + :hx-trigger "changed" + :hx-target "next *" + :hx-include "#bank-account-changer" + :hx-swap "innerHTML" + + :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" (fc/field-name)) + :x-init "$watch('clientId', cid => $dispatch('changed', $data))"}] + + (bank-account-typeahead* {:client-id (:transaction-rule/client (fc/field-value)) + :name (fc/field-name) + :value (fc/field-value)})])) + + (com/field (-> {:label "Amount" + :x-show "amountFilter"} + hx/alpine-appear) + [:div.flex.gap-2 + (fc/with-field :transaction-rule/amount-gte + [:div.flex.flex-col + (com/money-input {:name (fc/field-name) + :placeholder ">=" + :class "w-24" + :value (fc/field-value)}) + (com/errors {:errors (fc/field-errors)})]) + (fc/with-field :transaction-rule/amount-lte + [:div.flex.flex-col + (com/money-input {:name (fc/field-name) + :placeholder "<=" + :class "w-24" + :value (fc/field-value)}) + (com/errors {:errors (fc/field-errors)})])]) + + (com/field (-> {:label "Day of month" + :x-show "domFilter"} + hx/alpine-appear) + [:div.flex.gap-2 + (fc/with-field :transaction-rule/dom-gte + (com/validated-field + {:errors (fc/field-errors)} + (com/int-input {:name (fc/field-name) + :placeholder ">=" + :class "w-24" + :value (fc/field-value)}))) + (fc/with-field :transaction-rule/dom-lte + (com/validated-field + {:errors (fc/field-errors)} + (com/int-input {:name (fc/field-name) + :placeholder ">=" + :class "w-24" + :value (fc/field-value)})))])] + + [:h2.text-lg "Outcomes"] + (fc/with-field :transaction-rule/vendor + (com/validated-field {:label "Assign Vendor" + :errors (fc/field-errors)} + [:div.w-96 + (com/typeahead {:name (fc/field-name) + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes :vendor-search) + :class "w-96" + :value (fc/field-value) + :content-fn #(pull-attr (dc/db conn) :vendor/name %)})])) + + (fc/with-field :transaction-rule/accounts + (com/validated-field + {:errors (fc/field-errors)} + (let [client-locations (some->> (fc/field-value) :transaction-rule/client (pull-attr (dc/db conn) :client/locations))] + (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-rule-account-row* % (:transaction-rule/client (fc/field-value)) client-locations)) + (com/data-grid-new-row {:colspan 4 + :hx-get (bidi/path-for ssr-routes/only-routes + ::route/new-account) + :index (count (fc/field-value)) + :tr-params (hx/bind-alpine-vals {} {"client-id" "clientId"})} + "New account"))))) + + (fc/with-field :transaction-rule/transaction-approval-status + (com/validated-field {:label "Approval status" + :errors (fc/field-errors)} + (com/radio {:options (ref->radio-options "transaction-approval-status") + :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))) + +(defrecord TestModal [linear-wizard] + mm/ModalWizardStep + (step-name [_] + "Test") + + (step-key [_] + :test) + + (edit-path [_ _] []) + + (step-schema [_] + (mut/select-keys (mm/form-schema linear-wizard) #{})) + + (render-step [this request] + (mm/default-render-step + linear-wizard this + :head [:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"]] + :body [:div.space-y-1 {:class "w-[850px] h-[600px]"} + (transaction-rule-test-table* {:entity (:snapshot (:multi-form-state request)) + :clients (:clients request)})] + :footer + (mm/default-step-footer linear-wizard this :validation-route ::route/navigate) + :validation-route ::route/navigate))) + +(defrecord TransactionRuleWizard [transaction-rule current-step entity] + mm/LinearModalWizard + (hydrate-from-request + [this request] + this + #_(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 :edit))) + (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)))))) + (steps [_] + [:edit + :test]) + + (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 {:edit (->EditModal this) + :test (->TestModal this)} + step-key) + + 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 + (= :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 - (apply-middleware-to-all-handlers - (->> - {::route/page (helper/page-route grid-page) - ::route/table (helper/table-route grid-page) - ::route/delete (-> delete - (wrap-entity [:route-params :db/id] default-read) - (wrap-schema-enforce :route-params [:map [:db/id entity-id]])) - ::route/new-account (-> new-account - (wrap-schema-enforce :query-schema [:map - [:client-id {:optional true} - [:maybe entity-id]] - [:index {:optional true - :default 0} [nat-int? {:default 0}]]]) - wrap-admin wrap-client-redirect-unauthenticated) - ::route/location-select (-> location-select - (wrap-schema-enforce :query-schema [:map - [:name :string] - [:client-id {:optional true} - [:maybe entity-id]] - [:account-id {:optional true} - [:maybe entity-id]]])) - ::route/account-typeahead (-> account-typeahead - (wrap-schema-enforce :query-schema [:map - [:name :string] - [:client-id {:optional true} - [:maybe entity-id]] - [:value {:optional true} - [:maybe entity-id]]])) - ::route/save (-> save - (wrap-entity [:form-params :db/id] default-read) - (wrap-schema-enforce :form-schema form-schema) - (wrap-nested-form-params) - (wrap-form-4xx-2 (-> edit-dialog - (wrap-entity [:form-params :db/id] default-read)))) + (apply-middleware-to-all-handlers + (->> + {::route/page (helper/page-route grid-page) + ::route/table (helper/table-route grid-page) + ::route/delete (-> delete + (wrap-entity [:route-params :db/id] default-read) + (wrap-schema-enforce :route-params [:map [:db/id entity-id]])) + ::route/new-account + (-> + (add-new-entity-handler [:step-params :transaction-rule/accounts] + (fn render [cursor request] + (transaction-rule-account-row* + cursor + (:client-id (:query-params request)) + (some->> (:client-id (:query-params request)) (pull-attr (dc/db conn) :client/locations)))) + (fn build-new-row [base _] + (assoc base :transaction-rule-account/location "Shared"))) + (wrap-schema-enforce :query-schema [:map + [:client-id {:optional true} + [:maybe entity-id]]])) - ::route/execute (-> execute - (wrap-entity [:route-params :db/id] default-read) - (wrap-schema-enforce :route-schema [:map [:db/id entity-id]]) - (wrap-schema-enforce :form-schema - [:map - [:transaction-id {:optional true} - [:maybe [:vector {:decode/arbitrary (fn [x] ;; TODO make this easier - (if (sequential? x) - x - [x]))} - entity-id]]] - [:all {:optional true} [:maybe :string]]]) - #_(wrap-form-4xx-2 (-> edit-dialog ;; TODO for example not having a single one checked + ::route/location-select (-> location-select + (wrap-schema-enforce :query-schema [:map + [:name :string] + [:client-id {:optional true} + [:maybe entity-id]] + [:account-id {:optional true} + [:maybe entity-id]]])) + ::route/account-typeahead (-> account-typeahead + (wrap-schema-enforce :query-schema [:map + [:name :string] + [:client-id {:optional true} + [:maybe entity-id]] + [:value {:optional true} + [:maybe entity-id]]])) + ::route/save (-> mm/submit-handler + (mm/wrap-wizard rule-wizard) + (mm/wrap-decode-multi-form-state) + (wrap-entity [:form-params :db/id] default-read)) + + ::route/execute (-> execute + (wrap-entity [:route-params :db/id] default-read) + (wrap-schema-enforce :route-schema [:map [:db/id entity-id]]) + (wrap-schema-enforce :form-schema + [:map + [:transaction-id {:optional true} + [:maybe [:vector {:decode/arbitrary (fn [x] ;; TODO make this easier + (if (sequential? x) + x + [x]))} + entity-id]]] + [:all {:optional true} [:maybe :string]]]) + #_(wrap-form-4xx-2 (-> edit-dialog ;; TODO for example not having a single one checked (wrap-entity [:form-params :db/id] default-read)))) - ::route/test (-> test - (wrap-entity [:form-params :db/id] default-read) - (wrap-schema-enforce :form-schema form-schema) - (wrap-nested-form-params) - (wrap-form-4xx-2 (-> edit-dialog - (wrap-entity [:form-params :db/id] default-read)))) + ::route/check-badges (-> check-badges + (wrap-schema-enforce :query-schema [:map + [:transaction-id {:optional true} + [:maybe [:vector {:decode/arbitrary (fn [x] + (if (sequential? x) + x + [x]))} + entity-id]]] + [:all {:optional true} [:maybe :string]]])) + ::route/execute-dialog (-> execute-dialog + (wrap-entity [:route-params :db/id] default-read) + (wrap-schema-enforce :route-schema [:map [:db/id entity-id]])) - ::route/check-badges (-> check-badges - (wrap-schema-enforce :query-schema [:map - [:transaction-id {:optional true} - [:maybe [:vector {:decode/arbitrary (fn [x] - (if (sequential? x) - x - [x]))} - entity-id]]] - [:all {:optional true} [:maybe :string]]])) - ::route/execute-dialog (-> execute-dialog - (wrap-entity [:route-params :db/id] default-read) - (wrap-schema-enforce :route-schema [:map [:db/id entity-id]])) - - ::route/edit-dialog (-> edit-dialog - (wrap-entity [:route-params :db/id] default-read) - (wrap-schema-enforce :route-schema [:map [:db/id entity-id]])) - ::route/new-dialog edit-dialog}) - (fn [h] - (-> h - (wrap-admin) - (wrap-client-redirect-unauthenticated))))) + ::route/navigate (-> mm/next-handler + (mm/wrap-wizard rule-wizard) + (mm/wrap-decode-multi-form-state)) + ::route/edit-dialog (-> mm/open-wizard-handler + (mm/wrap-wizard rule-wizard) + (mm/wrap-init-multi-form-state (fn [request] + (mm/->MultiStepFormState (:entity request) + [] + (:entity request)))) + (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-wizard rule-wizard) + (mm/wrap-init-multi-form-state (fn [_] + (mm/->MultiStepFormState {} + [] + {}))))}) + (fn [h] + (-> h + (wrap-admin) + (wrap-client-redirect-unauthenticated))))) diff --git a/src/clj/auto_ap/ssr/admin/vendors.clj b/src/clj/auto_ap/ssr/admin/vendors.clj index 352ddb9e..901e8300 100644 --- a/src/clj/auto_ap/ssr/admin/vendors.clj +++ b/src/clj/auto_ap/ssr/admin/vendors.clj @@ -1,59 +1,42 @@ (ns auto-ap.ssr.admin.vendors - (:require - [auto-ap.cursor :as cursor] - [auto-ap.datomic - :refer [add-sorter-fields - apply-pagination - apply-sort-3 - audit-transact - 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 :as query-params] - [auto-ap.routes.admin.vendors :as route] - [auto-ap.routes.utils - :refer [wrap-admin wrap-client-redirect-unauthenticated]] - [auto-ap.solr :as solr] - [auto-ap.ssr-routes :as ssr-routes] - [auto-ap.ssr.components :as com] - [auto-ap.ssr.components.timeline :as timeline] - [auto-ap.ssr.form-cursor :as fc] - [auto-ap.ssr.grid-page-helper :as helper] - [auto-ap.ssr.hx :as hx] - [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] - [auto-ap.ssr.svg :as svg] - [auto-ap.ssr.utils - :refer [apply-middleware-to-all-handlers - entity-id - form-validation-error - html-response - main-transformer - many-entity - modal-response - ref->enum-schema - ref->select-options - strip - temp-id - wrap-entity - wrap-form-4xx-2 - wrap-schema-decode - wrap-schema-enforce]] - [bidi.bidi :as bidi] - [clojure.string :as str] - [datomic.api :as dc] - [hiccup.util :as hu] - [malli.core :as mc] - [malli.util :as mut])) + (: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]] + [auto-ap.datomic.accounts :as d-accounts] + [auto-ap.logging :as alog] + [auto-ap.query-params :as query-params] + [auto-ap.routes.admin.vendors :as route] + [auto-ap.routes.utils + :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 + add-new-primitive-handler]] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.components.multi-modal :as mm] + [auto-ap.ssr.components.timeline :as timeline] + [auto-ap.ssr.form-cursor :as fc] + [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.hx :as hx] + [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 modal-response ref->enum-schema + ref->select-options strip temp-id wrap-entity wrap-form-4xx-2 + wrap-schema-enforce]] + [bidi.bidi :as bidi] + [clojure.string :as str] + [datomic.api :as dc] + [malli.core :as mc] + [malli.transform :as mt] + [malli.util :as mut])) (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"} @@ -76,7 +59,7 @@ {:value "only-global" :content "Only global"} #_{:value "potential-duplicates" - :content "Potential duplicates"}]}))]]) + :content "Potential duplicates"}]}))]]) (def default-read '[:db/id :vendor/name @@ -103,9 +86,9 @@ (defn fetch-ids [db request] (let [query-params (:parsed-query-params request) query (cond-> {:query {:find [] - :in '[$ ] + :in '[$] :where '[]} - :args [db ]} + :args [db]} (:sort query-params) (add-sorter-fields {"name" ['[?e :vendor/name ?n] '[(clojure.string/upper-case ?n) ?sort-name]]} query-params) @@ -113,15 +96,14 @@ (merge-query {:query {:find [] :in ['?ns] :where ['[?e :vendor/name ?an] - '[(clojure.string/upper-case ?an) ?upper-an] - '[(clojure.string/includes? ?upper-an ?ns)]]} + '[(clojure.string/upper-case ?an) ?upper-an] + '[(clojure.string/includes? ?upper-an ?ns)]]} :args [(str/upper-case (:name query-params))]}) (some->> query-params :type not-empty (= "only-hidden")) (merge-query {:query {:find [] :in [] - :where ['[?e :vendor/hidden true] - ]} + :where ['[?e :vendor/hidden true]]} :args []}) (some->> query-params :type not-empty (= "only-global")) (merge-query {:query {:find [] @@ -158,23 +140,21 @@ :page-specific-nav filters :fetch-page fetch-page :parse-query-params (comp - (query-params/parse-key :code query-params/parse-long) - (helper/default-parse-query-params grid-page)) + (query-params/parse-key :code query-params/parse-long) + (helper/default-parse-query-params grid-page)) :action-buttons (fn [_] - [ - (com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes + [(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/merge)) :color :secondary} "Merge") (com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/new)) :color :primary} - "New Vendor") - ]) + "New Vendor")]) :row-buttons (fn [_ entity] [(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes - ::route/edit - :db/id (:db/id entity)))} + ::route/edit + :db/id (:db/id entity)))} svg/pencil)]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :admin)} @@ -215,27 +195,7 @@ (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) -(defn save [{:keys [form-params request-method] :as request}] - (let [entity (alog/peek (cond-> form-params - (= :post request-method) (assoc :db/id "new") - (not (some identity (vals (dissoc (:vendor/address form-params) :db/id)))) (assoc :vendor/address nil))) - {:keys [tempids]} (audit-transact [[:upsert-entity entity]] - (:identity request)) - updated-vendor (dc/pull (dc/db conn) - default-read - (or (get tempids (:db/id entity)) (:db/id entity)))] - (solr/index-documents-raw - solr/impl - "vendors" - [{"id" (:db/id updated-vendor) - "name" (:vendor/name updated-vendor) - "hidden" (boolean (:vendor/hidden updated-vendor))}]) - (html-response - (row* identity updated-vendor {: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-vendor))))))) + (defn merge-submit [{:keys [form-params request-method identity] :as request}] (if (= (:source-vendor form-params) @@ -243,7 +203,7 @@ (form-validation-error "Please select two different vendors" :form form-params)) (let [transaction (->> (dc/q {:find '[?x ?a2] - :in '[$ ?vendor-from ] + :in '[$ ?vendor-from] :where ['[?x ?a ?vendor-from] '[?a :db/ident ?a2]]} (dc/db conn) @@ -255,227 +215,94 @@ (alog/peek transaction) (audit-transact-batch transaction identity) (audit-transact [[:db/retractEntity (:source-vendor form-params)]] identity)) - (html-response - [:div] - :headers {"hx-trigger" (hx/json {"modalclose" "" - "notification" "Vendor merge successful."})})) + (html-response + [:div] + :headers {"hx-trigger" (hx/json {"modalclose" "" + "notification" "Vendor merge successful."})})) (defn back-button [] [:a {"@click" "$dispatch('modalprevious')" "class" "text-sm font-medium text-gray-700 cursor-pointer"} "Back"]) -;; TODO - -;; make new vendor work -;; generalize - - - -;; TODO warn about usage on the vendor based on the thing - (defn timeline [{:keys [active]}] (let [steps ["Info" "Terms" "Account" "Address" "Legal"] - active-index (.indexOf steps active )] + active-index (.indexOf steps active)] (timeline/timeline - {} - (for [[n i] (map vector steps (range))] - (timeline/timeline-step (cond-> {} - (= i active-index) (assoc :active? true) - (< i active-index) (assoc :visited? true) - (= i (dec (count steps))) (assoc :last? true)) - n))))) + {} + (for [[n i] (map vector steps (range))] + (timeline/timeline-step (cond-> {} + (= i active-index) (assoc :active? true) + (< i active-index) (assoc :visited? true) + (= i (dec (count steps))) (assoc :last? true)) + n))))) -(defn info-modal [{:keys [form-params form-errors entity]}] - (com/stacked-modal-card-2 - 0 - {"@keydown.enter.prevent.stop" "$refs.next.click()" - "hx-target" "this"} - (com/modal-header {} - [:div.flex [:div.p-2 "Basic Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 - [:span {:x-text "vendorName"}]]]) - (com/modal-header-attachment - {} - (timeline {:active "Info"})) - (com/modal-body {} - [:div.space-y-1.mt-4 {:class "w-[600px] h-[350px]"} - (when-let [id (:db/id entity)] - (com/hidden {:name "db/id" - :value id})) - - (fc/with-field :vendor/name - (com/validated-field {:label "Name" - :errors (fc/field-errors) - } - (com/text-input {:name (fc/field-name) - :value (fc/field-value) - :x-model "vendorName" - :autofocus true - :class "w-96"}))) - (com/validated-field - {} - [:div (com/checkbox - {:x-model "showPrintAs" "@change" "if (!showPrintAs) { printAs = ''; } "} - "Use different name for checks")]) - - (fc/with-field :vendor/print-as - (com/validated-field (-> {:label "Print as" - :errors (fc/field-errors) - :x-show "showPrintAs"} - hx/alpine-appear - hx/alpine-disappear) - (com/text-input {:name (fc/field-name) - :x-model "printAs" - :value (fc/field-value) - :class "w-96"}))) - - (fc/with-field :vendor/hidden - (alog/peek (cursor/path fc/*current*)) - (com/checkbox {:name (fc/field-name) - :value (boolean (fc/field-value)) ; - :checked (alog/peek :checked (fc/field-value))} - "Admin-only"))]) - (com/modal-footer - {} - [:div.flex.justify-end - (com/form-errors {:errors (:errors fc/*form-errors*)}) - [:div.flex.items-baseline.gap-x-4 - (com/validated-save-button {:errors (seq form-errors) - :x-ref "next" - :class "w-48" - :hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/validate) - {:step "Info"}) - - } - - "Terms" - [:div.w-5.h-5 svg/arrow-right])]]))) ;; TODO add plaid merchant ;; TODO each client only used once (defn terms-override-row [terms-override-cursor] (com/data-grid-row - (-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? terms-override-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)})) - (fc/with-field :vendor-terms-override/client - (com/data-grid-cell - {} - (com/validated-field - {:errors (fc/field-errors)} - (com/typeahead {:name (fc/field-name) - :error? (fc/error?) - :autofocuse true - :class "w-full grow shrink" - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes :company-search) - :value (fc/field-value) - :content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})))) - (fc/with-field :vendor-terms-override/terms - (com/data-grid-cell - {} - (com/validated-field - {:errors (fc/field-errors)} - [:div.flex.items-baseline.gap-x-4 - (com/int-input {:name (fc/field-name) - :value (fc/field-value) - :class "w-16"}) - "days"]))) + (-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? terms-override-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)})) + (fc/with-field :vendor-terms-override/client + (com/data-grid-cell + {} + (com/validated-field + {:errors (fc/field-errors)} + (com/typeahead {:name (fc/field-name) + :error? (fc/error?) + :autofocuse true + :class "w-full grow shrink" + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes :company-search) + :value (fc/field-value) + :content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})))) + (fc/with-field :vendor-terms-override/terms + (com/data-grid-cell + {} + (com/validated-field + {:errors (fc/field-errors)} + [:div.flex.items-baseline.gap-x-4 + (com/int-input {:name (fc/field-name) + :value (fc/field-value) + :class "w-16"}) + "days"]))) (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))))}) - :data-key "show" - :x-ref "p"} - hx/alpine-mount-then-appear) - (list - (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 + (-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? terms-override-cursor))))}) + :data-key "show" + :x-ref "p"} + hx/alpine-mount-then-appear) + (list + (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 - :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))))) + :content-fn #(pull-attr (dc/db conn) :client/name (:db/id %)) + :size :small}))) -(defn terms-modal [{:keys [form-errors entity form-params]}] - (com/stacked-modal-card-2 - 1 - {"@keydown.enter.prevent.stop" "$refs.next.click()" - "hx-target" "this"} - (com/modal-header {} - [:div.flex - [:div.p-2 "Vendor Terms"] - [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 - [:span {:x-text "vendorName"}]]]) - (com/modal-header-attachment {} - (timeline {:active "Terms"})) - (com/modal-body {} - [:div.space-y-1 {:class "w-[600px] h-[350px]"} - (fc/with-field :vendor/terms - (com/validated-field {:label "Terms" - :errors (fc/field-errors)} - [:div.flex.items-baseline.gap-x-4 - (com/int-input {:name (fc/field-name) - :value (fc/field-value) - }) - "days"])) - (fc/with-field :vendor/terms-overrides - (com/validated-field - {:errors (fc/field-errors) - :label "Terms Overrides"} - (com/data-grid {:headers [(com/data-grid-header {} "Client") - (com/data-grid-header {:class "w-16"} "Terms") - (com/data-grid-header {:class "w-16"})]} - (fc/cursor-map #(terms-override-row %)) - (com/data-grid-new-row {:colspan 3 - :hx-get (bidi/path-for ssr-routes/only-routes - ::route/new-terms-override) - :index (count (fc/field-value))} - "New override")))) - (fc/with-field :vendor/automatically-paid-when-due - (com/validated-field - {:errors (fc/field-errors) - :label "Automatically pay when due"} - (com/data-grid {:headers [(com/data-grid-header {} "Client") - (com/data-grid-header {:class "w-16"})]} - (fc/cursor-map #(automatically-paid-when-due-row %)) - (com/data-grid-new-row {:colspan 2 - :hx-get (bidi/path-for ssr-routes/only-routes ::route/new-automatic-payment) - :index (count (fc/field-value))} - "New automatic payment for client"))))]) - (com/modal-footer - {} - [:div.flex.justify-end - (com/form-errors {:errors (:errors fc/*form-errors*)}) - [:div.flex.items-baseline.gap-x-4 - (back-button) - (com/validated-save-button {:errors (seq form-errors) - :x-ref "next" - :class "w-48" - :hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/validate) - {:step "Terms"})} + (com/data-grid-cell {:class "align-top"} + (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))) - "Account assignments" - [:div.w-5.h-5 svg/arrow-right])]]))) (defn- account-typeahead* [{:keys [name value client-id x-model]}] @@ -494,468 +321,165 @@ (alog/peek @terms-override-cursor) (let [client-id (fc/field-value (:vendor-account-override/client terms-override-cursor))] (com/data-grid-row - (-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? terms-override-cursor)))) - :clientId client-id - :accountId (fc/field-value (:vendor-account-override/account terms-override-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)})) - (fc/with-field :vendor-account-override/client - (com/data-grid-cell - {} - (com/validated-field - {:errors (fc/field-errors)} - (com/typeahead {:name (fc/field-name) - :error? (fc/error?) - :x-model "clientId" - :autofocuse true - :class "w-full grow shrink" - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes :company-search) - :value (fc/field-value) - :content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})))) - (fc/with-field :vendor-account-override/account - (com/data-grid-cell - {} - (com/validated-field - {:errors (fc/field-errors)} - [:div {:hx-trigger "changed" - :hx-target "next div" - :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId, value: event.detail.accountId || ''}" (fc/field-name)) - :hx-get (str (bidi/path-for ssr-routes/only-routes ::route/account-typeahead)) - :x-init "$watch('clientId', cid => $dispatch('changed', $data));"}] - (account-typeahead* {:value (fc/field-value) - :client-id client-id - :name (fc/field-name) - :x-model "accountId"})))) - (com/data-grid-cell {:class "align-top"} - (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))))) + (-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? terms-override-cursor)))) + :clientId client-id + :accountId (fc/field-value (:vendor-account-override/account terms-override-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)})) + (fc/with-field :vendor-account-override/client + (com/data-grid-cell + {} + (com/validated-field + {:errors (fc/field-errors)} + (com/typeahead {:name (fc/field-name) + :error? (fc/error?) + :x-model "clientId" + :autofocuse true + :class "w-full grow shrink" + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes :company-search) + :value (fc/field-value) + :content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})))) + (fc/with-field :vendor-account-override/account + (com/data-grid-cell + {} + (com/validated-field + {:errors (fc/field-errors)} + [:div {:hx-trigger "changed" + :hx-target "next div" + :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId, value: event.detail.accountId || ''}" (fc/field-name)) + :hx-get (str (bidi/path-for ssr-routes/only-routes ::route/account-typeahead)) + :x-init "$watch('clientId', cid => $dispatch('changed', $data));"}] + (account-typeahead* {:value (fc/field-value) + :client-id client-id + :name (fc/field-name) + :x-model "accountId"})))) + (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-modal [{:keys [form-errors entity form-params]}] - (com/stacked-modal-card-2 - 2 - {"@keydown.enter.prevent.stop" "$refs.next.click()" - "hx-target" "this"} - (com/modal-header - {} - [:div.flex - [:div.p-2 "Vendor Account"] - [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 - [:span {:x-text "vendorName"}]]]) - (com/modal-header-attachment {} - (timeline {:active "Account"})) - (com/modal-body - {} - [:div.space-y-1 {:class "w-[600px] h-[350px] "} - (fc/with-field :vendor/default-account - (alog/info ::acount-check :a (fc/field-value)) - (com/validated-field {:label "Default Account" - :errors (fc/field-errors)} - (com/typeahead {:name (fc/field-name) - :error? (fc/error?) - :class "w-96" - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes :account-search) - :value (fc/field-value) - :content-fn (fn [c] (pull-attr (dc/db conn) :account/name c))}))) - (fc/with-field :vendor/account-overrides - (com/validated-field - {:errors (fc/field-errors) - :label "Account Overrides"} - (com/data-grid {:headers [(com/data-grid-header {} "Client") - (com/data-grid-header {} "Account") - (com/data-grid-header {:class "w-16"})]} - (fc/cursor-map #(account-override-row %)) - (com/data-grid-new-row {:colspan 3 - :hx-get (bidi/path-for ssr-routes/only-routes ::route/new-account-override) - :index (count (fc/field-value))} - "New override"))))]) - (com/modal-footer - {} - [:div.flex.justify-end - (com/form-errors {:errors (:errors fc/*form-errors*)}) - [:div.flex.items-baseline.gap-x-4 - (back-button) - (com/validated-save-button {:errors (seq form-errors) - :x-ref "next" - :hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/validate) - {:step "Account"})} - "Address" - [:div.w-5.h-5 svg/arrow-right])]]))) -(defn address-modal [{:keys [form-errors entity form-params]}] - (com/stacked-modal-card-2 - 3 - {"@keydown.enter.prevent.stop" "$refs.next.click()" - "hx-target" "this"} - (com/modal-header - {} - [:div.flex ;; TODO standardize how these headers are built - [:div.p-2 "Address"] - [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 - [:span {:x-text "vendorName"}]]]) - (com/modal-header-attachment {} - (timeline {:active "Address"})) - (com/modal-body - {} - [:div.space-y-1 {:class "w-[600px] h-[350px]"} - (fc/with-field :vendor/address - [:div.flex.flex-col.w-full - (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)})))]])]) - (com/modal-footer - {} - [:div.flex.justify-end - (com/form-errors {:errors (:errors fc/*form-errors*)}) - [:div.flex.items-baseline.gap-x-4 - (back-button) - (com/validated-save-button {:errors (seq form-errors) - :x-ref "next" - :hx-put (hu/url (bidi/path-for ssr-routes/only-routes ::route/validate) - {:step "Address"})} - "Legal Entity" - [:div.w-5.h-5 svg/arrow-right])]]))) -(defn legal-modal [{:keys [form-params entity form-errors]}] - (com/stacked-modal-card-2 - 4 - {"@keydown.enter.prevent.stop" "$refs.next.click()"} - (com/modal-header - {} - [:div.flex - [:div.p-2 "Legal Entity Info"] - [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 - [:span {:x-text "vendorName"}]]]) - (com/modal-header-attachment {} - (timeline {:active "Legal"})) - (com/modal-body - {} - [:div {:class "w-[600px] h-[350px]"} - [:div.grid.grid-cols-6.gap-x-4.gap-y-2 - [: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")})))]]]) - (com/modal-footer - {} - [:div.flex.justify-end - (com/form-errors {:errors (:errors fc/*form-errors*)}) - [:div.flex.items-baseline.gap-x-4 - (back-button) - (com/validated-save-button {:errors (seq form-errors) - :x-ref "next"} - "Save vendor")]]))) (defn dialog* [{:keys [entity form-params form-errors] :as params}] (alog/peek ::dialog-entity form-params) (fc/start-form form-params form-errors - [:div {:x-data (hx/json {"vendorName" (:vendor/name form-params) - "showPrintAs" (boolean (not-empty (:vendor/print-as form-params))) - "printAs" (:vendor/print-as form-params)}) - :class "w-full h-full"} - [:form#my-form (-> {:hx-ext "response-targets" - :hx-swap "outerHTML" - :hx-target-400 "#form-errors .error-content" - :hx-trigger "submit" - :class "h-full w-full"} - (assoc (if (:db/id entity) - :hx-put - :hx-post) - (str (bidi/path-for ssr-routes/only-routes ::route/save)))) - (com/modal - {} - (info-modal params) - (terms-modal params) - (account-modal params) - (address-modal params) - (legal-modal params))]])) + [:div {:x-data (hx/json {"vendorName" (:vendor/name form-params) + "showPrintAs" (boolean (not-empty (:vendor/print-as form-params))) + "printAs" (:vendor/print-as form-params)}) + :class "w-full h-full"} + [:form#my-form (-> {:hx-ext "response-targets" + :hx-swap "outerHTML" + :hx-target-400 "#form-errors .error-content" + :hx-trigger "submit" + :class "h-full w-full"} + (assoc (if (:db/id entity) + :hx-put + :hx-post) + (str (bidi/path-for ssr-routes/only-routes ::route/save)))) + (com/modal + {} + + #_(terms-modal params) + #_(account-modal params) + + [:div])]])) (def form-schema (mc/schema - [:map - [:db/id {:optional true} [:maybe entity-id]] - [:vendor/name [:string {:min 3 :decode/string strip}]] - [:vendor/print-as {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] - [:vendor/default-account entity-id] - [:vendor/terms {:optional true} [:maybe :int]] - [:vendor/automatically-paid-when-due {:optional true} - [:maybe - (many-entity {} [:db/id entity-id])]] - [:vendor/terms-overrides {:optional true} - (many-entity {} - [:db/id [:or entity-id temp-id]] - [:vendor-terms-override/terms :int] - [:vendor-terms-override/client entity-id])] - [:vendor/account-overrides {:optional true} - (many-entity {} - [:db/id [:or entity-id temp-id]] - [:vendor-account-override/account entity-id] - [:vendor-account-override/client entity-id])] - [:vendor/hidden {:default false} - [:boolean {:decode/string {:enter #(if (= % "on") true + [:map + [:db/id {:optional true} [:maybe entity-id]] + [:vendor/name [:string {:min 3 :decode/string strip}]] + [:vendor/print-as {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] + [:vendor/default-account entity-id] + [:vendor/terms {:optional true} [:maybe :int]] + [:vendor/automatically-paid-when-due {:optional true} + [:maybe + (many-entity {} [:db/id entity-id])]] + [:vendor/terms-overrides {:optional true} + (many-entity {} + [:db/id [:or entity-id temp-id]] + [:vendor-terms-override/terms :int] + [:vendor-terms-override/client entity-id])] + [:vendor/account-overrides {:optional true} + (many-entity {} + [:db/id [:or entity-id temp-id]] + [:vendor-account-override/account entity-id] + [:vendor-account-override/client entity-id])] + [:vendor/hidden {:default false} + [:boolean {:decode/string {:enter #(if (= % "on") true - (boolean %))}}]] - [:vendor/address {:default {}} - [:map - [:db/id {:default "new-account"} [:or entity-id temp-id]] - [:address/street1 {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] - [: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}]]]]] - [:vendor/legal-entity-tin {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] - [:vendor/legal-entity-name {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] - [:vendor/legal-entity-first-name {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] - [:vendor/legal-entity-middle-name {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] - [:vendor/legal-entity-last-name {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] - [:vendor/legal-entity-tin-type {:optional true} [:maybe (ref->enum-schema "legal-entity-tin-type")]] - [:vendor/legal-entity-1099-type {:optional true} [:maybe (ref->enum-schema "legal-entity-1099-type")]]])) + (boolean %))}}]] + [:vendor/address {:optional true} + [:maybe + [:map + [:db/id {:optional true} [:or entity-id temp-id]] + [:address/street1 {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] + [: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}]]]]]] + [:vendor/legal-entity-tin {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] + [:vendor/legal-entity-name {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] + [:vendor/legal-entity-first-name {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] + [:vendor/legal-entity-middle-name {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] + [:vendor/legal-entity-last-name {:optional true} [:maybe [:string {:min 3 :decode/string strip}]]] + [:vendor/legal-entity-tin-type {:optional true} [:maybe (ref->enum-schema "legal-entity-tin-type")]] + [:vendor/legal-entity-1099-type {:optional true} [:maybe (ref->enum-schema "legal-entity-1099-type")]]])) (def merge-form-schema (mc/schema - [:map - [:source-vendor {:optional false} entity-id] - [:target-vendor {:optional false} entity-id]])) - -(defn 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)) - (mc/decode form-schema {} main-transformer)) - :form-errors form-errors}))) + [:map + [:source-vendor {:optional false} entity-id] + [:target-vendor {:optional false} entity-id]])) (defn merge-dialog [{:keys [entity form-params form-errors]}] (modal-response - (fc/start-form form-params form-errors - [:div {:class "w-full h-full"} - [:form#my-form (-> {:hx-swap "outerHTML" - :hx-trigger "submit" - :class "h-full w-full" - :hx-put (str (bidi/path-for ssr-routes/only-routes ::route/merge-submit))}) - (com/modal - {} - (com/modal-card - {} - (com/modal-header {} - [:div.m-2 "Merge Vendors"]) - - (com/modal-body {} - [:div.space-y-6 - (fc/with-field :source-vendor - (com/validated-field {:label "Source vendor (to be deleted)" - :errors (fc/field-errors)} - (com/typeahead {:name (fc/field-name) - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes - :vendor-search) - :id (str "vendor-search") - :value (fc/field-value) - :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))}))) - (fc/with-field :target-vendor - (com/validated-field {:label "Target vendor" - :errors (fc/field-errors)} - (com/typeahead {:name (fc/field-name) - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes - :vendor-search) - :id (str "vendor-search") - :value (fc/field-value) - :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))}))) + (fc/start-form form-params form-errors + [:div {:class "w-full h-full"} + [:form#my-form (-> {:hx-swap "outerHTML" + :hx-trigger "submit" + :class "h-full w-full" + :hx-put (str (bidi/path-for ssr-routes/only-routes ::route/merge-submit))}) + (com/modal + {} + (com/modal-card + {} + [:div.m-2 "Merge Vendors"] - ] + [:div.space-y-6.m-1 + (fc/with-field :source-vendor + (com/validated-field {:label "Source vendor (to be deleted)" + :errors (fc/field-errors)} + (com/typeahead {:name (fc/field-name) + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes + :vendor-search) + :id (str "vendor-search") + :value (fc/field-value) + :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))}))) + (fc/with-field :target-vendor + (com/validated-field {:label "Target vendor" + :errors (fc/field-errors)} + (com/typeahead {:name (fc/field-name) + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes + :vendor-search) + :id (str "vendor-search") + :value (fc/field-value) + :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})))] + [:div.flex.justify-end + (com/form-errors {:errors (:errors fc/*form-errors*)}) + [:div.flex.items-baseline.gap-x-4 + (com/validated-save-button {:errors (seq form-errors) + :class "w-48"} - #_[:div.space-y-1.mt-4 {:class "w-[600px] h-[350px]"} - (when-let [id (:db/id entity)] - (com/hidden {:name "db/id" - :value id})) - - (fc/with-field :vendor/name - (com/validated-field {:label "Name" - :errors (fc/field-errors) - } - (com/text-input {:name (fc/field-name) - :value (fc/field-value) - :x-model "vendorName" - :autofocus true - :class "w-96"}))) - (com/validated-field - {} - [:div (com/checkbox - {:x-model "showPrintAs" "@change" "if (!showPrintAs) { printAs = ''; } "} - "Use different name for checks")]) - - (fc/with-field :vendor/print-as - (com/validated-field (-> {:label "Print as" - :errors (fc/field-errors) - :x-show "showPrintAs"} - hx/alpine-appear - hx/alpine-disappear) - (com/text-input {:name (fc/field-name) - :x-model "printAs" - :value (fc/field-value) - :class "w-96"}))) - - (fc/with-field :vendor/hidden - (alog/peek (cursor/path fc/*current*)) - (com/checkbox {:name (fc/field-name) - :value (boolean (fc/field-value)) ; - :checked (alog/peek :checked (fc/field-value))} - "Admin-only"))]) - (com/modal-footer - {} - [:div.flex.justify-end - (com/form-errors {:errors (:errors fc/*form-errors*)}) - [:div.flex.items-baseline.gap-x-4 - (com/validated-save-button {:errors (seq form-errors) - :class "w-48"} - - "Merge")]])))]]))) - - - -(defn new-terms-override [{{:keys [index]} :query-params}] - (html-response - (fc/start-form-with-prefix - [:vendor/terms-overrides (or index 0)] - {:db/id (str (java.util.UUID/randomUUID)) - :new? true} - [] - (terms-override-row fc/*current*)))) - -(defn new-automatic-payment [{{:keys [index]} :query-params}] - (html-response - (fc/start-form-with-prefix - [:vendor/automatically-paid-when-due (or index 0)] - {:db/id nil - :new? true} - [] - (automatically-paid-when-due-row fc/*current*)))) - -(defn new-account-override [{{:keys [index]} :query-params}] - (html-response - (fc/start-form-with-prefix - [:vendor/account-overrides (or index 0)] - {:db/id (str (java.util.UUID/randomUUID)) - :new? true} - [] - (account-override-row fc/*current*)))) - -(defn modal-next [{:keys [form-params form-errors] :as request}] - (html-response [:div] - :headers {"hx-trigger-after-settle" "modalnext" - "hx-reswap" "none"})) - -(defn single-modal [which {:keys [form-params form-errors] :as request}] - (fc/start-form form-params form-errors - (html-response (which request)))) - -(defn branch-handler [f & {:as matches}] - (fn branch-handler* [request] - (let [c (f request)] - (some->> matches - (filter (fn [[matching-key matching-handler]] - (= c matching-key))) - first - seq - ((fn [[_ matching-handler]] - (matching-handler request))))))) + "Merge")]]))]]))) (defn account-typeahead [{{:keys [name value client-id] :as qp} :query-params}] (html-response (account-typeahead* {:name name @@ -963,73 +487,432 @@ :client-id client-id :x-model "accountId"}))) +(defrecord LegalEntityModal [linear-wizard] + mm/ModalWizardStep + (step-key [this] + :legal) + (edit-path [this request] []) + (render-step [this _] + (mm/default-render-step + linear-wizard this + :head [:div.flex [:div.p-2 "Legal Entity"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 + [:span {:x-text "vendorName"}]]] + :body (mm/default-step-body + {} + [:div {:class "w-[600px] h-[350px]"} + [:div.grid.grid-cols-6.gap-x-4.gap-y-2 + [: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" + :autofocus true + :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")})))]]]) + :footer + (mm/default-step-footer linear-wizard this :validation-route ::route/navigate) + :validation-route ::route/navigate)) + (step-schema [this] + (mut/select-keys form-schema #{:vendor/legal-entity-1099-type :vendor/legal-entity-first-name :vendor/legal-entity-last-name :vendor/legal-entity-tin + :vendor/legal-entity-tin-type :vendor/legal-entity-middle-name :vendor/legal-entity-name})) + (step-name [this] + "Legal Entity")) + +(defrecord AddressModal [linear-wizard] + mm/ModalWizardStep + (step-key [this] + :address) + (edit-path [this request] []) + (render-step [this _] + (mm/default-render-step + linear-wizard this + :head [:div.flex [:div.p-2 "Address"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 + [:span {:x-text "vendorName"}]]] + :body (mm/default-step-body + {} + [:div.space-y-1 {:class "w-[600px] h-[350px]"} + (fc/with-field-default :vendor/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)})))]])]) + :footer + (mm/default-step-footer linear-wizard this :validation-route ::route/navigate) + :validation-route ::route/navigate)) + (step-schema [this] + (mut/select-keys form-schema #{:vendor/address})) + (step-name [this] + "Address")) + +(defrecord AccountModal [linear-wizard] + mm/ModalWizardStep + (step-key [this] + :account) + (edit-path [this request] + []) + (render-step [this _] + (mm/default-render-step + linear-wizard this + :head [:div.flex [:div.p-2 "Account Assignments"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 + [:span {:x-text "vendorName"}]]] + :body (mm/default-step-body + {} + [:div.space-y-1 {:class "w-[600px] h-[350px] "} + (fc/with-field :vendor/default-account + (alog/info ::acount-check :a (fc/field-value)) + (com/validated-field {:label "Default Account" + :errors (fc/field-errors)} + (com/typeahead {:name (fc/field-name) + :error? (fc/error?) + :autofocus true + :class "w-96" + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes :account-search) + :value (fc/field-value) + :content-fn (fn [c] (pull-attr (dc/db conn) :account/name c))}))) + (fc/with-field :vendor/account-overrides + (com/validated-field + {:errors (fc/field-errors) + :label "Account Overrides"} + (com/data-grid {:headers [(com/data-grid-header {} "Client") + (com/data-grid-header {} "Account") + (com/data-grid-header {:class "w-16"})]} + (fc/cursor-map #(account-override-row %)) + (com/data-grid-new-row {:colspan 3 + :hx-get (bidi/path-for ssr-routes/only-routes ::route/new-account-override) + :index (count (fc/field-value))} + "New override"))))]) + :footer + (mm/default-step-footer linear-wizard this :validation-route ::route/navigate) + :validation-route ::route/navigate)) + (step-schema [this] + (mut/select-keys (mm/form-schema linear-wizard) #{:vendor/account-overrides :vendor/default-account})) + (step-name [this] + "Account Assignment")) + +;; TODO signature +(defrecord TermsModal [linear-wizard] + mm/ModalWizardStep + (step-key [this] + :terms) + (edit-path [this request] + []) + (render-step [this _] + (mm/default-render-step + linear-wizard this + :head [:div.flex [:div.p-2 "Terms"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 + [:span {:x-text "vendorName"}]]] + :body (mm/default-step-body + {} + [:div.space-y-1 {:class "w-[600px] h-[350px]"} + (fc/with-field :vendor/terms + (com/validated-field {:label "Terms" + :errors (fc/field-errors)} + [:div.flex.items-baseline.gap-x-4 + (com/int-input {:name (fc/field-name) + :autofocus true + :value (fc/field-value)}) + "days"])) + (fc/with-field :vendor/terms-overrides + (com/validated-field + {:errors (fc/field-errors) + :label "Terms Overrides"} + (com/data-grid {:headers [(com/data-grid-header {} "Client") + (com/data-grid-header {:class "w-16"} "Terms") + (com/data-grid-header {:class "w-16"})]} + (fc/cursor-map #(terms-override-row %)) + (com/data-grid-new-row {:colspan 3 + :hx-get (bidi/path-for ssr-routes/only-routes + ::route/new-terms-override) + :index (count (fc/field-value))} + "New override")))) + + (fc/with-field :vendor/automatically-paid-when-due + (com/validated-field + {:errors (fc/field-errors) + :label "Automatically pay when due"} + (com/data-grid {:headers [(com/data-grid-header {} "Client") + (com/data-grid-header {:class "w-16"})]} + (fc/cursor-map #(automatically-paid-when-due-row %)) + (com/data-grid-new-row {:colspan 2 + :hx-get (bidi/path-for ssr-routes/only-routes ::route/new-automatic-payment) + :index (count (fc/field-value))} + "New automatic payment for client"))))]) + :footer + (mm/default-step-footer linear-wizard this :validation-route ::route/navigate) + :validation-route ::route/navigate)) + (step-schema [this] + (mut/select-keys (mm/form-schema linear-wizard) #{:vendor/terms :vendor/terms-overrides :vendor/automatically-paid-when-due})) + (step-name [this] + "Terms")) + +(defrecord InfoModal [linear-wizard] + mm/ModalWizardStep + (step-name [this] + "Basic Info") + + (step-key [this] + :info) + + (edit-path [this request] []) + + (step-schema [this] + (mut/select-keys (mm/form-schema linear-wizard) #{:vendor/name :vendor/print-as :vendor/hidden})) + + (render-step [this _] + (mm/default-render-step + linear-wizard this + :head [:div.flex [:div.p-2 "Basic Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 + [:span {:x-text "vendorName"}]]] + :body (mm/default-step-body {} + [:div.space-y-1.mt-4 {:class "w-[600px] h-[350px]"} + (fc/with-field :vendor/name + (com/validated-field {:label "Name" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :value (fc/field-value) + :x-model "vendorName" + :autofocus true + :class "w-96"}))) + (com/validated-field + {} + [:div (com/checkbox + {:x-model "showPrintAs" "@change" "if (!showPrintAs) { printAs = ''; } "} + "Use different name for checks")]) + + (fc/with-field :vendor/print-as + (com/validated-field (-> {:label "Print as" + :errors (fc/field-errors) + :x-show "showPrintAs"} + hx/alpine-appear + hx/alpine-disappear) + (com/text-input {:name (fc/field-name) + :x-model "printAs" + :value (fc/field-value) + :class "w-96"}))) + + (fc/with-field :vendor/hidden + (alog/peek (cursor/path fc/*current*)) + (com/checkbox {:name (fc/field-name) + :value (boolean (fc/field-value)) ; + :checked (alog/peek :checked (fc/field-value))} + "Admin-only"))]) + :footer + (mm/default-step-footer linear-wizard this :validation-route ::route/navigate) + :validation-route ::route/navigate))) + +;; have a clear way to set up the form to handle the unexpected-errors +;; TODO feature flags +;; TODO move signature to client page +(defrecord VendorWizard [current-step] + mm/LinearModalWizard + (hydrate-from-request [this _] + this) + (navigate [this step-key] + (assoc this :current-step step-key)) + (get-current-step [this] + (if current-step + (mm/get-step this current-step) + (mm/get-step this :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 {"vendorName" (:vendor/name (:snapshot multi-form-state)) + "showPrintAs" (boolean (not-empty (:vendor/print-as (:snapshot multi-form-state)))) + "printAs" (:vendor/print-as (:snapshot multi-form-state))}))))) + (steps [_] + [:info + :terms + :account + :address + :legal]) + + (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) + :terms (->TermsModal this) + :account (->AccountModal this) + :address (->AddressModal this) + :legal (->LegalEntityModal this)} + step-key)))) + (form-schema [this] form-schema) + (submit [this {:keys [multi-form-state request-method identity entity] :as request}] + (let [snapshot (mc/decode + form-schema + (:snapshot multi-form-state) + mt/strip-extra-keys-transformer) + entity (cond-> snapshot + (= :post request-method) (assoc :db/id "new")) + {:keys [tempids]} (audit-transact [[:upsert-entity entity]] + (:identity request)) + updated-vendor (dc/pull (dc/db conn) + default-read + (or (get tempids (:db/id entity)) (:db/id entity)))] + (solr/index-documents-raw + solr/impl + "vendors" + [{"id" (:db/id updated-vendor) + "name" (:vendor/name updated-vendor) + "hidden" (boolean (:vendor/hidden updated-vendor))}]) + (html-response + (row* identity updated-vendor {: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-vendor)) + "hx-reswap" "outerHTML")))))) + +(def vendor-wizard (->VendorWizard :info)) + + (def key->handler - (apply-middleware-to-all-handlers - (->> - {::route/page (helper/page-route grid-page) - ::route/table (helper/table-route grid-page) - ::route/new dialog - ::route/merge merge-dialog - ::route/merge-submit (-> merge-submit - (wrap-schema-enforce :form-schema merge-form-schema) - (wrap-form-4xx-2 merge-dialog)) - ::route/validate (-> (branch-handler (comp :step :query-params) - "Info" (-> modal-next - (wrap-schema-enforce :form-schema (mut/select-keys form-schema #{:vendor/name :vendor/print-as})) - (wrap-form-4xx-2 (partial single-modal info-modal))) - "Terms" (-> modal-next - (wrap-schema-enforce :form-schema - (mut/select-keys form-schema #{:vendor/terms :vendor/terms-overrides :vendor/automatically-paid-when-due})) - (wrap-form-4xx-2 (partial single-modal terms-modal))) - "Account" (-> modal-next - (wrap-schema-enforce :form-schema - (mut/select-keys form-schema #{:vendor/default-account :vendor/account-overrides})) - (wrap-form-4xx-2 (partial single-modal account-modal))) - "Address" - (-> modal-next - (wrap-schema-enforce :form-schema - (mut/select-keys form-schema #{:vendor/address})) - (wrap-form-4xx-2 (partial single-modal address-modal)))) - (wrap-schema-enforce :query-schema [:map [:step :string]]) - (wrap-schema-decode :form-schema form-schema) - (wrap-nested-form-params) - #_(wrap-form-4xx-2 (-> dialog - (wrap-entity [:form-params :db/id] default-read))) - ) - ::route/save (-> 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 dialog [:form-params :db/id] default-read))) - ::route/edit (-> dialog - (wrap-entity [:route-params :db/id] default-read) - (wrap-schema-enforce :route-schema [:map [:db/id entity-id]])) - ::route/edit-terms (-> terms-modal - (wrap-entity [:form-params :db/id] default-read) - (wrap-schema-enforce :form-schema form-schema) - (wrap-nested-form-params) - (wrap-form-4xx-2 (-> dialog - (wrap-entity [:form-params :db/id] default-read)))) - ::route/new-terms-override (-> new-terms-override - (wrap-schema-enforce :query-schema [:map - [:index {:optional true - :default 0} [nat-int? {:default 0}]]])) - ::route/account-typeahead (-> account-typeahead - (wrap-schema-enforce :query-schema [:map - [:name :string] - [:client-id {:optional true} - [:maybe entity-id]] - [:value {:optional true} - [:maybe entity-id]]])) - ::route/new-automatic-payment (-> new-automatic-payment - (wrap-schema-enforce :query-schema [:map - [:index {:optional true - :default 0} [nat-int? {:default 0}]]])) - ::route/new-account-override (-> new-account-override - (wrap-schema-enforce :query-schema [:map - [:index {:optional true - :default 0} [nat-int? {:default 0}]]]))}) - (fn [h] - (-> h - (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) + ::route/new (-> mm/open-wizard-handler + (mm/wrap-init-multi-form-state (fn [_] + (mm/->MultiStepFormState {} + [] + {}))) + (mm/wrap-wizard vendor-wizard)) + ::route/merge merge-dialog + ::route/merge-submit (-> merge-submit + (wrap-schema-enforce :form-schema merge-form-schema) + (wrap-form-4xx-2 merge-dialog)) + + ::route/save (-> mm/submit-handler + (mm/wrap-wizard vendor-wizard) + (mm/wrap-decode-multi-form-state) + (wrap-entity [:form-params :db/id] default-read)) + ::route/navigate + (-> mm/next-handler + (mm/wrap-wizard vendor-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/edit (-> mm/open-wizard-handler + (mm/wrap-wizard vendor-wizard) + (mm/wrap-init-multi-form-state (fn [request] + (mm/->MultiStepFormState (:entity request) + [] + (:entity request)))) + (wrap-entity [:route-params :db/id] default-read) + (wrap-schema-enforce :route-schema [:map [:db/id entity-id]])) + ::route/new-terms-override (add-new-entity-handler [:step-params :vendor/terms-overrides] + (fn [cursor _] (terms-override-row cursor))) + + ::route/account-typeahead (-> account-typeahead + (wrap-schema-enforce :query-schema [:map + [:name :string] + [:client-id {:optional true} + [:maybe entity-id]] + [:value {:optional true} + [:maybe entity-id]]])) + ::route/new-automatic-payment (add-new-primitive-handler [:step-params :vendor/automatically-paid-when-due] + {} + automatically-paid-when-due-row) + + ::route/new-account-override (add-new-entity-handler [:step-params :vendor/account-overrides] + (fn [cursor _] (account-override-row cursor)))}) + (fn [h] + (-> h + (wrap-admin) + (wrap-client-redirect-unauthenticated))))) + diff --git a/src/clj/auto_ap/ssr/common_handlers.clj b/src/clj/auto_ap/ssr/common_handlers.clj new file mode 100644 index 00000000..2c1774e5 --- /dev/null +++ b/src/clj/auto_ap/ssr/common_handlers.clj @@ -0,0 +1,32 @@ +(ns auto-ap.ssr.common-handlers + (: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 _] + 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) + [] + (render-fn fc/*current* request)))) + (wrap-schema-enforce :query-schema [:map + [:index {:optional true + :default 0} [nat-int? {:default 0}]]])))) + +(defn add-new-primitive-handler [path default-value render-fn] + (-> (fn new-location-match [{{:keys [index]} :query-params}] + (html-response + (fc/start-form-with-prefix (conj path (or index 0)) + default-value + [] + (render-fn fc/*current*)))) + (wrap-schema-enforce :query-schema [:map + [:index {:optional true + :default 0} [nat-int? {:default 0}]]]))) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/company.clj b/src/clj/auto_ap/ssr/company.clj index aa1a19d2..c609e086 100644 --- a/src/clj/auto_ap/ssr/company.clj +++ b/src/clj/auto_ap/ssr/company.clj @@ -1,87 +1,162 @@ (ns auto-ap.ssr.company - (:require - [auto-ap.datomic :refer [conn pull-attr]] - [auto-ap.datomic.clients :refer [full-read]] - [auto-ap.solr :as solr] - [auto-ap.ssr-routes :as ssr-routes] - [auto-ap.ssr.components :as com] - [auto-ap.ssr.svg :as svg] - [auto-ap.ssr.ui :refer [base-page]] - [auto-ap.ssr.utils :refer [html-response]] - [bidi.bidi :as bidi] - [cemerick.url :as url] - [clojure.string :as str] - [config.core :refer [env]] - [datomic.api :as dc] - [ring.middleware.json :refer [wrap-json-response]])) + (:require [amazonica.aws.s3 :as s3] + [auto-ap.datomic :refer [conn pull-attr]] + [auto-ap.datomic.clients :refer [full-read]] + [auto-ap.permissions :as permissions] + [auto-ap.solr :as solr] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [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]] + [bidi.bidi :as bidi] + [cemerick.url :as url] + [clojure.java.io :as io] + [clojure.string :as str] + [config.core :refer [env]] + [datomic.api :as dc] + [ring.middleware.json :refer [wrap-json-response]]) + (:import [java.util UUID] + (org.apache.commons.codec.binary Base64))) (defn please-select-client-screen* [] [:div.grid.grid-cols-3 (com/content-card {} [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} - "Please select a company"] - ])]) + "Please select a company"]])]) -(defn main-content* [{:keys [client]}] +(defn signature [request] + (let [signature-file (pull-attr (dc/db conn) :client/signature-file (:db/id (:client request)))] + (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 " + :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}"} + [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} + "Signature"] + [: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 + (when signature-file + [:img.rounded.rounded-lg.border.border-gray-300.bg-gray-50 {:src signature-file + :width 696 + :height 261 + :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()" + ":class" "editing ? 'bg-white' : 'bg-gray-50' " + :width 696 + :height 261 + :x-show "existing ? editing: true"}]] + + [:div.flex.gap-2.justify-end + (com/button {:color :primary + :x-show "!editing" + "@click" "signature.clear(); signature.on(); editing=true;"} + "New signature") + (com/button {:color :primary + :x-show "editing" + "@click" "signature.clear();"} + "Clear") + + (com/button {:color :primary + "@click" "$data.signatureData=signature.toDataURL('image/png'); signature.off(); editing=false; $dispatch('accepted', {signatureData: $data.signatureData}) " + :x-show "editing"} + "Accept")]]))) + +(defn upload-signature-data [{{:strs [signatureData]} :form-params client :client :as request}] + (let [prefix "data:image/png;base64,"] + (when signatureData + (when-not (str/starts-with? signatureData prefix) + (throw (ex-info "Invalid signature image" {:validation-error (str "Invalid signature image.")}))) + (let [signature-id (str (UUID/randomUUID)) + raw-bytes (Base64/decodeBase64 (subs signatureData (count prefix)))] + (s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env) + :key (str signature-id ".png") + :input-stream (io/make-input-stream raw-bytes {}) + :metadata {:content-type "image/png" + :content-length (count raw-bytes)} + :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 ".png")}]) + (html-response + (signature request)))))) + +(defn main-content* [{:keys [client identity] :as request}] (if-not client (please-select-client-screen*) (let [client (dc/pull (dc/db conn) full-read (:db/id client))] - [:div.grid.grid-cols-3.gap-4 - (com/content-card {} - [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} - [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} - (:client/name client)] - (when-let [address (-> client :client/address)] - [:div.flex.flex-col.gap-1.text-lg.dark:text-white.text-gray-700 - [:p (-> address :address/street1)] - [:p (-> address :address/street2)] - [:p (-> address :address/city) " " - (-> address :address/state) ", " - (-> address :address/zip)]])] - ) - (com/content-card {} - [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} - [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} - "Downloads"] - [:a {:href (str (assoc (url/url (str (:base-url env) "/api/vendors/company/export")) - :query {"client" (:client/code client)}))} - (com/button {:color :primary} - "Download vendor list" - (com/button-icon {} svg/download))]])]))) + [:div + [:div.grid.grid-cols-3.gap-4 + (com/content-card {} + [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} + [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} + (:client/name client)] + (when-let [address (-> client :client/address)] + [:div.flex.flex-col.gap-1.text-lg.dark:text-white.text-gray-700 + [:p (-> address :address/street1)] + [:p (-> address :address/street2)] + [:p (-> address :address/city) " " + (-> address :address/state) ", " + (-> address :address/zip)]])]) + (com/content-card {} + [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} + [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} + "Downloads"] + [:a {:href (str (assoc (url/url (str (:base-url env) "/api/vendors/company/export")) + :query {"client" (:client/code client)}))} + (com/button {:color :primary} + "Download vendor list" + (com/button-icon {} svg/download))]]) + [:div]] + (when (permissions/can? identity {:client client :subject :signature :activity :edit}) + (signature request))]))) (defn page [{:keys [identity matched-route] :as request}] (base-page - request - (com/page {:nav (com/company-aside-nav) - :client-selection (:client-selection (:session request)) - :client (:client request) - :identity (:identity request) - :app-params { - :hx-get (bidi/path-for ssr-routes/only-routes - :company) - :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 - :company)} - "My Company"]) - (main-content* {:client (:client request)})) - "My Company")) + request + (com/page {:nav (com/company-aside-nav) + :client-selection (:client-selection (:session request)) + :client (:client request) + :identity (:identity request) + :app-params {:hx-get (bidi/path-for ssr-routes/only-routes + :company) + :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 + :company)} + "My Company"]) + (main-content* request)) + "My Company")) (defn search [{:keys [clients query-params]}] (let [valid-client-ids (set (map :db/id clients)) name-like-ids (when (not-empty (get query-params "q")) - (set (map (comp #(Long/parseLong %) :id) - (solr/query solr/impl "clients" - {"query" (format "_text_:(%s*)" (str/upper-case (solr/escape (get query-params "q")))) - "fields" "id" - "limit" 300})))) + (set (map (comp #(Long/parseLong %) :id) + (solr/query solr/impl "clients" + {"query" (format "_text_:(%s*)" (str/upper-case (solr/escape (get query-params "q")))) + "fields" "id" + "limit" 300})))) valid-clients (for [n name-like-ids :when (valid-client-ids n)] - {"value" n "label" (pull-attr (dc/db conn) :client/name n)} - )] + {"value" n "label" (pull-attr (dc/db conn) :client/name n)})] {:body (take 10 valid-clients)})) (def search (wrap-json-response search)) @@ -109,13 +184,13 @@ (defn bank-account-typeahead* [{:keys [client-id name value]}] (if client-id (com/typeahead {:name name - :class "w-96" - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes :bank-account-search - :db/id client-id) - :value value - :value-fn (some-fn :db/id identity) - :content-fn (some-fn :bank-account/name #(pull-attr (dc/db conn) :bank-account/name %))}) + :class "w-96" + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes :bank-account-search + :db/id client-id) + :value value + :value-fn (some-fn :db/id identity) + :content-fn (some-fn :bank-account/name #(pull-attr (dc/db conn) :bank-account/name %))}) [:span.text-xs.text-gray-500 "Please select a client before selecting a bank account." [:input {:type "hidden" :name name}]])) diff --git a/src/clj/auto_ap/ssr/components.clj b/src/clj/auto_ap/ssr/components.clj index c3077285..d1e6a3e5 100644 --- a/src/clj/auto_ap/ssr/components.clj +++ b/src/clj/auto_ap/ssr/components.clj @@ -11,8 +11,7 @@ [auto-ap.ssr.components.tags :as tags] [auto-ap.ssr.components.paginator :as paginator] [auto-ap.ssr.components.radio :as radio])) - - +;; potemkin can be used here (def breadcrumbs breadcrumbs/breadcrumbs-) (def button buttons/button-) (def validated-save-button buttons/validated-save-button-) @@ -24,8 +23,7 @@ (def button-group-button buttons/group-button-) (def modal dialog/modal-) (def modal-card dialog/modal-card-) -(def stacked-modal-card dialog/stacked-modal-card-) -(def stacked-modal-card-2 dialog/stacked-modal-card-2-) +(def modal-card-advanced dialog/modal-card-advanced-) (def modal-header dialog/modal-header-) (def modal-header-attachment dialog/modal-header-attachment-) (def modal-body dialog/modal-body-) @@ -70,12 +68,9 @@ (def data-grid-new-row data-grid/new-row-) (defn link [params & children] - (into [:a (update params :class str " font-medium text-blue-600 dark:text-blue-500 hover:underline ")] + (into [:a (update params :class str " font-medium text-blue-600 dark:text-blue-500 hover:underline cursor-pointer")] children)) - - - (def paginator paginator/paginator-) (def data-grid-card data-grid/data-grid-card-) diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index 736bcd00..e48b7520 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -8,8 +8,10 @@ [auto-ap.routes.admin.transaction-rules :as transaction-rules] [auto-ap.ssr.hiccup-helper :as hh] [auto-ap.routes.admin.import-batch :as ib-routes] + [auto-ap.routes.admin.clients :as ac-routes] [auto-ap.routes.admin.excel-invoices :as ei-routes] - [auto-ap.routes.admin.vendors :as v-routes])) + [auto-ap.routes.admin.vendors :as v-routes] + [auto-ap.graphql.clients :as clients])) (defn menu-button- [params & children] [:div @@ -199,7 +201,7 @@ [:li (menu-button- {:icon svg/restaurant - :href (bidi/path-for client-routes/routes :admin-clients) + :href (bidi/path-for ssr-routes/only-routes ::ac-routes/page) :target "_new"} "Clients")] [:li diff --git a/src/clj/auto_ap/ssr/components/buttons.clj b/src/clj/auto_ap/ssr/components/buttons.clj index 90d664ba..151ece2e 100644 --- a/src/clj/auto_ap/ssr/components/buttons.clj +++ b/src/clj/auto_ap/ssr/components/buttons.clj @@ -176,7 +176,7 @@ (defn validated-save-button- [{:keys [errors class] :as params} & children] - (button- (-> {:color (or (:color params) :primary) + (button- (-> {:color (or (:color params) :primary) :type "submit" :class (cond-> (or class "") true (hh/add-class "w-32") (seq errors) (hh/add-class "animate-shake"))} diff --git a/src/clj/auto_ap/ssr/components/card.clj b/src/clj/auto_ap/ssr/components/card.clj index 47f6ac15..474bb89c 100644 --- a/src/clj/auto_ap/ssr/components/card.clj +++ b/src/clj/auto_ap/ssr/components/card.clj @@ -1,13 +1,17 @@ (ns auto-ap.ssr.components.card (:require [auto-ap.ssr.hiccup-helper :as hh] - [auto-ap.ssr.hx :as hx])) + [auto-ap.ssr.hx :as hx] + [clojure.string :as str])) (defn card- [params & children] - (into [:div (update params :class str " shadow-md dark:bg-gray-800 sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 bg-white overflow-hidden")] + (into [:div (update params :class + #(cond-> (or % "") + (not (str/includes? % "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")))] children)) (defn content-card- [params & children] - [:section {:class (hh/add-class " py-3 sm:py-5" (:class params))} + [:section (merge params {:class (hh/add-class " py-3 sm:py-5" (:class params))}) [:div {:class "max-w-screen-2xl"} (into [:div {:class "relative overflow-hidden shadow-md dark:bg-gray-800 sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 bg-white"}] diff --git a/src/clj/auto_ap/ssr/components/dialog.clj b/src/clj/auto_ap/ssr/components/dialog.clj index 510979a7..c630cd54 100644 --- a/src/clj/auto_ap/ssr/components/dialog.clj +++ b/src/clj/auto_ap/ssr/components/dialog.clj @@ -3,36 +3,19 @@ [auto-ap.ssr.hiccup-helper :as hh] [auto-ap.ssr.hx :as hx])) -(defn modal- [params & children] + +(defn modal- + "This modal function is used to create a modal window with a stack that allows for transitioning between modals. + + :params should include the following keys: + - :handle-unexpected-error? (default: true) - A boolean indicating whether to handle unexpected errors. + - :class (optional) - A string representing additional CSS classes to add to the modal. + + &children should include the child components to be rendered within the modal." + [{:as params} & children] [:div (-> params - (assoc "@click.outside" "open=false" - :x-data (hx/json {:index 0 :hidingIndex -1 :unexpectedError false :transitioning false}) - "x-on:htmx:response-error" "unexpectedError=true" - "x-on:htmx:before-request" "unexpectedError=false" - :x-ref "modalStack" - "@modalnext.window" - " $refs.modalStack.children[index].setAttribute('x-transition:leave-end', '-translate-x-full scale-0 opacity-0' ); - $refs.modalStack.children[index + 1].setAttribute('x-transition:enter-start', 'translate-x-full scale-0 opacity-0' ); - hidingIndex = index; - setTimeout(() => {index ++; transitioning=true; hidingIndex = -1; }, 150); - setTimeout(() => transitioning=false, 320)" - - "@modalprevious.window" - " $refs.modalStack.children[index].setAttribute('x-transition:leave-end', 'translate-x-full scale-0 opacity-0' ); - $refs.modalStack.children[index - 1].setAttribute('x-transition:enter-start', '-translate-x-full scale-0 opacity-0' ); - hidingIndex = index; - setTimeout(() => { index --; hidingIndex = -1; transitioning=true; }, 150); - setTimeout(() => transitioning=false, 320)" - - "@modalpop.window" - " $refs.modalStack.children[index].setAttribute('x-transition:leave-end', 'translate-x-full scale-0 opacity-0' ); - $refs.modalStack.children[index - 1].setAttribute('x-transition:enter-start', '-translate-x-full scale-0 opacity-0' ); - hidingIndex = index; - setTimeout(() => {index --; transitioning=true;}, 150); - setTimeout(() => { $refs.modalStack.removeChild($refs.modalStack.children[index+1]); hidingIndex=-1; }, 300); - setTimeout(() => transitioning=false, 320) - " - ) + (assoc "@click.outside" "open=false") + (dissoc :handle-unexpected-error?) (update :class (fnil hh/add-class "") "w-full h-full modal-stack")) children]) @@ -40,11 +23,10 @@ [:div (update params :class (fn [c] (-> c (or "") - (hh/add-class "w-full p-4 h-full modal-card") - ))) + (hh/add-class "w-full p-4 h-full modal-card")))) [:div {:class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content w-full flex flex-col h-full"} [:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} header] - [:div {:class "px-6 space-y-6 overflow-y-scroll w-full shrink"} + [:div {:class "px-6 py-2 space-y-6 overflow-y-scroll w-full shrink"} #_[:div.bg-green-300.w-full.h-64 "hello"] content] @@ -55,28 +37,6 @@ [:div {:class "shrink-0"} footer]])]]) - -(defn stacked-modal-card- [index params header content footer] - [:div (merge params - {:class (hh/add-class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col h-full" (:class params "")) - :x-data (hx/json {:i index}) - :x-show "index == i && hidingIndex != i" - "x-trap" "index == i && hidingIndex == -1 && !transitioning" - "x-transition:enter" "transition duration-150", - "x-transition:enter-end" "translate-x-0 scale-100 opacity-100", - "x-transition:leave" "transition duration-150", - "x-transition:leave-start" "translate-x-0 scale-100 opacity-100", - }) - [:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} header] ;; todo componentize these - [:div {:class "px-6 space-y-6 overflow-y-scroll w-full shrink"} - - content] ;; TODO componentize - (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 bg-red-500 rounded-full"}] - [:span.px-2.py-0.5 "An unexpected error has occured. Integreat staff have been notified."]] - [:div {:class "shrink-0"}]footer])]) - (defn modal-header- [params & children] [:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} children]) @@ -92,21 +52,14 @@ (defn modal-footer- [params & children] [: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.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 bg-red-500 rounded-full"}] [:span.px-2.py-0.5 "An unexpected error has occured. Integreat staff have been notified."]] [:div {:class "shrink-0"}] children]) -(defn stacked-modal-card-2- [index params & children] +(defn modal-card-advanced- [params & children] [:div (merge params - {:class (hh/add-class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col h-full" (:class params "")) - :x-data (hx/json {:i index}) - :x-show "index == i && hidingIndex != i" - "x-trap" "index == i && hidingIndex == -1 && !transitioning" - "x-transition:enter" "transition duration-150", - "x-transition:enter-end" "translate-x-0 scale-100 opacity-100", - "x-transition:leave" "transition duration-150", - "x-transition:leave-start" "translate-x-0 scale-100 opacity-100", - }) + {:class (hh/add-class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col h-full" (:class params "")) }) children]) diff --git a/src/clj/auto_ap/ssr/components/multi_modal.clj b/src/clj/auto_ap/ssr/components/multi_modal.clj new file mode 100644 index 00000000..f0866984 --- /dev/null +++ b/src/clj/auto_ap/ssr/components/multi_modal.clj @@ -0,0 +1,349 @@ +(ns auto-ap.ssr.components.multi-modal + (:require [auto-ap.ssr.components :as com] + [auto-ap.ssr.form-cursor :as fc] + + [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] + [auto-ap.ssr.utils + :refer [ html-response + assert-schema + main-transformer + modal-response + wrap-form-4xx-2 + wrap-schema-enforce]] + [auto-ap.ssr.components.timeline :as timeline] + [bidi.bidi :as bidi] + [hiccup.util :as hu] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.hx :as hx] + [malli.core :as mc] + [hiccup2.core :as hiccup2] + [hiccup2.core :as hiccup] + [auto-ap.cursor :as cursor] + [malli.core :as m] + [auto-ap.logging :as alog]) + (:import [auto_ap.cursor VecCursor])) + + +(def default-form-props {:hx-ext "response-targets" + :hx-swap "outerHTML" + :hx-target-400 "#form-errors .error-content" + :hx-trigger "submit" + :hx-target "this" + "x-trap" "true" + :class "h-full w-full" }) + +(defprotocol ModalWizardStep + (step-key [this]) + (edit-path [this request]) + (render-step [this request]) + (step-schema [this]) + (step-name [this])) + +(defprotocol Initializable + (init-step-params [this request])) + +(defprotocol Discardable + (can-discard? [this step-params]) + (discard-changes [this request])) + + +(defn- init-step-params- [step request] + (if (satisfies? Initializable step) + (init-step-params step request) + {})) + +(defprotocol LinearModalWizard + (hydrate-from-request [this request]) + (get-current-step [this]) + (navigate [this step-key]) + + (form-schema [this]) + (steps [this]) + (get-step [this step-key]) + (render-wizard [this request]) + (submit [this request])) + +(defrecord MultiStepFormState [snapshot edit-path step-params]) +(defn select-state [multi-form-state edit-path default] + (->MultiStepFormState (:snapshot multi-form-state) + edit-path + (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 + edit-cursor (cond-> cursor + (seq edit-path) (cursor/ensure-path! edit-path {}) + (seq edit-path) (get-in edit-path {})) + + _ (cursor/transact! edit-cursor (fn [spot] + (merge spot step-params)))] + (assoc multi-form-state + :snapshot @cursor + :edit-path [] + :step-params @cursor))) + +(def step-key-schema (mc/schema [:orn {:decode/arbitrary clojure.edn/read-string + :encode/arbitrary pr-str} + [:sub-step [:cat :keyword [:or :int :string]]] + [:step :keyword]])) + +(def encode-step-key + (m/-instrument {:schema [:=> [:cat step-key-schema] :any]} + (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))] + (timeline/vertical-timeline + {} + (for [[n i] (map vector (steps linear-wizard) (range))] + (timeline/vertical-timeline-step (cond-> {} + (= i active-index) (assoc :active? true) + (< i active-index) (assoc :visited? true) + (= i (dec (count step-names))) (assoc :last? true)) + [:a.cursor-pointer.whitespace-nowrap {:x-data (hx/json {:timelineIndex i}) + :hx-put (hu/url (bidi/path-for ssr-routes/only-routes validation-route) + {:from (encode-step-key (step-key current-step)) + :to (encode-step-key (step-key (get-step linear-wizard n)))})} + (step-name (get-step linear-wizard n))]))))) +(defn back-button [linear-wizard step validation-route] + [:a.cursor-pointer.whitespace-nowrap {:hx-put (hu/url (bidi/path-for ssr-routes/only-routes validation-route) + {:from (encode-step-key (step-key step)) + :to (encode-step-key (->> (partition-all 2 1 (steps linear-wizard)) + (filter (fn [[from to]] + (= to (step-key step)))) + ffirst))})} + "Back"]) + +(defn default-next-button [linear-wizard step validation-route] + (let [steps (steps linear-wizard) + last? (= (step-key step) (last steps)) + next-step (when-not last? (->> steps + (drop-while #(not= (step-key step) + %)) + (drop 1) + first + (get-step linear-wizard)))] + (com/validated-save-button (cond-> {:errors (seq fc/*form-errors*) + ;;:x-data (hx/json {}) + :x-ref "next" + :class "w-48"} + (not last?) (assoc :hx-put (hu/url (bidi/path-for ssr-routes/only-routes validation-route) + {:from (encode-step-key (step-key step)) + :to (encode-step-key (step-key next-step))}))) + + (if next-step + (step-name next-step) + "Save") + (when-not last? + [:div.w-5.h-5 svg/arrow-right])))) + +(defn default-step-body [params & children] + [:div.space-y-1 {:class "w-[600px] h-[700px]"} + children]) + +(defn default-step-footer [linear-wizard step & {:keys [validation-route + discard-button + next-button]}] + [:div.flex.justify-end + [:div.flex.items-baseline.gap-x-4 + (com/form-errors {:errors (:errors (:step-params fc/*form-errors*))}) + (when (not= (first (steps linear-wizard)) + (step-key step)) + (when validation-route + (back-button linear-wizard step validation-route))) + (when (and (satisfies? Discardable step) (can-discard? step @fc/*current*)) + discard-button) + (cond next-button + next-button + + validation-route + (default-next-button linear-wizard step validation-route) + + :else + [:div "No action possible."])]]) + +(defn default-render-step [linear-wizard step & {:keys [head body footer validation-route discard-route]}] + (let [is-last? (= (step-key step) (last (steps linear-wizard)))] + (com/modal-card-advanced + {"@keydown.enter.prevent.stop" "$refs.next.click()" + :class (str (when is-last? "last-modal-step") + " transition duration-300 ease-in-out + ") + ":class" (hiccup/raw "{ + \"htmx-swapping:-translate-x-2/3 htmx-swapping:opacity-0 htmx-swapping:scale-0 htmx-added:translate-x-2/3 htmx-added:opacity-0 htmx-added:scale-0 scale-100 translate-x-0 opacity-100\": $data.transitionType=='forward', + \"htmx-swapping:translate-x-2/3 htmx-swapping:opacity-0 htmx-swapping:scale-0 htmx-added:-translate-x-2/3 htmx-added:opacity-0 htmx-added:scale-0 scale-100 translate-x-0 opacity-100\": $data.transitionType=='backward' + } + ") + "x-data" ""} + (com/modal-header {} + head) + #_(com/modal-header-attachment {}) + [:div.flex.shrink + [:div.grow-0.pr-6.pt-2.bg-gray-100.self-stretch #_{:style "margin-left:-20px"} (render-timeline linear-wizard step validation-route)] + (com/modal-body {} + body)] + + (com/modal-footer {} + footer)))) + +(defn wrap-ensure-step [handler] + (-> + (fn [{:keys [wizard multi-form-state] :as request}] + (assert-schema (step-schema (get-current-step wizard)) (:step-params multi-form-state)) + (handler request)) + (wrap-form-4xx-2 (fn [{:keys [wizard] :as request}] ;; THIS MAY BE BETTER TO JUST MAKE THE LINEAR WIZARD POPULATE FROM THE REQUEST + (html-response + (render-wizard wizard request) + :headers {"x-transition-type" "none" + "HX-reswap" "outerHTML"}))))) + +(defn get-transition-type [wizard from-step-key to-step-key] + (let [to-step-index (.indexOf (steps wizard) to-step-key) + + from-step-index (.indexOf (steps wizard) + from-step-key)] + (cond (= -1 to-step-index) + nil + (= -1 from-step-index) + nil + (= from-step-index to-step-index) + nil + (> from-step-index to-step-index) + "backward" + :else + "forward"))) + +(def next-handler + (-> (fn [{:keys [wizard] :as request}] + (let [current-step (get-current-step wizard) + to-step (:to (:query-params request)) + wizard (navigate wizard to-step) + new-step (get-current-step wizard) + transition-type (get-transition-type wizard (step-key current-step) to-step)] + (html-response + (render-wizard wizard + (-> request + (assoc :multi-form-state (-> (:multi-form-state request) + (merge-multi-form-state) + (select-state + (edit-path new-step request) + (init-step-params- new-step request)))))) + :headers {"HX-reswap" (when transition-type "outerHTML swap:0.15s") + "x-transition-type" (or transition-type "none")}))) + (wrap-ensure-step) + (wrap-schema-enforce :query-schema + [:map + [:to step-key-schema]]))) + +(def discard-handler + (-> + (fn [{:keys [wizard multi-form-state] :as request}] + (let [current-step (get-current-step wizard) + to-step (:to (:query-params request)) + wizard (navigate wizard to-step) + transition-type (get-transition-type wizard (step-key current-step) to-step)] + (html-response + (render-wizard wizard + (-> request + (assoc :multi-form-state (discard-changes current-step multi-form-state)))) + :headers {"HX-reswap" (when transition-type "outerHTML swap:0.15s") + "x-transition-type" (or transition-type "none")}))) + (wrap-schema-enforce :query-schema + [:map + [:to step-key-schema]]))) + +(def submit-handler + (-> (fn [{:keys [wizard multi-form-state] :as request}] + (submit wizard (-> request + (assoc :multi-form-state (merge-multi-form-state multi-form-state))))) + (wrap-ensure-step))) + +(defn default-render-wizard [linear-wizard {:keys [multi-form-state form-errors snapshot current-step] :as request} & {:keys [form-params]}] + (let [current-step (get-current-step linear-wizard) + edit-path (edit-path current-step request)] + [:form#wizard-form form-params + (fc/start-form multi-form-state (when form-errors {:step-params form-errors}) + (list + (fc/with-field :snapshot + (com/hidden {:name (fc/field-name) + :value (pr-str (fc/field-value))})) + (fc/with-field :edit-path + (com/hidden {:name (fc/field-name) + :value (pr-str (or edit-path []))})) + (com/hidden {:name "current-step" + :value (pr-str (step-key current-step))}) + + (fc/with-field :step-params + (com/modal + {:id "wizardmodal"} + + (render-step current-step request)))))])) + +(defn wrap-wizard [handler linear-wizard] + (fn [request] + (let [current-step-key (if-let [current-step (get (:form-params request) "current-step")] + (mc/decode step-key-schema current-step main-transformer) + (first (steps linear-wizard))) + current-step (get-step linear-wizard current-step-key) + multi-form-state (-> (:multi-form-state request) + (update :snapshot (fn [snapshot] + (mc/decode (form-schema linear-wizard) + snapshot + main-transformer))) + (update :step-params (fn [step-params] + (or + (mc/decode (step-schema current-step) + step-params + main-transformer) + {} ;; Todo add a defaultable + )))) + request (-> request + (assoc :multi-form-state multi-form-state)) + linear-wizard (navigate linear-wizard current-step-key)] + (handler + (assoc request :wizard (hydrate-from-request linear-wizard request)))))) + +(defn open-wizard-handler [{:keys [wizard current-step] :as request}] + (modal-response + [:div {:x-data (hx/json {"transitionType" "none" + + } + ) + "@htmx:after-request" "if(event.detail.xhr.getResponseHeader('x-transition-type')) { $data.transitionType = event.detail.xhr.getResponseHeader('x-transition-type');}" + } + (render-wizard wizard request)])) + + + +(defn wrap-init-multi-form-state [handler get-multi-form-state] + (-> + (fn init-multi-form [request] + (handler (assoc request :multi-form-state (get-multi-form-state request)))) + (wrap-nested-form-params))) + +(defn wrap-decode-multi-form-state [handler] + (wrap-init-multi-form-state + handler + (fn parse-multi-form-state [request] + (map->MultiStepFormState (mc/decode [:map + [:snapshot {:optional true + :decode/arbitrary + #(clojure.edn/read-string {:readers clj-time.coerce/data-readers + :eof nil} + %)} + [:maybe :any]] + [:edit-path {:optional true :decode/arbitrary (fn [z] + (clojure.edn/read-string z))} [:maybe [:sequential {:min 0} any?]]] + [:step-params {:optional true} + [:maybe + :any]]] + (:form-params request) + main-transformer))))) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/components/timeline.clj b/src/clj/auto_ap/ssr/components/timeline.clj index b4716668..61124ee6 100644 --- a/src/clj/auto_ap/ssr/components/timeline.clj +++ b/src/clj/auto_ap/ssr/components/timeline.clj @@ -2,9 +2,9 @@ (:require [auto-ap.ssr.hiccup-helper :as hh])) (defn timeline-step [{:keys [active? visited? last?]} & children] - (if active? + (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"} ] + [: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 (when-not last? [:svg {:class "w-3 h-3 ml-2 sm:ml-4", :aria-hidden "true", :xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 12 10"} @@ -24,5 +24,24 @@ [:ol {:class "flex items-center w-full space-x-2 text-xs text-center text-gray-500 bg-white dark:text-gray-400 sm:text-base dark:bg-gray-800 sm:space-x-4 px-2"} children #_[:li {:class "flex items-center"} - [: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"} ]]]) + [: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"}]]]) + +(defn vertical-timeline-step [{:keys [active? visited? last?]} & children] + (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 ] + [: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 ])) + +(defn vertical-timeline [params & children] + [:ol {: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"} + children + #_[:li {:class "flex items-center"} + [: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"}]]]) diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index 0898fff3..f02a77f3 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -12,6 +12,7 @@ [auto-ap.ssr.admin.import-batch :as import-batch] [auto-ap.ssr.admin.transaction-rules :as admin-rules] [auto-ap.ssr.admin.vendors :as admin-vendors] + [auto-ap.ssr.admin.clients :as admin-clients] [auto-ap.ssr.auth :as auth] [auto-ap.ssr.company :as company] [auto-ap.ssr.company-dropdown :as company-dropdown] @@ -54,6 +55,7 @@ :company-plaid-table (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/table)) :company-plaid-link (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/link)) :company-plaid-relink (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/relink)) + :company-update-signature (wrap-client-redirect-unauthenticated (wrap-secure company/upload-signature-data)) :company-yodlee (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/page)) :company-yodlee-table (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/table)) :company-yodlee-fastlink-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/fastlink-dialog)) @@ -89,5 +91,6 @@ (into admin/key->handler) (into admin-jobs/key->handler) (into admin-vendors/key->handler) + (into admin-clients/key->handler) (into admin-rules/key->handler))) diff --git a/src/clj/auto_ap/ssr/form_cursor.clj b/src/clj/auto_ap/ssr/form_cursor.clj index b1617452..f5d6926d 100644 --- a/src/clj/auto_ap/ssr/form_cursor.clj +++ b/src/clj/auto_ap/ssr/form_cursor.clj @@ -2,7 +2,7 @@ (:require [auto-ap.ssr.utils :refer [path->name2]] [auto-ap.cursor :as cursor])) -(def ^:dynamic *prefix* []) +(def ^:dynamic *prefix* []) (def ^:dynamic *form-data*) (def ^:dynamic *form-errors*) (def ^:dynamic *prev-cursor* nil) @@ -22,18 +22,30 @@ `(binding [*prefix* ~prefix] (start-form ~form-data ~errors ~@rest))) +(defmacro with-prefix [prefix & rest] + `(binding [*prefix* (into (or *prefix* []) ~prefix)] + ~@rest)) + (defmacro with-cursor [cursor & rest] `(binding [*current* ~cursor] - ~@rest)) + ~@rest)) (defmacro with-field [field & rest] - `(with-cursor (get *current* ~field ) + `(with-cursor (get *current* ~field) ~@rest)) (defmacro with-field-default [field default & rest] - `(with-cursor (get *current* ~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#)] + (with-cursor new-cursor2# + ~@rest))) (defn field-name ([] (field-name *current*)) @@ -62,10 +74,10 @@ (defn cursor-map ([f] (cursor-map *current* f)) ([cursor f] - (when (field-value) + (when (seq (field-value)) (doall - (for [n cursor] - (with-cursor n - (f n))))))) + (for [n cursor] + (with-cursor n + (f n))))))) diff --git a/src/clj/auto_ap/ssr/hiccup_helper.clj b/src/clj/auto_ap/ssr/hiccup_helper.clj index 617b4a80..812ffb63 100644 --- a/src/clj/auto_ap/ssr/hiccup_helper.clj +++ b/src/clj/auto_ap/ssr/hiccup_helper.clj @@ -34,15 +34,14 @@ (remove-wildcard [this wildcard] (if (sequential? wildcard) (reduce - remove-wildcard - this - wildcard) + remove-wildcard + this + wildcard) (reduce - remove-class - this - (filter (fn [c] - (str/starts-with? c wildcard) - ) @class-set))) + remove-class + this + (filter (fn [c] + (str/starts-with? c wildcard)) @class-set))) this) (replace-wildcard [this wildcard add] (remove-wildcard this wildcard) @@ -51,7 +50,7 @@ (replace-tw [this add] ;; TODO ) - Object + Object (toString [this] (str/join " " @class-set))))) @@ -60,8 +59,7 @@ (add-class [this add] (add-class (string->class-list this) add)) (remove-class [this remove] - (remove-class (string->class-list this) remove) - ) + (remove-class (string->class-list this) remove)) (replace-class [this remove add] (replace-class (string->class-list this) remove add)) (remove-wildcard [this wildcard] @@ -70,29 +68,11 @@ (replace-wildcard (string->class-list this) wildcard add)) (add-tw [this tw] (replace-tw (string->class-list this) - tw) - )) + 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 " + (add-class "bg-blue-500") + #_(replace-wildcard ["hello-" "b"] ["hi" "there"]))}])) diff --git a/src/clj/auto_ap/ssr/pos/cash_drawer_shifts.clj b/src/clj/auto_ap/ssr/pos/cash_drawer_shifts.clj index 12cfe3fd..c99eb9ba 100644 --- a/src/clj/auto_ap/ssr/pos/cash_drawer_shifts.clj +++ b/src/clj/auto_ap/ssr/pos/cash_drawer_shifts.clj @@ -84,51 +84,51 @@ matching-count])) (def grid-page - (helper/build - {:id "cash-drawer-shift-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 "cash-drawer-shift-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-cash-drawer-shifts)} - "Cash Drawer Shifts"]] - :title "Cash drawer shifts" - :entity-name "Cash drawer shift" - :route :pos-cash-drawer-shift-table - :headers [{:key "client" - :name "Client" - :sort-key "client" - :hide? (fn [args] - (= (count (:clients args)) 1)) - :render #(-> % :cash-drawer-shift/client :client/code)} - {:key "date" - :name "Date" - :sort-key "date" - :render #(atime/unparse-local (:cash-drawer-shift/date %) atime/standard-time)} - {:key "paid-in" - :name "Paid in" - :sort-key "paid-in" - :render #(some->> % :cash-drawer-shift/paid-in (format "$%.2f"))} - {:key "paid-out" - :name "Paid out" - :sort-key "paid-out" - :render #(some->> % :cash-drawer-shift/paid-out (format "$%.2f"))} - {:key "expected-cash" - :name "Expected cash" - :sort-key "expected-cash" - :render #(some->> % :cash-drawer-shift/expected-cash (format "$%.2f"))} - {:key "opened-cash" - :name "Opened cash" - :sort-key "opened-cash" - :render #(some->> % :cash-drawer-shift/opened-cash (format "$%.2f"))} - ]})) + [:a {:href (bidi/path-for ssr-routes/only-routes + :pos-cash-drawer-shifts)} + "Cash Drawer Shifts"]] + :title "Cash drawer shifts" + :entity-name "Cash drawer shift" + :route :pos-cash-drawer-shift-table + :headers [{:key "client" + :name "Client" + :sort-key "client" + :hide? (fn [args] + (= (count (:clients args)) 1)) + :render #(-> % :cash-drawer-shift/client :client/code)} + {:key "date" + :name "Date" + :sort-key "date" + :render #(atime/unparse-local (:cash-drawer-shift/date %) atime/standard-time)} + {:key "paid-in" + :name "Paid in" + :sort-key "paid-in" + :render #(some->> % :cash-drawer-shift/paid-in (format "$%.2f"))} + {:key "paid-out" + :name "Paid out" + :sort-key "paid-out" + :render #(some->> % :cash-drawer-shift/paid-out (format "$%.2f"))} + {:key "expected-cash" + :name "Expected cash" + :sort-key "expected-cash" + :render #(some->> % :cash-drawer-shift/expected-cash (format "$%.2f"))} + {:key "opened-cash" + :name "Opened cash" + :sort-key "opened-cash" + :render #(some->> % :cash-drawer-shift/opened-cash (format "$%.2f"))}]})) (def row* (partial helper/row* grid-page)) diff --git a/src/clj/auto_ap/ssr/svg.clj b/src/clj/auto_ap/ssr/svg.clj index f544ab53..36026612 100644 --- a/src/clj/auto_ap/ssr/svg.clj +++ b/src/clj/auto_ap/ssr/svg.clj @@ -1,6 +1,6 @@ (ns auto-ap.ssr.svg) -(def pie +(def pie [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:defs] [:title "analytics-pie-2"] @@ -11,28 +11,28 @@ (def accounting-invoice-mail [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} - [:defs] - [:title "accounting-document"] - [:path {:d "M21.75,21.75a1.5,1.5,0,0,1-1.5,1.5H3.75a1.5,1.5,0,0,1-1.5-1.5V2.25A1.5,1.5,0,0,1,3.75.75H14.379a1.5,1.5,0,0,1,1.06.439l5.872,5.872a1.5,1.5,0,0,1,.439,1.06Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:path {:d "M21.75,8.25h-6a1.5,1.5,0,0,1-1.5-1.5v-6", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:path {:d "M6.2,11.594a2.224,2.224,0,0,0,1.858.875c1.139,0,2.063-.693,2.063-1.547S9.2,9.376,8.062,9.376,6,8.683,6,7.828s.924-1.547,2.062-1.547a2.221,2.221,0,0,1,1.858.875", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "12.469", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.062", :y2 "13.5", :x2 "8.062"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "5.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.062", :y2 "6.281", :x2 "8.062"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "15", :stroke-linecap "round", :stroke-width "1.5px", :x1 "12", :y2 "15", :x2 "18"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "19.5", :stroke-linecap "round", :stroke-width "1.5px", :x1 "6.75", :y2 "19.5", :x2 "18"}]]) + [:defs] + [:title "accounting-document"] + [:path {:d "M21.75,21.75a1.5,1.5,0,0,1-1.5,1.5H3.75a1.5,1.5,0,0,1-1.5-1.5V2.25A1.5,1.5,0,0,1,3.75.75H14.379a1.5,1.5,0,0,1,1.06.439l5.872,5.872a1.5,1.5,0,0,1,.439,1.06Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:path {:d "M21.75,8.25h-6a1.5,1.5,0,0,1-1.5-1.5v-6", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:path {:d "M6.2,11.594a2.224,2.224,0,0,0,1.858.875c1.139,0,2.063-.693,2.063-1.547S9.2,9.376,8.062,9.376,6,8.683,6,7.828s.924-1.547,2.062-1.547a2.221,2.221,0,0,1,1.858.875", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "12.469", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.062", :y2 "13.5", :x2 "8.062"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "5.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.062", :y2 "6.281", :x2 "8.062"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "15", :stroke-linecap "round", :stroke-width "1.5px", :x1 "12", :y2 "15", :x2 "18"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "19.5", :stroke-linecap "round", :stroke-width "1.5px", :x1 "6.75", :y2 "19.5", :x2 "18"}]]) (def receipt-register-1 [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} - [:g - [:path {:d "M17.63,18h3a1.5,1.5,0,0,1,1.5,1.5v2.25a1.5,1.5,0,0,1-1.5,1.5H3.38a1.5,1.5,0,0,1-1.5-1.5V18H7.13", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "17.03", :stroke-linecap "round", :stroke-width "1.5px", :x1 "11.45", :y2 "14.35", :x2 "8.71"}] - [:circle {:cx "12.48", :cy "18.11", :r "1.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:path {:d "M1.88,4.5H8.63L9.81,3.31A1.5,1.5,0,0,0,8.75.75H3.38a1.5,1.5,0,0,0-1.5,1.5V18", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "6.1", :stroke-linecap "round", :stroke-width "1.5px", :x1 "13.22", :y2 "6.59", :x2 "14.2"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "8.54", :stroke-linecap "round", :stroke-width "1.5px", :x1 "17.13", :y2 "9.25", :x2 "17.97"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "12.09", :stroke-linecap "round", :stroke-width "1.5px", :x1 "19.9", :y2 "13.03", :x2 "20.46"}] - [:path {:d "M8.63,4.5V7.74A12.22,12.22,0,0,1,18.92,18", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:circle {:cx "7.13", :cy "12.75", :r "2.25", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]]]) + [:g + [:path {:d "M17.63,18h3a1.5,1.5,0,0,1,1.5,1.5v2.25a1.5,1.5,0,0,1-1.5,1.5H3.38a1.5,1.5,0,0,1-1.5-1.5V18H7.13", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "17.03", :stroke-linecap "round", :stroke-width "1.5px", :x1 "11.45", :y2 "14.35", :x2 "8.71"}] + [:circle {:cx "12.48", :cy "18.11", :r "1.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:path {:d "M1.88,4.5H8.63L9.81,3.31A1.5,1.5,0,0,0,8.75.75H3.38a1.5,1.5,0,0,0-1.5,1.5V18", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "6.1", :stroke-linecap "round", :stroke-width "1.5px", :x1 "13.22", :y2 "6.59", :x2 "14.2"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "8.54", :stroke-linecap "round", :stroke-width "1.5px", :x1 "17.13", :y2 "9.25", :x2 "17.97"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "12.09", :stroke-linecap "round", :stroke-width "1.5px", :x1 "19.9", :y2 "13.03", :x2 "20.46"}] + [:path {:d "M8.63,4.5V7.74A12.22,12.22,0,0,1,18.92,18", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:circle {:cx "7.13", :cy "12.75", :r "2.25", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]]]) (def payments [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} @@ -45,38 +45,38 @@ (def bank [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} - [:g - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "23.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "0.75", :y2 "23.25", :x2 "23.25"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "19.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "0.75", :y2 "19.25", :x2 "23.25"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "2", :y2 "16.25", :x2 "2"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "5.5", :y2 "16.25", :x2 "5.5"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "10.25", :y2 "16.25", :x2 "10.25"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "13.75", :y2 "16.25", :x2 "13.75"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "18.5", :y2 "16.25", :x2 "18.5"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "22", :y2 "16.25", :x2 "22"}] - [:path {:d "M23.25,7.25H.75L11.19,1a1.49,1.49,0,0,1,1.62,0Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]]]) + [:g + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "23.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "0.75", :y2 "23.25", :x2 "23.25"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "19.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "0.75", :y2 "19.25", :x2 "23.25"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "2", :y2 "16.25", :x2 "2"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "5.5", :y2 "16.25", :x2 "5.5"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "10.25", :y2 "16.25", :x2 "10.25"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "13.75", :y2 "16.25", :x2 "13.75"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "18.5", :y2 "16.25", :x2 "18.5"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "22", :y2 "16.25", :x2 "22"}] + [:path {:d "M23.25,7.25H.75L11.19,1a1.49,1.49,0,0,1,1.62,0Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]]]) (def receipt [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} - [:g - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "21.75", :stroke-linecap "round", :stroke-width "1.5px", :x1 "17.25", :y2 "21.75", :x2 "11.25"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "13.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.75", :y2 "13.25", :x2 "15.25"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "9.75", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.75", :y2 "9.75", :x2 "15.25"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "6.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.75", :y2 "6.25", :x2 "15.25"}] - [:path {:d "M20.25.75h-1.5v5.5h4.5V3.75A3,3,0,0,0,20.25.75Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:path {:d "M13.5,17.25H3.75a3,3,0,0,0-3,3v3h10.5V19.5a2.25,2.25,0,0,1,4.5,0v.75a1.5,1.5,0,0,0,1.5,1.5h0a1.5,1.5,0,0,0,1.5-1.5V.75H8.25a3,3,0,0,0-3,3v13.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]]]) + [:g + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "21.75", :stroke-linecap "round", :stroke-width "1.5px", :x1 "17.25", :y2 "21.75", :x2 "11.25"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "13.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.75", :y2 "13.25", :x2 "15.25"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "9.75", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.75", :y2 "9.75", :x2 "15.25"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "6.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.75", :y2 "6.25", :x2 "15.25"}] + [:path {:d "M20.25.75h-1.5v5.5h4.5V3.75A3,3,0,0,0,20.25.75Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:path {:d "M13.5,17.25H3.75a3,3,0,0,0-3,3v3h10.5V19.5a2.25,2.25,0,0,1,4.5,0v.75a1.5,1.5,0,0,0,1.5,1.5h0a1.5,1.5,0,0,0,1.5-1.5V.75H8.25a3,3,0,0,0-3,3v13.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]]]) (defn spinner [{:keys [class]}] [:svg {:aria-hidden "true", :role "status", :class (str "animate-spin " class) :viewbox "0 0 100 101", :fill "none", :xmlns "http://www.w3.org/2000/svg"} - [:path {:d "M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z", :fill "#E5E7EB"}] - [:path {:d "M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z", :fill "currentColor"}]]) + [:path {:d "M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z", :fill "#E5E7EB"}] + [:path {:d "M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z", :fill "currentColor"}]]) (defn spinner-primary [{:keys [class]}] [:svg {:aria-hidden "true", :role "status", :class (str "animate-spin " class) :viewbox "0 0 100 101", :fill "none", :xmlns "http://www.w3.org/2000/svg"} [:path {:d "M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z", :fill "#79b52e"}] [:path {:d "M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z", :fill "currentColor"}]]) -(def search +(def search [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:defs] [:title "search"] @@ -93,11 +93,11 @@ (def home - [:svg { :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} + [: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"}]]) (def breadcrumb-component - [:svg { :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} + [:svg {: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"}]]) (def refresh @@ -106,7 +106,7 @@ (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"} + [: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"}]]) (def vendors @@ -122,29 +122,29 @@ (def report [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} - [:defs] - [:title "app-window-pie-chart"] - [:rect {:y "2.253", :rx "1.5", :stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "21", :stroke-linecap "round", :stroke-width "1.5px", :x "1.51", :ry "1.5", :height "19.5"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "6.753", :stroke-linecap "round", :stroke-width "1.5px", :x1 "1.51", :y2 "6.753", :x2 "22.51"}] - [:circle {:cx "9.01", :cy "14.253", :r "4.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:polyline {:points "9.01 9.753 9.01 14.253 12.192 17.435", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "11.253", :stroke-linecap "round", :stroke-width "1.5px", :x1 "16.51", :y2 "11.253", :x2 "19.51"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "14.253", :stroke-linecap "round", :stroke-width "1.5px", :x1 "16.51", :y2 "14.253", :x2 "19.51"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "17.253", :stroke-linecap "round", :stroke-width "1.5px", :x1 "16.51", :y2 "17.253", :x2 "19.51"}]]) + [:defs] + [:title "app-window-pie-chart"] + [:rect {:y "2.253", :rx "1.5", :stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "21", :stroke-linecap "round", :stroke-width "1.5px", :x "1.51", :ry "1.5", :height "19.5"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "6.753", :stroke-linecap "round", :stroke-width "1.5px", :x1 "1.51", :y2 "6.753", :x2 "22.51"}] + [:circle {:cx "9.01", :cy "14.253", :r "4.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:polyline {:points "9.01 9.753 9.01 14.253 12.192 17.435", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "11.253", :stroke-linecap "round", :stroke-width "1.5px", :x1 "16.51", :y2 "11.253", :x2 "19.51"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "14.253", :stroke-linecap "round", :stroke-width "1.5px", :x1 "16.51", :y2 "14.253", :x2 "19.51"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "17.253", :stroke-linecap "round", :stroke-width "1.5px", :x1 "16.51", :y2 "17.253", :x2 "19.51"}]]) (def government-building [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} - [:g - [:rect {:y "14.25", :stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "3", :stroke-linecap "round", :stroke-width "1.5px", :x "3.5", :height "6"}] - [:rect {:y "14.25", :stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "3", :stroke-linecap "round", :stroke-width "1.5px", :x "10.5", :height "6"}] - [:rect {:y "14.25", :stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "3", :stroke-linecap "round", :stroke-width "1.5px", :x "17.5", :height "6"}] - [:path {:d "M21.75,13.39a.87.87,0,0,1-.86.86H3.11a.86.86,0,0,1-.25-1.69l8.85-2.72a1,1,0,0,1,.58,0l8.85,2.72A.87.87,0,0,1,21.75,13.39Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:polyline {:points "15.5 8.25 18 8.25 18 11.6", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:polyline {:points "6 11.6 6 8.25 8.5 8.25", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:path {:d "M6,8.25a6,6,0,0,1,12,0", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "0.75", :stroke-linecap "round", :stroke-width "1.5px", :x1 "12", :y2 "2.25", :x2 "12"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "23.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "0.75", :y2 "23.25", :x2 "23.25"}] - [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "20.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "2", :y2 "20.25", :x2 "22"}]]]) + [:g + [:rect {:y "14.25", :stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "3", :stroke-linecap "round", :stroke-width "1.5px", :x "3.5", :height "6"}] + [:rect {:y "14.25", :stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "3", :stroke-linecap "round", :stroke-width "1.5px", :x "10.5", :height "6"}] + [:rect {:y "14.25", :stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "3", :stroke-linecap "round", :stroke-width "1.5px", :x "17.5", :height "6"}] + [:path {:d "M21.75,13.39a.87.87,0,0,1-.86.86H3.11a.86.86,0,0,1-.25-1.69l8.85-2.72a1,1,0,0,1,.58,0l8.85,2.72A.87.87,0,0,1,21.75,13.39Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:polyline {:points "15.5 8.25 18 8.25 18 11.6", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:polyline {:points "6 11.6 6 8.25 8.5 8.25", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:path {:d "M6,8.25a6,6,0,0,1,12,0", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "0.75", :stroke-linecap "round", :stroke-width "1.5px", :x1 "12", :y2 "2.25", :x2 "12"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "23.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "0.75", :y2 "23.25", :x2 "23.25"}] + [:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "20.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "2", :y2 "20.25", :x2 "22"}]]]) (def external-link [:svg @@ -168,7 +168,7 @@ "M14.387,13v5.5a1,1,0,0,1-1,1h-12a1,1,0,0,1-1-1V6.5a1,1,0,0,1,1-1h12a1,1,0,0,1,1,1V7", :fill "none", :stroke "currentColor", - :stroke-linecap "round", + :stroke-linecap "round", :stroke-linejoin "round"}]]) (def play [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "-0.5 -0.5 24 24"} @@ -201,9 +201,19 @@ {:d "M21.914,6.328,17.672,2.086l.707-.707a3,3,0,0,1,4.242,4.242Z", :fill "none", :stroke "currentColor", - :stroke-linecap "round", + :stroke-linecap "round", :stroke-linejoin "round"}]]) +(def dollar-tag + [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} + [:defs] + [:title "tag-dollar"] + [:path {:d "M17.5 5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 1 0 -3 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m14.257 12.571 -1.985 -1.985a1.107 1.107 0 0 0 -1.686 0c-0.925 0.924 1.2 4.46 0.272 5.384a1.171 1.171 0 0 1 -1.687 0l-1.985 -1.985", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m12.843 11.157 1.061 -1.061", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m7.54 16.46 1.061 -1.061", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "M0.854 12.646a1.207 1.207 0 0 0 0 1.708l8.792 8.792a1.207 1.207 0 0 0 1.708 0l11.439 -11.439A2.414 2.414 0 0 0 23.5 10V1.5a1 1 0 0 0 -1 -1H14a2.414 2.414 0 0 0 -1.707 0.707Z", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]]) + (def drop-down [:svg {:aria-hidden "true", :fill "none", :stroke "currentColor", :viewbox "0 0 24 24", :xmlns "http://www.w3.org/2000/svg"} [:path {:stroke-linecap "round", :stroke-linejoin "round", :stroke-width "2", :d "M19 9l-7 7-7-7"}]]) @@ -224,63 +234,63 @@ {:d "M23.5,18.5v4a1,1,0,0,1-1,1H1.5a1,1,0,0,1-1-1v-4", :fill "none", :stroke "currentColor", - :stroke-linecap "round", + :stroke-linecap "round", :stroke-linejoin "round"}]]) (def trash [:svg - {:xmlns "http://www.w3.org/2000/svg", :viewBox "0 0 24 24"} - [:defs] - [:title "bin-1"] - [:path - {:d - "M21,4.5,19.188,21.709A2,2,0,0,1,17.2,23.5H6.8a2,2,0,0,1-1.989-1.791L3,4.5", - :fill "none", - :stroke "currentColor", - :stroke-linecap "round", - :stroke-linejoin "round"}] - [:line - {:x1 "0.5", - :y1 "4.5", - :x2 "23.5", - :y2 "4.5", - :fill "none", - :stroke "currentColor", - :stroke-linecap "round", - :stroke-linejoin "round"}] - [:path - {:d "M7.5,4.5v-3a1,1,0,0,1,1-1h7a1,1,0,0,1,1,1v3", - :fill "none", - :stroke "currentColor", - :stroke-linecap "round", - :stroke-linejoin "round"}] - [:line - {:x1 "12", - :y1 "9", - :x2 "12", - :y2 "19.5", - :fill "none", - :stroke "currentColor", - :stroke-linecap "round", - :stroke-linejoin "round"}] - [:line - {:x1 "16.5", - :y1 "9", - :x2 "16", - :y2 "19.5", - :fill "none", - :stroke "currentColor", - :stroke-linecap "round", - :stroke-linejoin "round"}] - [:line - {:x1 "7.5", - :y1 "9", - :x2 "8", - :y2 "19.5", - :fill "none", - :stroke "currentColor", - :stroke-linecap "round", - :stroke-linejoin "round"}]]) + {:xmlns "http://www.w3.org/2000/svg", :viewBox "0 0 24 24"} + [:defs] + [:title "bin-1"] + [:path + {:d + "M21,4.5,19.188,21.709A2,2,0,0,1,17.2,23.5H6.8a2,2,0,0,1-1.989-1.791L3,4.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:line + {:x1 "0.5", + :y1 "4.5", + :x2 "23.5", + :y2 "4.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:path + {:d "M7.5,4.5v-3a1,1,0,0,1,1-1h7a1,1,0,0,1,1,1v3", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:line + {:x1 "12", + :y1 "9", + :x2 "12", + :y2 "19.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:line + {:x1 "16.5", + :y1 "9", + :x2 "16", + :y2 "19.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:line + {:x1 "7.5", + :y1 "9", + :x2 "8", + :y2 "19.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}]]) (def alert [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} @@ -442,3 +452,39 @@ [:defs] [:title "dislike"] [:path {:d "M4.5,8h0a1.5,1.5,0,0,1,0-3h1a1.5,1.5,0,0,1,0-3H12c4,0,3,1.87,11,1.87V13H20a7.811,7.811,0,0,0-7.5,7.856c0,1.582-3,1.813-3-1.187A29.774,29.774,0,0,1,10.5,14h-8a1.5,1.5,0,0,1,0-3h1a1.5,1.5,0,0,1,0-3h1", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]]) + +(def dollar + [:svg {:xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 24 24"} + [:path {:stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :d "M21 7H3a0.5 0.5 0 0 0 -0.5 0.5v9a0.5 0.5 0 0 0 0.5 0.5h18a0.5 0.5 0 0 0 0.5 -0.5v-9A0.5 0.5 0 0 0 21 7Z", :stroke-width "1"}] + [:path {:stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :d "M22.5 5h-21a1 1 0 0 0 -1 1v12a1 1 0 0 0 1 1h21a1 1 0 0 0 1 -1V6a1 1 0 0 0 -1 -1Z", :stroke-width "1"}] + [:path {:stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :d "M12 15a3 3 0 1 0 0 -6 3 3 0 0 0 0 6Z", :stroke-width "1"}] + [:path {:stroke "currentcolor", :d "M4.996 9.75a0.25 0.25 0 0 1 0 -0.5", :stroke-width "1"}] + [:path {:stroke "currentcolor", :d "M4.996 9.75a0.25 0.25 0 0 0 0 -0.5", :stroke-width "1"}] + [:g + [:path {:stroke "currentcolor", :d "M18.996 14.75a0.25 0.25 0 1 1 0 -0.5", :stroke-width "1"}] + [:path {:stroke "currentcolor", :d "M18.996 14.75a0.25 0.25 0 1 0 0 -0.5", :stroke-width "1"}]]]) + +(def credit-card + [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} + [:defs] + [:title "credit-card-1"] + [:path {:d "M2.504 4h19s2 0 2 2v12s0 2 -2 2h-19s-2 0 -2 -2V6s0 -2 2 -2", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m0.504 8 23 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m20.504 12 -3 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m11.504 12 -8 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m6.504 15 -3 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]]) + +(def check + [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} + [:defs] + [:title "check-payment-sign"] + [:path {:d "m2.5 20.5 7 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m14 19.616 0.751 -0.751a1 1 0 0 1 1.677 0.465l0.072 0.286 0.818 -0.545a1 1 0 0 1 1.262 0.125l0.42 0.42h2", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m2.504 17.616 5 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m14.075 5.505 2.122 2.122", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m18.813 2.465 0.425 0.424a1.2 1.2 0 0 1 0 1.697l-8.698 8.697 0 0 -2.121 -2.121 0 0 8.697 -8.697a1.2 1.2 0 0 1 1.697 0Z", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m8.418 11.162 -1.414 3.536 3.536 -1.414", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m19.025 2.677 1.293 -1.293", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "M6.5 9.616H1a0.5 0.5 0 0 0 -0.5 0.5v12a0.5 0.5 0 0 0 0.5 0.5h22a0.5 0.5 0 0 0 0.5 -0.5v-12a0.5 0.5 0 0 0 -0.5 -0.5h-5", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "M17.004 12.616h3s0.5 0 0.5 0.5v2s0 0.5 -0.5 0.5h-3s-0.5 0 -0.5 -0.5v-2s0 -0.5 0.5 -0.5", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}] + [:path {:d "m16.757 2.823 -0.8 -0.8a1 1 0 0 0 -1.414 0L12.5 4.07", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]]) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/ui.clj b/src/clj/auto_ap/ssr/ui.clj index 41d5cb5e..aad7f05b 100644 --- a/src/clj/auto_ap/ssr/ui.clj +++ b/src/clj/auto_ap/ssr/ui.clj @@ -15,8 +15,6 @@ hiccup))}) - - (defn base-page [request contents page-name] (html-page [:html.has-navbar-fixed-top @@ -38,10 +36,11 @@ [:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.js" :crossorigin= "anonymous"}] [:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.min.js" - :crossorigin= "anonymous"}]) + :crossorigin= "anonymous"}]) [:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/ext/class-tools.js" :crossorigin= "anonymous"}] + [:script {:src "https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"}] [:script {:src "https://unpkg.com/htmx.org/dist/ext/debug.js"}] [:script {:src "/js/htmx-disable.js"}] [:script {:type "text/javascript", :src "https://cdn.yodlee.com/fastlink/v4/initialize.js", :async "async"}]] @@ -57,7 +56,8 @@ [:link {:rel "stylesheet" :href "https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" :type "text/css"}] [:script {:defer true :src "https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"}] [:script {:defer true :src "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"}] - + [:script {:src "https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js"}] + [:style " input::-webkit-outer-spin-button, @@ -74,14 +74,17 @@ input[type=number] { [:body {:hx-ext "disable-submit, class-tools"} 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" + {:class "fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen" "x-show" "open" ":aria-hidden" "!open" - "x-data" (hx/json {"open" false}) - "@modalopen.document" "open=true" + "x-data" (hx/json {"open" false + "unexpectedError" false}) + "x-on:htmx:response-error" "unexpectedError=true;" + "x-on:htmx:before-request" "unexpectedError=false" + "@modalopen.document" "open=true; unexpectedError=null" "@modalclose.document" "open=false"} [:div {:class "bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40 md:p-12" @@ -94,21 +97,18 @@ input[type=number] { "x-transition:leave-start" "!bg-opacity-50" "x-transition:leave-end" "!bg-opacity-0"} - [:div { - :class "flex h-full w-full justify-stretch md:justify-center items-stretch md:items-center " - "x-trap.inert.noscroll" "open" - "x-trap.inert" "open" - "x-show" "open" - "x-transition:enter" "ease-out duration-300" - "x-transition:enter-start" "!bg-opacity-0 !translate-y-32" - "x-transition:enter-end" "!bg-opacity-100 !translate-y-0" - "x-transition:leave" "duration-300" - "x-transition:leave-start" "!opacity-100 !translate-y-0" - "x-transition:leave-end" "!opacity-0 !translate-y-32"} + [:div {:class "flex h-full w-full justify-stretch md:justify-center items-stretch md:items-center " + "x-trap.inert.noscroll" "open" + "x-trap.inert" "open" + "x-show" "open" + "x-transition:enter" "ease-out duration-300" + "x-transition:enter-start" "!bg-opacity-0 !translate-y-32" + "x-transition:enter-end" "!bg-opacity-100 !translate-y-0" + "x-transition:leave" "duration-300" + "x-transition:leave-start" "!opacity-100 !translate-y-0" + "x-transition:leave-end" "!opacity-0 !translate-y-32"} [:div.flex.items-center.justify-center.max-w-6xl {:class "min-w-[700px] max-h-full "} - + [:div#modal-content.flex.flex-col.self-stretch {:class "min-w-[700px] md:p-12"} ;;.overflow-scroll - - ] - ]]]]]])) + ]]]]]]])) diff --git a/src/clj/auto_ap/ssr/utils.clj b/src/clj/auto_ap/ssr/utils.clj index c4110c2b..6967e630 100644 --- a/src/clj/auto_ap/ssr/utils.clj +++ b/src/clj/auto_ap/ssr/utils.clj @@ -16,51 +16,51 @@ :headers (into {"Content-Type" "text/html"} headers) :body (str - (hiccup/html - {} - hiccup) - "\n" - (str/join "\n" - (map (fn [o] - (hiccup/html - {} - o)) - oob)))}) + (hiccup/html + {} + hiccup) + "\n" + (str/join "\n" + (map (fn [o] + (hiccup/html + {} + o)) + oob)))}) (defn modal-response [hiccup & {:as opts}] (apply html-response - (into - [hiccup] - (mapcat identity - (-> opts - (assoc-in [:headers "hx-trigger"] "modalopen") - (assoc-in [:headers "hx-retarget"] "#modal-content") - (assoc-in [:headers "hx-reswap"] "innerHTML")))))) + (into + [hiccup] + (mapcat identity + (-> opts + (assoc-in [:headers "hx-trigger"] "modalopen") + (assoc-in [:headers "hx-retarget"] "#modal-content") + (assoc-in [:headers "hx-reswap"] "innerHTML")))))) (defn next-step-modal-response [hiccup & {:as opts}] (apply html-response - (into - [hiccup] - (mapcat identity - (-> opts - (assoc-in [:headers "hx-retarget"] "#modal-content") - (assoc-in [:headers "hx-reswap"] "innerHTML")))))) + (into + [hiccup] + (mapcat identity + (-> opts + (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] - (cond (and (string? v) - (empty? v)) - acc + (fn [acc k v] + (cond (and (string? v) + (empty? v)) + acc - :else - (assoc-in acc (->> (str/split k #"_") - (mapv #(apply keyword (str/split % #"/")))) - v))) - {} - form-data)) + :else + (assoc-in acc (->> (str/split k #"_") + (mapv #(apply keyword (str/split % #"/")))) + v))) + {} + form-data)) (defn path->name [k] (cond (keyword? k) @@ -75,29 +75,32 @@ [:vector {:decode/json {:enter (fn [x] (if (sequential? x) x - [x]) - )}} + [x]))}} x]) (defn empty->nil [v] - (if (and (string? v) (clojure.string/blank? v)) - nil - v)) + (if (and (string? v) (clojure.string/blank? v)) + nil + v)) - (defn parse-empty-as-nil [] - (mt2/transformer - {:decoders - {:string empty->nil - :double empty->nil - :int empty->nil - :long empty->nil - 'nat-int? empty->nil}})) +(defn parse-empty-as-nil [] + (mt2/transformer + {:decoders + {:map (fn [m] + (if (not (seq (filter identity (vals m)))) + nil + m)) + :string empty->nil + :double empty->nil + :int empty->nil + :long empty->nil + 'nat-int? empty->nil}})) (def entity-id (mc/schema [nat-int? {:error/message "required" - :decode/arbitrary (fn [e] - (if (and (map? e) (:db/id e)) - (:db/id e) - e))} ])) + :decode/arbitrary (fn [e] + (if (and (map? e) (:db/id e)) + (:db/id e) + e))}])) (def temp-id (mc/schema [:string {:min 1}])) (def money (mc/schema [:double])) @@ -110,12 +113,12 @@ (def regex (mc/schema [:fn {:error/message "not a regex"} (fn check-regx [x] - (try - (and (string? x) - (. java.util.regex.Pattern (compile x java.util.regex.Pattern/CASE_INSENSITIVE))) - true - (catch Exception _ - false)))])) + (try + (and (string? x) + (. java.util.regex.Pattern (compile x java.util.regex.Pattern/CASE_INSENSITIVE))) + true + (catch Exception _ + false)))])) (def map->db-id-decoder {:enter (fn [x] @@ -127,12 +130,12 @@ (defn many-entity [params & keys] (mc/schema - [:vector (merge params {:decode/json map->db-id-decoder - :decode/arbitrary (fn [x] - (if (sequential? x) - x - [x]))}) - (into [:map] keys)])) + [:vector (merge params {:decode/json map->db-id-decoder + :decode/arbitrary (fn [x] + (if (sequential? x) + x + [x]))}) + (into [:map] keys)])) (defn str->keyword [s] (if (string? s) @@ -156,23 +159,38 @@ :form-validation-errors [m]})))) (def main-transformer - (mt2/transformer - parse-empty-as-nil - (mt2/key-transformer {:encode keyword->str :decode str->keyword}) - mt2/string-transformer - mt2/json-transformer - (mt2/transformer {:name :arbitrary}) - mt2/default-value-transformer)) + (mt2/transformer + parse-empty-as-nil + (mt2/key-transformer {:encode keyword->str :decode str->keyword}) + mt2/string-transformer + mt2/json-transformer + (mt2/transformer {:name :arbitrary}) + mt2/default-value-transformer)) (defn strip [s] (cond (and (string? s) (str/blank? s)) - nil + nil - (string? s) - (str/trim s) + (string? s) + (str/trim s) + + :else + s)) +(defn assert-schema [schema entity] + (when-not (mc/validate schema entity) + (throw (ex-info #_(->> (-> (mc/explain schema entity) + (me/humanize {:errors (assoc me/default-errors + ::mc/missing-key {:error/message {:en "required"}})})) + (map (fn [[k v]] + (str (if (keyword? k) + (name k) + k) ": " (str/join ", " v)))) + (str/join ", ")) + "validation failed" + {:type :schema-validation + :decoded entity + :error {:explain (mc/explain schema entity)}})))) - :else - s)) (defn schema-enforce-request [{:keys [form-params query-params params] :as request} & {:keys [form-schema query-schema route-schema params-schema]}] (let [request (try @@ -180,35 +198,35 @@ (and (:params request) params-schema) (assoc :params (mc/coerce - params-schema - (:params request) - main-transformer)) + params-schema + (:params request) + main-transformer)) (and (:route-params request) route-schema) (assoc :route-params (mc/coerce - route-schema - (:route-params request) - main-transformer)) + route-schema + (:route-params request) + main-transformer)) (and form-schema form-params) (assoc :form-params (mc/coerce - form-schema - form-params - main-transformer)) + form-schema + form-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 :error e) (throw (ex-info (->> (-> e - (ex-data ) + (ex-data) :data :explain (me/humanize {:errors (assoc me/default-errors @@ -237,30 +255,30 @@ (and (:params request) params-schema) (assoc :params (mc/decode - params-schema - (:params request) - main-transformer)) + params-schema + (:params request) + main-transformer)) (and (:route-params request) route-schema) (assoc :route-params (mc/decode - route-schema - (:route-params request) - main-transformer)) + route-schema + (:route-params request) + main-transformer)) (and form-schema form-params) (assoc :form-params (mc/decode - form-schema - form-params - main-transformer)) + form-schema + form-params + main-transformer)) (and query-schema query-params) (assoc :query-params (mc/decode - query-schema - query-params - main-transformer)))] + query-schema + query-params + main-transformer)))] request)) (defn wrap-schema-decode [handler & {:keys [form-schema query-schema route-schema params-schema]}] @@ -275,8 +293,7 @@ (into [:enum {:decode/string #(if (keyword? %) % (when (not-empty %) - (keyword n %)) - )}] + (keyword n %)))}] (for [{:db/keys [ident]} (all-schema) :when (= n (namespace ident))] ident))) @@ -301,41 +318,39 @@ (defn wrap-form-4xx-2 [handler form-handler] (fn [request] (try+ - (handler request) - (catch [:type :schema-validation] e - - (let [humanized (-> e :error :explain (me/humanize {:errors (assoc me/default-errors - ::mc/missing-key {:error/message {:en "required"}})})) - errors (map - (fn [e] - {:path (:in e) - :message (get-in humanized (:in e))}) - (:errors (:explain (:error e))))] - (alog/warn ::form-4xx :errors errors) - (form-handler (assoc request - :form-params (:decoded e) - :field-validation-errors errors - :form-errors humanized))) - #_(html-response [:span.error-content.text-red-500 (:message &throw-context)] - :status 400)) - (catch [:type :field-validation] e - (form-handler (assoc request - :form-params (:form e) - :form-errors (:form-errors e)))) - (catch [:type :form-validation] e - (form-handler (assoc request - :form-params (:form e) - :form-validation-errors (:form-validation-errors e) - :form-errors {:errors (:form-validation-errors e)})))))) + (handler request) + (catch [:type :schema-validation] e + (let [humanized (-> e :error :explain (me/humanize {:errors (assoc me/default-errors + ::mc/missing-key {:error/message {:en "required"}})})) + errors (map + (fn [e] + {:path (:in e) + :message (get-in humanized (:in e))}) + (:errors (:explain (:error e))))] + (alog/warn ::form-4xx :errors errors) + (form-handler (assoc request + :form-params (:decoded e) + :field-validation-errors errors + :form-errors humanized))) + #_(html-response [:span.error-content.text-red-500 (:message &throw-context)] + :status 400)) + (catch [:type :field-validation] e + (form-handler (assoc request + :form-params (:form e) + :form-errors (:form-errors e)))) + (catch [:type :form-validation] e + (form-handler (assoc request + :form-params (:form e) + :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 - (fn [key-handler [k v]] - (assoc key-handler k (f v))) - key->handler) - )) + (fn [key-handler [k v]] + (assoc key-handler k (f v))) + key->handler))) (defn path->name2 [k & rest] (let [k->n (fn [k] @@ -354,10 +369,10 @@ (defn wrap-entity [handler path read] (fn wrap-entity-request [request] (let [entity (some->> - (get-in request path) - (#(if (string? %) (Long/parseLong %) %)) - (dc/pull (dc/db conn) read ))] + (get-in request path) + (#(if (string? %) (Long/parseLong %) %)) + (dc/pull (dc/db conn) read))] (handler (if entity (assoc request :entity entity) - request))))) + request))))) \ No newline at end of file diff --git a/src/clj/user.clj b/src/clj/user.clj index 411ff5b7..a468adc4 100644 --- a/src/clj/user.clj +++ b/src/clj/user.clj @@ -1,10 +1,13 @@ -(ns user + (ns user (:require [amazonica.aws.s3 :as s3] + [clojure.tools.namespace.repl :refer [set-refresh-dirs refresh]] [auto-ap.datomic :refer [conn pull-attr random-tempid]] - [auto-ap.ledger :as l ] + [auto-ap.ledger :as l] [clj-http.core :as http] [clj-http.client :as client] + [figwheel.main.api] + [hawk.core] [auto-ap.server] [auto-ap.time :as atime] [auto-ap.utils :refer [by]] @@ -29,6 +32,7 @@ (:import (org.apache.commons.io.input BOMInputStream))) + (defn println-event [item] (printf "%s: %s - %s:%s by %s\n" (str (c/to-date-time (:mulog/timestamp item))) @@ -36,23 +40,22 @@ (if (:mulog/duration item) (str " " (int (/ (:mulog/duration item) 1000000)) "ms") "") - (:user-name item) - ) + (:user-name item)) (println (reduce - (fn [acc [k v]] - (assoc acc k v)) - {} - (dissoc - item - :user))) - #_(puget/cprint (reduce (fn [acc [k v]] (assoc acc k v)) {} - (dissoc - item - :user)) - {:seq-limit 10}) + (dissoc + item + :user))) + #_(puget/cprint (reduce + (fn [acc [k v]] + (assoc acc k v)) + {} + (dissoc + item + :user)) + {:seq-limit 10}) (println)) @@ -70,8 +73,7 @@ (publish [_ buffer] ;; items are pairs [offset ] (doseq [item (transform (map second (rb/items buffer)))] - (println-event item) - ) + (println-event item)) (flush) (rb/clear buffer))) @@ -91,23 +93,23 @@ (defn load-accounts [conn] (let [[header & rows] (-> "master-account-list.csv" (io/resource) io/input-stream (BOMInputStream.) (io/reader) csv/read-csv) code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code - :db/id])] + :db/id])] :in ['$] :where ['[?e :account/name]]} - (dc/db conn)))) + (dc/db conn)))) also-merge-txes (fn [also-merge old-account-id] - (if old-account-id + (if old-account-id (let [[sunset-account] - (first (dc/q {:find ['?a ] - :in ['$ '?ac ] + (first (dc/q {:find ['?a] + :in ['$ '?ac] :where ['[?a :account/numeric-code ?ac]]} (dc/db conn) also-merge))] (into (mapv (fn [[entity id _]] [:db/add entity id old-account-id]) - (dc/q {:find ['?e '?id '?a ] - :in ['$ '?ac ] + (dc/q {:find ['?e '?id '?a] + :in ['$ '?ac] :where ['[?a :account/numeric-code ?ac] '[?e ?at ?a] '[?at :db/ident ?id]]} @@ -122,7 +124,7 @@ (into {} (map vector header r)))) (map (fn parse-map [r] {:old-account-id (:db/id (code->existing-account - (or + (or (if (= (get r "IOL Account #") "NEW") nil @@ -157,8 +159,7 @@ (if also-merge (into tx (also-merge-txes also-merge old-account-id)) - tx) - )))) + tx))))) conj @@ -168,7 +169,7 @@ #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn find-bad-accounts [] - (set (map second (dc/q {:find ['(pull ?x [*]) '?z] + (set (map second (dc/q {:find ['(pull ?x [*]) '?z] :in ['$] :where ['[?e :account/numeric-code ?z] '[(<= ?z 9999)] @@ -177,32 +178,31 @@ #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn delete-4-digit-accounts [] - @(dc/transact conn - (transduce + @(dc/transact conn + (transduce (comp - (map first) - (map (fn [old-account-id] - [:db/retractEntity old-account-id]))) + (map first) + (map (fn [old-account-id] + [:db/retractEntity old-account-id]))) conj [] - (dc/q {:find ['?e] + (dc/q {:find ['?e] :in ['$] :where ['[?e :account/numeric-code ?z] '[(<= ?z 9999)]]} - (dc/db conn)))) - ) + (dc/db conn))))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn find-conflicting-accounts [] - (filter + (filter (fn [[_ v]] (> (count v) 1)) - (reduce + (reduce (fn [acc [e z]] (update acc z conj e)) {} - (dc/q {:find ['?e '?z] + (dc/q {:find ['?e '?z] :in ['$] :where ['[?e :account/numeric-code ?z]]} (dc/db conn))))) @@ -214,31 +214,29 @@ :in ['$ '?z] :where [['?e :client/code '?z]]} (dc/db conn) customer))) - _ (println client-id) code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code {:account/applicability [:db/ident]} - :db/id])] + :db/id])] :in ['$] :where ['[?e :account/name]]} - (dc/db conn)))) + (dc/db conn)))) existing-account-overrides (dc/q {:find ['?e] :in ['$ '?client-id] :where [['?e :account-client-override/client '?client-id]]} (dc/db conn) client-id) - - + + _ (when-let [bad-rows (seq (->> rows - (group-by (fn [[_ account]] - account)) - vals - (filter #(> (count %) 1)) - (filter (fn [duplicates] - (apply not= (map rest duplicates)))) - #_(map (fn [[[_ account]]] - account)) - ))] + (group-by (fn [[_ account]] + account)) + vals + (filter #(> (count %) 1)) + (filter (fn [duplicates] + (apply not= (map rest duplicates)))) + #_(map (fn [[[_ account]]] + account))))] (throw (Exception. (str "These accounts are duplicated:" (str bad-rows))))) rows (vec (set (map rest rows))) @@ -256,8 +254,7 @@ (:db/ident (:account/applicability existing))) (and (not-empty override-name) (not-empty account-name) - (not= override-name account-name) - ))) + (not= override-name account-name)))) [{:db/id (:db/id existing) :account/client-overrides [{:account-client-override/client client-id :account-client-override/name (or (not-empty override-name) @@ -284,34 +281,30 @@ [:db/retractEntity x]) existing-account-overrides) rows)] - + txes #_@(d/transact conn txes))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn fix-transactions-without-locations [client-code location] - (->> - (dc/q {:find ['(pull ?e [*])] - :in ['$ '?client-code] - :where ['[?e :transaction/accounts ?ta] - '[?e :transaction/matched-rule] - '[?e :transaction/approval-status :transaction-approval-status/approved] - '(not [?ta :transaction-account/location]) - '[?e :transaction/client ?c] - '[?c :client/code ?client-code] - ]} - (dc/db conn) client-code) + (->> + (dc/q {:find ['(pull ?e [*])] + :in ['$ '?client-code] + :where ['[?e :transaction/accounts ?ta] + '[?e :transaction/matched-rule] + '[?e :transaction/approval-status :transaction-approval-status/approved] + '(not [?ta :transaction-account/location]) + '[?e :transaction/client ?c] + '[?c :client/code ?client-code]]} + (dc/db conn) client-code) (mapcat (fn [[{:transaction/keys [accounts]}]] (mapv (fn [a] {:db/id (:db/id a) - :transaction-account/location location} - ) - accounts) - ) - ) + :transaction-account/location location}) + accounts))) vec)) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} @@ -323,21 +316,21 @@ #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn entity-history [i] (vec (sort-by first (dc/q - {:find ['?tx '?z '?v ] - :in ['?i '$] - :where ['[?i ?a ?v ?tx ?ad] - '[?a :db/ident ?z] - '[(= ?ad true)]]} - i (dc/history (dc/db conn)))))) + {:find ['?tx '?z '?v] + :in ['?i '$] + :where ['[?i ?a ?v ?tx ?ad] + '[?a :db/ident ?z] + '[(= ?ad true)]]} + i (dc/history (dc/db conn)))))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn entity-history-with-revert [i] (vec (sort-by first (dc/q - {:find ['?tx '?z '?v '?ad ] - :in ['?i '$] - :where ['[?i ?a ?v ?tx ?ad] - '[?a :db/ident ?z]]} - i (dc/history (dc/db conn)))))) + {:find ['?tx '?z '?v '?ad] + :in ['?i '$] + :where ['[?i ?a ?v ?tx ?ad] + '[?a :db/ident ?z]]} + i (dc/history (dc/db conn)))))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn tx-detail [i] @@ -357,47 +350,71 @@ #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn start-db [] (mu/start-publisher! {:type :dev}) - (mount.core/start (mount.core/only #{#'auto-ap.datomic/conn }))) + (mount.core/start (mount.core/only #{#'auto-ap.datomic/conn}))) + + +(defn- auto-reset-handler [ctx event] + (require 'figwheel.main.api) + (binding [*ns* *ns*] + (clojure.tools.namespace.repl/refresh) + ctx)) + +(defn auto-reset + "Automatically reset the system when a Clojure or edn file is changed in + `src` or `resources`." + [] + (println "starting auto reset") + (hawk.core/watch! [{:paths ["src/"] + :handler auto-reset-handler}])) + +(defn start-http [] + (mount.core/start (mount.core/only #{#'auto-ap.server/port #'auto-ap.server/jetty}))) + + +(defn start-dev [] + (set-refresh-dirs "src") + (start-db) + (start-http) + (auto-reset)) #_(defn start-search [] - (mount.core/start (mount.core/only #{#'auto-ap.graphql.vendors/indexer #'auto-ap.graphql.accounts/indexer}))) + (mount.core/start (mount.core/only #{#'auto-ap.graphql.vendors/indexer #'auto-ap.graphql.accounts/indexer}))) (defn restart-db [] #_(require 'datomic.dev-local) #_(datomic.dev-local/release-db {:system "dev" :db-name "prod-migration"}) - (mount.core/stop (mount.core/only #{#'auto-ap.datomic/conn })) + (mount.core/stop (mount.core/only #{#'auto-ap.datomic/conn})) (start-db)) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} -(defn spit-csv [columns data ] +(defn spit-csv [columns data] (csv/write-csv *out* (into [(map name columns)] (for [r data] - ((apply juxt columns) r ))))) + ((apply juxt columns) r))))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn find-queries [words] (let [obj (s3/list-objects-v2 :bucket-name (:data-bucket env) - :prefix (str "queries/")) + :prefix (str "queries/")) concurrent 30 output-chan (async/chan)] (async/pipeline-blocking concurrent output-chan - (comp - (map #(do - [(:key %) - (str (slurp (:object-content (s3/get-object - :bucket-name (:data-bucket env) - :key (:key %)))))])) - - (filter #(->> words - (every? (fn [w] (str/includes? (second %) w))))) - (map first) - (map #(str/replace % #"queries/" "")) - ) + (comp + (map #(do + [(:key %) + (str (slurp (:object-content (s3/get-object + :bucket-name (:data-bucket env) + :key (:key %)))))])) + + (filter #(->> words + (every? (fn [w] (str/includes? (second %) w))))) + (map first) + (map #(str/replace % #"queries/" ""))) (async/to-chan! (:object-summaries obj)) true (fn [e] @@ -408,13 +425,13 @@ #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn upsert-invoice-amounts [tsv] (let [data (with-open [reader (io/reader (char-array tsv))] - (doall (csv/read-csv reader :separator \tab))) + (doall (csv/read-csv reader :separator \tab))) db (dc/db conn) - i->invoice-id (fn [i] - (try (Long/parseLong i) - (catch Exception e - (:db/id (dc/pull db '[:db/id] - [:invoice/original-id (Long/parseLong (first (str/split i #"-")))]))))) + i->invoice-id (fn [i] + (try (Long/parseLong i) + (catch Exception e + (:db/id (dc/pull db '[:db/id] + [:invoice/original-id (Long/parseLong (first (str/split i #"-")))]))))) invoice-totals (->> data (drop 1) (group-by first) @@ -423,13 +440,11 @@ (reduce + 0.0 (->> values (map (fn [[_ _ _ _ amount]] - (- (Double/parseDouble amount)))))) - ])) + (- (Double/parseDouble amount))))))])) (into {}))] - (->> + (->> (for [[i invoice-expense-account-id target-account target-date amount _ location] (drop 1 data) - :let [ - invoice-id (i->invoice-id i) + :let [invoice-id (i->invoice-id i) invoice (dc/pull db '[FILL_IN] invoice-id) current-total (:invoice/total invoice) @@ -441,34 +456,32 @@ (:db/id (first (:invoice/expense-accounts invoice))) (random-tempid)) invoice-expense-account (when-not new-account? - (dc/pull db '[FILL_IN]invoice-expense-account-id)) - current-account-id (:db/id (:invoice-expense-account/account invoice-expense-account)) + (dc/pull db '[FILL_IN] invoice-expense-account-id)) + current-account-id (:db/id (:invoice-expense-account/account invoice-expense-account)) target-account-id (Long/parseLong (str/trim target-account)) target-date (clj-time.coerce/to-date (atime/parse target-date atime/normal-date)) current-date (:invoice/date invoice) - - current-expense-account-amount (:invoice-expense-account/amount invoice-expense-account 0.0) + + current-expense-account-amount (:invoice-expense-account/amount invoice-expense-account 0.0) target-expense-account-amount (- (Double/parseDouble amount)) - current-expense-account-location (:invoice-expense-account/location invoice-expense-account) + current-expense-account-location (:invoice-expense-account/location invoice-expense-account) target-expense-account-location location [[_ _ invoice-payment]] (vec (dc/q - '[:find ?p ?a ?ip - :in $ ?i - :where [?ip :invoice-payment/invoice ?i] - [?ip :invoice-payment/amount ?a] - [?ip :invoice-payment/payment ?p] - ] - db invoice-id))] + '[:find ?p ?a ?ip + :in $ ?i + :where [?ip :invoice-payment/invoice ?i] + [?ip :invoice-payment/amount ?a] + [?ip :invoice-payment/payment ?p]] + db invoice-id))] :when current-total] - [ - (when (not (auto-ap.utils/dollars= current-total target-total)) + [(when (not (auto-ap.utils/dollars= current-total target-total)) {:db/id invoice-id :invoice/total target-total}) @@ -486,7 +499,7 @@ [:db/retractEntity invoice-payment]) (when (or new-account? - (not (auto-ap.utils/dollars= current-expense-account-amount target-expense-account-amount))) + (not (auto-ap.utils/dollars= current-expense-account-amount target-expense-account-amount))) {:db/id invoice-expense-account-id :invoice-expense-account/amount target-expense-account-amount}) @@ -495,7 +508,7 @@ {:db/id invoice-expense-account-id :invoice-expense-account/location target-expense-account-location}) - (when (not= current-account-id target-account-id ) + (when (not= current-account-id target-account-id) {:db/id invoice-expense-account-id :invoice-expense-account/account target-account-id})]) (mapcat identity) @@ -506,18 +519,18 @@ #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn get-schema [prefix] (->> (dc/q '[:find ?i - :in $ ?p - :where [_ :db/ident ?i] - [(namespace ?i) ?p]] (dc/db auto-ap.datomic/conn) prefix) + :in $ ?p + :where [_ :db/ident ?i] + [(namespace ?i) ?p]] (dc/db auto-ap.datomic/conn) prefix) (mapcat identity) vec)) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn get-idents [] (->> (dc/q '[:find ?i - :in $ - :where [_ :db/ident ?i]] - (dc/db conn) ) + :in $ + :where [_ :db/ident ?i]] + (dc/db conn)) (mapcat identity) (map str) (sort) @@ -532,45 +545,45 @@ (defn sample-ledger-import ([client-code] (sample-ledger-import client-code 10)) - ([client-code n] + ([client-code n] (let [client-location (ffirst (d/q '[:find ?l :in $ ?c :where [?c :client/locations ?l]] (dc/db conn) [:client/code client-code]))] (clojure.data.csv/write-csv - *out* - (for [n (range n) - :let [v (rand-nth (map first (d/q '[:find ?vn :where [_ :vendor/name ?vn]] (dc/db conn)))) - [{a-1 :account/numeric-code a-1-location :account/location} - {a-2 :account/numeric-code a-2-location :account/location}] (->> (d/q '[:find (pull ?a [:account/numeric-code :account/location]) :where [?a :account/numeric-code]] - (dc/db conn)) - (map first) - (shuffle) - (take 2)) - amount (rand-int 2000) - d (-> (t/now) - (t/minus (t/days (rand-int 60))) - (atime/unparse atime/normal-date)) - id (rand-int 100000)] - a [[(str id) client-code "synthetic" v d a-1 (or a-1-location client-location) 0 amount ] - [(str id) client-code "synthetic" v d a-2 (or a-2-location client-location) amount 0]]] - a) - :separator \tab)))) + *out* + (for [n (range n) + :let [v (rand-nth (map first (d/q '[:find ?vn :where [_ :vendor/name ?vn]] (dc/db conn)))) + [{a-1 :account/numeric-code a-1-location :account/location} + {a-2 :account/numeric-code a-2-location :account/location}] (->> (d/q '[:find (pull ?a [:account/numeric-code :account/location]) :where [?a :account/numeric-code]] + (dc/db conn)) + (map first) + (shuffle) + (take 2)) + amount (rand-int 2000) + d (-> (t/now) + (t/minus (t/days (rand-int 60))) + (atime/unparse atime/normal-date)) + id (rand-int 100000)] + a [[(str id) client-code "synthetic" v d a-1 (or a-1-location client-location) 0 amount] + [(str id) client-code "synthetic" v d a-2 (or a-2-location client-location) amount 0]]] + a) + :separator \tab)))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defn sample-manual-yodlee ([client-code] (sample-ledger-import client-code 10)) - ([client-code n] + ([client-code n] (let [bank-accounts (map first (d/q '[:find ?bac :in $ ?c :where [?c :client/bank-accounts ?b] [?b :bank-account/code ?bac]] (dc/db conn) [:client/code client-code]))] (clojure.data.csv/write-csv - *out* - (for [n (range n) - :let [amount (rand-int 2000) - d (-> (t/now) - (t/minus (t/days (rand-int 60))) - (atime/unparse atime/normal-date)) - id (rand-int 100000)]] - ["posted" d (str "Random Description - " id) "Travel" nil nil (- amount) nil nil nil nil nil (rand-nth bank-accounts) client-code]) - :separator \tab)))) + *out* + (for [n (range n) + :let [amount (rand-int 2000) + d (-> (t/now) + (t/minus (t/days (rand-int 60))) + (atime/unparse atime/normal-date)) + id (rand-int 100000)]] + ["posted" d (str "Random Description - " id) "Travel" nil nil (- amount) nil nil nil nil nil (rand-nth bank-accounts) client-code]) + :separator \tab)))) @@ -580,8 +593,7 @@ :in $ :where [?i :invoice/invoice-number] (not [?i :invoice/status :invoice-status/voided])] - :args [ - (dc/db conn)]}) + :args [(dc/db conn)]}) (map first) (partition-all 1000))] (print ".") @@ -627,4 +639,4 @@ (print ".") @(dc/transact auto-ap.datomic/conn n))) - + \ No newline at end of file diff --git a/src/clj/user.fiddle b/src/clj/user.fiddle index 2ed4574f..3d53fccf 100644 --- a/src/clj/user.fiddle +++ b/src/clj/user.fiddle @@ -31,3 +31,87 @@ [?c :client/code ?cd] [?c :client/locations ?l]] (dc/db conn)) + + +(init-repl) + +(seq (dc/q + '[:find ?a ?n ?n2 + :where [?a :account/name ?n] + [?a :account/numeric-code ?n2] + (not [?a :account/code])] + (dc/db conn))) + +(dc/q + '[:find (pull ?a [* {:account/applicability [:db/ident] :account/default-allowance [*]}]) + :where [?a :account/numeric-code 34090]] + (dc/db conn)) + + +(dc/q + '[:find ?a + :where [?a :account/numeric-code ?nc] + (not [?a :account/default-allowance])] + (dc/since (dc/db conn) #inst "2023-02-01")) + +@(dc/transact conn + (->> + (dc/q + '[:find ?a + :where [?a :account/numeric-code ?nc] + (not [?a :account/default-allowance])] + (dc/since (dc/db conn) #inst "2023-02-01")) + (map (fn [[a]] + {:db/id a + :account/default-allowance :allowance/allowed}))) + + ) + + +(dc/q '[:find (pull ?l [*]) + :in $ ?a + :where [?a :invoice/client] + [?l :journal-entry/original-entity ?a]] + (dc/db conn) + 17592316421929) + +(dc/pull (dc/db conn) '[*] 17592316421929) + +(entity-history 17592316421929) + + +(dc/q '[:find (pull ?l [*]) + :in $ ?a + :where [?a :invoice/client] + [?l :journal-entry/original-entity ?a]] + (dc/db conn) + 17592316421929) + + +;; Find journal entries that have been divorced from the original entity +@(dc/transact auto-ap.datomic/conn + (->> + (dc/q '[:find ?l + :in $ $$ $$$ + :where [$$ ?l :journal-entry/amount] + (not [$ ?l :journal-entry/external-id]) + [$ ?l :journal-entry/source "invoice"] + (not [$ ?l :journal-entry/original-entity]) + [$ ?l :journal-entry/client ?c] + [$ ?c :client/code ?cd] + [$$$ ?l :journal-entry/original-entity _ ?tx false]] + (dc/db conn) + (dc/since (dc/db conn) #inst "2024-02-04") + (dc/history (dc/db conn))) + (map (fn [[jl]] + [:db/retractEntity jl])) + seq)) + + +(entity-history 13194269907490) + +(user/tx-detail 13194269907766) + +(dc/tx-range (dc/log conn) + 13194269907490 + 13194269907490) diff --git a/src/cljc/auto_ap/client_routes.cljc b/src/cljc/auto_ap/client_routes.cljc index 9eb04744..4e58345d 100644 --- a/src/cljc/auto_ap/client_routes.cljc +++ b/src/cljc/auto_ap/client_routes.cljc @@ -6,10 +6,7 @@ "needs-activation/" :needs-activation "needs-activation" :needs-activation "payments/" :payments - "admin/" {"clients/" {"" :admin-clients - [:id] {"" :admin-specific-client - "/bank-accounts/" {[:bank-account] :admin-specific-bank-account}}} - "vendors" :admin-vendors} + "admin/" { "vendors" :admin-vendors} "invoices/" {"" :invoices "import" :import-invoices "unpaid" :unpaid-invoices @@ -22,8 +19,6 @@ "requires-feedback" :requires-feedback-transactions "excluded" :excluded-transactions} "reports/" {"" :reports} - "plaid" :plaid - "yodlee2" :yodlee2 "ledger/" {"" :ledger "profit-and-loss" :profit-and-loss "cash-flows" :cash-flows diff --git a/src/cljc/auto_ap/permissions.cljc b/src/cljc/auto_ap/permissions.cljc index 06efda41..ee183252 100644 --- a/src/cljc/auto_ap/permissions.cljc +++ b/src/cljc/auto_ap/permissions.cljc @@ -1,11 +1,25 @@ (ns auto-ap.permissions) +;; TODO after getting rid of cljs, use malli schemas to decode this +(defn get-client-id [client] + (cond (nat-int? client) + client + + (:db/id client) + (:db/id client) + + :else + nil)) + (defn can? [user {:keys [client subject activity]}] - (let [role (or (:user/role user) (:role user) user)] - (println "ROLE IS" role) + (let [role (or (:user/role user) (:role user) user) + client-id (get-client-id client)] (cond (#{:user-role/admin "admin"} role) true + (and client-id (not (get (into #{} (map :db/id (:clients user))) client-id))) + false + (#{:user-role/power-user "power-user"} role) (cond (#{:invoice-page :payment-page :my-company-page :transaction-page :ledger-page} subject) @@ -49,6 +63,9 @@ (= [:vendor :edit] [subject activity]) true + (= [:signature :edit] [subject activity]) + true + :else false) :else diff --git a/src/cljc/auto_ap/routes/admin/clients.cljc b/src/cljc/auto_ap/routes/admin/clients.cljc new file mode 100644 index 00000000..4d1601cb --- /dev/null +++ b/src/cljc/auto_ap/routes/admin/clients.cljc @@ -0,0 +1,19 @@ +(ns auto-ap.routes.admin.clients) +(def routes {"" {:get ::page + :put ::save + :post ::save} + "/table" ::table + + "/navigate" ::navigate + "/bank-accounts/sort" ::sort-bank-accounts + "/discard" ::discard + "/square-locations" ::refresh-square-locations + + "/location/new" ::new-location + "/match/new" ::new-match + "/location-match/new" ::new-location-match + "/email-contact/new" ::new-email-contact + "/feature-flag/new" ::new-feature-flag + "/new" {:get ::new-dialog} + ["/" [#"\d+" :db/id] "/sales-powerquery"] ::biweekly-sales-powerquery + ["/" [#"\d+" :db/id] "/edit"] ::edit-dialog}) diff --git a/src/cljc/auto_ap/routes/admin/transaction_rules.cljc b/src/cljc/auto_ap/routes/admin/transaction_rules.cljc index 4548b42e..cdb39a20 100644 --- a/src/cljc/auto_ap/routes/admin/transaction_rules.cljc +++ b/src/cljc/auto_ap/routes/admin/transaction_rules.cljc @@ -10,9 +10,10 @@ "/account/typeahead" ::account-typeahead "/test" ::test "/new" {:get ::new-dialog} + "/navigate" ::navigate ["/" [#"\d+" :db/id] "/edit"] ::edit-dialog ["/" [#"\d+" :db/id] "/delete"] ::delete ["/" [#"\d+" :db/id] "/run"] {:get ::execute-dialog - :post ::execute} + :post ::execute} "/check-badges" ::check-badges }) diff --git a/src/cljc/auto_ap/routes/admin/vendors.cljc b/src/cljc/auto_ap/routes/admin/vendors.cljc index 5eddad4b..110e4c1d 100644 --- a/src/cljc/auto_ap/routes/admin/vendors.cljc +++ b/src/cljc/auto_ap/routes/admin/vendors.cljc @@ -9,6 +9,7 @@ "/account-override" ::new-account-override "/account-typeahead" ::account-typeahead "/validate" ::validate + "/navigat" ::navigate "/new" {:get ::new} "/merge" {:get ::merge :put ::merge-submit} diff --git a/src/cljc/auto_ap/shared_views/company/sidebar.cljc b/src/cljc/auto_ap/shared_views/company/sidebar.cljc index 4bd1ae63..af7e25c3 100644 --- a/src/cljc/auto_ap/shared_views/company/sidebar.cljc +++ b/src/cljc/auto_ap/shared_views/company/sidebar.cljc @@ -30,16 +30,6 @@ [:i {:class icon-class}]]) [:span {:class "name"} label]]]) -(defn menu-item [{:keys [label route test-route active-route icon-class icon-style]}] - [:p.menu-item - [:a.item {:href (bidi/path-for all-client-visible-routes route) - :class (when (test-route active-route) "is-active")} - (if icon-style - [:span {:class icon-class :style icon-style}] - [:span {:class "icon"} - [:i {:class icon-class}]]) - [:span {:class "name"} label]]]) - (defn company-side-bar-impl [active-route] [:div (menu-item {:label "Reports" diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index dcea1625..098051cb 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -3,6 +3,7 @@ [auto-ap.routes.admin.excel-invoices :as ei-routes] [auto-ap.routes.admin.import-batch :as ib-routes] [auto-ap.routes.admin.vendors :as v-routes] + [auto-ap.routes.admin.clients :as ac-routes] [auto-ap.routes.admin.transaction-rules :as tr-routes])) (def routes {"impersonate" :impersonate @@ -15,6 +16,7 @@ "/update" {:patch :invoice-glimpse-update-textract-invoice}}}}} "account" {"/search" {:get :account-search}} "admin" {"" :auto-ap.routes.admin/page + "/client" ac-routes/routes "/history" {"" :admin-history "/" :admin-history #"/search/?" :admin-history-search @@ -61,8 +63,10 @@ "/table" {:get :pos-cash-drawer-shift-table}}} "vendor" {"/search" :vendor-search} + ;; TODO Include IDS in routes for company-specific things, as opposed to headers "company" {"" :company "/dropdown" :company-dropdown-search-results + "/signature" {"/put" :company-update-signature} "/search" :company-search "/bank-account/typeahead" :bank-account-typeahead ["/" [#"\d+" :db/id] "/bank-account"] {"/search" :bank-account-search} diff --git a/src/cljs/auto_ap/forms/builder.cljs b/src/cljs/auto_ap/forms/builder.cljs index 12f1d3a9..b8c55223 100644 --- a/src/cljs/auto_ap/forms/builder.cljs +++ b/src/cljs/auto_ap/forms/builder.cljs @@ -47,7 +47,7 @@ (defn builder [{:keys [value on-change can-submit data-sub error-messages change-event submit-event id fullwidth? schema validation-error-string]}] (when (and change-event on-change) - (throw "Either the form is to be managed by ::forms, or it should have value and on-change passed in")) + (throw (js/Error. "Either the form is to be managed by ::forms, or it should have value and on-change passed in"))) (let [data-sub (or data-sub [::forms/form id]) change-event (when-not on-change (or change-event [::forms/change id])) diff --git a/src/cljs/auto_ap/views/components/vendor_dialog.cljs b/src/cljs/auto_ap/views/components/vendor_dialog.cljs index bcd4c178..fd27d5d7 100644 --- a/src/cljs/auto_ap/views/components/vendor_dialog.cljs +++ b/src/cljs/auto_ap/views/components/vendor_dialog.cljs @@ -14,7 +14,6 @@ [auto-ap.views.components.typeahead :refer [typeahead-v3]] [auto-ap.views.components.typeahead.vendor :refer [search-backed-typeahead]] - [auto-ap.views.pages.admin.vendors.common :as common] [auto-ap.views.utils :refer [dispatch-event str->int with-is-admin? with-user]] [malli.core :as m] @@ -23,9 +22,25 @@ ;; Remaining cleanup todos: ;; test minification +(def default-read [:id :name :hidden :terms [:default-account [:name :id :location]] + [:account-overrides [[:client [:id :name]] :id [:account [:id :numeric-code :name]]]] + [:automatically-paid-when-due [:id :name]] + [:terms-overrides [[:client [:id :name]] :id :terms]] + [:schedule-payment-dom [[:client [:id :name]] :id :dom]] + [:usage [:client-id :count]] + [:primary-contact [:name :phone :email :id]] + [:plaid-merchant [:name :id]] + [:secondary-contact [:id :name :phone :email]] + :print-as :invoice-reminder-schedule :code + :legal-entity-name + :legal-entity-first-name :legal-entity-middle-name :legal-entity-last-name + :legal-entity-tin :legal-entity-tin-type + :legal-entity-1099-type + [:address [:id :street1 :street2 :city :state :zip]]]) + (def terms-override-schema (m/schema [:map - [:client schema/reference] - [:terms :int]])) + [:client schema/reference] + [:terms :int]])) (def automatically-paid-schema (m/schema [:map [:client schema/reference]])) @@ -35,8 +50,8 @@ [:dom [:int {:max 30}]]])) (def account-override-schema (m/schema [:map - [:client schema/reference] - [:account schema/reference]])) + [:client schema/reference] + [:account schema/reference]])) (def schema (m/schema [:map [:name schema/not-empty-string] [:print-as {:optional true} @@ -72,73 +87,73 @@ (re-frame/reg-event-fx ::save-complete [(forms/triggers-stop ::vendor-form)] - (fn [_ [_ _ ]] - {:dispatch [::modal/modal-closed ]})) + (fn [_ [_ _]] + {:dispatch [::modal/modal-closed]})) (re-frame/reg-event-fx - ::save - [with-user with-is-admin? (forms/triggers-loading ::vendor-form) (forms/in-form ::vendor-form)] - (fn [{:keys [user is-admin?] {{:keys [name hidden print-as terms invoice-reminder-schedule plaid-merchant primary-contact automatically-paid-when-due schedule-payment-dom secondary-contact address default-account terms-overrides account-overrides id legal-entity-name legal-entity-tin legal-entity-tin-type legal-entity-first-name legal-entity-last-name legal-entity-middle-name legal-entity-1099-type] :as data} :data} :db} _] - (if (m/validate schema data) - (let [query [:upsert-vendor - {:vendor (cond-> {:id id - :name name - :print-as print-as - :terms (or terms - nil) - :default-account-id (:id default-account) - :address address - :primary-contact primary-contact - :secondary-contact secondary-contact - :invoice-reminder-schedule invoice-reminder-schedule} - is-admin? (assoc :hidden hidden - :terms-overrides (mapv - (fn [{:keys [client terms id]}] - {:id id - :client-id (:id client) - :terms (or (str->int terms) 0)}) - terms-overrides) - :account-overrides (mapv - (fn [{:keys [client account id]}] - {:id id - :client-id (:id client) - :account-id (:id account)}) - account-overrides) - :schedule-payment-dom (mapv - (fn [{:keys [client dom id]}] - {:id id - :client-id (:id client) - :dom (or (str->int dom) - 0)}) - schedule-payment-dom) - :automatically-paid-when-due (mapv - (comp :id :client) - automatically-paid-when-due) - :plaid-merchant (:id plaid-merchant) - :legal-entity-name legal-entity-name - :legal-entity-first-name legal-entity-first-name - :legal-entity-middle-name legal-entity-middle-name - :legal-entity-last-name legal-entity-last-name - :legal-entity-tin legal-entity-tin - :legal-entity-tin-type (some-> legal-entity-tin-type clojure.core/name not-empty keyword) - :legal-entity-1099-type (some-> legal-entity-1099-type clojure.core/name not-empty keyword)))} - common/default-read]] - { :graphql - {:token user - :owns-state {:single ::vendor-form} - :query-obj {:venia/operation - {:operation/type :mutation - :operation/name "UpsertVendor"} :venia/queries [{:query/data query}]} - :on-success [::save-complete]}}) + ::save + [with-user with-is-admin? (forms/triggers-loading ::vendor-form) (forms/in-form ::vendor-form)] + (fn [{:keys [user is-admin?] {{:keys [name hidden print-as terms invoice-reminder-schedule plaid-merchant primary-contact automatically-paid-when-due schedule-payment-dom secondary-contact address default-account terms-overrides account-overrides id legal-entity-name legal-entity-tin legal-entity-tin-type legal-entity-first-name legal-entity-last-name legal-entity-middle-name legal-entity-1099-type] :as data} :data} :db} _] + (if (m/validate schema data) + (let [query [:upsert-vendor + {:vendor (cond-> {:id id + :name name + :print-as print-as + :terms (or terms + nil) + :default-account-id (:id default-account) + :address address + :primary-contact primary-contact + :secondary-contact secondary-contact + :invoice-reminder-schedule invoice-reminder-schedule} + is-admin? (assoc :hidden hidden + :terms-overrides (mapv + (fn [{:keys [client terms id]}] + {:id id + :client-id (:id client) + :terms (or (str->int terms) 0)}) + terms-overrides) + :account-overrides (mapv + (fn [{:keys [client account id]}] + {:id id + :client-id (:id client) + :account-id (:id account)}) + account-overrides) + :schedule-payment-dom (mapv + (fn [{:keys [client dom id]}] + {:id id + :client-id (:id client) + :dom (or (str->int dom) + 0)}) + schedule-payment-dom) + :automatically-paid-when-due (mapv + (comp :id :client) + automatically-paid-when-due) + :plaid-merchant (:id plaid-merchant) + :legal-entity-name legal-entity-name + :legal-entity-first-name legal-entity-first-name + :legal-entity-middle-name legal-entity-middle-name + :legal-entity-last-name legal-entity-last-name + :legal-entity-tin legal-entity-tin + :legal-entity-tin-type (some-> legal-entity-tin-type clojure.core/name not-empty keyword) + :legal-entity-1099-type (some-> legal-entity-1099-type clojure.core/name not-empty keyword)))} + default-read]] + {:graphql + {:token user + :owns-state {:single ::vendor-form} + :query-obj {:venia/operation + {:operation/type :mutation + :operation/name "UpsertVendor"} :venia/queries [{:query/data query}]} + :on-success [::save-complete]}}) - {:dispatch-n [[::forms/attempted-submit ::vendor-form] - [::status/error ::vendor-form [{:message "Please fix the errors and try again."}]]]}))) + {:dispatch-n [[::forms/attempted-submit ::vendor-form] + [::status/error ::vendor-form [{:message "Please fix the errors and try again."}]]]}))) (defn contact-field [{:keys [name field]}] [form-builder/with-scope {:scope field} [form-builder/vertical-control name - [left-stack + [left-stack [form-builder/vertical-control {:is-small? true} "Name" [:div.control.has-icons-left @@ -196,23 +211,22 @@ [form-builder/section {:title "Terms"} [form-builder/field-v2 {:field :terms} "Terms" - [number-input ]] + [number-input]] (when is-admin? [form-builder/field-v2 {:field [:terms-overrides]} "Overrides" - [multi-field-v2 {:template [[form-builder/raw-field-v2 {:field [:client]} + [multi-field-v2 {:template [[form-builder/raw-field-v2 {:field [:client]} [typeahead-v3 {:entities clients :entity->text :name :style {:width "13em"} - :type "typeahead-v3" - }]] + :type "typeahead-v3"}]] [form-builder/raw-field-v2 {:field :terms} [number-input]]] :schema [:sequential terms-override-schema] :key-fn :id :next-key (random-uuid) :new-text "New Terms Override"}]])] - + (when is-admin? [form-builder/section {:title "Schedule payment when due"} [form-builder/field-v2 {:field [:automatically-paid-when-due]} @@ -228,7 +242,7 @@ (when is-admin? [form-builder/section {:title "Schedule payment on day of month"} - [form-builder/field-v2 {:field [:schedule-payment-dom]} + [form-builder/field-v2 {:field [:schedule-payment-dom]} "Overrides" [multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :client} [typeahead-v3 {:entities clients @@ -249,8 +263,7 @@ {:query i :allowance :vendor} [:name :id :warning]]) - :style {:width "19em"}}] - ] + :style {:width "19em"}}]] (when (:warning (:default-account vendor)) [:div.notification.is-warning.is-light (:warning (:default-account vendor))]) @@ -258,23 +271,22 @@ [form-builder/field-v2 {:field [:account-overrides]} "Overrides" [multi-field-v2 {:template (fn [entity] - [[form-builder/raw-field-v2 {:field :client} - [typeahead-v3 {:entities clients - :entity->text :name - :style {:width "19em"} - }]] - [form-builder/raw-field-v2 {:field :account} - [search-backed-typeahead {:search-query (fn [i] - [:search_account - {:query i - :client_id (:id (:client entity)) - :allowance :vendor} - [:name :id :warning]]) - :style {:width "15em"}}]]]) - :schema [:sequential account-override-schema] - :key-fn :id - :next-key (random-uuid) - :new-text "Add override"}]])] + [[form-builder/raw-field-v2 {:field :client} + [typeahead-v3 {:entities clients + :entity->text :name + :style {:width "19em"}}]] + [form-builder/raw-field-v2 {:field :account} + [search-backed-typeahead {:search-query (fn [i] + [:search_account + {:query i + :client_id (:id (:client entity)) + :allowance :vendor} + [:name :id :warning]]) + :style {:width "15em"}}]]]) + :schema [:sequential account-override-schema] + :key-fn :id + :next-key (random-uuid) + :new-text "Add override"}]])] [form-builder/section {:title "Address"} [:div {:style {:width "30em"}} @@ -319,8 +331,7 @@ [form-builder/raw-field-v2 {:field :legal-entity-tin} [:input.input {:type "text" :placeholder "SSN or EIN" - :size "12" - }]] + :size "12"}]] [:div.control [form-builder/raw-field-v2 {:field :legal-entity-tin-type} @@ -336,25 +347,25 @@ :allow-nil? true}]]]) [form-builder/hidden-submit-button]])) -(defn vendor-dialog [ ] +(defn vendor-dialog [] (let [{:keys [data]} @(re-frame/subscribe [::forms/form ::vendor-form])] [:div [form-content {:data data}]])) (re-frame/reg-event-fx - ::vendor-selected - [with-user (forms/in-form ::select-vendor-form)] - (fn [{{:keys [data]} :db :keys [user]} _] - (if (:vendor data) - {:graphql {:token user - :query-obj {:venia/queries [[:vendor-by-id - {:id (:id (:vendor data))} - common/default-read]]} - :owns-state {:single ::select-vendor-form} - :on-success (fn [r] - [::started (:vendor-by-id r)])}} - {:dispatch-n [[::forms/attempted-submit ::select-vendor-form] - [::status/error ::select-vendor-form [{:message "Please select a vendor."}]]]}))) + ::vendor-selected + [with-user (forms/in-form ::select-vendor-form)] + (fn [{{:keys [data]} :db :keys [user]} _] + (if (:vendor data) + {:graphql {:token user + :query-obj {:venia/queries [[:vendor-by-id + {:id (:id (:vendor data))} + default-read]]} + :owns-state {:single ::select-vendor-form} + :on-success (fn [r] + [::started (:vendor-by-id r)])}} + {:dispatch-n [[::forms/attempted-submit ::select-vendor-form] + [::status/error ::select-vendor-form [{:message "Please select a vendor."}]]]}))) (defn select-vendor-form-content [] [form-builder/builder {:submit-event [::vendor-selected] diff --git a/src/cljs/auto_ap/views/main.cljs b/src/cljs/auto_ap/views/main.cljs index 40f6bf26..8faaf7b3 100644 --- a/src/cljs/auto_ap/views/main.cljs +++ b/src/cljs/auto_ap/views/main.cljs @@ -18,9 +18,7 @@ [auto-ap.views.pages.ledger.profit-and-loss-detail :refer [profit-and-loss-detail-page]] [auto-ap.views.pages.login :refer [login-page]] [auto-ap.views.pages.payments :refer [payments-page]] - [auto-ap.views.pages.home :refer [home-page]] - [auto-ap.views.pages.admin.clients :refer [admin-clients-page]] - [auto-ap.views.pages.admin.vendors :refer [admin-vendors-page]])) + [auto-ap.views.pages.home :refer [home-page]])) (defmulti page (fn [active-page] active-page)) (defmethod page :unpaid-invoices [_] @@ -93,14 +91,6 @@ (when (p/can? @(re-frame/subscribe [::subs/user]) {:subject :ledger-page}) (balance-sheet-page))) -(defmethod page :admin-clients [_] - (when (p/can? @(re-frame/subscribe [::subs/user]) {:subject :admin-page}) - (admin-clients-page))) - -(defmethod page :admin-specific-client [_] - (when (p/can? @(re-frame/subscribe [::subs/user]) {:subject :admin-page}) - (admin-clients-page))) - (defmethod page :admin-vendors [_] (when (p/can? @(re-frame/subscribe [::subs/user]) {:subject :admin-page}) (admin-vendors-page))) diff --git a/src/cljs/auto_ap/views/pages/admin/clients.cljs b/src/cljs/auto_ap/views/pages/admin/clients.cljs deleted file mode 100644 index 3dfd0deb..00000000 --- a/src/cljs/auto_ap/views/pages/admin/clients.cljs +++ /dev/null @@ -1,106 +0,0 @@ -(ns auto-ap.views.pages.admin.clients - (:require - [auto-ap.routes :as routes] - [auto-ap.status :as status] - [auto-ap.subs :as subs] - [auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]] - [auto-ap.views.pages.data-page :as data-page] - [auto-ap.views.components.grid :as grid] - [auto-ap.views.components.layouts :refer [side-bar-layout]] - [auto-ap.views.pages.admin.clients.form :as form] - [auto-ap.views.pages.admin.clients.side-bar :as side-bar] - [auto-ap.views.pages.admin.clients.table :as table] - [auto-ap.views.pages.page-stack :as page-stack] - [auto-ap.views.utils :refer [with-user]] - [bidi.bidi :as bidi] - [clojure.string :as str] - [clojure.set :as set] - [re-frame.core :as re-frame] - [vimsical.re-frame.fx.track :as track] - [auto-ap.events :as events] - [auto-ap.forms :as forms] - [auto-ap.db :as db])) - -(re-frame/reg-event-db - ::received-intuit-bank-accounts - (fn [db [_ result]] - (assoc db ::subs/intuit-bank-accounts (:intuit-bank-accounts result)))) - -(re-frame/reg-event-fx - ::mounted - [with-user] - (fn [{:keys [user db]} _] - {::track/register [{:id ::params - :subscription [::data-page/params ::page] - :event-fn (fn [params] [::params-change params])} - {:id ::active-route - :subscription [::subs/active-route] - :event-fn (fn [params] [::params-change params])}] - :db (-> db - (forms/stop-form [::form/form])) - :graphql {:token user - :query-obj {:venia/queries [[:intuit_bank_accounts [:external_id :id :name]]]} - :owns-state {:single [::load-intuit-bank-accounts]} - :on-success [::received-intuit-bank-accounts]} })) - -(re-frame/reg-event-fx - ::unmounted - (fn [{:keys [db]} _] - {:db (dissoc db ::table/params ::side-bar/filter-params) - ::track/dispose [{:id ::params} - {:id ::active-route}]})) - -(defn data-params->query-params [params] - {:start (:start params 0) - :per-page (:per-page params) - :sort (:sort params) - :name-like (:name-like params) - :code (:code params)}) - -(re-frame/reg-event-fx - ::params-change - [with-user] - (fn [{:keys [user]} [_ params]] - {:graphql {:token user - :owns-state {:single [::data-page/page ::page]} - :query-obj {:venia/queries [[:client-page - {:filters (data-params->query-params params)} - [[:clients (events/client-detail-query user)] - :total - :start - :end]]]} - :on-success (fn [result] - [::data-page/received ::page (set/rename-keys (:client-page result) - {:clients :data})])}})) - -(def admin-clients-content - (with-meta - (fn [] - [:div - [page-stack/page-stack - {:active @(re-frame/subscribe [::subs/active-route]) - :pages [{:key :admin-clients - :breadcrumb "Clients" - :content [:<> - [:div.is-pulled-right - [:a.button.is-primary.is-outlined {:href (bidi/path-for routes/routes :admin-specific-client :id "new")} "New client"]] - [table/clients-table {:data-page ::page - :id :clients}]]} - - {:key :admin-specific-client - :breadcrumb [:span [:a {:href (bidi/path-for routes/routes :admin-clients)} - "Clients"] - " / " - (or (:name (:data @(re-frame/subscribe [::forms/form ::form/form]))) - [:i "New client"])] - :content [form/new-client-form]} - ]}]]) - {:component-did-mount #(re-frame/dispatch [::mounted]) - :component-will-unmount #(re-frame/dispatch-sync [::unmounted])})) - - -(defn admin-clients-page [] - [side-bar-layout {:side-bar [admin-side-bar {} - [side-bar/client-side-bar {:data-page ::page}]] - :main [admin-clients-content]}]) - diff --git a/src/cljs/auto_ap/views/pages/admin/clients/form.cljs b/src/cljs/auto_ap/views/pages/admin/clients/form.cljs deleted file mode 100644 index 22738b74..00000000 --- a/src/cljs/auto_ap/views/pages/admin/clients/form.cljs +++ /dev/null @@ -1,761 +0,0 @@ -(ns auto-ap.views.pages.admin.clients.form - (:require - [auto-ap.events :as events] - [auto-ap.forms :as forms] - [auto-ap.forms.builder :as form-builder] - [auto-ap.routes :as routes] - [auto-ap.subs :as subs] - [auto-ap.views.components.address :refer [address2-field]] - [react-signature-canvas] - [auto-ap.views.components.typeahead :refer [typeahead-v3]] - [auto-ap.views.components.level :refer [left-stack] :as level] - [auto-ap.views.components :as com] - [auto-ap.views.components.typeahead.vendor - :refer [search-backed-typeahead]] - [auto-ap.views.utils - :refer [date-picker - with-user - dispatch-event]] - [bidi.bidi :as bidi] - [cljs-time.coerce :as coerce] - [cljs-time.core :as t] - [re-frame.core :as re-frame] - [reagent.core :as r] - [vimsical.re-frame.cofx.inject :as inject] - [auto-ap.schema :as schema] - [malli.core :as m])) - -(def signature-canvas (r/adapt-react-class (.-default react-signature-canvas))) - -(def location-schema (m/schema [:map - [:location schema/not-empty-string]])) - -(def feature-flag-schema (m/schema [:map - [:feature-flag schema/not-empty-string]])) - -(def square-location-schema (m/schema [:map - [:square-location schema/reference] - [:client-location schema/not-empty-string]])) - -(def ezcater-schema (m/schema [:map - [:caterer schema/reference] - [:client-location schema/not-empty-string]])) - -(def name-match-schema (m/schema [:map - [:match schema/not-empty-string]])) -(def location-match-schema (m/schema [:map - [:match schema/not-empty-string] - [:location schema/not-empty-string]])) -(def email-schema [:map - [:email schema/not-empty-string] - [:description schema/not-empty-string]]) - -(def client-schema [:map - [:name schema/not-empty-string] - [:code schema/code-string] - [:locations [:sequential location-schema]] - [:feature-flags {:optional true} [:maybe [:sequential feature-flag-schema]]] - [:emails {:optional true} - [:maybe [:sequential email-schema]]] - [:matches {:optional true} - [:maybe [:sequential name-match-schema]]] - [:location-matches {:optional true} - [:maybe [:sequential location-match-schema]]] - [:selected-square-locations {:optional true} - [:maybe [:sequential square-location-schema]]]]) - -(defn upload-replacement-button [{:keys [on-change]} text] - (let [button (atom nil)] - (r/create-class {:display-name "Upload button" - :reagent-render - (fn [] - [:<> - [:label.button {:for "upload_replacement_signature"} text] - [:input.button {:type "file" :id "upload_replacement_signature" - :style {:display "none"} - :on-change (fn [] - (let [fr (js/FileReader.)] - (.addEventListener fr "load" (fn [] - (on-change (.-result fr)))) - - (.readAsDataURL fr (aget (.-files @button) 0))) - ) - :ref (fn [i] (reset! button i))} ]])}))) - -(defn signature [_] - (let [canvas (atom nil) - edit-mode? (r/atom false) - w (* 1.5 464) - h (* 1.5 174)] - (fn [{:keys [signature-file signature-data on-change]}] - [:div - (if @edit-mode? - [:div - [signature-canvas {"canvasProps" {"width" w - "height" h - "style" #js {"border" "1px solid #CCC" - "border-radius" "10px"}} - "backgroundColor" "#FFF" - :ref (fn [el] - (reset! canvas el))}] - [:div.buttons - [:a.button.is-primary.is-outlined {:on-click (fn [] - (on-change (.toDataURL @canvas "image/jpeg")) - (reset! edit-mode? false))} - "Accept"] - [:a.button.is-warning.is-outlined {:on-click (fn [] - (.clear @canvas) - (reset! edit-mode? false))} - "Cancel"]]] - (if (or signature-data signature-file) - [:div - [:img {:src (or signature-data signature-file) - :style {:width w - :height h - :border "1px solid #CCC" - :border-radius "10px"}}] - [:div.buttons - [:a.button {:on-click (fn [] - (reset! edit-mode? true))} - "Replace Signature"] - - [upload-replacement-button {:on-change on-change} "Upload replacement"]]] - [:div - [:div.has-text-centered.is-vcentered {:style {:width w - :height h - :margin-bottom "8px" - :border "1px solid #CCC" - :border-radius "10px" - :background "#EEE" - }} - "No signature"] - [:div.buttons - [:a.button.is-primary.is-outlined {:on-click (fn [] - (reset! edit-mode? true))} - "New Signature"] - - [upload-replacement-button {:on-change on-change} "Upload signature"]]])) - ]))) - -(re-frame/reg-sub - ::new-client-request - :<- [::forms/form ::form] - (fn [{new-client-data :data} _] - (cond-> - {:id (:id new-client-data), - :name (:name new-client-data) - :code (:code new-client-data) ;; TODO add validation can't change - :emails (map #(select-keys % [:id :email :description]) - (:emails new-client-data)) - :square-auth-token (:square-auth-token new-client-data) - :square-locations (map - (fn [x] - {:id (:id (:square-location x)) - :client-location (:client-location x)}) - (:selected-square-locations new-client-data)) - - :ezcater-locations (map - (fn [x] - {:id (:id x) - :caterer (:id (:caterer x)) - :location (:location x)}) - (:ezcater-locations new-client-data)) - - :locked-until (:locked-until new-client-data) - :locations (mapv :location (:locations new-client-data)) - :feature-flags (mapv :feature-flag (:feature-flags new-client-data)) - :matches (mapv :match (:matches new-client-data)) - :location-matches (:location-matches new-client-data) - :week-a-credits (:week-a-credits new-client-data) - :week-a-debits (:week-a-debits new-client-data) - :week-b-credits (:week-b-credits new-client-data) - :week-b-debits (:week-b-debits new-client-data) - :address {:id (:id (:address new-client-data)) - :street1 (:street1 (:address new-client-data)) - :street2 (:street2 (:address new-client-data)), - :city (:city (:address new-client-data)) - :state (:state (:address new-client-data)) - :zip (:zip (:address new-client-data))} - :signature-data (:signature-data new-client-data) - :forecasted-transactions (map (fn [{:keys [id day-of-month identifier amount]}] - {:id id - :day-of-month (js/parseInt day-of-month) - :identifier identifier - :amount amount}) - (:forecasted-transactions new-client-data)) - :bank-accounts (map-indexed (fn [i {:keys [number name check-number plaid-account intuit-bank-account include-in-reports type id code numeric-code start-date bank-name routing bank-code new? visible locations yodlee-account use-date-instead-of-post-date]}] - {:number number - :name name - :check-number check-number - :numeric-code numeric-code - :include-in-reports include-in-reports - :start-date start-date - :type type - :id id - :sort-order i - :visible visible - :locations (mapv :location locations) - :use-date-instead-of-post-date use-date-instead-of-post-date - :yodlee-account (:id yodlee-account) - :plaid-account (:id plaid-account) - :intuit-bank-account (:id intuit-bank-account) - :code (if new? - (str (:code new-client-data) "-" code) - code) - :bank-name bank-name - :routing routing - :bank-code bank-code}) - (:bank-accounts new-client-data))}))) - -(re-frame/reg-event-fx - ::mounted - [with-user (re-frame/inject-cofx ::inject/sub [::subs/route-params])] - (fn [{:keys [user db] ::subs/keys [route-params]} _] - (when-let [id (some-> (:id route-params) (js/parseInt ) (#(if (js/Number.isNaN %) nil %)))] - {:graphql {:token user - :query-obj {:venia/queries [[:admin-client - {:id id} - (events/client-detail-query user)]]} - :on-success (fn [result] - [::received (:admin-client result)])} - :db (-> db - (forms/stop-form ::form))}))) - -(re-frame/reg-event-db - ::received - (fn [db [_ client]] - (-> db - (forms/stop-form ::form) - (forms/start-form ::form (-> client - (assoc :selected-square-locations (->> (:square-locations client) - (filter :client-location ) - (mapv (fn [sl] - {:id (:id sl) - :square-location sl - :client-location (:client-location sl)})))) - (update :locations #(mapv (fn [l] {:location l - :id (random-uuid)}) %)) - - (update :feature-flags #(mapv (fn [l] {:feature-flag l - :id (random-uuid)}) %)) - (update :matches #(mapv (fn [l] {:match l - :id (random-uuid)}) %)) - (update :bank-accounts - (fn [bas] - (mapv (fn [ba] - (update ba :locations (fn [ls] - (map (fn [l] {:location l - :id (random-uuid)}) - ls)))) - bas)))))))) - -(re-frame/reg-event-fx - ::save-new-client - [(forms/in-form ::form)] - (fn [_ _] - - (let [new-client-req @(re-frame/subscribe [::new-client-request]) - user @(re-frame/subscribe [::subs/token])] - - {:graphql - {:token user - :owns-state {:single ::form} - :query-obj {:venia/operation {:operation/type :mutation - :operation/name "EditClient"} - :venia/queries [{:query/data [:edit-client - {:edit-client new-client-req} - (events/client-detail-query user)]}]} - :on-success [::save-complete] - :on-error [::forms/save-error ::form]}}))) - -(re-frame/reg-event-fx - ::save-complete - (fn [{:keys [db]} [_ client]] - {:db - (-> db - #_(forms/stop-form ::form) - - (assoc-in [:clients (:id (:edit-client client))] (update (:edit-client client) :bank-accounts (fn [bas] (->> bas (sort-by :sort-order) vec))))) - :redirect (bidi/path-for routes/routes :admin-clients)})) - - - -(re-frame/reg-event-db - ::add-new-bank-account - [(forms/in-form ::form) (re-frame/path [:data])] - (fn [client [_ type]] - (update client :bank-accounts conj {:type type :active? true :new? true :visible true :sort-order (count (:bank-accounts client))}))) - -(re-frame/reg-event-db - ::bank-account-activated - [(forms/in-form ::form) (re-frame/path [:data :bank-accounts])] - (fn [bank-accounts [_ index]] - (update (vec (sort-by :sort-order bank-accounts)) index assoc :active? true))) - -(re-frame/reg-event-db - ::bank-account-deactivated - [(forms/in-form ::form) (re-frame/path [:data :bank-accounts])] - (fn [bank-accounts [_ index]] - (update (vec (sort-by :sort-order bank-accounts)) index assoc :active? false))) - -(re-frame/reg-event-db - ::bank-account-removed - [(forms/in-form ::form) (re-frame/path [:data :bank-accounts])] - (fn [bank-accounts [_ index]] - (vec (concat (take index bank-accounts) - (drop (inc index) bank-accounts))))) - -(re-frame/reg-event-db - ::sort-swapped - [(forms/in-form ::form) (re-frame/path [:data :bank-accounts])] - (fn [bank-accounts [_ source dest]] - (->> (-> bank-accounts - (assoc-in [source :sort-order] (get-in bank-accounts [dest :sort-order])) - (assoc-in [dest :sort-order] (get-in bank-accounts [source :sort-order])) - - ) - (sort-by :sort-order) - vec))) - -(re-frame/reg-event-db - ::toggle-visible - [(forms/in-form ::form) (re-frame/path [:data :bank-accounts])] - (fn [bank-accounts [_ account]] - (-> (->> bank-accounts - (sort-by :sort-order) - vec) - (update-in [account :visible] #(not %))))) - - - - - -(def first-week-a (coerce/to-date-time #inst "1999-12-27T00:00:00.000-07:00")) - -(defn is-week-a? [d] - (= 0 (mod (t/in-weeks (t/interval first-week-a d)) 2))) - - -(re-frame/reg-sub - ::yodlee-accounts - :<- [::subs/clients-by-id] - (fn [clients [_ id]] - - (if id - (mapcat :accounts (:yodlee-provider-accounts (get clients id) )) - []))) - -(re-frame/reg-sub - ::plaid-accounts - :<- [::subs/clients-by-id] - (fn [clients [_ id]] - - (if id - (mapcat :accounts (:plaid-items (get clients id) )) - []))) - - -(defn bank-account-card [new-client {:keys [active? new? type visible code name sort-order]} first? last?] - [:div.card {:style {:margin-bottom "1em" - :width "600px"}} - [:header.card-header.has-background-primary-light - [:div.card-header-title {:style {:text-overflow "ellipsis"}} - [:div.level {:style {:width "100%"}} - [:div.level-left - [:div.level-item - [:span.icon.inline - (cond - (#{:check ":check"} type) [:span.icon-check-payment-sign] - - (#{:credit ":credit"} type) [:span.icon-credit-card-1] - - :else [:span.icon-accounting-bill])]] - [:div.level-item code ": " name]] - [:div.level-right - [:div.level-item - [:div.buttons - [:a.button {:on-click (dispatch-event [::toggle-visible sort-order])} [:span.icon (if visible - [:span.fa.fa-eye] - [:span.fa.fa-eye-slash] - )]] - (when-not last? - [:a.button {:on-click (dispatch-event [::sort-swapped sort-order (inc sort-order)])} [:span.icon [:span.fa.fa-sort-down]]]) - (when-not first? - [:a.button {:on-click (dispatch-event [::sort-swapped sort-order (dec sort-order)])} [:span.icon [:span.fa.fa-sort-up]]])]]]]] - (if active? - [:a.card-header-icon - {:on-click (dispatch-event [::bank-account-deactivated sort-order])} - [:span.icon - [:span.fa.fa-angle-up]]] - [:a.card-header-icon - {:on-click (dispatch-event [::bank-account-activated sort-order])} - [:span.icon - [:span.fa.fa-angle-down]]])] - (when active? - [:div.card-content - [:label.label "General"] - [level/left-stack - [:div.control - [:p.help "Account Code"] - (if new? - [:div.field.has-addons - [:p.control [:a.button.is-static (:code new-client) "-" ]] - [:p.control - [form-builder/raw-field-v2 {:field :code} - [:input.input {:type "text"}]]]] - [:div.field [:p.control code]])] - - [form-builder/field-v2 {:field :name} - "Nickname" - [:input.input {:placeholder "BOA Checking #1" - :type "text"}]] - [form-builder/field-v2 {:field :numeric-code} - "Numeric Code" - [com/number-input {:placeholder "20101" - :style {:width "8em"}}]] - [form-builder/field-v2 {:field :start-date} - "Start date" - [date-picker {:output :cljs-date}]]] - - (when (#{:check ":check"} type ) - [:div - - [:label.label "Bank"] - [level/left-stack - [form-builder/field-v2 {:field :bank-name} - "Bank Name" - [:input.input {:placeholder "Bank of America" - :type "text"}]] - [form-builder/field-v2 {:field [:routing]} - "Routing #" - [:input.input {:placeholder "104819123" - :style {:width "9em"} - :type "text"}]] - [form-builder/field-v2 {:field :bank-code} - "Bank code" - [:input.input {:placeholder "12/10123" - :type "text"}]]] - - [level/left-stack - [form-builder/field-v2 {:field :number} - "Account #" - [:input.input {:placeholder "123456789" - :type "text" - :style {:width "20em"}}]] - - [form-builder/field-v2 {:field :check-number} - "Check Number" - [com/number-input {:style {:width "8em"} - :placeholder "10000"}]]] - - [form-builder/field-v2 {:field :yodlee-account} - "Yodlee Account (new)" - [typeahead-v3 {:entities (mapcat :accounts (:yodlee-provider-accounts new-client )) - :entity->text (fn [m] (str (:name m) " - " (:number m)))}]] - - - [form-builder/raw-field-v2 {:field :use-date-instead-of-post-date} - [com/checkbox {:label " (Yodlee only) Use 'date' instead of 'postDate'"}]] - - [form-builder/field-v2 {:field :intuit-bank-account} - "Intuit Bank Account" - [typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts]) - :entity->text (fn [m] (str (:name m)))}]] - [form-builder/field-v2 {:field :plaid-account} - "Plaid Account" - [typeahead-v3 {:entities (mapcat :accounts (:plaid-items new-client )) - :entity->text (fn [m] (str (:name m)))}]]]) - - (when (#{:credit ":credit"} type ) - [:div - - - [:label.label "Account"] - [form-builder/field-v2 {:field :bank-name} - "Bank Name" - [:input.input {:placeholder "Bank of America" - :type "text"}]] - - [form-builder/field-v2 {:field :number} - "Account #" - [:input.input {:placeholder "123456789" - :type "text" - :style {:width "20em"}}]] - - [form-builder/field-v2 {:field :yodlee-account} - "Yodlee Account (new)" - [typeahead-v3 {:entities (mapcat :accounts (:yodlee-provider-accounts new-client )) - :entity->text (fn [m] (str (:name m) " - " (:number m)))}]] - - [form-builder/raw-field-v2 {:field :use-date-instead-of-post-date} - [com/checkbox {:label "(Yodlee only) Use 'date' instead of 'postDate'"}] - [:input {:type "checkbox" - :field [:use-date-instead-of-post-date]}]] - - [form-builder/field-v2 {:field :intuit-bank-account} - "Intuit Bank Account" - [typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts]) - :entity->text (fn [m] (str (:name m)))}]] - - [form-builder/field-v2 {:field :plaid-account} - "Plaid Account" - [typeahead-v3 {:entities (mapcat :accounts (:plaid-items new-client )) - :entity->text (fn [m] (str (:name m)))}]]]) - [:div.field - [:label.label "Locations"] - [:div.control - [:p.help "If this account is location-specific, add the valid locations"] - [form-builder/raw-field-v2 {:field :locations} - [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :location} - [com/select-field {:options (map (fn [l] - [(:location l) (:location l)]) - (get-in new-client [:locations])) - :allow-nil? true - :style {:width "7em"} - }]]] - :schema [:sequential location-schema] - :key-fn :id}]]]] - - - - - [form-builder/raw-field-v2 {:field :include-in-reports} - [com/checkbox {:label "Include in reports"}] - ] - ]) - - (when active? - [:footer.card-footer - [:a.card-footer-item {:href "#" :on-click (dispatch-event [::bank-account-deactivated sort-order])} "Done"] - (when new? - [:a.card-footer-item.is-warning {:href "#" :on-click (dispatch-event [::bank-account-removed sort-order])} "Remove"])])]) - - -(defn general-section [] - (let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])] - [form-builder/section {:title "General"} - [form-builder/field-v2 {:field :name} - "Name" - [:input.input {:type "text" - :style {:width "20em"}}]] - [form-builder/field-v2 {:field :code} - "Client code" - [:input.input {:type "code" - :style {:width "5em"} - :disabled (boolean (:id new-client))}]] - [:div.field - [:label.label "Feature Flags"] - [:div.control - [:p.help "These are specific new features that can be enabled or disabled on a per-client basis"] - [form-builder/raw-field-v2 {:field :feature-flags} - [com/multi-field-v2 {:allow-change? true - :template [[form-builder/raw-field-v2 {:field :feature-flag} - [com/select-field {:options [[nil nil] - ["new-square" "New Square+Ezcater (no effect)"] - ["manually-pay-cintas" "Manually Pay Cintas"] - ["include-in-ntg-corp-reports" "Include in NTG Corporate reports"]] - :allow-nil? false - :style {:width "18em"}}]]] - :key-fn :id - :schema [:sequential feature-flag-schema] - :next-key (random-uuid)}]]]] - - [form-builder/field-v2 {:field :locations} - "Locations" - [com/multi-field-v2 {:allow-change? false - :template [[form-builder/raw-field-v2 {:field :location} - [:input.input {:max-length 2 - :style {:width "4em"}}]]] - :disable-remove? true - :key-fn :id - :schema [:sequential location-schema] - :next-key (random-uuid)}]] - - [form-builder/vertical-control - "Signature" - [signature {:signature-file (:signature-file new-client) - :signature-data (:signature-data new-client) - :on-change (fn [uri] - (re-frame/dispatch [::forms/change ::form [:signature-data] uri]))}]] - - [form-builder/field-v2 {:field :locked-until} - "Locked Until" - [date-picker {:output :cljs-date - :style {:width "15em"}}]]])) - -(defn contacts-section [] - [form-builder/section {:title "Contacts"} - - [form-builder/field-v2 {:field :emails} - "Emails (address/description)" - [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :email} - [:input.input {:type "email" - :placeholder "tom@myspace.com"}]] - [form-builder/raw-field-v2 {:field :description} - [:input.input {:type "text" - :placeholder "Manager"}]]] - :key-fn :id - :schema [:sequential email-schema] - :next-key (random-uuid)}]] - - [form-builder/vertical-control - "Address" - [:div {:style {:width "30em"}} - [form-builder/raw-field-v2 {:field :address} - [address2-field]]]]]) - -;; TODO Name matches, locations, bank account locations are all "single field multis", and require weird mounting and -;; unmounting. A new field could sort that out easily -(defn matching-section [] - [form-builder/section {:title "Matching"} - [form-builder/field-v2 {:field :matches} - "Name matches" - [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field [:match]} - [:input.input {:placeholder "Harry's burger joint" - :style { :width "15em"}}]]] - :key-fn :id - :next-key (random-uuid) - :schema [:sequential name-match-schema]}]] - - - [form-builder/field-v2 {:field :location-matches} - "Location Matches" - [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :match} - [:input.input {:placeholder "Downtown" - :style { :width "15em"}}]] - [form-builder/raw-field-v2 {:field :location} - [:input.input {:placeholder "DT" - :max-length 2 - :style { :width "4em"}}]]] - :schema [:sequential location-match-schema] - :next-key (random-uuid) - :key-fn :id}]]]) - -(defn bank-accounts-section [] - (let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])] - [form-builder/section {:title "Bank Accounts"} - (for [bank-account (sort-by :sort-order (:bank-accounts new-client))] - ^{:key (:sort-order bank-account)} - [form-builder/with-scope {:scope [:bank-accounts (:sort-order bank-account)]} - [bank-account-card new-client bank-account (= 0 (:sort-order bank-account)) (= (:sort-order bank-account) (dec (count (:bank-accounts new-client))))]]) - - [:div.buttons - [:a.button.is-primary.is-outlined {:on-click (dispatch-event [::add-new-bank-account :credit])} "Add Credit Account"] - [:a.button.is-primary.is-outlined {:on-click (dispatch-event [::add-new-bank-account :check])} "Add Checking Account"] - [:a.button.is-primary.is-outlined {:on-click (dispatch-event [::add-new-bank-account :cash])} "Add Cash Account"]]])) - -(defn cash-flow-section [] - (let [next-week-a (if (is-week-a? (t/now)) - "This week" - "Next week") - next-week-b (if (is-week-a? (t/now)) - "Next week" - "This week")] - - - [form-builder/section {:title "Cash Flow"} - [:label.label (str "Week A (" next-week-a ")")] - [left-stack - [form-builder/field-v2 {:field :week-a-credits} - "Regular Credits" - [com/money-input]] - [form-builder/field-v2 {:field :week-a-debits} - "Regular Debits" - [com/money-input]]] - [:label.label (str "Week B (" next-week-b ")")] - [left-stack - [form-builder/field-v2 {:field :week-b-credits} - "Regular Credits" - [com/money-input]] - [form-builder/field-v2 {:field :week-b-debits} - "Regular Debits" - [com/money-input]]] - - [form-builder/field-v2 {:field :forecasted-transactions} - "Forecasted transactions" - - [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :identifier} - [:input.input {:type "text" - :placeholder "Identifier" - :style {:width "10em"}}]] - [form-builder/raw-field-v2 {:field :day-of-month} - [com/number-input {:placeholder "DOM"}]] - [form-builder/raw-field-v2 {:field :amount - :placeholder "AMT"} - [com/money-input]]] - :key-fn :id}]]])) - -(defn square-section [] - (let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])] - [form-builder/section {:title "Square Integration"} - [form-builder/field-v2 {:field :square-auth-token} - "Square Authentication Token" - [:input.input {:type "text" - :style {:width "40em"}}]] - [form-builder/field-v2 {:field :selected-square-locations} - "Square Locations" - [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :square-location} - [typeahead-v3 {:entities (:square-locations new-client) - :entity->text :name - :style {:width "15em"}}]] - [form-builder/raw-field-v2 {:field :client-location} - [com/select-field {:options (map (fn [l] - [(:location l) (:location l)]) - (get-in new-client [:locations])) - :allow-nil? true - :style {:width "7em"} - }]]] - :disable-remove? true - :key-fn :id - :schema [:sequential square-location-schema]}]]])) - -(defn ezcater-section [] - (let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])] - [form-builder/section {:title "EZCater integration"} - - [form-builder/field-v2 {:field :ezcater-locations} - "EZCater Locations" - [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :caterer} - [search-backed-typeahead {:search-query (fn [i] - [:search_ezcater_caterer - {:query i} - [:name :id]]) - :entity->text :name - :style {:width "20em"}}]] - [form-builder/raw-field-v2 {:field [:location]} - [com/select-field {:options (map (fn [l] - [(:location l) (:location l)]) - (get-in new-client [:locations])) - :allow-nil? true - :style {:width "7em"}}]]] - :key-fn :id - :schema [:sequential ezcater-schema] - :disable-remove? true}]]])) - - -(defn form-content [] - (let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])] - - ^{:key (or (:id new-client) - "new")} - [form-builder/builder {:submit-event [::save-new-client ] - :id ::form - :fullwidth? false - :schema client-schema} - - [general-section] - [contacts-section] - [matching-section] - [bank-accounts-section] - [cash-flow-section] - [square-section] - [ezcater-section] - [form-builder/error-notification] - [form-builder/submit-button "Save"]])) - -(def new-client-form - (with-meta - (fn [] - (let [_ @(re-frame/subscribe [::subs/route-params])] - [form-content])) - {:component-did-mount #(re-frame/dispatch [::mounted])})) diff --git a/src/cljs/auto_ap/views/pages/admin/clients/side_bar.cljs b/src/cljs/auto_ap/views/pages/admin/clients/side_bar.cljs deleted file mode 100644 index fe4578e3..00000000 --- a/src/cljs/auto_ap/views/pages/admin/clients/side_bar.cljs +++ /dev/null @@ -1,23 +0,0 @@ -(ns auto-ap.views.pages.admin.clients.side-bar - (:require - [re-frame.core :as re-frame] - [auto-ap.views.utils :refer [dispatch-value-change]] - [auto-ap.views.pages.data-page :as data-page])) - -(defn client-side-bar [{:keys [data-page]}] - [:div - [:p.menu-label "Name"] - - [:div.field - [:div.control [:input.input {:placeholder "Harry's Food Products" - :value @(re-frame/subscribe [::data-page/filter data-page :name-like]) - :on-change (dispatch-value-change [::data-page/filter-changed data-page :name-like])} ]]] - - [:p.menu-label "Code"] - - [:div.field - [:div.control [:input.input {:placeholder "CBC" - :value @(re-frame/subscribe [::data-page/filter data-page :code]) - :on-change (dispatch-value-change [::data-page/filter-changed data-page :code])} ]]]]) - - diff --git a/src/cljs/auto_ap/views/pages/admin/clients/table.cljs b/src/cljs/auto_ap/views/pages/admin/clients/table.cljs deleted file mode 100644 index d27a2d51..00000000 --- a/src/cljs/auto_ap/views/pages/admin/clients/table.cljs +++ /dev/null @@ -1,123 +0,0 @@ -(ns auto-ap.views.pages.admin.clients.table - (:require [auto-ap.subs :as subs] - [clojure.string :as str] - [re-frame.core :as re-frame] - [auto-ap.views.utils :refer [action-cell-width date->str with-user]] - [auto-ap.views.components.grid :as grid] - [auto-ap.views.components.modal :as modal] - [auto-ap.views.components.buttons :as buttons] - [auto-ap.status :as status] - [bidi.bidi :as bidi] - [auto-ap.routes :as routes] - [auto-ap.views.pages.data-page :as data-page])) - -(re-frame/reg-sub - ::specific-params - (fn [db] - (::params db))) - -(re-frame/reg-event-fx - ::params-changed - (fn [{:keys [db]} [_ p]] - {:db (assoc db ::params p)})) - -(re-frame/reg-event-fx - ::sales-queries-setup - (fn [_ [_ results]] - {:dispatch [::modal/modal-requested {:title "Sales Queries" - :body [:div [:pre (:message (:setup-sales-queries results))]]}]})) - -(re-frame/reg-event-fx - ::setup-sales-queries - [with-user] - (fn [{:keys [user]} [_ client-id]] - {:graphql - {:token user - :owns-state {:multi ::setup-sales-queries - :which client-id} - :query-obj {:venia/operation {:operation/type :mutation - :operation/name "SetupSalesQueries"} - :venia/queries [{:query/data [:setup-sales-queries - {:client-id client-id} - [:message]]}]} - :on-success [::sales-queries-setup]}} - )) - -(re-frame/reg-sub - ::params - :<- [::specific-params] - :<- [::subs/query-params] - (fn [[specific-params query-params]] - (merge (select-keys query-params #{:start :sort}) specific-params ))) - - -(defn integration-status-badge [name status] - (condp = (:state status) - :success - [:div.tag.has-tooltip-right.has-tooltip-arrow {:data-tooltip (str "Last updated:" (date->str (:last-updated status)) - "\n" - "Last Attempted:" (date->str (:last-attempt status)))} [:span.icon [:i.has-text-success.fa.fa-check]] [:span name]] - - :failed - [:div.tag.is-danger.is-light.has-tooltip-right.has-tooltip-arrow {:data-tooltip (str "Last updated:" (date->str (:last-updated status)) - "\n" - "Last Attempted:" (date->str (:last-attempt status)) - "\n" - (:message status)) - } [:span.icon [:i.has-text-danger.fa.fa-warning]] [:span name]] - - :unauthorized - [:div.tag.is-danger.is-light.has-tooltip-right.has-tooltip-arrow {:data-tooltip (str "Last updated:" (date->str (:last-updated status)) - "\n" - "Last Attempted:" (date->str (:last-attempt status)) - "\n" - "Your user is unauthorized. Detail:\n" - (:message status)) - } [:span.icon [:i.has-text-danger.fa.fa-warning]] [:span name]] - nil - )) - -(defn clients-table [{:keys [data-page status]}] - (let [states @(re-frame/subscribe [::status/multi ::setup-sales-queries]) - {:keys [data]} @(re-frame/subscribe [::data-page/page data-page])] - [grid/grid {:on-params-change (fn [p] - (re-frame/dispatch [::params-changed p])) - :data-page data-page - :status status - :params @(re-frame/subscribe [::params]) - :column-count 5} - [grid/controls data] - [grid/table {:fullwidth true} - [grid/header - [grid/row {} - [grid/header-cell {} "Name"] - [grid/header-cell {:style {:width "20em"}} "Code"] - [grid/header-cell {} "Locations"] - [grid/header-cell {} "Status"] - [grid/header-cell {} "Email"] - [grid/header-cell {:style {:width (action-cell-width 2)}}]]] - [grid/body - (for [{:keys [id name email square-integration-status locked-until code locations bank-accounts]} (:data data)] - ^{:key (str name "-" id)} - [grid/row {:id id} - [grid/cell {} name] - [grid/cell {} code] - [grid/cell {} (str/join ", " locations)] - [grid/cell {:class "expandable"} [:div.tags - - [:div.tag (or (some-> locked-until date->str (#(str "Locked " %))) "Not locked")] - [integration-status-badge "Square" square-integration-status] - [:<> - (for [bank-account bank-accounts - :let [code (:code bank-account) - integration-status (:integration-status bank-account)] - :when (:id integration-status)] - ^{:key (:id integration-status)} - [integration-status-badge code integration-status])]]] - [grid/cell {} email] - [grid/cell {} [:div.buttons [buttons/fa-icon {:event [::setup-sales-queries id] - :class (status/class-for (get states id)) - :icon :fa-dollar}] - [buttons/fa-icon {:href (bidi/path-for routes/routes :admin-specific-client :id id) - :icon :fa-pencil}]]]])]] - [grid/bottom-paginator data]])) diff --git a/src/cljs/auto_ap/views/pages/admin/vendors.cljs b/src/cljs/auto_ap/views/pages/admin/vendors.cljs deleted file mode 100644 index 681b5301..00000000 --- a/src/cljs/auto_ap/views/pages/admin/vendors.cljs +++ /dev/null @@ -1,94 +0,0 @@ -(ns auto-ap.views.pages.admin.vendors - (:require - [auto-ap.effects.forward :as forward] - [auto-ap.subs :as subs] - [auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]] - [auto-ap.views.components.layouts :refer [side-bar-layout]] - [auto-ap.views.pages.admin.vendors.merge-dialog :as merge-dialog] - [auto-ap.views.pages.admin.vendors.side-bar :as side-bar] - [auto-ap.views.pages.admin.vendors.table :as table] - [auto-ap.views.pages.data-page :as data-page] - [auto-ap.views.utils :refer [dispatch-event with-user]] - [clojure.set :as set] - [re-frame.core :as re-frame] - [vimsical.re-frame.fx.track :as track] - [auto-ap.views.components.vendor-dialog :as vendor-dialog])) - -(def default-read [:id :name :hidden :terms [:default-account [:name :id :location]] - [:account-overrides [[:client [:id :name]] :id [:account [:id :numeric-code :name]]]] - [:automatically-paid-when-due [:id :name]] - [:terms-overrides [[:client [:id :name]] :id :terms]] - [:schedule-payment-dom [[:client [:id :name]] :id :dom]] - [:usage [:client-id :count]] - [:primary-contact [:name :phone :email :id]] - [:secondary-contact [:id :name :phone :email]] - [:plaid-merchant [:id :name]] - :print-as :invoice-reminder-schedule :code - :legal-entity-name - :legal-entity-first-name :legal-entity-middle-name :legal-entity-last-name - :legal-entity-tin :legal-entity-tin-type - :legal-entity-1099-type - [:address [:id :street1 :street2 :city :state :zip]]]) - -(re-frame/reg-event-fx - ::params-change - [with-user] - (fn [{:keys [user]} [_ params]] - {:graphql {:token user - :owns-state {:single [::data-page/page ::page]} - :query-obj {:venia/queries [{:query/data [:vendor - {:sort (:sort params) - :start (:start params 0) - :per-page (:per-page params) - :name-like (:name-like params)} - [[:vendors default-read] - :total - :start - :end]] - :query/alias :result}]} - :on-success (fn [result] - [::data-page/received ::page - (set/rename-keys (:result result) - {:vendors :data})])}})) - -(re-frame/reg-event-fx - ::mounted - (fn [_ _] - {::forward/register [{:id ::merge-complete - :events #{::merge-dialog/complete} - :event-fn (fn [_] - [::params-change {}])} - {:id ::save-complete - :events #{::vendor-dialog/save-complete} - :event-fn (fn [_] - [::params-change {}])}] - ::track/register {:id ::params - :subscription [::data-page/params ::page] - :event-fn (fn [params] - [::params-change params])}})) - -(re-frame/reg-event-fx - ::unmounted - (fn [_ _] - {:dispatch [::data-page/dispose ::page] - ::forward/dispose [{:id ::merge-complete} {:id ::save-complete}] - ::track/dispose {:id ::params}})) - -(defn admin-vendors-content [] - [(with-meta - (fn [] - [:div.inbox-messages - (when-let [banner (:banner @(re-frame/subscribe [::subs/admin]))] - [:div.notification banner]) - [:div - [:h1.title "Vendors"] - [:div.is-pulled-right [:a.button.is-primary.is-outlined {:on-click (dispatch-event [::merge-dialog/show])} "Merge vendors"]] - [table/vendors-table {:id :vendors - :data-page ::page}]]]) - {:component-did-mount #(re-frame/dispatch [::mounted]) - :component-will-unmount #(re-frame/dispatch-sync [::unmounted])})]) - -(defn admin-vendors-page [] - [side-bar-layout {:side-bar [admin-side-bar {} - [side-bar/vendor-side-bar {:data-page ::page}]] - :main [admin-vendors-content]}]) diff --git a/src/cljs/auto_ap/views/pages/admin/vendors/common.cljs b/src/cljs/auto_ap/views/pages/admin/vendors/common.cljs deleted file mode 100644 index b60ddbd5..00000000 --- a/src/cljs/auto_ap/views/pages/admin/vendors/common.cljs +++ /dev/null @@ -1,17 +0,0 @@ -(ns auto-ap.views.pages.admin.vendors.common) - -(def default-read [:id :name :hidden :terms [:default-account [:name :id :location]] - [:account-overrides [[:client [:id :name]] :id [:account [:id :numeric-code :name]]]] - [:automatically-paid-when-due [:id :name]] - [:terms-overrides [[:client [:id :name]] :id :terms]] - [:schedule-payment-dom [[:client [:id :name]] :id :dom]] - [:usage [:client-id :count]] - [:primary-contact [:name :phone :email :id]] - [:plaid-merchant [:name :id]] - [:secondary-contact [:id :name :phone :email]] - :print-as :invoice-reminder-schedule :code - :legal-entity-name - :legal-entity-first-name :legal-entity-middle-name :legal-entity-last-name - :legal-entity-tin :legal-entity-tin-type - :legal-entity-1099-type - [:address [:id :street1 :street2 :city :state :zip]]]) diff --git a/src/cljs/auto_ap/views/pages/admin/vendors/merge_dialog.cljs b/src/cljs/auto_ap/views/pages/admin/vendors/merge_dialog.cljs deleted file mode 100644 index 1328c22d..00000000 --- a/src/cljs/auto_ap/views/pages/admin/vendors/merge_dialog.cljs +++ /dev/null @@ -1,80 +0,0 @@ -(ns auto-ap.views.pages.admin.vendors.merge-dialog - (:require - [auto-ap.forms :as forms] - [auto-ap.forms.builder :as form-builder] - [auto-ap.schema :as schema] - [auto-ap.status :as status] - [auto-ap.subs :as subs] - [auto-ap.views.components :as com] - [auto-ap.views.components.modal :as modal] - [auto-ap.views.utils :refer [dispatch-event]] - [malli.core :as m] - [re-frame.core :as re-frame])) - -(def merge-schema - (m/schema [:map - [:from schema/reference] - [:to schema/reference]])) - -(defn form [] - [form-builder/builder {:submit-event [::try-save] - :id ::form - :schema merge-schema} - [form-builder/field-v2 {:field :from} - "Form Vendor (will be deleted)" - [com/search-backed-typeahead {:search-query (fn [i] - [:search_vendor - {:query i} - [:name :id]]) - :auto-focus true}]] - - - [form-builder/field-v2 {:field :to} - "To Vendor" - [com/search-backed-typeahead {:search-query (fn [i] - [:search_vendor - {:query i} - [:name :id]])}]] - [form-builder/hidden-submit-button]]) - -(re-frame/reg-event-fx - ::show - (fn [{:keys [db]} _] - {:dispatch [::modal/modal-requested {:title "Merge Vendors" - :body [form] - :confirm {:value "Merge" - :status-from [::status/single ::form] - :class "is-primary" - :on-click (dispatch-event [::try-save]) - :close-event [::status/completed ::form]}}] - :db (forms/start-form db ::form {})})) - -(re-frame/reg-event-fx - ::complete - (fn [{:keys [db]} _] - {:db (forms/stop-form db ::form) - :dispatch [::modal/modal-closed ]})) - -(re-frame/reg-event-fx - ::save - [(forms/in-form ::form)] - (fn [{{{:keys [from to]} :data} :db} _] - (let [user @(re-frame/subscribe [::subs/token])] - {:graphql - {:token user - :owns-state {:single ::form} - :query-obj {:venia/operation {:operation/type :mutation - :operation/name "MergeVendors"} - :venia/queries [{:query/data [:merge-vendors - {:from (:id from) :to (:id to)} []]}]} - :on-success [::complete]}}))) - - -(re-frame/reg-event-fx - ::try-save - [(forms/in-form ::form)] - (fn [{:keys [db]}] - (if (not (m/validate merge-schema (:data db))) - {:dispatch-n [[::status/error ::form [{:message "Please correct any errors and try again"}]] - [::forms/attempted-submit ::form]]} - {:dispatch [::save]}))) diff --git a/src/cljs/auto_ap/views/pages/admin/vendors/side_bar.cljs b/src/cljs/auto_ap/views/pages/admin/vendors/side_bar.cljs deleted file mode 100644 index 39821a9d..00000000 --- a/src/cljs/auto_ap/views/pages/admin/vendors/side_bar.cljs +++ /dev/null @@ -1,16 +0,0 @@ -(ns auto-ap.views.pages.admin.vendors.side-bar - (:require - [auto-ap.views.pages.data-page :as data-page] - [auto-ap.views.utils :refer [dispatch-value-change]] - [re-frame.core :as re-frame])) - -(defn vendor-side-bar [{:keys [data-page]}] - [:div - [:p.menu-label "Name"] - [:div - [:div.field - [:div.control [:input.input {:placeholder "HOME DEPOT" - :value @(re-frame/subscribe [::data-page/filter data-page :name-like]) - :on-change (dispatch-value-change [::data-page/filter-changed data-page :name-like])} ]]]]]) - - diff --git a/src/cljs/auto_ap/views/pages/admin/vendors/table.cljs b/src/cljs/auto_ap/views/pages/admin/vendors/table.cljs deleted file mode 100644 index 528ac3e9..00000000 --- a/src/cljs/auto_ap/views/pages/admin/vendors/table.cljs +++ /dev/null @@ -1,36 +0,0 @@ -(ns auto-ap.views.pages.admin.vendors.table - (:require - [auto-ap.views.components.buttons :as buttons] - [auto-ap.views.components.grid :as grid] - [auto-ap.views.components.vendor-dialog :as vendor-dialog] - [auto-ap.views.pages.data-page :as data-page] - [auto-ap.views.utils :refer [action-cell-width]] - [re-frame.core :as re-frame])) - -(defn vendors-table [{:keys [data-page]}] - (let [{:keys [data]} @(re-frame/subscribe [::data-page/page data-page])] - [grid/grid {:data-page data-page - :column-count 4} - [grid/controls data] - [grid/table {:fullwidth true} - [grid/header - [grid/row {} - [grid/header-cell {} "Name"] - [grid/header-cell {} "Email"] - [grid/header-cell {} "Default Account"] - [grid/header-cell {:style {:width (action-cell-width 1)}}]]] - [grid/body - (for [v (:data data)] - ^{:key (str (:id v))} - [grid/row {:class (:class v) :id (:id v)} - [grid/cell {} (:name v) - (let [total-usage (reduce + 0 (map :count (:usage v)))] - (if (> total-usage 0) - [:div.mx-2.tag.is-info.is-light total-usage " usages, " (count (:usage v)) " clients"] - [:div.mx-2.tag.is-warning.is-light "Unused"]))] - [grid/cell {} (:email (:primary-contact v))] - [grid/cell {} (-> v :default-account :name)] - [grid/cell {} - [buttons/fa-icon {:event [::vendor-dialog/started v] - :icon "fa-pencil"}]]])]] - [grid/bottom-paginator data]])) diff --git a/tailwind.config.js b/tailwind.config.js index dc5cc907..2055a69a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,4 +1,6 @@ /** @type {import('tailwindcss').Config} */ + +const plugin = require('tailwindcss/plugin'); module.exports = { darkMode: "class", content: ["./src/**/*.{cljs,clj,cljc}", @@ -103,6 +105,12 @@ module.exports = { } } , plugins: [ - require('flowbite/plugin') + require('flowbite/plugin'), + plugin(function ({ addVariant }) { + addVariant('htmx-settling', ['&.htmx-settling', '.htmx-settling &']) + addVariant('htmx-request', ['&.htmx-request', '.htmx-request &']) + addVariant('htmx-swapping', ['&.htmx-swapping', '.htmx-swapping &']) + addVariant('htmx-added', ['&.htmx-added', '.htmx-added &']) + }), ] } diff --git a/test/clj/auto_ap/integration/graphql/accounts.clj b/test/clj/auto_ap/integration/graphql/accounts.clj index 09330b4a..0fedb0dd 100644 --- a/test/clj/auto_ap/integration/graphql/accounts.clj +++ b/test/clj/auto_ap/integration/graphql/accounts.clj @@ -162,7 +162,7 @@ :type :default_allowance))))))) -(deftest upsert-account +#_(deftest upsert-account (testing "should create a new account" (let [result (sut/upsert-account {:id (admin-token)} {:account {:client_overrides [] :numeric_code 123 diff --git a/test/clj/auto_ap/integration/graphql/clients.clj b/test/clj/auto_ap/integration/graphql/clients.clj deleted file mode 100644 index 37861aab..00000000 --- a/test/clj/auto_ap/integration/graphql/clients.clj +++ /dev/null @@ -1,91 +0,0 @@ -(ns auto-ap.integration.graphql.clients - (:require - [auto-ap.time-reader] - [auto-ap.datomic :refer [conn pull-attr]] - [auto-ap.graphql.clients :as sut] - [auto-ap.integration.util :refer [wrap-setup user-token admin-token]] - [datomic.api :as dc] - [clojure.test :as t :refer [deftest is testing use-fixtures]])) - -(use-fixtures :each wrap-setup) - - -(deftest upsert-client - (testing "Should create a new client" - (let [create-client-request {:code "TEST" - :name "Test Co" - :matches ["Test Company"] - :email "hello@hi.com" - :locked_until #clj-time/date-time "2022-01-01" - :locations ["DT"] - :week_a_debits 1000.0 - :week_a_credits 2000.0 - :week_b_debits 3000.0 - :week_b_credits 9000.0 - :location_matches [{:location "DT" - :match "DOWNTOWN"}] - :address {:street1 "hi street" - :street2 "downtown" - :city "seattle" - :state "wa" - :zip "1238"} - :feature_flags ["new-square"] - :bank_accounts [{:code "TEST-1" - :bank_name "Bank of America" - :bank_code "BANKCODE" - :start_date #clj-time/date-time "2022-01-01" - :routing "1235" - :include_in_reports true - - :name "Bank of Am Check" - :visible true - :number "1000" - :check_number 1001 - :numeric_code 12001 - :sort_order 1 - :locations ["DT"] - :use_date_instead_of_post_date? false - :type :cash}] - - } - create-result (sut/edit-client {:id (admin-token)} {:edit_client create-client-request} nil)] - (is (some? (-> create-result :id))) - (is (some? (-> create-result :bank_accounts first :id))) - (is (= (set (keys create-client-request)) (disj (set (keys create-result)) - :square_integration_status :yodlee_provider_accounts :plaid_items :id))) - - (testing "Should be able to retrieve created client" - (let [created-client (sut/get-admin-client {:id (admin-token)} {:id (:id create-result)} nil)] - (is (some? (-> created-client :id))) - (is (some? (-> created-client :bank_accounts first :id))) - (is (= (set (keys create-client-request)) (disj (set (keys created-client)) - :square_integration_status :yodlee_provider_accounts :plaid_items :id))))) - - (testing "Should edit an existing client" - (let [edit-result (sut/edit-client {:id (admin-token)} {:edit_client {:id (:id create-result) - :name "New Company Name" - :code "TEST"}} nil)] - (is (some? (:id edit-result))) - (is (= "New Company Name" (:name edit-result))))) - - (testing "Should support removing collections" - (let [edit-result (sut/edit-client {:id (admin-token)} {:edit_client {:id (:id create-result) - :matches [] - :location_matches [] - :feature_flags []}} - nil)] - (is (some? (:id edit-result))) - (is (seq (:location_matches create-result))) - (is (not (seq (:location_matches edit-result)))) - - (is (seq (:matches create-result))) - (is (not (seq (:matches edit-result)))) - - (is (seq (:feature_flags create-result))) - (is (not (seq (:feature_flags edit-result)))) - )) - )) - - (testing "Only admins can create clients" - (is (thrown? Exception (sut/edit-client {:id (user-token)} {:edit_client {:code "INVALID"}} nil))))) -