diff --git a/data/solr/data/clients/conf/schema.xml b/data/solr/data/clients/conf/schema.xml index 72f2331e..0303c54d 100755 --- a/data/solr/data/clients/conf/schema.xml +++ b/data/solr/data/clients/conf/schema.xml @@ -160,6 +160,7 @@ + diff --git a/project.clj b/project.clj index fa0c0049..898e3939 100644 --- a/project.clj +++ b/project.clj @@ -1,5 +1,5 @@ (defproject auto-ap "0.1.0-SNAPSHOT" - :description "FIXME: write description" + :description "FIXME: write description" :url "http://example.com/FIXME" :min-lein-version "2.0.0" :dependencies [[com.google.guava/guava "31.1-jre"] @@ -109,7 +109,7 @@ [lein-ancient "0.6.15"]] :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] #_#_:ring {:handler auto-ap.handler/app} - :source-paths ["src/clj" "src/cljc" "src/cljs" "iol_ion/src" ] + :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"] @@ -117,10 +117,8 @@ "fig:min" ["run" "-m" "figwheel.main" "-O" "whitespace" "-bo" "min"]} - :profiles { - :dev - { - :resource-paths ["resources" "target"] + :profiles {:dev + {:resource-paths ["resources" "target"] :dependencies [#_[binaryage/devteols "1.0.2"] [postgresql/postgresql "9.3-1102.jdbc41"] [org.clojure/tools.namespace "1.4.5"] @@ -144,13 +142,13 @@ [com.bhauman/rebel-readline-cljs "0.1.4" :exclusions [org.clojure/clojurescript]] [javax.servlet/servlet-api "2.5"]] :plugins [[lein-pdo "0.1.1"]] - :jvm-opts ["-Dconfig=config/dev.edn" "-Xms4G" "-Xmx20G" "-XX:-OmitStackTraceInFastThrow" ]} + :jvm-opts ["-Dconfig=config/dev.edn" "-Xms4G" "-Xmx20G" "-XX:-OmitStackTraceInFastThrow"]} :uberjar - { - :java-cmd "/usr/lib/jvm/java-11-openjdk/bin/java" - :prep-tasks ["fig:min" ] - :aot [auto-ap.server auto-ap.time clj-time.core clj-time.coerce clj-time.format clojure.tools.logging.impl ] + {:java-cmd "/usr/lib/jvm/java-11-openjdk/bin/java" + :main auto-ap.main + :prep-tasks ["fig:min"] + :aot [auto-ap.main auto-ap.time clj-time.core clj-time.coerce clj-time.format clojure.tools.logging.impl] :dependencies [[com.bhauman/figwheel-main "0.2.18" :exclusions [org.clojure/clojurescript ring ring/ring-core @@ -169,7 +167,7 @@ :provided {:dependencies [[org.clojure/clojurescript "1.11.4" :exclusions [com.google.code.findbugs/jsr305 com.fasterxml.jackson.core/jackson-core]] - [reagent "1.0.0" :exclusions [cljsjs/react cljsjs/react-dom cljsjs/react-dom-server] ] + [reagent "1.0.0" :exclusions [cljsjs/react cljsjs/react-dom cljsjs/react-dom-server]] [re-frame "1.1.2" :exclusions [reagent @@ -177,11 +175,8 @@ [re-frame-utils "0.1.0"] [com.andrewmcveigh/cljs-time "0.5.2"] [cljs-http "0.1.46"] - [kibu/pushy "0.3.8"]]} + [kibu/pushy "0.3.8"]]}} - } - - :main auto-ap.server :uberjar-name "auto-ap.jar" :test-paths ["test/clj"] :test-selectors {:integration (fn [m] @@ -190,10 +185,10 @@ (clojure.string/includes? (str (:name m)) "integration"))) :functional (fn [m] - (or (clojure.string/includes? (str (:ns m)) - "functional") - (clojure.string/includes? (str (:name m)) - "functional")))} + (or (clojure.string/includes? (str (:ns m)) + "functional") + (clojure.string/includes? (str (:name m)) + "functional")))} - :prep-tasks [ "compile"]) + :prep-tasks ["compile"]) diff --git a/resources/schema.edn b/resources/schema.edn index b0aa4e06..700ef49e 100644 --- a/resources/schema.edn +++ b/resources/schema.edn @@ -337,6 +337,10 @@ :db/cardinality #:db{:ident :db.cardinality/many}, :db/doc "A client's locations", :db/ident :client/locations} + {:db/valueType #:db{:ident :db.type/string}, + :db/cardinality #:db{:ident :db.cardinality/many}, + :db/doc "The groups that this client belongs in, for fast lookup", + :db/ident :client/groups} {:db/valueType #:db{:ident :db.type/ref}, :db/isComponent true, :db/cardinality #:db{:ident :db.cardinality/one}, @@ -1800,47 +1804,47 @@ {:db/ident :invoice/client+date :db/valueType :db.type/tuple - :db/tupleAttrs [ :invoice/client :invoice/date] + :db/tupleAttrs [:invoice/client :invoice/date] :db/cardinality :db.cardinality/one :db/index true} {:db/ident :transaction/client+date :db/valueType :db.type/tuple - :db/tupleAttrs [ :transaction/client :transaction/date] + :db/tupleAttrs [:transaction/client :transaction/date] :db/cardinality :db.cardinality/one :db/index true} {:db/ident :journal-entry/client+date :db/valueType :db.type/tuple - :db/tupleAttrs [ :journal-entry/client :journal-entry/date] + :db/tupleAttrs [:journal-entry/client :journal-entry/date] :db/cardinality :db.cardinality/one :db/index true} {:db/ident :payment/client+date :db/valueType :db.type/tuple - :db/tupleAttrs [ :payment/client :payment/date] + :db/tupleAttrs [:payment/client :payment/date] :db/cardinality :db.cardinality/one :db/index true} - + {:db/ident :charge/client+date :db/valueType :db.type/tuple - :db/tupleAttrs [ :charge/client :charge/date] + :db/tupleAttrs [:charge/client :charge/date] :db/cardinality :db.cardinality/one :db/index true} {:db/ident :sales-refund/client+date :db/valueType :db.type/tuple - :db/tupleAttrs [ :sales-refund/client :sales-refund/date] + :db/tupleAttrs [:sales-refund/client :sales-refund/date] :db/cardinality :db.cardinality/one :db/index true} {:db/ident :cash-drawer-shift/client+date :db/valueType :db.type/tuple - :db/tupleAttrs [ :cash-drawer-shift/client :cash-drawer-shift/date] + :db/tupleAttrs [:cash-drawer-shift/client :cash-drawer-shift/date] :db/cardinality :db.cardinality/one :db/index true} {:db/ident :expected-deposit/client+date :db/valueType :db.type/tuple - :db/tupleAttrs [ :expected-deposit/client :expected-deposit/date] + :db/tupleAttrs [:expected-deposit/client :expected-deposit/date] :db/cardinality :db.cardinality/one :db/index true}] diff --git a/src/clj/auto_ap/datomic/clients.clj b/src/clj/auto_ap/datomic/clients.clj index 7ab7681d..a408e7ff 100644 --- a/src/clj/auto_ap/datomic/clients.clj +++ b/src/clj/auto_ap/datomic/clients.clj @@ -79,6 +79,7 @@ (defn get-minimal [] (->> (dc/q '[:find (pull ?e [:client/name :client/code :client/locations :db/id + :client/groups {:client/bank-accounts [{:bank-account/type [:db/ident]} :bank-account/name :bank-account/sort-order diff --git a/src/clj/auto_ap/graphql/clients.clj b/src/clj/auto_ap/graphql/clients.clj index 4d004881..bc4591d5 100644 --- a/src/clj/auto_ap/graphql/clients.clj +++ b/src/clj/auto_ap/graphql/clients.clj @@ -102,6 +102,7 @@ :locked_until {:type :iso_date} :code {:type 'String} :feature_flags {:type '(list String)} + :groups {:type '(list String)} :square_auth_token {:type 'String} :signature_file {:type 'String} :square_integration_status {:type :integration_status} diff --git a/src/clj/auto_ap/handler.clj b/src/clj/auto_ap/handler.clj index ffc61e4f..7e370ac8 100644 --- a/src/clj/auto_ap/handler.clj +++ b/src/clj/auto_ap/handler.clj @@ -72,7 +72,7 @@ (defn render-index [_] (response/resource-response "index.html" {:root "public"})) -(def match->handler-lookup +(def match->handler-lookup (-> {:not-found not-found} (merge ssr/key->handler) (merge graphql/match->handler) @@ -83,13 +83,13 @@ (merge auth/match->handler) (merge invoices/match->handler) (merge exports/match->handler) - (merge - (into {} - (map + (merge + (into {} + (map - (fn [k] - [k render-index]) - client-routes/all-matches))))) + (fn [k] + [k render-index]) + client-routes/all-matches))))) (def match->handler (fn [route] @@ -102,7 +102,7 @@ match->handler)) (defn wrap-guess-route [handler] - (fn [{:keys [uri request-method] :as request} ] + (fn [{:keys [uri request-method] :as request}] (let [matched-route (:handler (bidi.bidi/match-route all-routes uri @@ -136,21 +136,21 @@ (not= "/api/graphql" (:uri request)) (assoc :query-params (:query-params request))) (mu/trace ::http-request-trace - {:pairs [] - :capture (fn [r] {:status (:status r)})} - (when-not (str/includes? (:uri request) "health-check") - (alog/info ::http-request-starting)) - (try - (let [response (handler request)] - response) - (catch Exception e - (alog/error ::request-error - :status 500 - :exception e) - (throw e))))))) + {:pairs [] + :capture (fn [r] {:status (:status r)})} + (when-not (str/includes? (:uri request) "health-check") + (alog/info ::http-request-starting)) + (try + (let [response (handler request)] + response) + (catch Exception e + (alog/error ::request-error + :status 500 + :exception e) + (throw e))))))) (defn wrap-idle-session-timeout - [handler ] + [handler] (fn [request] (let [session (:session request {}) end-time (coerce/to-date-time (::idle-timeout session))] @@ -170,9 +170,9 @@ (assoc response :session (assoc session ::idle-timeout (coerce/to-date end-time))))))))))) (defn wrap-hx-current-url-params - [handler ] + [handler] (fn [request] - (let [query-params (some-> (get-in request [:headers "hx-current-url"]) (url/url ) :query) + (let [query-params (some-> (get-in request [:headers "hx-current-url"]) (url/url) :query) request (assoc request :hx-query-params query-params)] (handler request)))) @@ -183,30 +183,40 @@ identity (or (-> request :identity) (-> request :session :identity)) ideal-ids (set (cond - (or (= :all x-clients) - (nil? x-clients)) - (->> (dc/q '[:find ?c - :where [?c :client/code]] - (dc/db conn)) - (map first)) + (or (= :all x-clients) + (nil? x-clients)) + (->> (dc/q '[:find ?c + :where [?c :client/code]] + (dc/db conn)) + (map first)) - (= :mine x-clients) - (map :db/id (:user/clients identity)) + (= :mine x-clients) + (map :db/id (:user/clients identity)) + + (= :group (first x-clients)) + (->> + (dc/q '[:find ?c + :in $ ?g + :where [?c :client/groups ?g]] + (dc/db conn) + (str/upper-case (or (second x-clients) "INVALID"))) + (map first) + set) + + (seq x-clients) + (->> x-clients + (map (fn [c] + (if (string? c) + (try + (Long/parseLong c) + (catch Exception e + nil)) + c))) + (filter #(not (nil? %))) + set))) - (seq x-clients) - (->> x-clients - (map (fn [c] - (if (string? c) - (try - (Long/parseLong c) - (catch Exception e - nil)) - c))) - (filter #(not (nil? %))) - set))) - limited-clients (some->> (limited-clients identity) - (map :db/id ) + (map :db/id) set) client-ids (if (= "admin" (:user/role identity)) ideal-ids @@ -215,8 +225,8 @@ clients (some->> client-ids seq (pull-many (dc/db conn) - d-clients/full-read))] - + d-clients/full-read))] + (mu/with-context {:clients (take 10 (map :client/code clients))} (handler (assoc request :clients clients @@ -229,8 +239,10 @@ (let [x-clients (edn/read-string (get headers "x-clients")) x-clients (try (if-let [client-id (and x-clients (sequential? x-clients) + (first x-clients) + (not= :group (first x-clients)) (first x-clients))] - (do + (do (assert-can-see-client identity (cond-> client-id (string? client-id) (Long/parseLong))) [(if (string? client-id) @@ -261,16 +273,16 @@ (auth/gunzip gz-clients)) (catch Exception e (alog/error :cant-gunzip-clients - :error e) + :error e) request)) request)] (handler request)))) #_(defn wrap-pprint-session - [handler] - (fn [request] - (clojure.pprint/pprint (:session request)) - (handler request))) + [handler] + (fn [request] + (clojure.pprint/pprint (:session request)) + (handler request))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defonce app @@ -289,13 +301,11 @@ #_(wrap-pprint-session) (wrap-idle-session-timeout) (wrap-session {:store (cookie-store - {:key - (byte-array - [42, 52, -31, 105, -126, -33, -118, -69, -82, -59, -15, -69, -38, 103, -102, -1])} )}) + {:key + (byte-array + [42, 52, -31, 105, -126, -33, -118, -69, -82, -59, -15, -69, -38, 103, -102, -1])})}) #_(wrap-reload) (wrap-params) (mp/wrap-multipart-params) - (wrap-edn-params) - )) - + (wrap-edn-params))) diff --git a/src/clj/auto_ap/main.clj b/src/clj/auto_ap/main.clj new file mode 100644 index 00000000..ca9d506b --- /dev/null +++ b/src/clj/auto_ap/main.clj @@ -0,0 +1,107 @@ +(ns auto-ap.main + (:gen-class) + (:require + [auto-ap.handler :refer [app]] + [auto-ap.server :as server] + [auto-ap.jobs.restore-from-backup :as job-restore-from-backup] + [auto-ap.jobs.bulk-journal-import :as job-bulk-journal-import] + [auto-ap.jobs.close-auto-invoices :as job-close-auto-invoices] + [auto-ap.jobs.current-balance-cache :as job-current-balance-cache] + [auto-ap.jobs.ezcater-upsert :as job-ezcater-upsert] + [auto-ap.jobs.import-uploaded-invoices :as job-import-uploaded-invoices] + [auto-ap.jobs.intuit :as job-intuit] + [auto-ap.jobs.ntg :as job-ntg] + #_[auto-ap.backup :as backup] + [auto-ap.jobs.ledger-reconcile :as job-reconcile-ledger] + [auto-ap.jobs.load-historical-sales :as job-load-historical-sales] + [auto-ap.jobs.plaid :as job-plaid] + [auto-ap.jobs.register-invoice-import :as job-register-invoice-import] + [auto-ap.jobs.square :as job-square] + [auto-ap.jobs.sysco :as job-sysco] + [auto-ap.jobs.vendor-usages :as job-vendor-usages] + [auto-ap.jobs.yodlee2 :as job-yodlee2] + [auto-ap.logging :as alog] + [com.unbounce.dogstatsd.core :as statsd] + [config.core :refer [env]] + [mount.core :as mount] + [nrepl.server :refer [start-server]] + [ring.adapter.jetty :refer [run-jetty]] + [yang.scheduler :as scheduler] + [auto-ap.jobs.insight-outcome-recommendation :as insight-outcome-recommendation]) + (:import + (org.eclipse.jetty.server.handler StatisticsHandler) + (org.eclipse.jetty.server.handler.gzip GzipHandler))) + +(defn add-shutdown-hook! [^Runnable f] + (.addShutdownHook (Runtime/getRuntime) + (Thread. f))) + +(defn shutdown-mount [] + (mount/stop)) + +(defn -main [& _] + (let [job (System/getenv "INTEGREAT_JOB")] + (println "JOB is" job) + (cond (= job "square-import-job") + (job-square/-main) + + (= job "reconcile-ledger") + (job-reconcile-ledger/-main) + + (= job "current-balance-cache") + (job-current-balance-cache/-main) + + (= job "yodlee2") + (job-yodlee2/-main) + + (= job "yodlee2-accounts") + (job-yodlee2/accounts-only) + + (= job "plaid") + (job-plaid/-main) + + (= job "intuit") + (job-intuit/-main) + + (= job "vendor-usages") + (job-vendor-usages/-main) + + (= job "import-uploaded-invoices") + (job-import-uploaded-invoices/-main) + + (= job "sysco") + (job-sysco/-main) + + (= job "close-auto-invoices") + (job-close-auto-invoices/-main) + + (= job "ezcater-upsert") + (job-ezcater-upsert/-main) + + (= job "register-invoice-import") + (job-register-invoice-import/-main) + + (= job "load-historical-sales") + (job-load-historical-sales/-main) + + (= job "bulk-journal-import") + (job-bulk-journal-import/-main) + + (= job "restore-from-backup") + (job-restore-from-backup/-main) + + (= job "insight-outcome-recommendation") + (insight-outcome-recommendation/-main) + + ;; (= job "export-backup") + ;; (backup/-main) + + (= job "ntg") + (job-ntg/-main) + + :else + (do + (add-shutdown-hook! shutdown-mount) + (start-server :port 9000 :bind "0.0.0.0" #_#_:handler (cider-nrepl-handler)) + (mount/start) + #_(alter-var-root #'nrepl.middleware.print/*print-fn* (constantly clojure.pprint/pprint)))))) \ No newline at end of file diff --git a/src/clj/auto_ap/server.clj b/src/clj/auto_ap/server.clj index 0f53c817..23608219 100644 --- a/src/clj/auto_ap/server.clj +++ b/src/clj/auto_ap/server.clj @@ -1,40 +1,13 @@ (ns auto-ap.server - (:gen-class) - (:require - [auto-ap.handler :refer [app]] - [auto-ap.jobs.restore-from-backup :as job-restore-from-backup] - [auto-ap.jobs.bulk-journal-import :as job-bulk-journal-import] - [auto-ap.jobs.close-auto-invoices :as job-close-auto-invoices] - [auto-ap.jobs.current-balance-cache :as job-current-balance-cache] - [auto-ap.jobs.ezcater-upsert :as job-ezcater-upsert] - [auto-ap.jobs.import-uploaded-invoices :as job-import-uploaded-invoices] - [auto-ap.jobs.intuit :as job-intuit] - [auto-ap.jobs.ntg :as job-ntg] - #_[auto-ap.backup :as backup] - [auto-ap.jobs.ledger-reconcile :as job-reconcile-ledger] - [auto-ap.jobs.load-historical-sales :as job-load-historical-sales] - [auto-ap.jobs.plaid :as job-plaid] - [auto-ap.jobs.register-invoice-import :as job-register-invoice-import] - [auto-ap.jobs.square :as job-square] - [auto-ap.jobs.sysco :as job-sysco] - [auto-ap.jobs.vendor-usages :as job-vendor-usages] - [auto-ap.jobs.yodlee2 :as job-yodlee2] - [auto-ap.logging :as alog] - [com.unbounce.dogstatsd.core :as statsd] - [config.core :refer [env]] - [mount.core :as mount] - [nrepl.server :refer [start-server]] - [ring.adapter.jetty :refer [run-jetty]] - [yang.scheduler :as scheduler] - [auto-ap.jobs.insight-outcome-recommendation :as insight-outcome-recommendation]) - (:import - (org.eclipse.jetty.server.handler StatisticsHandler) - (org.eclipse.jetty.server.handler.gzip GzipHandler))) - -(defn add-shutdown-hook! [^Runnable f] - (.addShutdownHook (Runtime/getRuntime) - (Thread. f))) - + (:require [auto-ap.handler :refer [app]] + [auto-ap.logging :as alog] + [com.unbounce.dogstatsd.core :as statsd] + [config.core :refer [env]] + [mount.core :as mount] + [ring.adapter.jetty :refer [run-jetty]] + [yang.scheduler :as scheduler]) + (:import (org.eclipse.jetty.server.handler StatisticsHandler) + (org.eclipse.jetty.server.handler.gzip GzipHandler))) (defn gzip-handler [] (doto (GzipHandler.) @@ -88,73 +61,3 @@ :start (scheduler/every (* 1000 10) collect-jetty-stats) :stop (scheduler/stop jetty-stats)) -(defn shutdown-mount [] - (mount/stop)) - -(defn -main [& _] - (let [job (System/getenv "INTEGREAT_JOB")] - (println "JOB is" job) - (cond (= job "square-import-job") - (job-square/-main) - - (= job "reconcile-ledger") - (job-reconcile-ledger/-main) - - (= job "current-balance-cache") - (job-current-balance-cache/-main) - - (= job "yodlee2") - (job-yodlee2/-main) - - (= job "yodlee2-accounts") - (job-yodlee2/accounts-only) - - (= job "plaid") - (job-plaid/-main) - - (= job "intuit") - (job-intuit/-main) - - (= job "vendor-usages") - (job-vendor-usages/-main) - - (= job "import-uploaded-invoices") - (job-import-uploaded-invoices/-main) - - (= job "sysco") - (job-sysco/-main) - - (= job "close-auto-invoices") - (job-close-auto-invoices/-main) - - (= job "ezcater-upsert") - (job-ezcater-upsert/-main) - - (= job "register-invoice-import") - (job-register-invoice-import/-main) - - (= job "load-historical-sales") - (job-load-historical-sales/-main) - - (= job "bulk-journal-import") - (job-bulk-journal-import/-main) - - (= job "restore-from-backup") - (job-restore-from-backup/-main) - - (= job "insight-outcome-recommendation") - (insight-outcome-recommendation/-main) - - ;; (= job "export-backup") - ;; (backup/-main) - - (= job "ntg") - (job-ntg/-main) - - :else - (do - (add-shutdown-hook! shutdown-mount) - (start-server :port 9000 :bind "0.0.0.0" #_#_:handler (cider-nrepl-handler)) - (mount/start) - #_(alter-var-root #'nrepl.middleware.print/*print-fn* (constantly clojure.pprint/pprint)))))) - diff --git a/src/clj/auto_ap/ssr/admin/clients.clj b/src/clj/auto_ap/ssr/admin/clients.clj index c466330a..e9b9d088 100644 --- a/src/clj/auto_ap/ssr/admin/clients.clj +++ b/src/clj/auto_ap/ssr/admin/clients.clj @@ -74,6 +74,7 @@ :client/name :client/code :client/locations + :client/groups :client/matches :client/week-a-credits :client/week-b-credits @@ -277,6 +278,9 @@ [:client/locations [:vector {:decode/arbitrary (fn [m] (if (map? m) (vals m) m))} :string]] + [:client/groups [: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] @@ -394,6 +398,19 @@ (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x)))) +(defn group-row [_] + (com/data-grid-row + {:x-ref "p" + :x-data (hx/json {})} + (com/data-grid-cell + {} + (com/validated-field {} + (com/text-input {:name (fc/field-name) + :value (fc/field-value) + :class "w-24"}))) + (com/data-grid-cell {:class "align-top"} + (com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x)))) + (defn feature-flag-row [_] (com/data-grid-row {:x-ref "p" @@ -1318,7 +1335,7 @@ (edit-path [_ _] []) (step-schema [_] - (mut/select-keys (mm/form-schema linear-wizard) #{:client/feature-flags})) + (mut/select-keys (mm/form-schema linear-wizard) #{:client/feature-flags :client/groups})) (render-step [this _] (mm/default-render-step @@ -1335,7 +1352,19 @@ (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"))))) + "New flag")))) + (fc/with-field :client/groups + (com/validated-field + {:errors (fc/field-errors) + :label "Groups"} + (com/data-grid {:headers [(com/data-grid-header {} "Group") + (com/data-grid-header {:class "w-16"})]} + (fc/cursor-map #(group-row %)) + (com/data-grid-new-row {:colspan 2 + :hx-get (bidi/path-for ssr-routes/only-routes ::route/new-group) + :index (count (fc/field-value))} + + "New group"))))) :footer (mm/default-step-footer linear-wizard this :validation-route ::route/navigate) :validation-route ::route/navigate))) @@ -1387,14 +1416,17 @@ (first step-key))))) (form-schema [_] form-schema-2) (submit [_ {:keys [multi-form-state request-method identity] :as request}] - (let [snapshot (mc/decode + (let [ + snapshot (mc/decode form-schema-2 (:snapshot multi-form-state) mt/strip-extra-keys-transformer) entity (cond-> snapshot (= :post request-method) (assoc :db/id "new") (= :put request-method) (dissoc :client/code) + (:client/locked-until snapshot) (update :client/locked-until clj-time.coerce/to-date) + (seq (:client/groups snapshot)) (update :client/groups #(mapv str/upper-case %)) (seq (:client/bank-accounts snapshot)) (update :client/bank-accounts (fn [bank-accounts] (mapv @@ -1418,6 +1450,7 @@ "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 @@ -1669,6 +1702,9 @@ ::route/new-match (add-new-primitive-handler [:step-params :client/matches] "" match-row) + ::route/new-group (add-new-primitive-handler [:step-params :client/groups] + "" + group-row) ::route/new-location-match (add-new-entity-handler [:step-params :client/location-matches] (fn [cursor _] (location-match-row cursor))) ::route/new-email-contact (add-new-entity-handler [:step-params :client/emails] diff --git a/src/clj/auto_ap/ssr/company_dropdown.clj b/src/clj/auto_ap/ssr/company_dropdown.clj index 976a59d2..1d527d08 100644 --- a/src/clj/auto_ap/ssr/company_dropdown.clj +++ b/src/clj/auto_ap/ssr/company_dropdown.clj @@ -1,76 +1,92 @@ (ns auto-ap.ssr.company-dropdown - (:require - [auto-ap.datomic :refer [conn pull-attr pull-many]] - [auto-ap.graphql.utils :refer [assert-can-see-client cleanse-query]] - [auto-ap.solr :as solr] - [auto-ap.ssr-routes :as ssr-routes] - [auto-ap.ssr.svg :as svg] - [auto-ap.ssr.utils :refer [html-response]] - [bidi.bidi :as bidi] - [datomic.api :as dc] - [hiccup2.core :as hiccup] - [iol-ion.query :refer [can-see-client?]])) + (:require [auto-ap.datomic :refer [conn pull-attr pull-many]] + [auto-ap.graphql.utils :refer [cleanse-query]] + [auto-ap.solr :as solr] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.hx :as hx] + [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.utils :refer [html-response]] + [bidi.bidi :as bidi] + [clojure.string :as str] + [datomic.api :as dc] + [hiccup2.core :as hiccup] + [iol-ion.query :refer [can-see-client?]])) (defn dropdown-search-results* [{:keys [options]}] - [:ul - (for [{:keys [id name]} options] - [:li + [:ul + (for [{:keys [id name group]} options] + [:li [:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"} - [:a {:href "#" :class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300" - :hx-put (bidi/path-for ssr-routes/only-routes - :active-client - :request-method :put) - :hx-target "#company-dropdown" - :hx-headers (format "{\"x-clients\": \"[%d]\"}" id) - :hx-swap "outerHTML" - :hx-trigger "click"} - name]]])]) + (if group + [:a {:href "#" :class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300" + + :hx-put (bidi/path-for ssr-routes/only-routes + :active-client + :request-method :put) + :hx-target "#company-dropdown" + :hx-headers (hx/json {"x-clients" (pr-str [:group group])}) + :hx-swap "outerHTML" + :hx-trigger "click"} + name] + [:a {:href "#" :class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300" + :hx-put (bidi/path-for ssr-routes/only-routes + :active-client + :request-method :put) + :hx-target "#company-dropdown" + :hx-headers (format "{\"x-clients\": \"[%d]\"}" id) + :hx-swap "outerHTML" + :hx-trigger "click"} + name])]])]) + (defn get-clients [identity query] - (if-let [query (not-empty (cleanse-query query))] - (let [search-query (str "name:(" query ")")] + (if (str/starts-with? query "g:") + (let [search-query (str "groups:(" (subs query 2) ")")] + [{:group (subs query 2) + :name (str "All clients matching " (subs query 2))}]) + (if-let [query (not-empty (cleanse-query query))] + (let [search-query (str "name:(" query ")")] - (for [n (pull-many (dc/db conn) [:client/name :db/id] - (for [{:keys [id name]} (solr/query solr/impl "clients" {"query" search-query - "fields" "id, name"}) - :let [client-id (Long/parseLong id)] - :when (can-see-client? identity client-id)] - client-id))] - {:id (:db/id n) - :name (:client/name n)})) - [])) + (for [n (pull-many (dc/db conn) [:client/name :db/id] + (for [{:keys [id name]} (solr/query solr/impl "clients" {"query" search-query + "fields" "id, name"}) + :let [client-id (Long/parseLong id)] + :when (can-see-client? identity client-id)] + client-id))] + {:id (:db/id n) + :name (:client/name n)})) + []))) (defn dropdown-search-results [{:keys [identity] :as request}] - (html-response - (dropdown-search-results* {:options (get-clients identity (get (:query-params request) "search-text"))}))) + (html-response + (dropdown-search-results* {:options (get-clients identity (get (:query-params request) "search-text"))}))) -(defn dropdown [{:keys [client-selection client identity]}] +(defn dropdown [{:keys [client-selection client identity clients]}] [:div#company-dropdown [:script (hiccup/raw - "localStorage.setItem(\"last-client-id\", \"" (:db/id client) "\")" "\n" - "localStorage.setItem(\"last-selected-clients\", \"" client-selection "\")" - )] - [:div - [:button#company-dropdown-button { :class "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" + "localStorage.setItem(\"last-client-id\", \"" (:db/id client) "\")" "\n" + "localStorage.setItem(\"last-selected-clients\", " (pr-str (pr-str client-selection)) ")")] + [:div + [:button#company-dropdown-button {:class "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" :type "button"} (cond (= :mine client-selection) "My Companies" (= :all client-selection) "All Companies" - (and (sequential? client-selection) - (= 1 (count client-selection))) - (pull-attr (dc/db conn) :client/name (first client-selection)) + + (and client + (= 1 (count clients))) + ( :client/name client) :else - (str (count client-selection) " Companies")) + (str (count clients) " Companies")) [:div.w-4.h-4.ml-2 svg/drop-down]] - [:div#company-dropdown-list.hidden {"_" (hiccup/raw "init call initCompanyDropdown()") - } + [:div#company-dropdown-list.hidden {"_" (hiccup/raw "init call initCompanyDropdown()")} [:div {:class "z-10 bg-white rounded-lg shadow w-64 dark:bg-gray-700 slide-up duration-500 transition-all"} [:div {:class "p-3"} [:label {:for "input-group-search", :class "sr-only"} "Search"] @@ -83,27 +99,27 @@ :class "block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" :autoFocus true :tab-index -1 - :hx-trigger "keyup changed delay:500ms, search" + :hx-trigger "keyup changed delay:500ms, search" :hx-get (bidi/path-for ssr-routes/only-routes :company-dropdown-search-results) :hx-target "#company-search-results" - :hx-swap "innerHTML"} ]] + :hx-swap "innerHTML"}]] [:input#company-search-value {:type "hidden" :name "x-clients"}]] [:div.divide-y.divide-gray-100 [:div#company-search-results {:class "h-48 px-3 pb-3 overflow-y-auto text-sm text-gray-700 dark:text-gray-200"}] (when (= "admin" (:user/role identity)) - [:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"} + [:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"} - [:button {:class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300" - :hx-put (bidi/path-for ssr-routes/only-routes - :active-client - :request-method :put) - :hx-target "#company-dropdown" - :hx-headers "{\"x-clients\": \":mine\"}" - :hx-swap "outerHTML" - :hx-trigger "click"} - "Mine"]]) + [:button {:class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300" + :hx-put (bidi/path-for ssr-routes/only-routes + :active-client + :request-method :put) + :hx-target "#company-dropdown" + :hx-headers "{\"x-clients\": \":mine\"}" + :hx-swap "outerHTML" + :hx-trigger "click"} + "Mine"]]) [:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"} [:button {:class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300" @@ -114,11 +130,10 @@ :hx-headers "{\"x-clients\": \":all\"}" :hx-swap "outerHTML" :hx-trigger "click"} - "All"]]] - ]] + "All"]]]]] [:script {:lang "text/javascript"} (hiccup/raw - " + " function initCompanyDropdown() { var $dropdownTargetEl = document.getElementById('company-dropdown-list'); @@ -145,9 +160,11 @@ function initCompanyDropdown() { (defn active-client [{:keys [identity params] :as request}] (assoc - (html-response - (dropdown {:client-selection (:client-selection (:session request)) - :client (:client request) - :identity identity})) - :headers - {"hx-trigger" "clientSelected"})) + (html-response + (dropdown {:client-selection (:client-selection (:session request)) + :clients (:clients request) + :client (:client request) + :identity identity})) + :headers + {"hx-trigger" "clientSelected"})) + diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index e48b7520..e4ec2bde 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -201,8 +201,7 @@ [:li (menu-button- {:icon svg/restaurant - :href (bidi/path-for ssr-routes/only-routes ::ac-routes/page) - :target "_new"} + :href (bidi/path-for ssr-routes/only-routes ::ac-routes/page) } "Clients")] [:li (menu-button- {:icon svg/vendors diff --git a/src/clj/auto_ap/ssr/components/navbar.clj b/src/clj/auto_ap/ssr/components/navbar.clj index 3759e741..28672eb5 100644 --- a/src/clj/auto_ap/ssr/components/navbar.clj +++ b/src/clj/auto_ap/ssr/components/navbar.clj @@ -8,7 +8,7 @@ [auto-ap.ssr.svg :as svg] [bidi.bidi :as bidi])) -(defn navbar- [{:keys [client-selection client identity]}] +(defn navbar- [{:keys [client-selection client identity clients]}] [:nav {:class "fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700"} [:div {:class "px-3 py-3 lg:px-5 lg:pl-3"} [:div {:class "flex items-center justify-between"} @@ -38,6 +38,7 @@ :hx-target "#modal-holder" :hx-swap "outerHTML"} svg/search) - (cd/dropdown {:client-selection client-selection :client client :identity identity}) + (cd/dropdown {:client-selection client-selection :client client :identity identity + :clients clients}) (user-dropdown/dropdown {:identity identity})]]]]) diff --git a/src/clj/auto_ap/ssr/components/page.clj b/src/clj/auto_ap/ssr/components/page.clj index a83fed42..de908372 100644 --- a/src/clj/auto_ap/ssr/components/page.clj +++ b/src/clj/auto_ap/ssr/components/page.clj @@ -6,13 +6,14 @@ [auto-ap.ssr.svg :as svg] [auto-ap.ssr.hx :as hx])) -(defn page- [{:keys [nav page-specific client client-selection identity app-params] :or {app-params {}}} & children] +(defn page- [{:keys [nav page-specific client clients client-selection identity app-params] :or {app-params {}}} & children] [:div#app {"_" (hiccup/raw " on notification from body put event.detail.value into #notification-details then add .htmx-added to #notification-holder then remove .hidden from #notification-holder then wait 30ms then remove .htmx-added from #notification-holder on htmx:responseError put event.detail.xhr.response into #error-details then add .htmx-added to #error-holder then remove .hidden from #error-holder then wait 30ms then remove .htmx-added from #error-holder" ) :x-data (hx/json {:leftNavShow true})} (navbar- {:client-selection client-selection + :clients clients :client client :identity identity}) [:div#app-contents.flex.pt-16.overflow-hidden (assoc app-params diff --git a/src/clj/auto_ap/ssr/transaction/insights.fiddle b/src/clj/auto_ap/ssr/transaction/insights.fiddle new file mode 100644 index 00000000..e69de29b diff --git a/src/cljc/auto_ap/routes/admin/clients.cljc b/src/cljc/auto_ap/routes/admin/clients.cljc index 4d1601cb..0d1aba66 100644 --- a/src/cljc/auto_ap/routes/admin/clients.cljc +++ b/src/cljc/auto_ap/routes/admin/clients.cljc @@ -13,6 +13,7 @@ "/match/new" ::new-match "/location-match/new" ::new-location-match "/email-contact/new" ::new-email-contact + "/group/new" ::new-group "/feature-flag/new" ::new-feature-flag "/new" {:get ::new-dialog} ["/" [#"\d+" :db/id] "/sales-powerquery"] ::biweekly-sales-powerquery diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index 098051cb..d7f11681 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -71,6 +71,7 @@ "/bank-account/typeahead" :bank-account-typeahead ["/" [#"\d+" :db/id] "/bank-account"] {"/search" :bank-account-search} "/active" {:put :active-client} + "/active-group" {:put :active-client-group} "/1099" :company-1099 "/1099/table" {:get :company-1099-vendor-table} "/1099/vendor-dialog" {["/" [#"\d+" :vendor-id]] {:get :company-1099-vendor-dialog diff --git a/src/cljs/auto_ap/effects.cljs b/src/cljs/auto_ap/effects.cljs index 6b477e18..ea49046e 100644 --- a/src/cljs/auto_ap/effects.cljs +++ b/src/cljs/auto_ap/effects.cljs @@ -19,6 +19,7 @@ (defn maybe-add-x-clients [headers] (if (or (and (sequential? (:selected-clients @re-frame.db/app-db)) (every? int? (:selected-clients @re-frame.db/app-db))) (and (sequential? (:selected-clients @re-frame.db/app-db)) (every? string? (:selected-clients @re-frame.db/app-db))) + (and (sequential? (:selected-clients @re-frame.db/app-db)) (= :group (first (:selected-clients @re-frame.db/app-db)))) (keyword? (:selected-clients @re-frame.db/app-db))) (assoc headers "x-clients" (pr-str (:selected-clients @re-frame.db/app-db))) headers)) diff --git a/src/cljs/auto_ap/events.cljs b/src/cljs/auto_ap/events.cljs index 6420f31e..8a3e08ce 100644 --- a/src/cljs/auto_ap/events.cljs +++ b/src/cljs/auto_ap/events.cljs @@ -27,7 +27,7 @@ (defn client-query [] - (cond-> [:id :name :code :email :locations :feature-flags + (cond-> [:id :name :code :email :locations :feature-flags :groups [:emails [:id :email :description]] [:bank-accounts [:id :code :bank-name :name :type :visible :locations :include-in-reports :current-balance diff --git a/src/cljs/auto_ap/subs.cljs b/src/cljs/auto_ap/subs.cljs index 9d4337e8..e16f2c39 100644 --- a/src/cljs/auto_ap/subs.cljs +++ b/src/cljs/auto_ap/subs.cljs @@ -46,10 +46,10 @@ (fn [[selected-clients user clients]] (println "SELECTED" selected-clients "USER" user - "CLIENTS" clients) + "CLIENTS" (count clients)) (cond (= :mine selected-clients) - (sort-by :name + (sort-by :name (:user/clients user)) (or (and (sequential? selected-clients) @@ -58,6 +58,15 @@ (nil? selected-clients)) clients + (= :group (and (sequential? selected-clients) + (first selected-clients))) + (let [group (second selected-clients)] + (filterv + (fn [c] + (println "GROUP" group (:groups c)) + ((set (:groups c)) group)) + clients)) + (sequential? selected-clients) (filter (comp (set (map coerce-string-version selected-clients)) coerce-string-version :id) clients) diff --git a/src/cljs/auto_ap/views/components/layouts.cljs b/src/cljs/auto_ap/views/components/layouts.cljs index 82def69d..ca216162 100644 --- a/src/cljs/auto_ap/views/components/layouts.cljs +++ b/src/cljs/auto_ap/views/components/layouts.cljs @@ -86,13 +86,13 @@ clients (if-let [exact-match (first (filter (fn [client] - (= (str/lower-case (:code client)) (str/lower-case client-search))) + (= (str/lower-case (or (:code client) "INVALID")) (str/lower-case client-search))) clients))] [exact-match] (filter (fn [client] (or - (str/includes? (str/lower-case (:code client)) (str/lower-case client-search)) + (str/includes? (str/lower-case (or (:code client) "INVALID")) (str/lower-case client-search)) (str/includes? (str/lower-case (:name client)) (str/lower-case client-search)))) clients)))))