diff --git a/iol_ion/src/iol_ion/query.clj b/iol_ion/src/iol_ion/query.clj index ba4159fc..76ed64d7 100644 --- a/iol_ion/src/iol_ion/query.clj +++ b/iol_ion/src/iol_ion/query.clj @@ -48,6 +48,8 @@ [client end])))) + + (defn can-see-client? [identity client] (when (not client) (println "WARNING - permission checking for null client")) @@ -72,6 +74,51 @@ (time/plus (time/days 1)) coerce/to-date)) +(defn scan-sales-orders [db clients start end] + (for [c clients + :let [c (entid db c)] + r (seq (dc/index-range db + :sales-order/client+date + [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + [(:e r) (first (:v r)) (second (:v r))])) + +(defn scan-charges [db clients start end] + (for [c clients + :let [c (entid db c)] + r (seq (dc/index-range db + :charge/client+date + [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + [(:e r) (first (:v r)) (second (:v r))])) + +(defn scan-sales-refunds [db clients start end] + (for [c clients + :let [c (entid db c)] + r (seq (dc/index-range db + :sales-refund/client+date + [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + [(:e r) (first (:v r)) (second (:v r))])) + +(defn scan-expected-deposits [db clients start end] + (for [c clients + :let [c (entid db c)] + r (seq (dc/index-range db + :expected-deposit/client+date + [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + [(:e r) (first (:v r)) (second (:v r))])) + +(defn scan-cash-drawer-shifts [db clients start end] + (for [c clients + :let [c (entid db c)] + r (seq (dc/index-range db + :cash-drawer-shift/client+date + [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] + [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] + [(:e r) (first (:v r)) (second (:v r))])) + (defn scan-invoices [db clients start end] (for [c clients :let [c (entid db c)] @@ -107,3 +154,6 @@ [c (or start #inst "2001-01-01T08:00:00.000-00:00") ] [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))] [(:e r) (first (:v r)) (second (:v r))])) + +(defn ident [x] + (:db/ident x)) diff --git a/resources/datomic/extensions.edn b/resources/datomic/extensions.edn new file mode 100644 index 00000000..5b7271be --- /dev/null +++ b/resources/datomic/extensions.edn @@ -0,0 +1 @@ +{:xforms [clj-time.coerce/to-date clj-time.coerce/from-date iol-ion.query/ident ]} diff --git a/resources/public/js/htmx-disable.js b/resources/public/js/htmx-disable.js index 2920025f..97af300b 100644 --- a/resources/public/js/htmx-disable.js +++ b/resources/public/js/htmx-disable.js @@ -16,3 +16,7 @@ htmx.defineExtension('disable-submit', { */ } }) + +initDatepicker = function(elem) { +elem.dp = new Datepicker(elem, {format: "mm/dd/yyyy", autohide: true}); +} \ No newline at end of file diff --git a/resources/public/output.css b/resources/public/output.css index b42016fb..e1644126 100644 --- a/resources/public/output.css +++ b/resources/public/output.css @@ -1075,6 +1075,10 @@ input:checked + .toggle-bg { left: 0px; } +.left-1\/2 { + left: 50%; +} + .right-0 { right: 0px; } @@ -1091,6 +1095,10 @@ input:checked + .toggle-bg { top: 0.5rem; } +.top-2\/4 { + top: 50%; +} + .top-5 { top: 1.25rem; } @@ -1348,6 +1356,10 @@ input:checked + .toggle-bg { width: 1rem; } +.w-48 { + width: 12rem; +} + .w-5 { width: 1.25rem; } @@ -1408,11 +1420,21 @@ input:checked + .toggle-bg { flex-basis: 25%; } +.-translate-x-1\/2 { + --tw-translate-x: -50%; + 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%; 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-1\/2 { + --tw-translate-y: -50%; + 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%; 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)); @@ -1446,6 +1468,16 @@ input:checked + .toggle-bg { transform: none; } +@keyframes pulse { + 50% { + opacity: .5; + } +} + +.animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + @keyframes spin { to { transform: rotate(360deg); @@ -1476,6 +1508,12 @@ input:checked + .toggle-bg { 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)); } @@ -1520,6 +1558,10 @@ input:checked + .toggle-bg { align-items: center; } +.items-baseline { + align-items: baseline; +} + .items-stretch { align-items: stretch; } @@ -1585,6 +1627,18 @@ input:checked + .toggle-bg { margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); } +.space-y-1 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); +} + +.space-y-1\.5 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.375rem * var(--tw-space-y-reverse)); +} + .space-y-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); @@ -1662,6 +1716,10 @@ input:checked + .toggle-bg { border-radius: 0.5rem; } +.rounded-md { + border-radius: 0.375rem; +} + .rounded-l-lg { border-top-left-radius: 0.5rem; border-bottom-left-radius: 0.5rem; @@ -1677,6 +1735,11 @@ input:checked + .toggle-bg { border-top-right-radius: 0.25rem; } +.rounded-t-lg { + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; +} + .border { border-width: 1px; } @@ -2052,6 +2115,10 @@ input:checked + .toggle-bg { text-align: right; } +.align-baseline { + vertical-align: baseline; +} + .text-2xl { font-size: 1.5rem; line-height: 2rem; @@ -2114,10 +2181,19 @@ input:checked + .toggle-bg { 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)); @@ -2511,6 +2587,10 @@ input:checked + .toggle-bg { 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)); @@ -2521,6 +2601,11 @@ input:checked + .toggle-bg { border-color: rgb(121 181 46 / var(--tw-border-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 transparent; outline-offset: 2px; @@ -2573,6 +2658,11 @@ input:checked + .toggle-bg { --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)); @@ -2714,6 +2804,11 @@ input:checked + .toggle-bg { --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)); @@ -2794,6 +2889,10 @@ input:checked + .toggle-bg { 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; } @@ -2868,11 +2967,21 @@ input:checked + .toggle-bg { 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)); @@ -2883,6 +2992,11 @@ input:checked + .toggle-bg { --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)); @@ -2898,6 +3012,10 @@ input:checked + .toggle-bg { --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)); diff --git a/resources/schema.edn b/resources/schema.edn index 287f359d..9c2a6558 100644 --- a/resources/schema.edn +++ b/resources/schema.edn @@ -1806,6 +1806,28 @@ :db/valueType :db.type/tuple :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/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/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/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/cardinality :db.cardinality/one :db/index true}] diff --git a/scratch-sessions/fix-page-performance.repl b/scratch-sessions/fix-page-performance.repl index 4bfe274c..d641f408 100644 --- a/scratch-sessions/fix-page-performance.repl +++ b/scratch-sessions/fix-page-performance.repl @@ -180,4 +180,48 @@ {:user/name "hydrate-tuples"}) +(auto-ap.datomic/audit-transact-batch (->> (dc/q '[:find ?e ?c + :in $ + :where [?e :charge/client ?c]] + (dc/db conn) + ) + (map (fn [[i c]] + {:db/id i + :charge/client c}))) + + {:user/name "hydrate-tuples"}) + +(auto-ap.datomic/audit-transact-batch (->> (dc/q '[:find ?e ?c + :in $ + :where [?e :cash-drawer-shift/client ?c]] + (dc/db conn) + ) + (map (fn [[i c]] + {:db/id i + :cash-drawer-shift/client c}))) + + {:user/name "hydrate-tuples"}) + +(auto-ap.datomic/audit-transact-batch (->> (dc/q '[:find ?e ?c + :in $ + :where [?e :sales-refund/client ?c]] + (dc/db conn) + ) + (map (fn [[i c]] + {:db/id i + :sales-refund/client c}))) + + {:user/name "hydrate-tuples"}) + +(auto-ap.datomic/audit-transact-batch (->> (dc/q '[:find ?e ?c + :in $ + :where [?e :expected-deposit/client ?c]] + (dc/db conn) + ) + (map (fn [[i c]] + {:db/id i + :expected-deposit/client c}))) + + {:user/name "hydrate-tuples"}) + ) diff --git a/src/clj/auto_ap/datomic.clj b/src/clj/auto_ap/datomic.clj index bfc086f2..6ad41259 100644 --- a/src/clj/auto_ap/datomic.clj +++ b/src/clj/auto_ap/datomic.clj @@ -623,14 +623,16 @@ (defn apply-pagination-raw [args results] {:entries (->> results - (drop (:start args 0)) - (take (:count args (or (:per-page args) default-pagination-size)))) + (drop (or (:start args) 0)) + (take (or (:count args) (:per-page args) default-pagination-size))) :count (count results)}) (defn apply-pagination [args results] {:ids (->> results - (drop (:start args 0)) - (take (:count args (or (:per-page args) default-pagination-size))) + (drop (or (:start args) 0)) + (take (or (:count args ) + (:per-page args) + default-pagination-size)) (map last)) :count (count results)}) diff --git a/src/clj/auto_ap/datomic/reports.clj b/src/clj/auto_ap/datomic/reports.clj deleted file mode 100644 index 2f030134..00000000 --- a/src/clj/auto_ap/datomic/reports.clj +++ /dev/null @@ -1,67 +0,0 @@ -(ns auto-ap.datomic.reports - (:require - [auto-ap.datomic - :refer [add-sorter-fields - apply-pagination - apply-sort-3 - conn - merge-query - pull-many - query2]] - [auto-ap.graphql.utils :refer [can-see-client? extract-client-ids]] - [clj-time.coerce :as c] - [datomic.api :as dc])) - -(def default-read '[:db/id :report/client :report/created :report/url :report/name :report/creator]) - -(defn raw-graphql-ids [db args] - - (let [valid-clients (extract-client-ids (:clients args) - (:client-id args) - (when (:client-code args) - [:client/code (:client-code args)])) - query (cond-> {:query {:find [] - :in ['$ ] - :where []} - :args [db]} - - - (seq (:clients args)) - (merge-query {:query {:in ['[?xx ...]] - :where ['[?e :report/client ?xx]]} - :args [valid-clients]}) - - (:sort args) (add-sorter-fields {"client" ['[?e :report/client ?c] - '[?c :client/name ?sort-client]] - "created" ['[?e :report/created ?sort-created]] - "creator" ['[?e :report/creator ?sort-creator]] - "name" ['[?e :report/name ?sort-name] - ]} - args) - - true - (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :report/created ?sort-default]]}}))] - (->> (query2 query) - (apply-sort-3 (update args :sort conj {:sort-key "default-2" :asc true})) - (apply-pagination args)))) - -(defn graphql-results [ids db args] - (let [results (->> (pull-many db default-read ids) - (map #(update % :report/created c/from-date)) - (group-by :db/id))] - (->> ids - (map results) - (filter identity) - - (map first) - (filter (fn [r] - (every? - #(can-see-client? (:id args) %) - (map :db/id (:report/client r)))))))) - -(defn get-graphql [args] - (let [db (dc/db conn) - {ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args)] - - [(->> (graphql-results ids-to-retrieve db args)) - matching-count])) diff --git a/src/clj/auto_ap/datomic/sales_orders.clj b/src/clj/auto_ap/datomic/sales_orders.clj index f5e383c2..09f9c409 100644 --- a/src/clj/auto_ap/datomic/sales_orders.clj +++ b/src/clj/auto_ap/datomic/sales_orders.clj @@ -70,22 +70,17 @@ :else visible-clients) - (take 3) + (take 10) set) _ (mu/log ::selected-clients :selected-clients selected-clients) query (cond-> {:query {:find [] - :in ['$ '[?c ...]] - :where []} - :args [db selected-clients]} + :in ['$ '[?clients ?start-date ?end-date]] + :where '[[(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]]} + :args [db [selected-clients + (some-> (:start (:date-range args)) c/to-date) + (some-> (:end (:date-range args)) c/to-date )]]} - - true - (merge-query {:query {:in ['?start-date '?end-date] - :where '[[(iol-ion.query/sales-orders-in-range $ ?c ?start-date ?end-date) [?e ...]]]} - :args [(or (some-> (:start (:date-range args)) c/to-date) (iol-ion.query/recent-date 5)) - (or (some-> (:end (:date-range args)) c/to-date ) - (c/to-date (time/now)))]}) (:sort args) (add-sorter-fields-2 {"client" ['[?e :sales-order/client ?c] '[?c :client/name ?sort-client]] "location" ['[?e :sales-order/location ?sort-location]] @@ -169,3 +164,8 @@ matching-count (summarize-orders ids-to-retrieve)])) +(defn summarize-graphql [args] + (let [db (dc/db conn) + {ids-to-retrieve :ids matching-count :count} (mu/trace ::get-sales-order-ids [] (raw-graphql-ids db args))] + (summarize-orders ids-to-retrieve))) + diff --git a/src/clj/auto_ap/datomic/yodlee2.clj b/src/clj/auto_ap/datomic/yodlee2.clj deleted file mode 100644 index ea65ae13..00000000 --- a/src/clj/auto_ap/datomic/yodlee2.clj +++ /dev/null @@ -1,68 +0,0 @@ -(ns auto-ap.datomic.yodlee2 - (:require - [auto-ap.datomic - :refer [add-sorter-fields - apply-pagination - apply-sort-3 - conn - merge-query - pull-many - query2]] - [auto-ap.graphql.utils :refer [extract-client-ids]] - [clj-time.coerce :as c] - [datomic.api :as dc])) - -(def default-read '[*]) - -(defn <-datomic [x] - (-> x (update :yodlee-provider-account/last-updated c/from-date))) - -(defn raw-graphql-ids [db args] - (let [valid-clients (extract-client-ids (:clients args) - (:client-id args) - (when (:client-code args) - [:client/code (:client-code args)]))] - (->> (cond-> {:query {:find [] - :in ['$ '[?xx ...]] - :where ['[?e :yodlee-provider-account/id] - '[?e :yodlee-provider-account/client ?xx]]} - :args [db valid-clients]} - - - (:client-id args) - (merge-query {:query {:in ['?client-id] - :where ['[?e :yodlee-provider-account/client ?client-id]]} - :args [ (:client-id args)]}) - - (:client-code args) - (merge-query {:query {:in ['?client-code] - :where ['[?e :yodlee-provider-account/client ?client-id] - '[?client-id :client/code ?client-code]]} - :args [ (:client-code args)]}) - - (:sort args) (add-sorter-fields {"status" ['[?e :yodlee-provider-account/status ?sort-status]]} - args) - true - (merge-query {:query {:find ['?e ] - :where ['[?e :yodlee-provider-account/id]]}}) ) - - (query2) - (apply-sort-3 args) - (apply-pagination args)))) - - -(defn graphql-results [ids db _] - (let [results (->> (pull-many db default-read ids) - (group-by :db/id))] - (->> ids - (map results) - (map first) - (mapv <-datomic)))) - - -(defn get-graphql [args] - (let [db (dc/db conn) - {ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args)] - [(->> (graphql-results ids-to-retrieve db args)) - matching-count])) - diff --git a/src/clj/auto_ap/query_params.clj b/src/clj/auto_ap/query_params.clj new file mode 100644 index 00000000..3f4f8d4b --- /dev/null +++ b/src/clj/auto_ap/query_params.clj @@ -0,0 +1,119 @@ +(ns auto-ap.query-params + (:require [auto-ap.time :as atime] + [auto-ap.ssr.svg :as svg] + [clj-time.core :as time] + [clojure.string :as str])) + + +(defn wrap-parse-query-params [handler parser] + (fn parsed-handler [request] + (handler (assoc request :parsed-query-params (parser (->> (concat (:hx-query-params request) (:query-params request)) + (map (fn [[k v]] [(keyword k) v])) + (into {}))))))) + +(defn parse-key [k parser] + (fn [query-params] + (if (contains? query-params k) + (update query-params k #(some-> % not-empty parser)) + query-params))) + +(defn parse-date [d] + (atime/parse d atime/normal-date)) + +(defn parse-keyword [ns name] + (some->> name (keyword ns))) + +(defn parse-sort [grid-spec q] + (if (not-empty q) + (->> + (str/split q #",") + (map (fn [k] + (let [[key asc?] (str/split k #":") + matching-header (first (filter #(= (str key) (:sort-key %)) (:headers grid-spec)))] + {:sort-key (str key) + :asc (boolean (= "asc" asc?)) + :matching-header matching-header + :name (:name matching-header) + :sort-icon (if (= (boolean (= "asc" asc?)) true) + svg/sort-down + svg/sort-up)}))) + (filter :matching-header) + (into [])) + [])) + +(defn parse-long [l] + (try + (Long/parseLong l) + (catch Exception e + nil))) + +(defn parse-double [l] + (try + (Double/parseDouble l) + (catch Exception e + nil))) + +(defn apply-date-range [source-key start-date-key end-date-key] + (fn [query-params] + (dissoc + (condp = (source-key query-params) + "week" + (assoc query-params + start-date-key (time/plus (time/now) (time/days -7)) + end-date-key (time/now)) + + "month" + (assoc query-params + start-date-key (time/plus (time/now) (time/months -1)) + end-date-key (time/now)) + + "year" + (assoc query-params + start-date-key (time/plus (time/now) (time/years -1)) + end-date-key (time/now)) + + "all" + (assoc query-params + start-date-key (time/plus (time/now) (time/years -3)) + end-date-key (time/now)) + + query-params) + :date-range))) + +(defn apply-toggle-sort [grid-spec] + (fn toggle-sort [query-params] + (if (:toggle-sort query-params) + (let [key-to-toggle (:toggle-sort query-params) + current-sort (:sort query-params) + presently-sorted? ((set (map :sort-key current-sort)) key-to-toggle) + new-sort (if presently-sorted? + (mapv + (fn [s] + (if (= (:sort-key s) + key-to-toggle) + (-> s + (update :asc + #(boolean (not %))) + (update :sort-icon (fn [x] + (if (= x svg/sort-down) + svg/sort-up + svg/sort-down)))) + s)) + current-sort) + (conj current-sort {:sort-key key-to-toggle + :asc true + :name (:name (first (filter #(= (str key-to-toggle) (:sort-key %)) (:headers grid-spec)))) + :sort-icon svg/sort-down}))] + (-> query-params + (assoc :sort new-sort) + (dissoc :toggle-sort))) + query-params))) + +(defn apply-remove-sort [] + (fn remove-sort [query-params] + (if-let [remove-sort-key (:remove-sort query-params)] + (-> query-params + (update :sort (fn [current-sort] + (filterv (comp (complement #{remove-sort-key}) :sort-key) current-sort))) + (dissoc :remove-sort)) + query-params))) diff --git a/src/clj/auto_ap/ssr/auth.clj b/src/clj/auto_ap/ssr/auth.clj index b5afa02e..6f863dc4 100644 --- a/src/clj/auto_ap/ssr/auth.clj +++ b/src/clj/auto_ap/ssr/auth.clj @@ -1,6 +1,16 @@ -(ns auto-ap.ssr.auth) +(ns auto-ap.ssr.auth + (:require [buddy.sign.jwt :as jwt] + [config.core :refer [env]])) (defn logout [request] {:status 301 :headers {"Location" "/login"} :session {}}) + + +(defn impersonate [request] + {:status 200 + :session {:identity (dissoc (jwt/unsign (get-in request [:query-params "jwt"]) + (:jwt-secret env) + {:alg :hs512}) + :exp)}}) diff --git a/src/clj/auto_ap/ssr/company/company_1099.clj b/src/clj/auto_ap/ssr/company/company_1099.clj index a2a6ef91..f994d169 100644 --- a/src/clj/auto_ap/ssr/company/company_1099.clj +++ b/src/clj/auto_ap/ssr/company/company_1099.clj @@ -44,161 +44,117 @@ client-id vendor-id))) -(defn get-1099-companies [user {:keys [clients] :as args}] - (let [valid-clients (extract-client-ids (:clients args) - (:client-id args) - (when (:client-code args) - [:client/code (:client-code args)])) - results (cond - clients - (dc/q '[:find - (pull ?c [:client/code :db/id]) - (pull ?v vendor-read) - (sum ?a) - :with ?d - :in $ [?c ...] vendor-read - :where - [?p :payment/client ?c] - [?p :payment/date ?d ] - [(>= ?d #inst "2022-01-01T08:00")] - [(< ?d #inst "2023-01-01T08:00")] - [?p :payment/type :payment-type/check] - [?p :payment/amount ?a] - [?p :payment/vendor ?v]] - (dc/db conn) - valid-clients - vendor-read) - - (is-admin? user) - (dc/q '[:find - (pull ?c [:client/code :db/id]) - (pull ?v vendor-read) - (sum ?a) - :with ?d - :in $ vendor-read - :where - [?p :payment/date ?d ] - [(>= ?d #inst "2022-01-01T08:00")] - [(< ?d #inst "2023-01-01T08:00")] - [?p :payment/type :payment-type/check] - [?p :payment/client ?c] - [?p :payment/amount ?a] - [?p :payment/vendor ?v]] - (dc/db conn) - vendor-read) - - :else - (dc/q '[:find - (pull ?c [:client/code :db/id]) - (pull ?v vendor-read) - (sum ?a) - :with ?d - :in $ [?c ...] vendor-read - :where - [?p :payment/client ?c] - [?p :payment/date ?d ] - [(>= ?d #inst "2022-01-01T08:00")] - [(< ?d #inst "2023-01-01T08:00")] - [?p :payment/type :payment-type/check] - [?p :payment/amount ?a] - [?p :payment/vendor ?v]] - (dc/db conn) - valid-clients - vendor-read)) +(defn fetch-page [{:keys [trimmed-clients parsed-query-params] :as request user :identity}] + (let [results (dc/q '[:find + (pull ?c [:client/code :db/id]) + (pull ?v vendor-read) + (sum ?a) + :with ?d + :in $ [?c ...] vendor-read + :where + [?p :payment/client ?c] + [?p :payment/date ?d ] + [(>= ?d #inst "2022-01-01T08:00")] + [(< ?d #inst "2023-01-01T08:00")] + [?p :payment/type :payment-type/check] + [?p :payment/amount ?a] + [?p :payment/vendor ?v]] + (dc/db conn) + trimmed-clients + vendor-read) all (->> results (filter (fn [[_ _ a]] (>= (or a 0.0) 600.0))) (sort-by (fn [[client _ amount]] [(:client/code client ) amount])) (into [])) - paginated (apply-pagination-raw args all)] + paginated (apply-pagination-raw {:start (:start parsed-query-params) + :per-page (:per-page parsed-query-params)} all)] [(:entries paginated) (:count paginated)])) -(def grid-page {:id "vendor-table" - :nav (com/company-aside-nav) - :id-fn (comp :db/id second) - :fetch-page (fn [user args] - (get-1099-companies user args) - #_(r/get-graphql (into args {:id user}))) - :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes - :company)} - "My Company"] +(def grid-page + (helper/build + {:id "vendor-table" + :nav (com/company-aside-nav) + :id-fn (comp :db/id second) + :fetch-page fetch-page + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"] - [:a {:href (bidi/path-for ssr-routes/only-routes - :company-1099)} - "1099 Vendor Info"]] - :title "1099 Vendors" - :entity-name "Vendors" - :route :company-1099-vendor-table - :action-buttons (fn [user _] - nil) - :row-buttons (fn [user e] - [(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes - :company-1099-vendor-dialog - :vendor-id (:db/id (second e))) - "?" - (url/map->query {:client-id (:db/id (first e))})) - :hx-ext "debug" - :hx-target "#modal-holder" - :hx-swap "outerHTML"} - svg/pencil)]) - :headers [{:key "Client" - :name "Client" - :sort-key "client" - :render (comp :client/code first)} - {:key "vendor-name" - :name "Vendor Name" - :sort-key "vendor" - :render (fn [[_ vendor]] - [:div.flex.whitespace-nowrap.items-center.gap-4 - [:div [:div (:vendor/name vendor)] - [:div.text-sm.text-gray-400 - (or (-> vendor :vendor/legal-entity-name not-empty) - (str (-> vendor :vendor/legal-entity-first-name) " " - (-> vendor :vendor/legal-entity-middle-name) " " - (-> vendor :vendor/legal-entity-last-name)))]] - (when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)] - (com/pill - {:class "text-xs font-medium" - :color :primary} - (str/capitalize t99-type)) - )])} - {:key "tin" - :name "TIN" - :sort-key "tin" - :show-starting "md" - :render (fn [[_ vendor]] - [:div.flex.gap-4 - (when-let [tin (-> vendor :vendor/legal-entity-tin)] - [:span {:class "text-xs font-medium py-0.5 "} - tin]) - (when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)] - (com/pill {:class "text-xs font-medium" - :color :yellow} - (name tin-type)))] - )} - {:key "address" - :name "Address" - :sort-key "address" - :show-starting "lg" - :render (fn [[_ vendor]] - (if (-> vendor :vendor/address :address/street1) - [:div - [:div (-> vendor :vendor/address :address/street1)] " " - [:div - (-> vendor :vendor/address :address/street2)] " " - [:div - (-> vendor :vendor/address :address/city) " " - (-> vendor :vendor/address :address/state) "," - (-> vendor :vendor/address :address/zip)]] - [:p.text-sm.italic.text-gray-400 "No address"]))} - {:key "paid" - :name "Paid" - :sort-key "paid" - :render (fn [[_ _ paid]] - (com/pill {:class "text-xs font-medium" - :color :primary} - "Paid $" (Math/round paid)))}]}) + [:a {:href (bidi/path-for ssr-routes/only-routes + :company-1099)} + "1099 Vendor Info"]] + :title "1099 Vendors" + :entity-name "Vendors" + :route :company-1099-vendor-table + :row-buttons (fn [request e] + [(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes + :company-1099-vendor-dialog + :vendor-id (:db/id (second e))) + "?" + (url/map->query {:client-id (:db/id (first e))})) + :hx-ext "debug" + :hx-target "#modal-holder" + :hx-swap "outerHTML"} + svg/pencil)]) + :headers [{:key "Client" + :name "Client" + :sort-key "client" + :render (comp :client/code first)} + {:key "vendor-name" + :name "Vendor Name" + :sort-key "vendor" + :render (fn [[_ vendor]] + [:div.flex.whitespace-nowrap.items-center.gap-4 + [:div [:div (:vendor/name vendor)] + [:div.text-sm.text-gray-400 + (or (-> vendor :vendor/legal-entity-name not-empty) + (str (-> vendor :vendor/legal-entity-first-name) " " + (-> vendor :vendor/legal-entity-middle-name) " " + (-> vendor :vendor/legal-entity-last-name)))]] + (when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)] + (com/pill + {:class "text-xs font-medium" + :color :primary} + (str/capitalize t99-type)) + )])} + {:key "tin" + :name "TIN" + :sort-key "tin" + :show-starting "md" + :render (fn [[_ vendor]] + [:div.flex.gap-4 + (when-let [tin (-> vendor :vendor/legal-entity-tin)] + [:span {:class "text-xs font-medium py-0.5 "} + tin]) + (when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)] + (com/pill {:class "text-xs font-medium" + :color :yellow} + (name tin-type)))] + )} + {:key "address" + :name "Address" + :sort-key "address" + :show-starting "lg" + :render (fn [[_ vendor]] + (if (-> vendor :vendor/address :address/street1) + [:div + [:div (-> vendor :vendor/address :address/street1)] " " + [:div + (-> vendor :vendor/address :address/street2)] " " + [:div + (-> vendor :vendor/address :address/city) " " + (-> vendor :vendor/address :address/state) "," + (-> vendor :vendor/address :address/zip)]] + [:p.text-sm.italic.text-gray-400 "No address"]))} + {:key "paid" + :name "Paid" + :sort-key "paid" + :render (fn [[_ _ paid]] + (com/pill {:class "text-xs font-medium" + :color :primary} + "Paid $" (Math/round paid)))}]})) @@ -317,5 +273,5 @@ "Save")]]] [:div])]])))) -(def vendor-table (partial helper/table grid-page)) -(def page (partial helper/page grid-page)) +(def vendor-table (helper/table-route grid-page)) +(def page (helper/page-route grid-page)) diff --git a/src/clj/auto_ap/ssr/company/plaid.clj b/src/clj/auto_ap/ssr/company/plaid.clj index 16ab4255..8ab4e736 100644 --- a/src/clj/auto_ap/ssr/company/plaid.clj +++ b/src/clj/auto_ap/ssr/company/plaid.clj @@ -9,8 +9,7 @@ pull-attr pull-many-by-id query2]] - [auto-ap.graphql.utils - :refer [assert-can-see-client extract-client-ids]] + [auto-ap.graphql.utils :refer [assert-can-see-client]] [auto-ap.logging :as alog] [auto-ap.plaid.core :as p] [auto-ap.ssr-routes :as ssr-routes] @@ -36,43 +35,36 @@ :plaid-account/balance :plaid-account/name]}]) -(defn raw-graphql-ids [db args] - (let [valid-clients (extract-client-ids (:clients args) - (:client-id args) - (when (:client-code args) - [:client/code (:client-code args)])) +(defn fetch-ids [db request] + (let [query-params (:parsed-query-params request) query (cond-> {:query {:find [] :in ['$ '[?xx ...]] :where ['[?e :plaid-item/client ?xx]]} - :args [db valid-clients]} + :args [db (:trimmed-clients request)]} - (:sort args) (add-sorter-fields {"external-id" ['[?e :plaid-item/external-id ?sort-external-id]] + (:sort query-params) (add-sorter-fields {"external-id" ['[?e :plaid-item/external-id ?sort-external-id]] "status" ['[?e :plaid-item/status ?sort-status]]} - args) - - (:client-id args) - (merge-query {:query {:in '[?client-id] - :where ['[?e :plaid-item/client ?client-id]]} - :args [(:client-id args)]}) + query-params) true (merge-query {:query {:find ['?e] :where ['[?e :plaid-item/external-id]]}}))] + (clojure.pprint/pprint query-params) (cond->> (query2 query) - true (apply-sort-3 args) - true (apply-pagination args)))) + true (apply-sort-3 query-params) + true (apply-pagination query-params)))) -(defn graphql-results [ids db _] +(defn hydrate-results [ids db _] (let [results (pull-many-by-id db default-read ids)] (->> ids (map results)))) -(defn get-page [args] +(defn fetch-page [request] (let [db (dc/db conn) - {ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args)] - [(graphql-results ids-to-retrieve db args) + {ids-to-retrieve :ids matching-count :count} (fetch-ids db request)] + [(hydrate-results ids-to-retrieve db request) matching-count])) @@ -138,60 +130,61 @@ "Start relink")]))) -(def grid-page {:id "plaid-table" - :nav (com/company-aside-nav) - :id-fn :db/id - :fetch-page (fn [user args] - (get-page (assoc args :id user))) - :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes - :company)} - "My Company"] +(def grid-page + (helper/build + {:id "plaid-table" + :nav (com/company-aside-nav) + :fetch-page fetch-page + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"] - [:a {:href (bidi/path-for ssr-routes/only-routes - :company-plaid)} - "Plaid"]] - :title "Plaid Accounts" - :entity-name "Plaid accounts" - :route :company-plaid-table - :action-buttons (fn [user params] - (when-let [client-code (:client/code (:client params))] - [[:div {:hx-post (str (bidi/path-for ssr-routes/only-routes - :company-plaid-link - :request-method :post)) - :hx-vals (hiccup/raw (format "js:{client_code: \"%s\", public_token: event.detail.public_token}", client-code)) - :hx-trigger "linked"} - [:script (hiccup/raw (plaid-link-script (p/get-link-token client-code)))] - (com/button {:color :primary - :id "link-account" - :onClick "window.plaid.open()"} - (com/button-icon {} svg/refresh) - "Link new account")]])) - :row-buttons (fn [user e] - [[:div (com/button {:hx-put (str (bidi/path-for ssr-routes/only-routes - :company-plaid-relink) - "?plaid-item-id=" (:db/id e)) - :color :primary - :hx-target "closest div"} - "Reauthenticate")]]) - :headers [{:key "plaid-item" - :name "Plaid Item" - :sort-key "id" - :render :db/id} - {:key "status" - :name "Status" - :sort-key "status" - :render #(when-let [status (:plaid-item/status %)] - [:div [:div (com/pill {:color :primary} - status)] - [:div (atime/unparse-local (coerce/to-date-time (:plaid-item/last-updated %)) atime/normal-date)]])} + [:a {:href (bidi/path-for ssr-routes/only-routes + :company-plaid)} + "Plaid"]] + :title "Plaid Accounts" + :entity-name "Plaid accounts" + :route :company-plaid-table + :action-buttons (fn [request] + (when-let [client-code (:client/code (:client request))] + [[:div {:hx-post (str (bidi/path-for ssr-routes/only-routes + :company-plaid-link + :request-method :post)) + :hx-vals (hiccup/raw (format "js:{client_code: \"%s\", public_token: event.detail.public_token}", client-code)) + :hx-trigger "linked"} + [:script (hiccup/raw (plaid-link-script (p/get-link-token client-code)))] + (com/button {:color :primary + :id "link-account" + :onClick "window.plaid.open()"} + (com/button-icon {} svg/refresh) + (format "Link %s account" client-code))]])) + :row-buttons (fn [request e] + [[:div (com/button {:hx-put (str (bidi/path-for ssr-routes/only-routes + :company-plaid-relink) + "?plaid-item-id=" (:db/id e)) + :color :primary + :hx-target "closest div"} + "Reauthenticate")]]) + :headers [{:key "plaid-item" + :name "Plaid Item" + :sort-key "external-id" + :render :plaid-item/external-id} + {:key "status" + :name "Status" + :sort-key "status" + :render #(when-let [status (:plaid-item/status %)] + [:div [:div (com/pill {:color :primary} + status)] + [:div (atime/unparse-local (coerce/to-date-time (:plaid-item/last-updated %)) atime/normal-date)]])} - {:key "accounts" - :name "Accounts" - :show-starting "md" - :render (fn [e] - [:ul - (for [a (:plaid-item/accounts e)] - [:li (:plaid-account/name a) " - " (:plaid-account/number a)])])}]}) + {:key "accounts" + :name "Accounts" + :show-starting "md" + :render (fn [e] + [:ul + (for [a (:plaid-item/accounts e)] + [:li (:plaid-account/name a) " - " (:plaid-account/number a)])])}]})) -(def page (partial helper/page grid-page)) -(def table (partial helper/table grid-page)) + +(def page (helper/page-route grid-page)) +(def table (helper/table-route grid-page)) diff --git a/src/clj/auto_ap/ssr/company/reports.clj b/src/clj/auto_ap/ssr/company/reports.clj index 62c93e72..2b764d0e 100644 --- a/src/clj/auto_ap/ssr/company/reports.clj +++ b/src/clj/auto_ap/ssr/company/reports.clj @@ -1,7 +1,14 @@ (ns auto-ap.ssr.company.reports (:require [amazonica.aws.s3 :as s3] - [auto-ap.datomic :refer [conn]] + [auto-ap.datomic + :refer [add-sorter-fields + apply-pagination + apply-sort-3 + conn + merge-query + pull-many + query2]] [auto-ap.datomic.reports :as r] [auto-ap.graphql.utils :refer [assert-can-see-client is-admin?]] [auto-ap.ssr-routes :as ssr-routes] @@ -11,57 +18,101 @@ [auto-ap.ssr.utils :refer [html-response]] [auto-ap.time :as atime] [bidi.bidi :as bidi] + [clojure.set :as set] [config.core :refer [env]] - [datomic.api :as dc] - [com.brunobonacci.mulog :as mu])) + [datomic.api :as dc])) -(def grid-page {:id "report-table" - :nav (com/company-aside-nav) - :id-fn :db/id - :fetch-page (fn [user args] - (r/get-graphql (into args {:id user}))) - :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes - :company)} - "My Company"] +(def default-read '[:db/id :report/client [:report/created :xform clj-time.coerce/from-date] :report/url :report/name :report/creator]) - [:a {:href (bidi/path-for ssr-routes/only-routes - :company-reports)} - "Reports"]] - :title "Reports" - :entity-name "Reports" - :route :company-reports-table - :action-buttons (fn [user _] - nil) - :row-buttons (fn [user e] - (com/a-icon-button {:href (:report/url e)} - svg/download)[ - (when (is-admin? user) - (com/icon-button {:hx-delete (str (bidi/path-for ssr-routes/only-routes - :company-reports-delete - :request-method :delete)) - :hx-target "closest tr"} - svg/trash))]) - :headers [{:key "name" - :name "Name" - :sort-key "name" - :render :report/name} - {:key "created-by" - :name "Created by" - :sort-key "creator" - :render (fn [report] - (when (:report/creator report) - (com/pill {:color :primary } - (:report/creator report))))} - {:key "created" - :name "Created" - :sort-key "created" - :render #(atime/unparse-local (:report/created %) - atime/normal-date)}]}) +(defn fetch-ids [db request] + + (let [query-params (:parsed-query-params request) + query (cond-> {:query {:find [] + :in '[$ [?c ...]] + :where '[[?e :report/client ?c]]} + :args [db (:trimmed-clients request)]} + + + (:sort query-params) (add-sorter-fields {"client" ['[?e :report/client ?c] + '[?c :client/name ?sort-client]] + "created" ['[?e :report/created ?sort-created]] + "creator" ['[?e :report/creator ?sort-creator]] + "name" ['[?e :report/name ?sort-name] + ]} + query-params) + + true + (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :report/created ?sort-default]]}}))] + (->> (query2 query) + (apply-sort-3 (update query-params :sort conj {:sort-key "default-2" :asc true})) + (apply-pagination query-params)))) + +(defn hydrate-results [ids db request] + (let [results (->> (pull-many db default-read ids) + (group-by :db/id)) + valid-clients (:trimmed-clients request)] + (->> ids + (map results) + (filter identity) + + (map first) + (filter (fn [r] + (let [used-clients (set (map :db/id (:report/client r)))] + (= used-clients + (set/intersection valid-clients + used-clients)))))))) + +(defn fetch-page [args] + (let [db (dc/db conn) + {ids-to-retrieve :ids matching-count :count} (fetch-ids db args)] + + [(->> (hydrate-results ids-to-retrieve db args)) + matching-count])) + +(def grid-page + (helper/build {:id "report-table" + :nav (com/company-aside-nav) + :fetch-page fetch-page + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"] + + [:a {:href (bidi/path-for ssr-routes/only-routes + :company-reports)} + "Reports"]] + :title "Reports" + :entity-name "Reports" + :route :company-reports-table + :row-buttons (fn [request e] + [(com/a-icon-button {:href (:report/url e)} + svg/download) + (when (is-admin? (:identity request)) + (com/icon-button {:hx-delete (str (bidi/path-for ssr-routes/only-routes + :company-reports-delete + :request-method :delete)) + :hx-target "closest tr"} + svg/trash))]) + :headers [{:key "name" + :name "Name" + :sort-key "name" + :render :report/name} + {:key "created-by" + :name "Created by" + :sort-key "creator" + :render (fn [report] + (when (:report/creator report) + (com/pill {:color :primary } + (:report/creator report))))} + {:key "created" + :name "Created" + :sort-key "created" + :render #(atime/unparse-local (:report/created %) + atime/normal-date)}]})) (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) -(def table (partial helper/table grid-page)) -(def page (partial helper/page grid-page)) +(def table (helper/table-route grid-page)) +(def page (helper/page-route grid-page)) (defn delete-report [{:keys [form-params identity]}] @@ -70,7 +121,7 @@ :where [?i :report/key ?k]] (dc/db conn) (some-> (get form-params "id") not-empty Long/parseLong))) - report (dc/pull (dc/db conn) r/default-read id-to-delete)] + report (dc/pull (dc/db conn) r/default-read id-to-delete)] (assert-can-see-client identity (:report/client report)) (when id-to-delete (s3/delete-object :bucket-name (:data-bucket env) diff --git a/src/clj/auto_ap/ssr/company/yodlee.clj b/src/clj/auto_ap/ssr/company/yodlee.clj index 0f7b6380..699c40fa 100644 --- a/src/clj/auto_ap/ssr/company/yodlee.clj +++ b/src/clj/auto_ap/ssr/company/yodlee.clj @@ -1,32 +1,73 @@ (ns auto-ap.ssr.company.yodlee (:require - [auto-ap.datomic :refer [conn]] - [auto-ap.datomic.yodlee2 :as yodlee2] - [auto-ap.graphql.utils :refer [is-admin?]] + [auto-ap.datomic + :refer [add-sorter-fields + apply-pagination + apply-sort-3 + conn + merge-query + pull-attr + pull-many + query2]] + [auto-ap.graphql.utils :refer [assert-can-see-client is-admin?]] [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.grid-page-helper :as helper] + [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils :refer [html-response]] [auto-ap.time :as atime] [auto-ap.yodlee.core2 :as yodlee] [bidi.bidi :as bidi] [config.core :refer [env]] [datomic.api :as dc] - [hiccup2.core :as hiccup] - [auto-ap.datomic :refer [pull-attr]])) + [hiccup2.core :as hiccup])) (def default-read '[:db/id - :yodlee-provider-account/last-updated + [:yodlee-provider-account/last-updated :xform clj-time.coerce/from-date] :yodlee-provider-account/status :yodlee-provider-account/id :yodlee-provider-account/detailed-status {:yodlee-provider-account/accounts [:yodlee-account/name :yodlee-account/number] - :yodlee-provider-account/client [:client/code]}]) + :yodlee-provider-account/client [:client/code]}]) +(defn fetch-ids [db request] + (let [query-params (:parsed-query-params request)] + (->> (cond-> {:query {:find [] + :in ['$ '[?xx ...]] + :where ['[?e :yodlee-provider-account/id] + '[?e :yodlee-provider-account/client ?xx]]} + :args [db (:trimmed-clients request)]} + + + (:sort query-params) (add-sorter-fields {"status" ['[?e :yodlee-provider-account/status ?sort-status]] + "last-updated" ['[?e :yodlee-provider-account/last-updated ?sort-last-updated]]} + query-params) + true + (merge-query {:query {:find ['?e ] + :where ['[?e :yodlee-provider-account/id]]}})) + + (query2) + (apply-sort-3 query-params) + (apply-pagination query-params)))) + + +(defn hydrate-results [ids db _] + (let [results (->> (pull-many db default-read ids) + (group-by :db/id))] + (->> ids + (map results) + (map first)))) + + +(defn fetch-page [request] + (let [db (dc/db conn) + {ids-to-retrieve :ids matching-count :count} (fetch-ids db request)] + [(->> (hydrate-results ids-to-retrieve db request)) + matching-count])) + + (defn fastlink-dialog [{:keys [client]}] (html-response (com/modal @@ -53,7 +94,10 @@ fastlink.open({fastLinkURL: '%s', ] [:div])))) -(defn reauthenticate [{:keys [form-params]}] +(defn reauthenticate [{:keys [form-params identity]}] + (assert-can-see-client identity (-> (dc/pull (dc/db conn) '[{:yodlee-provider-account/client [:db/id]}] (Long/parseLong (get form-params "id"))) + :yodlee-provider-account/client + :db/id)) (html-response (com/modal {} @@ -82,27 +126,27 @@ fastlink.open({fastLinkURL: '%s', (pull-attr (dc/db conn) :yodlee-provider-account/id (Long/parseLong (get form-params "id")))))]] [:div])))) -(def grid-page {:id "yodlee-table" - :nav (com/company-aside-nav) - :id-fn :db/id - :fetch-page (fn [user args] - (yodlee2/get-graphql (assoc args :id user))) - :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes - :company)} - "My Company"] - - [:a {:href (bidi/path-for ssr-routes/only-routes - :company-yodlee)} - "Yodlee"]] - :title "Yodlee Accounts" - :entity-name "Yodlee accounts" - :route :company-yodlee-table - :action-buttons (fn [user args] - [[:div.flex.flex-col.flex-shrink +(def grid-page + (helper/build + {:id "yodlee-table" + :nav (com/company-aside-nav) + :id-fn :db/id + :fetch-page fetch-page + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"] + [:a {:href (bidi/path-for ssr-routes/only-routes + :company-yodlee)} + "Yodlee"]] + :title "Yodlee Accounts" + :entity-name "Yodlee accounts" + :route :company-yodlee-table + :action-buttons (fn [request] + [[:div.flex.flex-col.flex-shrink [:div.flex-shrink (com/button {:color :primary :on-click "openFastlink()" - :disabled (if (:client args) + :disabled (if (:client request) false true) :hx-get (bidi/path-for ssr-routes/only-routes @@ -110,53 +154,59 @@ fastlink.open({fastLinkURL: '%s', :hx-target "#modal-holder"} (com/button-icon {} svg/refresh) "Link new account")] - (when-not (:client args) + (when-not (:client request) [:div.text-xs "Note: please select a specific customer to link a new account."])]]) - :row-buttons (fn [user e] - [ - (com/button {:hx-put (bidi/path-for ssr-routes/only-routes - :company-yodlee-provider-account-reauthenticate) - :color :primary - :hx-target "#modal-holder"} - "Reauthenticate") - (when (is-admin? user) - (com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes - :company-yodlee-provider-account-refresh) - :hx-target "closest tr"} - svg/refresh))]) - :headers [{:key "provider-account" - :name "Provider Account" - :sort-key "provider-account" - :render :yodlee-provider-account/id} - {:key "status" - :name "Status" - :sort-key "status" - :render #(when-let [status (:yodlee-provider-account/status %)] - (com/pill {:color (if (not= status "SUCCESS") - :yellow - :primary) } - status))} - {:key "detailed-status" - :name "Detailed Status" - :sort-key "detailed-status" - :render #(when-let [status (:yodlee-provider-account/detailed-status %)] - status)} + :row-buttons (fn [request _] + [ + (com/button {:hx-put (bidi/path-for ssr-routes/only-routes + :company-yodlee-provider-account-reauthenticate) + :color :primary + :hx-target "#modal-holder"} + "Reauthenticate") + (when (is-admin? (:identity request)) + (com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes + :company-yodlee-provider-account-refresh) + :hx-target "closest tr"} + svg/refresh))]) + :headers [{:key "client" + :name "Client" + :sort-key "client" + :hide? (fn [args] + (= (count (:clients args)) 1)) + :render #(-> % :yodlee-provider-account/client :client/code)} + {:key "provider-account" + :name "Provider Account" + :sort-key "provider-account" + :render :yodlee-provider-account/id} + {:key "status" + :name "Status" + :sort-key "status" + :render #(when-let [status (:yodlee-provider-account/status %)] + (com/pill {:color (if (not= status "SUCCESS") + :yellow + :primary) } + status))} + {:key "detailed-status" + :name "Detailed Status" + :sort-key "detailed-status" + :render #(when-let [status (:yodlee-provider-account/detailed-status %)] + status)} - {:key "last-updated" - :name "Last Updated" - :sort-key "last-updated" - :render #(atime/unparse-local (:yodlee-provider-account/last-updated %) - atime/normal-date)} - {:key "accounts" - :name "Accounts" - :show-starting "md" - :render (fn [e] - [:ul - (for [a (:yodlee-provider-account/accounts e)] - [:li (:yodlee-account/name a) " - " (:yodlee-account/number a)])])}]}) + {:key "last-updated" + :name "Last Updated" + :sort-key "last-updated" + :render #(atime/unparse-local (:yodlee-provider-account/last-updated %) + atime/normal-date)} + {:key "accounts" + :name "Accounts" + :show-starting "md" + :render (fn [e] + [:ul + (for [a (:yodlee-provider-account/accounts e)] + [:li (:yodlee-account/name a) " - " (:yodlee-account/number a)])])}]})) -(def page (partial helper/page grid-page)) -(def table (partial helper/table grid-page)) +(def page (helper/page-route grid-page)) +(def table (helper/table-route grid-page)) ;; TODO delete-after-settle (defn refresh-provider-account [{:keys [form-params identity]}] diff --git a/src/clj/auto_ap/ssr/components.clj b/src/clj/auto_ap/ssr/components.clj index 5df4f447..86dae15f 100644 --- a/src/clj/auto_ap/ssr/components.clj +++ b/src/clj/auto_ap/ssr/components.clj @@ -9,14 +9,18 @@ [auto-ap.ssr.components.page :as page] [auto-ap.ssr.components.data-grid :as data-grid] [auto-ap.ssr.components.tags :as tags] - [auto-ap.ssr.components.paginator :as paginator])) + [auto-ap.ssr.components.paginator :as paginator] + [auto-ap.ssr.components.radio :as radio])) (def breadcrumbs breadcrumbs/breadcrumbs-) (def button buttons/button-) +(def a-button buttons/a-button-) (def button-icon buttons/button-icon-) (def icon-button buttons/icon-button-) (def a-icon-button buttons/a-icon-button-) +(def button-group buttons/group-) +(def button-group-button buttons/group-button-) (def modal dialog/modal-) (def modal-card dialog/modal-card-) @@ -36,6 +40,7 @@ (def navbar navbar/navbar-) (def page page/page-) +(def radio radio/radio-) (def pill tags/pill-) (def badge tags/badge-) diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index 03b66b65..6b7804ed 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -10,8 +10,9 @@ [:a (-> params (dissoc :icon) (assoc :type "button") - (update :class str " cursor-pointer flex items-center p-2 w-full text-sm text-gray-600 rounded-lg transition duration-75 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700") + (update :class str " cursor-pointer flex items-center p-2 w-full text-xs text-gray-600 rounded-lg transition duration-75 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700") (assoc :hx-indicator "find .htmx-indicator") + (assoc :hx-boost "true") (assoc :hx-select "#app-contents") (assoc :hx-target "#app-contents") (assoc :hx-swap "outerHTML")) @@ -28,7 +29,7 @@ (svg/spinner-primary {:class "inline w-4 h-4 text-white"})]]]) (defn sub-menu- [params & children] - [:ul {:id (:id params) :class "hidden py-2 space-y-2"} + [:ul {:id (:id params) :class "hidden py-2 space-y-1.5"} (for [c children] [:li (update-in c [1 1 :class ] str " flex items-center p-2 pl-11 w-full text-base font-normal text-gray-900 rounded-lg transition duration-75 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700")])]) @@ -148,7 +149,7 @@ ")]]) (defn main-aside-nav- [] - [:ul {:class "space-y-2"} + [:ul {:class "space-y-1"} [:li (menu-button- {:icon svg/pie @@ -178,10 +179,22 @@ :icon svg/receipt-register-1} "Sales") (sub-menu- {:id "dropdown-sales"} - (menu-button- {:href (bidi/path-for client-routes/routes - :sales-orders)} "Sales") - (menu-button- {:href (bidi/path-for client-routes/routes - :expected-deposits)} "Expected Deposits") + (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes + :pos-sales) + "?date-range=week")} "Sales") + (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes + :pos-expected-deposits) + "?date-range=week")} "Expected Deposits") + (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes + :pos-tenders) + "?date-range=week")} "Tenders") + + (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes + :pos-refunds) + "?date-range=week")} "Refunds") + (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes + :pos-cash-drawer-shifts) + "?date-range=week")} "Cash drawer shifts") #_(menu-button- {:href "Sales"} "Cash Shifts") #_(menu-button- {:href "Sales"} "Tenders"))] [:li diff --git a/src/clj/auto_ap/ssr/components/buttons.clj b/src/clj/auto_ap/ssr/components/buttons.clj index cdff1fed..40574e20 100644 --- a/src/clj/auto_ap/ssr/components/buttons.clj +++ b/src/clj/auto_ap/ssr/components/buttons.clj @@ -96,7 +96,20 @@ (svg/spinner {:class "inline w-4 h-4 text-white"}) [:div.ml-3 "Loading..."]] (into [:div.htmx-indicator-hidden.inline-flex.gap-2.items-center.justify-center] children)]) -;; => #'auto-ap.ssr.components.buttons/button-;; => #'auto-ap.ssr.components.buttons/button- + +(defn a-button- [params & children] + [:a (update params + :class #(cond-> % + true (str " focus:ring-4 font-bold rounded-lg text-xs p-3 text-center mr-2 inline-flex items-center hover:scale-105 transition duration-100 justify-center") + (= :secondary (:color params)) (str " text-white bg-blue-500 hover:bg-blue-600 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700") + (= :primary (:color params)) (str " text-white bg-green-500 hover:bg-green-600 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 ") + (nil? (:color params)) + (str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 font-medium border border-gray-300 dark:border-gray-700"))) + [:div.htmx-indicator.flex.items-center + (svg/spinner {:class "inline w-4 h-4 text-white"}) + [:div.ml-3 "Loading..."]] + (into [:div.htmx-indicator-hidden.inline-flex.gap-2.items-center.justify-center] children)]) + (defn icon-button- [params & children] (into [:button @@ -126,3 +139,31 @@ (svg/spinner {:class "inline w-4 h-4 text-white"}) [:div.ml-3 "Loading..."]] (into [:div.htmx-indicator-hidden ] children)]) + + + +(defn group-button- [{:keys [size] :or {size :normal} :as params} & children] + (into [:button (cond-> params + true (assoc :type (or (:type params) "button")) + true (update :class (fn [c] + (cond-> c + true (str " font-medium text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:ring-2 focus:ring-green-700 focus:text-green-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-green-500 dark:focus:text-white") + + (= :small size) + (str " text-xs px-3 py-2") + + (= :normal size) + (str " text-sm px-4 py-2") + ) + )) + true (dissoc :size))] children )) + +(defn group- [{:keys [name]} & children] + (let [children (-> children + vec + (update-in [0 1 :class] str " rounded-l-lg") + (update-in [(dec (count children)) 1 :class] str " rounded-r-lg"))] + (into [:div {:class "inline-flex rounded-md shadow-sm", :role "group" + :hx-on:click "this.querySelector(\"input\").value = event.target.value; this.querySelector(\"input\").dispatchEvent(new Event('change', {bubbles: true}));"} + [:input {:type "hidden" :name name}]] + children))) diff --git a/src/clj/auto_ap/ssr/components/data_grid.clj b/src/clj/auto_ap/ssr/components/data_grid.clj index 5625818e..8e4000d8 100644 --- a/src/clj/auto_ap/ssr/components/data_grid.clj +++ b/src/clj/auto_ap/ssr/components/data_grid.clj @@ -69,6 +69,7 @@ :hx-trigger "clientSelected from:body" :hx-swap "outerHTML swap:300ms" :id id} + (content-card- {} [:div {:class "flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"} @@ -97,4 +98,8 @@ :request-method :get) "?start=" (* page per-page)) :hx-target (str "#" id) - :hx-swap "outerHTML show:#app:top"})})))]) + :hx-swap "outerHTML show:#app:top" + :hx-indicator (str "#" id)})})) + [:div {:class "htmx-indicator absolute -translate-x-1/2 -translate-y-1/2 top-2/4 left-1/2 overflow-hidden w-full h-full"} + [:div {:class "flex items-center justify-center w-full h-full border border-gray-200 rounded-lg bg-gray-50 dark:bg-gray-800 dark:border-gray-700 bg-opacity-50" } + [:div {:class "px-3 py-1 text-xs font-medium leading-none text-center text-blue-800 bg-blue-200 rounded-full animate-pulse dark:bg-blue-900 dark:text-blue-200"} "loading..."]]])]) diff --git a/src/clj/auto_ap/ssr/components/inputs.clj b/src/clj/auto_ap/ssr/components/inputs.clj index e781045a..e9c9fc38 100644 --- a/src/clj/auto_ap/ssr/components/inputs.clj +++ b/src/clj/auto_ap/ssr/components/inputs.clj @@ -1,4 +1,5 @@ -(ns auto-ap.ssr.components.inputs) +(ns auto-ap.ssr.components.inputs + (:require [hiccup2.core :as hiccup])) (defn select- [params & children] (into @@ -11,28 +12,43 @@ (:allow-blank? params) (conj [:option {:value "" :selected (not (:value params))} ""]))] children)) -(defn text-input- [params] +(defn use-size [size] + (if (= :small size) + (str " " "text-xs p-2") + (str " " "text-sm p-2.25"))) +(defn text-input- [{:keys [size] :as params}] [:input - (update params - :class str " bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500") + (-> params + (update + :class str " bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500") + (update :class #(str % (use-size size))) + ) ]) -(defn money-input- [params] +(defn money-input- [{:keys [size] :as params}] [:input (-> params (update - :class str " bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 text-right" + :class str " bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 text-right appearance-none" ) + (update :class #(str % (use-size size))) (assoc :type "number" - :step "0.01")) + :step "0.01") + (dissoc :size)) ]) -(defn date-input- [params] - [:input - (-> params - (update - :class str " bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500") - (assoc :type "date"))]) +(defn date-input- [{:keys [size] :as params}] + [:div + [:input + (-> params + (update + :class str " bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500") + (assoc :type "text") + (assoc "_" (hiccup/raw "init initDatepicker(me)")) + (assoc "hx-on" (hiccup/raw "changeDate: htmx.trigger(this, \"change\") + htmx:beforeCleanupElement: this.dp.destroy()")) + (update :class #(str % (use-size size))) + (dissoc :size))]]) (defn field- [params & rest] (into diff --git a/src/clj/auto_ap/ssr/components/page.clj b/src/clj/auto_ap/ssr/components/page.clj index e6df4eac..ebda28be 100644 --- a/src/clj/auto_ap/ssr/components/page.clj +++ b/src/clj/auto_ap/ssr/components/page.clj @@ -6,7 +6,10 @@ [auto-ap.ssr.svg :as svg])) (defn page- [{:keys [nav page-specific client client-selection identity app-params] :or {app-params {}}} & children] - [:div#app + [:div#app {"_" (hiccup/raw " + on notification 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" + )} (navbar- {:client-selection client-selection :client client :identity identity}) @@ -14,10 +17,7 @@ (left-aside- {:nav nav :page-specific page-specific}) [:div#main-content {:class "relative w-full h-full lg:pl-64 overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content " - "_" (hiccup/raw " - on notification 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" - )} + } [:div#notification-holder.hidden [:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg [:div.relative diff --git a/src/clj/auto_ap/ssr/components/paginator.clj b/src/clj/auto_ap/ssr/components/paginator.clj index c89caffb..6a27ff20 100644 --- a/src/clj/auto_ap/ssr/components/paginator.clj +++ b/src/clj/auto_ap/ssr/components/paginator.clj @@ -1,4 +1,5 @@ -(ns auto-ap.ssr.components.paginator) +(ns auto-ap.ssr.components.paginator + (:require [auto-ap.ssr.svg :as svg])) (defn bound [x y z] (cond @@ -34,7 +35,10 @@ (not= current-page x) (str " text-gray-500 bg-white border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"))) (assoc :href "#")) - (inc x)]])) + [:div.htmx-indicator.flex.items-center + (svg/spinner {:class "inline w-4 h-4 text-black"})] + [:div.htmx-indicator-hidden + (inc x)]]])) last-page-button (Math/min (long total-pages) (long (+ max-buttons first-page-button))) diff --git a/src/clj/auto_ap/ssr/components/radio.clj b/src/clj/auto_ap/ssr/components/radio.clj new file mode 100644 index 00000000..f9a701a3 --- /dev/null +++ b/src/clj/auto_ap/ssr/components/radio.clj @@ -0,0 +1,27 @@ +(ns auto-ap.ssr.components.radio) + +(defn radio- [{:keys [options name title size] :or {size :medium}}] + [:h3 {:class "mb-4 font-semibold text-gray-900 dark:text-white"} title] + [:ul {:class "w-48 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white"} + (for [{:keys [value content]} options] + [:li {:class "w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600"} + [:div {:class "flex items-center pl-3"} + [:input {:id (str "list-" name "-" value) + :type "radio", + :value value + :name name + :class + (cond-> "w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500" + (= size :small) + (str " " "text-xs") + + (= size :medium) + (str " " "text-sm"))}] + [:label {:for (str "list-" name "-" value) + :class + (cond-> "w-full ml-2 font-medium text-gray-900 dark:text-gray-300" + (= size :small) + (str " " "text-xs py-2") + + (= size :medium) + (str " " "text-sm py-3"))} content]]])]) diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index 8dc4bd5d..a26400b0 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -12,47 +12,59 @@ [auto-ap.ssr.company-dropdown :as company-dropdown] [auto-ap.ssr.company.reports :as company-reports] [auto-ap.ssr.invoice.glimpse :as invoice-glimpse] + [auto-ap.ssr.pos.sales-orders :as pos-sales] + [auto-ap.ssr.pos.refunds :as pos-refunds] + [auto-ap.ssr.pos.expected-deposits :as pos-expected-deposits] + [auto-ap.ssr.pos.cash-drawer-shifts :as pos-cash-drawer-shifts] + [auto-ap.ssr.pos.tenders :as pos-tenders] [auto-ap.routes.ezcater-xls :as ezcater-xls] [auto-ap.ssr.company :as company])) ;; from auto-ap.ssr-routes, because they're shared -(def key->handler {:logout auth/logout - :admin-history (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page))) - :admin-history-search (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page))) - :admin-history-inspect (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/inspect))) - :active-client (wrap-client-redirect-unauthenticated (wrap-secure (wrap-secure company-dropdown/active-client))) - :company-dropdown-search-results - (wrap-client-redirect-unauthenticated (wrap-secure company-dropdown/dropdown-search-results)) - :company (wrap-client-redirect-unauthenticated (wrap-secure company/page)) - :company-1099 (wrap-client-redirect-unauthenticated (wrap-secure company-1099/page)) - :company-1099-vendor-table (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-table)) - :company-1099-vendor-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-dialog)) - :company-1099-vendor-save (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-save)) - :company-plaid (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/page)) - :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-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)) - :company-yodlee-provider-account-refresh (wrap-client-redirect-unauthenticated (wrap-admin company-yodlee/refresh-provider-account)) - :company-yodlee-provider-account-reauthenticate (wrap-client-redirect-unauthenticated (wrap-admin company-yodlee/reauthenticate)) - :company-reports (wrap-client-redirect-unauthenticated (wrap-secure company-reports/page)) - :company-reports-table (wrap-client-redirect-unauthenticated (wrap-secure company-reports/table)) - :company-reports-delete (wrap-client-redirect-unauthenticated (wrap-admin company-reports/delete-report)) - :invoice-glimpse (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/page)) - :invoice-glimpse-upload (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/upload)) - :invoice-glimpse-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/textract-invoice)) - :invoice-glimpse-create-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/create-invoice)) - :invoice-glimpse-update-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/update-textract-invoice)) - :transaction-insights (wrap-client-redirect-unauthenticated (wrap-admin insights/page)) - :transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-admin insights/insight-table)) - :transaction-insight-rows (wrap-client-redirect-unauthenticated (wrap-admin insights/transaction-rows)) - :transaction-insight-code (wrap-client-redirect-unauthenticated (wrap-admin insights/code)) - :transaction-insight-disapprove (wrap-client-redirect-unauthenticated (wrap-admin insights/disapprove)) - :transaction-insight-explain (wrap-client-redirect-unauthenticated (wrap-admin insights/explain)) - :admin-ezcater-xls (wrap-client-redirect-unauthenticated (wrap-admin ezcater-xls/page)) - :search (wrap-client-redirect-unauthenticated (wrap-secure search/dialog-contents))}) +(def key->handler + (-> {:logout auth/logout + :impersonate (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin auth/impersonate))) + :admin-history (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page))) + :admin-history-search (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page))) + :admin-history-inspect (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/inspect))) + :active-client (wrap-client-redirect-unauthenticated (wrap-secure (wrap-secure company-dropdown/active-client))) + :company-dropdown-search-results + (wrap-client-redirect-unauthenticated (wrap-secure company-dropdown/dropdown-search-results)) + :company (wrap-client-redirect-unauthenticated (wrap-secure company/page)) + :company-1099 (wrap-client-redirect-unauthenticated (wrap-secure company-1099/page)) + :company-1099-vendor-table (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-table)) + :company-1099-vendor-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-dialog)) + :company-1099-vendor-save (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-save)) + :company-plaid (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/page)) + :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-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)) + :company-yodlee-provider-account-refresh (wrap-client-redirect-unauthenticated (wrap-admin company-yodlee/refresh-provider-account)) + :company-yodlee-provider-account-reauthenticate (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/reauthenticate)) + :company-reports (wrap-client-redirect-unauthenticated (wrap-secure company-reports/page)) + :company-reports-table (wrap-client-redirect-unauthenticated (wrap-secure company-reports/table)) + :company-reports-delete (wrap-client-redirect-unauthenticated (wrap-admin company-reports/delete-report)) + :invoice-glimpse (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/page)) + :invoice-glimpse-upload (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/upload)) + :invoice-glimpse-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/textract-invoice)) + :invoice-glimpse-create-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/create-invoice)) + :invoice-glimpse-update-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/update-textract-invoice)) + :transaction-insights (wrap-client-redirect-unauthenticated (wrap-admin insights/page)) + :transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-admin insights/insight-table)) + :transaction-insight-rows (wrap-client-redirect-unauthenticated (wrap-admin insights/transaction-rows)) + :transaction-insight-code (wrap-client-redirect-unauthenticated (wrap-admin insights/code)) + :transaction-insight-disapprove (wrap-client-redirect-unauthenticated (wrap-admin insights/disapprove)) + :transaction-insight-explain (wrap-client-redirect-unauthenticated (wrap-admin insights/explain)) + :admin-ezcater-xls (wrap-client-redirect-unauthenticated (wrap-admin ezcater-xls/page)) + :search (wrap-client-redirect-unauthenticated (wrap-secure search/dialog-contents))} + (into pos-sales/key->handler) + (into pos-expected-deposits/key->handler) + (into pos-tenders/key->handler) + (into pos-cash-drawer-shifts/key->handler) + (into pos-refunds/key->handler))) diff --git a/src/clj/auto_ap/ssr/grid_page_helper.clj b/src/clj/auto_ap/ssr/grid_page_helper.clj index 4f3068d8..50d666e4 100644 --- a/src/clj/auto_ap/ssr/grid_page_helper.clj +++ b/src/clj/auto_ap/ssr/grid_page_helper.clj @@ -1,28 +1,39 @@ (ns auto-ap.ssr.grid-page-helper (:require + [auto-ap.graphql.utils :refer [extract-client-ids]] + [auto-ap.query-params :as query-params] + [auto-ap.routes.utils + :refer [wrap-client-redirect-unauthenticated wrap-secure]] + [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] + [auto-ap.ssr.svg :as svg] [auto-ap.ssr.ui :refer [base-page]] [auto-ap.ssr.utils :refer [html-response]] - [hiccup2.core :as hiccup] + [auto-ap.time :as atime] + [malli.core :as m] [bidi.bidi :as bidi] - [auto-ap.ssr-routes :as ssr-routes] [cemerick.url :as url] [clojure.string :as str] - [auto-ap.ssr.svg :as svg] - [unilog.context :as lc] - [com.brunobonacci.mulog :as mu])) + [hiccup2.core :as hiccup] + [malli.transform :as mt2])) -(defn row* [gridspec user entity {:keys [flash? delete-after-settle?] :as options}] - (let [cells (mapv (fn [header] - (com/data-grid-cell {:class (if-let [show-starting (:show-starting header)] - (format "hidden %s:table-cell" show-starting) - (:class header))} - ((:render header) entity))) - (:headers gridspec)) +(defn row* [gridspec user entity {:keys [flash? delete-after-settle? request] :as options}] + (let [cells (->> gridspec + :headers + (filter (fn [h] + (if (and (:hide? h) + ((:hide? h) request)) + nil + h))) + (mapv (fn [header] + (com/data-grid-cell {:class (if-let [show-starting (:show-starting header)] + (format "hidden %s:table-cell" show-starting) + (:class header))} + ((:render header) entity))))) cells (conj cells (com/data-grid-right-stack-cell {} (into [:form [:input {:type :hidden :name "id" :value ((:id-fn gridspec) entity)}]] - ((:row-buttons gridspec) user entity))))] + ((:row-buttons gridspec) request entity))))] ;; TODO double check usage of row buttons user and identity in callers (apply com/data-grid-row {:class (when flash? "live-added") @@ -38,154 +49,241 @@ first :sort-icon)) -(defn sort-by-list [sort] +(defn sort-by-list [grid-spec sort] (if (seq sort) (into [:div.flex.gap-2.items-center + "sorted by" ] - (for [{:keys [name sort-icon ]} sort] + (for [{:keys [name sort-icon sort-key ]} sort] [:div.py-1.px-3.text-sm.rounded.bg-gray-100.dark:bg-gray-600.flex.items-center.gap-2.relative name [:div.h-4.w-4.mr-3 sort-icon] [:div {:class "absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white hover:scale-110 transition-all duration-300 bg-gray-400 border-2 border-white rounded-full -top-2 -right-2 dark:border-gray-900"} - [:div.h-4.w-4 svg/x] + [:a {:href (str (bidi/path-for ssr-routes/only-routes + (:route grid-spec)) "?remove-sort=" sort-key) + :hx-boost "true" + :hx-target (str "#" (:id grid-spec)) + + } + [:div.h-4.w-4 svg/x]] ]] )) "default sort")) -(defn table* [grid-spec user {:keys [start per-page clients flash-id sort] :as params}] +(defn table* [grid-spec user {{:keys [start per-page flash-id sort]} :parsed-query-params :as request}] (let [start (or start 0) per-page (or per-page 30) [entities total] ((:fetch-page grid-spec) - user - {:start start - :per-page per-page - :clients clients - :sort sort})] + request)] + (com/data-grid-card {:id (:id grid-spec) :title (:title grid-spec) :route (:route grid-spec) :start start :per-page per-page :total total - :subtitle [:div.flex.items-center.gap-2 - [:span (format "Total %s: %d, " (:entity-name grid-spec) total)] - (sort-by-list sort)] - :action-buttons ((:action-buttons grid-spec) user params) + :subtitle [:div.flex.items-center.gap-2 + [:span (format "Total %s: %d, " (:entity-name grid-spec) total)] + (sort-by-list grid-spec sort)] + :action-buttons ((:action-buttons grid-spec) request) :rows (for [entity entities] - (row* grid-spec user entity {:flash? (= flash-id ((:id-fn grid-spec) entity))})) + (row* grid-spec user entity {:flash? (= flash-id ((:id-fn grid-spec) entity)) :request request})) :thead-params {:hx-get (bidi/path-for ssr-routes/only-routes (:route grid-spec)) :hx-target (str "#" (:id grid-spec)) + :hx-indicator (str "#" (:id grid-spec)) :hx-trigger "sorted once" :hx-vals "js:{\"toggle-sort\": event.detail.key || \"\"}"} :headers (conj - (mapv - (fn [h] - (if (:sort-key h) - (com/data-grid-sort-header {:class (if-let [show-starting (:show-starting h)] + (->> grid-spec + :headers + (map + (fn [h] + (cond + (and (:hide? h) + ((:hide? h) request)) + nil + + (:sort-key h) + (com/data-grid-sort-header {:class (if-let [show-starting (:show-starting h)] + (format "hidden %s:table-cell" show-starting) + (:class h)) + :sort-key (:sort-key h)} + + [:div.flex.gap-4.items-center + (:name h) + [:div.h-6.w-6.text-gray-400.dark:text-gray-500 (sort-icon sort (:sort-key h))]]) + + :else + (com/data-grid-header {:class (if-let [show-starting (:show-starting h)] (format "hidden %s:table-cell" show-starting) (:class h)) :sort-key (:sort-key h)} + (:name h)) - [:div.flex.gap-4.items-center - (:name h) - [:div.h-6.w-6.text-gray-400.dark:text-gray-500 (sort-icon sort (:sort-key h))]]) - (com/data-grid-header {:class (if-let [show-starting (:show-starting h)] - (format "hidden %s:table-cell" show-starting) - (:class h)) - :sort-key (:sort-key h)} - (:name h)) - - )) - (:headers grid-spec)) + ))) + (filter identity) + (into [])) (com/data-grid-header {}))}))) - -(defn parse-sort [grid-spec q] - (if (not-empty q) - (into [] - (map (fn [k] - (let [[k v] (str/split k #":")] - {:sort-key (str k) - :asc (boolean (= "asc" v)) - :name (:name (first (filter #(= (str k) (:sort-key %)) (:headers grid-spec)))) - :sort-icon (if (= (boolean (= "asc" v)) true) - svg/sort-down - svg/sort-up)})) - (str/split q #","))) - [])) - -(defn toggle-sort [grid-spec q k] - (if ((set (map :sort-key q)) k) - (mapv - (fn [s] - (if (= (:sort-key s) - k) - (-> s - (update :asc - #(boolean (not %))) - (update :sort-icon (fn [x] - (if (= x svg/sort-down) - svg/sort-up - svg/sort-down)))) - s)) - q) - (conj q {:sort-key k - :asc true - :name (:name (first (filter #(= (str k) (:sort-key %)) (:headers grid-spec)))) - :sort-icon svg/sort-down}))) - (defn sort->query [s] (str/join "," (map (fn [k] (format "%s:%s" (:sort-key k) (if (= true (:asc k)) "asc" "desc"))) s))) -(defn params->query-string [q] - (-> q - (dissoc :client :session) - (update :sort sort->query) - (url/map->query))) +(defn default-unparse-query-params [query-params] + (reduce + (fn [query-params [k value]] + (assoc query-params k + (cond (= k :sort) + (sort->query value) -(defn extract-params [grid-spec {:keys [query-params hx-query-params identity session] :as request}] - (let [{hx-start "start" hx-per-page "per-page" hx-sort "sort" } hx-query-params - {q-start "start" q-per-page "per-page" q-sort "sort" q-toggle-sort "toggle-sort"} query-params] - (cond-> {} - hx-start (assoc :start (some-> hx-start not-empty (Long/parseLong ))) - q-start (assoc :start (some-> q-start not-empty (Long/parseLong ))) - hx-per-page (assoc :per-page (some-> hx-per-page not-empty (Long/parseLong ))) - q-per-page (assoc :per-page (some-> q-per-page not-empty (Long/parseLong ))) - hx-sort (assoc :sort (parse-sort grid-spec hx-sort)) - q-sort (assoc :sort (parse-sort grid-spec q-sort)) - (not-empty q-toggle-sort) (update :sort #(toggle-sort grid-spec % q-toggle-sort) ) - (:session request) (assoc :session (:session request)) - (:client-selection (:session request)) (assoc :client-selection (:client-selection (:session request))) - (:clients request) (assoc :clients (:clients request)) - (:client request) (assoc :client (:client request))))) + (instance? org.joda.time.base.AbstractInstant value) + (atime/unparse-local value atime/normal-date) + + (instance? Long value) + (str value) + + (instance? Double value) + (format "%.2f" value) + + (instance? Float value) + (format "%.2f" value) + + (keyword? value) + (name value) + + :else + value))) + query-params + query-params)) + +(defn default-parse-query-params [grid-spec] + (comp + (query-params/apply-remove-sort) + (query-params/apply-toggle-sort grid-spec) + (query-params/apply-date-range :date-range :start-date :end-date) + (query-params/parse-key :exact-match-id query-params/parse-long) + (query-params/parse-key :sort #(query-params/parse-sort grid-spec %)) + (query-params/parse-key :per-page query-params/parse-long) + (query-params/parse-key :start query-params/parse-long) + (query-params/parse-key :start-date query-params/parse-date) + (query-params/parse-key :end-date query-params/parse-date))) + +(defn wrap-trim-client-ids [handler] + (fn trim-client-ids [request] + (let [valid-clients (extract-client-ids (:clients request) + (:client request) + (:client-id (:parsed-query-params request)) + (when (:client-code (:parsed-query-params request)) + [:client/code (:client-code (:parsed-query-params request))])) + valid-clients (->> valid-clients + (take 20) + set)] + (println "VALID CLIENTS ARE" valid-clients) + (handler (assoc request :trimmed-clients valid-clients))))) + +(defn table-route [grid-spec] + (-> (fn table [{:keys [identity] :as request}] + (let [unparse-query-params (or (:unparse-query grid-spec) + default-unparse-query-params)] + (html-response (table* + grid-spec + identity + request) + :headers {"hx-push-url" (str "?" (url/map->query (unparse-query-params (:parsed-query-params request))))} + :oob (when-let [oob-render (:oob-render grid-spec)] + (oob-render request))))) + (wrap-trim-client-ids) + (query-params/wrap-parse-query-params (or (:parse-query-params grid-spec) + (default-parse-query-params grid-spec))) + (wrap-secure) + (wrap-client-redirect-unauthenticated))) + +(defn page-route [grid-spec ] + (-> (fn page [{:keys [identity] :as request}] + (base-page + request + (com/page {:nav (:nav grid-spec) + :page-specific (when-let [page-specific-nav (:page-specific-nav grid-spec)] + [:div#page-specific-nav (page-specific-nav request)]) + :client-selection (:client-selection (:session request)) + :clients (:clients request) + :client (:client request) + :identity (:identity request)} + (apply com/breadcrumbs {} (:breadcrumbs grid-spec)) + (table* grid-spec + identity + request)) + (:title grid-spec))) + (wrap-trim-client-ids) + (query-params/wrap-parse-query-params (or (:parse-query-params grid-spec) + (default-parse-query-params grid-spec))) + (wrap-secure) + (wrap-client-redirect-unauthenticated))) + +(def request-spec (m/schema [:map])) +(def entity-spec (m/schema [:map])) +(def header-spec (m/schema [:map + [:key :string] + [:name :string] + [:sort-key {:optional true} :string] + [:render [:=> [:cat entity-spec] :any]] + [:hide? {:optional true} [:=> [:cat entity-spec] :boolean]]])) +(def grid-spec (m/schema [:map + [:id :string] + [:nav vector?] + [:page-specific-nav + {:optional true + :default (fn [request])} + [:maybe [:=> + [:cat request-spec] + vector?]]] + [:id-fn {:default :db/id + :optional true} + [:=> + [:cat map?] + nat-int?]] + [:fetch-page [:=> + [:cat request-spec] + [:cat [:vector entity-spec] :int]]] + [:parse-query-params + {:optional true} + [:=> + [:cat [:map-of :keyword :any]] + [:map-of :keyword :any]]] + [:oob-render + {:optional true + :default (fn [request])} + [:=> + [:cat request-spec] + vector?]] + [:breadcrumbs [:vector vector?]] + [:title :string] + [:entity-name :string] + [:route :keyword] + [:action-buttons + {:default (fn [request]) + :optional true} + [:=> + [:cat request-spec] + [:maybe [:vector vector?]]]] + [:row-buttons + {:default (fn [request entity]) + :optional true} + [:=> + [:cat request-spec entity-spec] + [:maybe [:vector vector?]]]] + [:headers [:vector header-spec]]])) + +(defn build [grid-page] + (when-not (m/validate grid-spec grid-page) + (throw (ex-info "Could not validate grid" + (m/explain grid-spec grid-page)))) + (m/decode grid-spec grid-page (mt2/default-value-transformer {::mt2/add-optional-keys true}))) -(defn table [grid-spec {:keys [query-params hx-query-params identity session] :as request}] - (let [params (extract-params grid-spec request) - query-string (params->query-string params)] - (html-response (table* - grid-spec - identity - params - ) - :headers {"hx-push-url" (str "?" query-string)}))) -(defn page [grid-spec {:keys [identity] :as request}] - (base-page - request - (com/page {:nav (:nav grid-spec) - :client-selection (:client-selection (:session request)) - :clients (:clients request) - :client (:client request) - :identity (:identity request)} - (apply com/breadcrumbs {} (:breadcrumbs grid-spec)) - (table* grid-spec - identity - (extract-params grid-spec request))) - (:title grid-spec))) diff --git a/src/clj/auto_ap/ssr/pos/cash_drawer_shifts.clj b/src/clj/auto_ap/ssr/pos/cash_drawer_shifts.clj new file mode 100644 index 00000000..12cfe3fd --- /dev/null +++ b/src/clj/auto_ap/ssr/pos/cash_drawer_shifts.clj @@ -0,0 +1,139 @@ +(ns auto-ap.ssr.pos.cash-drawer-shifts + (:require + [auto-ap.datomic + :refer [add-sorter-fields + apply-pagination + apply-sort-3 + conn + merge-query + pull-many + query2]] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.pos.common :refer [date-range-field* total-field*]] + [auto-ap.time :as atime] + [bidi.bidi :as bidi] + [clj-time.coerce :as c] + [datomic.api :as dc] + [malli.core :as m])) + +;; always should be fast + +(defn filters [params] + [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" + "hx-get" (bidi/path-for ssr-routes/only-routes + :pos-cash-drawer-shift-table) + "hx-target" "#cash-drawer-shift-table" + "hx-indicator" "#cash-drawer-shift-table" + #_#_:hx-disabled-elt "find fieldset"} + + [:fieldset.space-y-6 + (date-range-field* params) + (total-field* params)]]) + +(def default-read '[:db/id + :cash-drawer-shift/paid-in :cash-drawer-shift/paid-out :cash-drawer-shift/expected-cash :cash-drawer-shift/opened-cash + [:cash-drawer-shift/date :xform clj-time.coerce/from-date] + {:cash-drawer-shift/client [:client/name :db/id :client/code]}]) + +(defn fetch-ids [db request] + (let [query-params (:parsed-query-params request) + query (cond-> {:query {:find [] + :in ['$ '[?clients ?start-date ?end-date]] + :where '[[(iol-ion.query/scan-cash-drawer-shifts $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]]} + :args [db [(:trimmed-clients request) + (some-> (:start-date query-params) c/to-date) + (some-> (:end-date query-params) c/to-date )]]} + (:sort query-params) (add-sorter-fields {"client" ['[?e :cash-drawer-shift/client ?c] + '[?c :client/name ?sort-client]] + "date" ['[?e :cash-drawer-shift/date ?sort-date]] + "paid-in" ['[?e :cash-drawer-shift/paid-in ?sort-paid-in]] + "paid-out" ['[?e :cash-drawer-shift/paid-out ?sort-paid-out]] + "expected-cash" ['[?e :cash-drawer-shift/expected-cash ?sort-expected-cash]] + "opened-cash" ['[?e :cash-drawer-shift/opened-cash ?sort-opened-cash]] + } + query-params) + + (:exact-match-id query-params) + (merge-query {:query {:in ['?e] + :where []} + :args [(:exact-match-id query-params)]}) + + true + (merge-query {:query {:find ['?sort-default '?e] + :where ['[?e :cash-drawer-shift/date ?sort-default]]}}))] + + (cond->> (query2 query) + true (apply-sort-3 query-params) + true (apply-pagination query-params)))) + +(defn hydrate-results [ids db _] + (let [results (->> (pull-many db default-read ids) + (group-by :db/id)) + cash-drawer-shifts (->> ids + (map results) + (map first))] + cash-drawer-shifts)) + +(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 "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"))} + ]})) + + +(def row* (partial helper/row* grid-page)) +(def table* (partial helper/table* grid-page)) + +(def key->handler + {:pos-cash-drawer-shifts (helper/page-route grid-page) + :pos-cash-drawer-shift-table (helper/table-route grid-page)}) diff --git a/src/clj/auto_ap/ssr/pos/common.clj b/src/clj/auto_ap/ssr/pos/common.clj new file mode 100644 index 00000000..ef37e0af --- /dev/null +++ b/src/clj/auto_ap/ssr/pos/common.clj @@ -0,0 +1,86 @@ +(ns auto-ap.ssr.pos.common + (:require [auto-ap.ssr.components :as com] + [auto-ap.time :as atime] + [auto-ap.ssr.svg :as svg])) + +;; TODO make date-input take clj date +;; TODO make total fields take decimals + +(defn date-range-field* [request] + [:div#date-range {} + (com/field {:label "Date Range"} + [:div.space-y-4 + [:div + (com/button-group {:name "date-range"} + (com/button-group-button {:size :small :value "all" :hx-trigger "click"} "All") + (com/button-group-button {:size :small :value "week" :hx-trigger "click"} "Week") + (com/button-group-button {:size :small :value "month" :hx-trigger "click"} "Month") + (com/button-group-button {:size :small :value "year" :hx-trigger "click"} "Year")) + ] + [:div.flex.space-x-1.items-baseline + (com/date-input {:name "start-date" + :value (some-> request + :parsed-query-params + :start-date + (atime/unparse-local atime/normal-date)) + :placeholder "Date" + :size :small}) + + (com/date-input {:name "end-date" + :value (some-> request + :parsed-query-params + :end-date + (atime/unparse-local atime/normal-date)) + :placeholder "Date" + :size :small})]])]) + +(defn processor-field* [request] + (com/field {:label "Processor"} + (com/radio {:size :small + :name "processor" + :options [{:value "" + :content "All"} + {:value "square" + :content [:div.flex.space-x-2 [:img.align-center {:src "/img/square.png" :style {:width "16px" :height "16px"}}] [:div "Square"]]} + {:value "doordash" + :content [:div.flex.space-x-2 [:img.align-center {:src "/img/doordash.png" :style {:width "16px" :height "16px"}}] [:div "Doordash"]]} + {:value "uber-eats" + :content [:div.flex.space-x-2 [:img.align-center {:src "/img/ubereats.png" :style {:width "16px" :height "16px"}}] [:div "Uber eats"]]} + {:value "grubhub" + :content [:div.flex.space-x-2 [:img.align-center {:src "/img/grubhub.png" :style {:width "16px" :height "16px"}}] [:div "Grubhub"]]} + {:value "koala" + :content [:div.flex.space-x-2 [:img.align-center {:src "/img/koala.png" :style {:width "16px" :height "16px"}}] [:div "Koala"]]} + {:value "ezcater" + :content [:div.flex.space-x-2 [:img.align-center {:src "/img/ezcater.png" :style {:width "16px" :height "16px"}}] [:div "EZCater"]]} + {:value "na" + :content "No Processor"}]}))) + +(defn total-field* [request] + (com/field {:label "Total"} + [:div.flex.space-x-4.items-baseline + (com/money-input {:name "total-gte" + :id "total-gte" + :hx-preserve "true" + :class "hot-filter" + :value (:total-gte (:parsed-query-params request)) + :placeholder "0.01" + :size :small}) + [:div.align-baseline + "to"] + (com/money-input {:name "total-lte" + :hx-preserve "true" + :id "total-lte" + :class "hot-filter" + :value (:total-lte (:parsed-query-params request)) + :placeholder "9999.34" + :size :small})])) + +(defn exact-match-id-field* [request] + (when-let [exact-match-id (:exact-match-id (:parsed-query-params request))] + [:div + (com/field {:label "Exact match"} + (com/pill {:color :primary} + [:span.inline-flex.gap-2 + exact-match-id + [:a {:href "?exact-match-id="} + [:div.h-4.w-4 svg/x]]]))])) diff --git a/src/clj/auto_ap/ssr/pos/expected_deposits.clj b/src/clj/auto_ap/ssr/pos/expected_deposits.clj new file mode 100644 index 00000000..a11b17bc --- /dev/null +++ b/src/clj/auto_ap/ssr/pos/expected_deposits.clj @@ -0,0 +1,197 @@ +(ns auto-ap.ssr.pos.expected-deposits + (:require + [auto-ap.datomic + :refer [add-sorter-fields + apply-pagination + apply-sort-3 + conn + merge-query + pull-many + query2]] + [auto-ap.graphql.utils :refer [extract-client-ids]] + [auto-ap.query-params :as query-params] + [auto-ap.routes.utils + :refer [wrap-client-redirect-unauthenticated wrap-secure]] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.pos.common :refer [date-range-field* total-field* exact-match-id-field*]] + [auto-ap.ssr.svg :as svg] + [auto-ap.time :as atime] + [bidi.bidi :as bidi] + [clj-time.coerce :as c] + [datomic.api :as dc] + [auto-ap.client-routes :as client-routes] + [malli.core :as m])) + +;; make params parsing composable + +(defn filters [request] + [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" + "hx-get" (bidi/path-for ssr-routes/only-routes + :pos-expected-deposit-table) + "hx-target" "#expected-deposit-table" + "hx-indicator" "#expected-deposit-table" + #_#_:hx-disabled-elt "find fieldset"} + + [:fieldset.space-y-6 + (date-range-field* request) + (exact-match-id-field* request)]]) + +(def default-read '[:db/id + :expected-deposit/location + :expected-deposit/total + :expected-deposit/fee + [:expected-deposit/date :xform clj-time.coerce/from-date] + {:expected-deposit/client [:client/name :db/id :client/code] + [:expected-deposit/status :xform iol-ion.query/ident] [:db/ident] + :transaction/_expected-deposit [:transaction/date :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 ['$ '[?clients ?start-date ?end-date]] + :where '[[(iol-ion.query/scan-expected-deposits $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]]} + :args [db [valid-clients + (some-> (:start-date query-params) c/to-date) + (some-> (:end-date query-params) c/to-date)]]} + (:sort query-params) (add-sorter-fields {"client" ['[?e :expected-deposit/client ?c] + '[?c :client/name ?sort-client]] + "location" ['[?e :expected-deposit/location ?sort-location]] + "date" ['[?e :expected-deposit/date ?sort-date]] + "total" ['[?e :expected-deposit/total ?sort-total]] + "fee" ['[?e :expected-deposit/fee ?sort-fee]]} + query-params) + + (:exact-match-id query-params) + (merge-query {:query {:in ['?e] + :where []} + :args [(:exact-match-id query-params)]}) + + + (:total-gte query-params) + (merge-query {:query {:in ['?total-gte] + :where ['[?e :expected-deposit/total ?a] + '[(>= ?a ?total-gte)]]} + :args [(:total-gte query-params)]}) + + (:total-lte query-params) + (merge-query {:query {:in ['?total-lte] + :where ['[?e :expected-deposit/total ?a] + '[(<= ?a ?total-lte)]]} + :args [(:total-lte query-params)]}) + + (:total query-params) + (merge-query {:query {:in ['?total] + :where ['[?e :expected-deposit/total ?expected-deposit-total] + '[(iol-ion.query/dollars= ?expected-deposit-total ?total)]]} + :args [(:total query-params)]}) + + true + (merge-query {:query {:find ['?sort-default '?e] + :where ['[?e :expected-deposit/date ?sort-default]]}}))] + + (cond->> (query2 query) + true (apply-sort-3 query-params) + true (apply-pagination query-params)))) + +(defn hydrate-results [ids db _] + (let [results (->> (pull-many db default-read ids) + (group-by :db/id)) + payments (->> ids + (map results) + (map first) + (map (fn get-totals [ed] + (assoc ed :totals + (->> (dc/q '[:find ?d4 (count ?c) (sum ?a) + :in $ ?ed + :where [?ed :expected-deposit/charges ?c] + [?c :charge/total ?a] + [?o :sales-order/charges ?c] + [?o :sales-order/date ?d] + [(clj-time.coerce/from-date ?d) ?d2] + [(auto-ap.time/localize ?d2) ?d3] + [(clj-time.coerce/to-local-date ?d3) ?d4]] + (dc/db conn) + (:db/id ed)) + (map (fn [[date count amount]] + {:date (c/to-date-time date) + :count count + :amount amount})))))))] + payments)) + +(defn fetch-page [args] + (let [db (dc/db conn) + {ids-to-retrieve :ids matching-count :count} (fetch-ids db args)] + + [(->> (hydrate-results ids-to-retrieve db args)) + matching-count])) + +(def grid-page + (helper/build + {:id "expected-deposit-table" + :nav (com/main-aside-nav) + :page-specific-nav filters + :fetch-page fetch-page + :parse-query-params (comp + (query-params/parse-key :total-gte query-params/parse-double) + (query-params/parse-key :total-lte query-params/parse-double) + (helper/default-parse-query-params grid-page)) + :oob-render + (fn [request] + [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)]) + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "POS"] + + [:a {:href (bidi/path-for ssr-routes/only-routes + :pos-expected-deposits)} + "Expected deposits"]] + :title "Expected deposits" + :entity-name "Expected deposit" + :route :pos-expected-deposit-table + :row-buttons (fn [_ e] + [ + (when (:expected-deposit/reference-link e) + (com/a-icon-button {:href (:expected-deposit/reference-link e)} + svg/external-link)) + (when-let [transaction-id (-> e (:transaction/_expected-deposit) first :db/id)] + (com/a-button {:href (str (bidi/path-for client-routes/routes + :transactions) + "?exact-match-id=" + transaction-id)} "Transaction"))]) + :headers [{:key "client" + :name "Client" + :sort-key "client" + :hide? (fn [args] + (= (count (:clients args)) 1)) + :render #(-> % :expected-deposit/client :client/code)} + {:key "date" + :name "Date" + :sort-key "date" + :render #(atime/unparse-local (:expected-deposit/date %) atime/standard-time)} + {:key "sales-date" + :name "Sales Date" + :sort-key "sales-date" + :render #(atime/unparse-local (:expected-deposit/sales-date %) atime/standard-time)} + {:key "total" + :name "Total" + :sort-key "total" + :render #(some->> % :expected-deposit/total (format "$%.2f"))} + {:key "fee" + :name "Fee" + :sort-key "fee" + :render #(some->> % :expected-deposit/fee (format "$%.2f"))}]})) + +(def row* (partial helper/row* grid-page)) +(def table* (partial helper/table* grid-page)) + +(def key->handler + {:pos-expected-deposits (helper/page-route grid-page) + :pos-expected-deposit-table (helper/table-route grid-page)}) diff --git a/src/clj/auto_ap/ssr/pos/refunds.clj b/src/clj/auto_ap/ssr/pos/refunds.clj new file mode 100644 index 00000000..49f741b1 --- /dev/null +++ b/src/clj/auto_ap/ssr/pos/refunds.clj @@ -0,0 +1,156 @@ +(ns auto-ap.ssr.pos.refunds + (:require + [auto-ap.datomic + :refer [add-sorter-fields + apply-pagination + apply-sort-3 + conn + merge-query + pull-many + query2]] + [auto-ap.graphql.utils :refer [extract-client-ids]] + [auto-ap.routes.utils + :refer [wrap-client-redirect-unauthenticated wrap-secure]] + [auto-ap.ssr.pos.common :refer [date-range-field* processor-field* total-field*]] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.svg :as svg] + [auto-ap.time :as atime] + [bidi.bidi :as bidi] + [clj-time.coerce :as c] + [datomic.api :as dc] + [clojure.set :as set] + [auto-ap.query-params :as query-params] + [malli.core :as m])) + +;; TODO refunds +;; always should be fast +;; make params parsing composable + +(defn filters [request] + [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" + "hx-get" (bidi/path-for ssr-routes/only-routes + :pos-refund-table) + "hx-target" "#refund-table" + "hx-indicator" "#refund-table" + #_#_:hx-disabled-elt "find fieldset"} + + [:fieldset.space-y-6 + (date-range-field* request) + (total-field* request)]]) + +(def default-read '[:db/id + :sales-refund/total + :sales-refund/fee + :sales-refund/type + [:sales-refund/date :xform clj-time.coerce/from-date] + {:sales-refund/client [:client/name :db/id :client/code]}]) + +(defn fetch-ids [db request] + (let [query-params (:parsed-query-params request) + query (cond-> {:query {:find [] + :in ['$ '[?clients ?start-date ?end-date]] + :where '[[(iol-ion.query/scan-sales-refunds $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]]} + :args [db [(:trimmed-clients request) + (some-> query-params :start-date c/to-date) + (some-> query-params :end-date c/to-date )]]} + (:sort query-params) (add-sorter-fields {"client" ['[?e :sales-refund/client ?c] + '[?c :client/name ?sort-client]] + "date" ['[?e :sales-refund/date ?sort-date]] + "total" ['[?e :sales-refund/total ?sort-total]] + "fee" ['[?e :sales-refund/fee ?sort-tip]] + "type" ['[?e :sales-refund/type ?sort-type]]} + query-params) + + (:exact-match-id query-params) + (merge-query {:query {:in ['?e] + :where []} + :args [(:exact-match-id query-params)]}) + + (:total-gte query-params) + (merge-query {:query {:in ['?total-gte] + :where ['[?e :sales-refund/total ?a] + '[(>= ?a ?total-gte)]]} + :args [(:total-gte query-params)]}) + + (:total-lte query-params) + (merge-query {:query {:in ['?total-lte] + :where ['[?e :sales-refund/total ?a] + '[(<= ?a ?total-lte)]]} + :args [(:total-lte query-params)]}) + + true + (merge-query {:query {:find ['?sort-default '?e] + :where ['[?e :sales-refund/date ?sort-default]]}}))] + + (cond->> (query2 query) + true (apply-sort-3 query-params) + true (apply-pagination query-params)))) + +(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 "refund-table" + :nav (com/main-aside-nav) + :page-specific-nav filters + :fetch-page fetch-page + :parse-query-params (comp + (query-params/parse-key :total-gte query-params/parse-double) + (query-params/parse-key :total-lte query-params/parse-double) + (helper/default-parse-query-params grid-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-refunds)} + "Refunds"]] + :title "Refunds" + :entity-name "Refund" + :route :pos-refund-table + :headers [{:key "client" + :name "Client" + :sort-key "client" + :hide? (fn [args] + (= (count (:clients args)) 1)) + :render #(-> % :sales-refund/client :client/code)} + {:key "date" + :name "Date" + :sort-key "date" + :render #(atime/unparse-local (:sales-refund/date %) atime/standard-time)} + {:key "total" + :name "Total" + :sort-key "total" + :render #(some->> % :sales-refund/total (format "$%.2f"))} + {:key "type" + :name "Type" + :sort-key "type" + :render :sales-refund/type} + {:key "fee" + :name "Fee" + :sort-key "fee" + :render #(some->> % :sales-refund/fee (format "$%.2f"))}]})) + +(def row* (partial helper/row* grid-page)) +(def table* (partial helper/table* grid-page)) + +(def key->handler + {:pos-refunds (helper/page-route grid-page) + :pos-refund-table (helper/table-route grid-page)}) diff --git a/src/clj/auto_ap/ssr/pos/sales_orders.clj b/src/clj/auto_ap/ssr/pos/sales_orders.clj new file mode 100644 index 00000000..7398c9f7 --- /dev/null +++ b/src/clj/auto_ap/ssr/pos/sales_orders.clj @@ -0,0 +1,263 @@ +(ns auto-ap.ssr.pos.sales-orders + (:require + [auto-ap.datomic + :refer [add-sorter-fields + apply-pagination + apply-sort-3 + conn + merge-query + pull-many + query2]] + [auto-ap.datomic.sales-orders :as d-sales] + [auto-ap.graphql.utils :refer [extract-client-ids]] + [auto-ap.query-params :as query-params] + [auto-ap.routes.utils + :refer [wrap-client-redirect-unauthenticated wrap-secure]] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.pos.common + :refer [date-range-field* processor-field* total-field*]] + [auto-ap.ssr.svg :as svg] + [auto-ap.time :as atime] + [bidi.bidi :as bidi] + [clj-time.coerce :as c] + [datomic.api :as dc] + [malli.core :as m])) + +(defn filters [request] + [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" + "hx-get" (bidi/path-for ssr-routes/only-routes + :pos-sales-table) + "hx-target" "#sales-table" + "hx-indicator" "#sales-table" + #_#_:hx-disabled-elt "find fieldset"} + + [:fieldset.space-y-6 + (date-range-field* request) + (total-field* request) + [:div + (com/field {:label "Payment Method"} + (com/radio {:size :small + :name "payment-method" + :options [{:value "" + :content "All"} + {:value "CASH" + :content "Cash"} + {:value "CARD" + :content "Card"} + {:value "SQUARE_GIFT_CARD" + :content "Gift Card"} + {:value "OTHER" + :content "Other"} + ]}))] + [:div + (processor-field* request)] + + [:div + (com/field {:label "Category"} + (com/text-input {:name "category" + :class "hot-filter" + :id "category" + :hx-preserve "true" + :value (:category (:parsed-query-params request)) + :placeholder "Fries" + :size :small}))]]]) + +(def default-read '[:db/id + :sales-order/external-id, + :sales-order/location, + [:sales-order/date, :xform clj-time.coerce/from-date] + :sales-order/total, + :sales-order/tax, + :sales-order/tip, + :sales-order/line-items, + :sales-order/discount, + :sales-order/returns, + :sales-order/service-charge, + :sales-order/vendor, + :sales-order/source, + :sales-order/reference-link, + {:sales-order/client [:client/name :db/id :client/code] + :sales-order/charges [ + :charge/type-name, + :charge/total, + :charge/tax, + :charge/tip, + :charge/external-id, + :charge/note, + :charge/date, + :charge/client, + :charge/location, + :charge/reference-link, + {:charge/processor [:db/ident]} {:expected-deposit/_charges [:db/id]}]}]) + +(defn fetch-ids [db request] + (let [query-params (:parsed-query-params request) + query (cond-> {:query {:find [] + :in ['$ '[?clients ?start-date ?end-date]] + :where '[[(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]]} + :args [db [(:trimmed-clients request) + (some-> (:start-date query-params) c/to-date) + (some-> (:end-date query-params) c/to-date)]]} + (:sort query-params) (add-sorter-fields {"client" ['[?e :sales-order/client ?c] + '[?c :client/name ?sort-client]] + "date" ['[?e :sales-order/date ?sort-date]] + "total" ['[?e :sales-order/total ?sort-total]] + "tax" ['[?e :sales-order/tax ?sort-tax]] + "tip" ['[?e :sales-order/tip ?sort-tip]] + "source" ['[?e :sales-order/source ?sort-source]] + "processor" ['[?e :sales-order/processor ?p] + '[?p :db/ident ?p2] + '[(name ?p2) ?sort-processor]]} + query-params) + + (:exact-match-id query-params) + (merge-query {:query {:in ['?e] + :where []} + :args [(:exact-match-id query-params)]}) + + (:total-gte query-params) + (merge-query {:query {:in ['?total-gte] + :where ['[?e :sales-order/total ?a] + '[(>= ?a ?total-gte)]]} + :args [(:total-gte query-params)]}) + + (:total-lte query-params) + (merge-query {:query {:in ['?total-lte] + :where ['[?e :sales-order/total ?a] + '[(<= ?a ?total-lte)]]} + :args [(:total-lte query-params)]}) + + (not-empty (:payment-method query-params)) + (merge-query {:query {:in ['?type-name] + :where ['[?e :sales-order/charges ?chg] + '[?chg :charge/type-name ?type-name]]} + :args [(:payment-method query-params)]}) + + (not-empty (:category query-params)) + (merge-query {:query {:in ['?category] + :where ['[?e :sales-order/line-items ?li] + '[?li :order-line-item/category ?category]]} + :args [(:category query-params)]}) + + (:processor query-params) + (merge-query {:query {:in ['?processor] + :where ['[?e :sales-order/charges ?chg] + '[?chg :charge/processor ?processor]]} + :args [(:processor query-params)]}) + + + true + (merge-query {:query {:find ['?sort-default '?e]}}))] + (clojure.pprint/pprint query) + (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)) + charges (->> ids + (map results) + (map first))] + charges)) + +(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 "sales-table" + :nav (com/main-aside-nav) + :page-specific-nav filters + :fetch-page fetch-page + :parse-query-params (comp + (query-params/parse-key :processor #(query-params/parse-keyword "ccp-processor" %)) + (query-params/parse-key :total-gte query-params/parse-double) + (query-params/parse-key :total-lte query-params/parse-double) + (helper/default-parse-query-params grid-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-sales)} + "Sales"]] + :title "Sales orders" + :entity-name "Sales orders" + :route :pos-sales-table + :action-buttons (fn [request] + (let [{:keys [total tax]} (d-sales/summarize-orders (:ids (fetch-ids (dc/db conn) request)))] + (when (and total tax) + [ + (com/pill {:color :primary} + (format "Total $%.2f" total) + ) + (com/pill {:color :secondary} + (format "Tax $%.2f" tax ) + )]))) + :row-buttons (fn [_ e] + (when (:sales-order/reference-link e) + [(com/a-icon-button {:href (:sales-order/reference-link e)} + svg/external-link)])) + :headers [{:key "client" + :name "Client" + :sort-key "client" + :hide? (fn [args] + (= (count (:clients args)) 1)) + :render #(-> % :sales-order/client :client/code)} + {:key "date" + :name "Date" + :sort-key "date" + :render #(atime/unparse-local (:sales-order/date %) atime/standard-time)} + {:key "source" + :name "Source" + :sort-key "source" + :render (fn [sales-order] + (when (:sales-order/source sales-order) + (com/pill {:color :primary } + (:sales-order/source sales-order))))} + {:key "total" + :name "Total" + :sort-key "total" + :render #(some->> % :sales-order/total (format "$%.2f"))} + {:key "tax" + :name "Tax" + :sort-key "tax" + :render #(some->> % :sales-order/tax (format "$%.2f"))} + {:key "tip" + :name "Tip" + :sort-key "tip" + :render #(some->> % :sales-order/tip (format "$%.2f"))} + {:key "Payment methods" + :name "Payment Methods" + :render (fn [sales-order] + [:div.flex.space-x-2 + (for [payment-method (->> sales-order :sales-order/charges (map :charge/type-name) set)] + (com/pill {:color :primary } + (condp = payment-method + "CASH" "cash" + "" nil + "ALL" nil + "CARD" "card" + "SQUARE_GIFT_CARD" "gift card" + "OTHER" "other" + nil)))])}]} + )) + + +(def row* (partial helper/row* grid-page)) +(def table* (partial helper/table* grid-page)) + +(def key->handler + {:pos-sales (helper/page-route grid-page) + :pos-sales-table (helper/table-route grid-page)}) diff --git a/src/clj/auto_ap/ssr/pos/tenders.clj b/src/clj/auto_ap/ssr/pos/tenders.clj new file mode 100644 index 00000000..27c4de7e --- /dev/null +++ b/src/clj/auto_ap/ssr/pos/tenders.clj @@ -0,0 +1,181 @@ +(ns auto-ap.ssr.pos.tenders + (:require + [auto-ap.datomic + :refer [add-sorter-fields + apply-pagination + apply-sort-3 + conn + merge-query + pull-many + query2]] + [auto-ap.query-params :as query-params] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.pos.common + :refer [date-range-field* processor-field* total-field*]] + [auto-ap.ssr.svg :as svg] + [auto-ap.time :as atime] + [bidi.bidi :as bidi] + [clj-time.coerce :as c] + [datomic.api :as dc] + [malli.core :as m] + [malli.transform :as mt2])) + +;; always should be fast + +(defn filters [request] + [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" + "hx-get" (bidi/path-for ssr-routes/only-routes + :pos-tender-table) + "hx-target" "#tender-table" + "hx-indicator" "#tender-table" + #_#_:hx-disabled-elt "find fieldset"} + + [:fieldset.space-y-6 + (date-range-field* request) + (processor-field* request) + (total-field* request)]]) + +(def default-read '[:db/id + :charge/reference-link + :charge/total + :charge/tip + [:charge/date :xform clj-time.coerce/from-date] + {:charge/client [:client/name :db/id :client/code] + [:charge/processor :xform iol-ion.query/ident] [:db/ident] + :expected-deposit/_charges [:expected-deposit/date :db/id]}]) + +(defn fetch-ids [db request] + (let [query-params (:parsed-query-params request) + query (cond-> {:query {:find [] + :in ['$ '[?clients ?start-date ?end-date]] + :where '[[(iol-ion.query/scan-charges $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]]} + :args [db [(:trimmed-clients request) + (some-> (:start-date query-params) c/to-date) + (some-> (:end-date query-params) c/to-date)]]} + (:sort query-params) (add-sorter-fields {"client" ['[?e :charge/client ?c] + '[?c :client/name ?sort-client]] + "date" ['[?e :charge/date ?sort-date]] + "total" ['[?e :charge/total ?sort-total]] + "tip" ['[?e :charge/tip ?sort-tip]] + "processor" ['[?e :charge/processor ?p] + '[?p :db/ident ?p2] + '[(name ?p2) ?sort-processor]]} + query-params) + + (:exact-match-id query-params) + (merge-query {:query {:in ['?e] + :where []} + :args [(:exact-match-id query-params)]}) + + (:total-gte query-params) + (merge-query {:query {:in ['?total-gte] + :where ['[?e :charge/total ?a] + '[(>= ?a ?total-gte)]]} + :args [(:total-gte query-params)]}) + + (:total-lte query-params) + (merge-query {:query {:in ['?total-lte] + :where ['[?e :charge/total ?a] + '[(<= ?a ?total-lte)]]} + :args [(:total-lte query-params)]}) + + (:processor query-params) + (merge-query {:query {:in '[?processor] + :where ['[?e :charge/processor ?processor]]} + :args [(:processor query-params)]}) + + + true + (merge-query {:query {:find ['?sort-default '?e]}}))] + + (cond->> (query2 query) + true (apply-sort-3 query-params) + true (apply-pagination query-params)))) + +(defn hydrate-results [ids db _] + (let [results (->> (pull-many db default-read ids) + (group-by :db/id)) + charges (->> ids + (map results) + (map first))] + charges)) + +(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 "tender-table" + :nav (com/main-aside-nav) + :page-specific-nav filters + :fetch-page fetch-page + :parse-query-params (comp + (query-params/parse-key :processor #(query-params/parse-keyword "ccp-processor" %)) + (query-params/parse-key :total-gte query-params/parse-double) + (query-params/parse-key :total-lte query-params/parse-double) + (helper/default-parse-query-params grid-page)) + :oob-render + (fn [request] + [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)]) + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "POS"] + + [:a {:href (bidi/path-for ssr-routes/only-routes + :pos-tenders)} + "Tenders"]] + :title "Tenders" + :entity-name "Tender" + :route :pos-tender-table + :row-buttons (fn [request e] + (when (:charge/reference-link e) + [(com/a-icon-button {:href (:charge/reference-link e)} + svg/external-link)])) + :headers [{:key "client" + :name "Client" + :sort-key "client" + :hide? (fn [request] + (= (count (:clients request)) 1)) + :render #(-> % :charge/client :client/code)} + {:key "date" + :name "Date" + :sort-key "date" + :render #(atime/unparse-local (:charge/date %) atime/standard-time)} + {:key "total" + :name "Total" + :sort-key "total" + :render #(some->> % :charge/total (format "$%.2f"))} + {:key "processor" + :name "Processor" + :sort-key "processor" + :render (fn [sales-order] + (when (:charge/processor sales-order) + (com/pill {:color :primary } + (name (:charge/processor sales-order)))))} + {:key "tip" + :name "Tip" + :sort-key "tip" + :render #(some->> % :charge/tip (format "$%.2f"))} + {:key "links" + :name "Links" + :render (fn [entity] + (when-let [expected-deposit-id (some->> entity :expected-deposit/_charges first :db/id)] + [:a {:href (str (bidi/path-for ssr-routes/only-routes + :pos-expected-deposits) + "?exact-match-id=" expected-deposit-id) + :hx-boost "true"} + (com/pill {:color :secondary} "expected deposit")]))}]})) + + +(def row* (partial helper/row* grid-page)) +(def table* (partial helper/table* grid-page)) + +(def key->handler + {:pos-tenders (helper/page-route grid-page) + :pos-tender-table (helper/table-route grid-page)}) diff --git a/src/clj/auto_ap/ssr/ui.clj b/src/clj/auto_ap/ssr/ui.clj index c2f72c99..b10c0e4f 100644 --- a/src/clj/auto_ap/ssr/ui.clj +++ b/src/clj/auto_ap/ssr/ui.clj @@ -21,7 +21,7 @@ [:title (str "Integreat | " page-name)] [:link {:href "/css/font.min.css", :rel "stylesheet"}] [:link {:rel "icon" :type "image/png" :href "/favicon.png"}] - [:link {:rel "stylesheet", :href "/css/react-datepicker.min.inc.css"}] + #_[:link {:rel "stylesheet", :href "/css/react-datepicker.min.inc.css"}] [:link {:rel "stylesheet", :href "/output.css"}] [:script {:src "https://unpkg.com/hyperscript.org@0.9.7/dist/_hyperscript.min.js"}] @@ -30,14 +30,32 @@ #_[:script {:src "https://unpkg.com/htmx.org@1.8.4" :integrity "sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV" :crossorigin= "anonymous"}] - [:script {:src "https://unpkg.com/htmx.org@1.9.0/dist/htmx.js" + [:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.min.js" :crossorigin= "anonymous"}] [: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"}]] + [:link {:rel "stylesheet" :href "https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.1.4/dist/css/datepicker.min.css"}] + + [:script {:type "text/javascript" :src "https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.1.4/dist/js/datepicker-full.min.js"}] [:script {:type "text/javascript", :src "https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.7/dist/autoComplete.min.js"}] [:script {:src "https://unpkg.com/dropzone@5.9.3/dist/min/dropzone.min.js"}] [:link {:rel "stylesheet" :href "https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" :type "text/css"}] + [:style + " +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ +} +input[type=number] { + -moz-appearance:textfield; /* Firefox */ +} + + " + + ] [:body {:hx-ext "disable-submit"} contents [:script {:src "/js/flowbite.min.js"}]]])) diff --git a/src/clj/auto_ap/ssr/utils.clj b/src/clj/auto_ap/ssr/utils.clj index 929d5d64..df5d8968 100644 --- a/src/clj/auto_ap/ssr/utils.clj +++ b/src/clj/auto_ap/ssr/utils.clj @@ -5,14 +5,21 @@ [hiccup2.core :as hiccup] [clojure.string :as str])) -(defn html-response [hiccup & {:keys [status headers] :or {status 200 headers {}}}] +(defn html-response [hiccup & {:keys [status headers oob] :or {status 200 headers {} oob []}}] {:status status :headers (into {"Content-Type" "text/html"} headers) :body (str (hiccup/html {} - hiccup))}) + hiccup) + "\n" + (str/join "\n" + (map (fn [o] + (hiccup/html + {} + o)) + oob)))}) (defn wrap-error-response [handler] (fn [request] diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index cd0d1949..282fc13d 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -1,6 +1,7 @@ (ns auto-ap.ssr-routes) -(def routes {"logout" :logout +(def routes {"impersonate" :impersonate + "logout" :logout "search" :search "invoice" {"/glimpse" {"" {:get :invoice-glimpse :post :invoice-glimpse-upload @@ -19,6 +20,17 @@ ["/disapprove/" [#"\d+" :transaction-id]] {:delete :transaction-insight-disapprove} ["/rows/" [#"\d+" :after]] {:get :transaction-insight-rows} ["/explain/" [#"\d+" :transaction-id]] {:get :transaction-insight-explain}}} + "pos" {"/sales" {"" {:get :pos-sales} + "/table" {:get :pos-sales-table}} + "/expected-deposit" {"" {:get :pos-expected-deposits} + "/table" {:get :pos-expected-deposit-table}} + "/tenders" {"" {:get :pos-tenders} + "/table" {:get :pos-tender-table}} + "/refunds" {"" {:get :pos-refunds} + "/table" {:get :pos-refund-table}} + "/cash-drawer-shifts" {"" {:get :pos-cash-drawer-shifts} + "/table" {:get :pos-cash-drawer-shift-table}}} + "company" {"" :company "/dropdown" :company-dropdown-search-results "/active" {:put :active-client} diff --git a/src/cljs/auto_ap/views/components/layouts.cljs b/src/cljs/auto_ap/views/components/layouts.cljs index e74cf2e3..1b8c0eb6 100644 --- a/src/cljs/auto_ap/views/components/layouts.cljs +++ b/src/cljs/auto_ap/views/components/layouts.cljs @@ -173,8 +173,8 @@ :href (bidi/path-for routes/routes :payments)} "Payments" ] (when (= "admin" (:user/role @user)) - [:a.navbar-item {:class [(active-when ap = :sales-orders)] - :href (bidi/path-for routes/routes :sales-orders)} + [:a.navbar-item {:class [(active-when ap = :pos-sales)] + :href (str (bidi/path-for ssr-routes/only-routes :pos-sales) "?date-range=week")} "POS" ]) [:a.navbar-item {:class [(active-when ap = :transactions)] :href (bidi/path-for routes/routes :transactions)} diff --git a/src/cljs/auto_ap/views/pages/admin/users/table.cljs b/src/cljs/auto_ap/views/pages/admin/users/table.cljs index 3decaa90..7baa9576 100644 --- a/src/cljs/auto_ap/views/pages/admin/users/table.cljs +++ b/src/cljs/auto_ap/views/pages/admin/users/table.cljs @@ -1,11 +1,32 @@ (ns auto-ap.views.pages.admin.users.table - (:require - [clojure.string :as str] - [re-frame.core :as re-frame] - [auto-ap.views.utils :refer [action-cell-width]] - [auto-ap.views.pages.admin.users.form :as form] + (:require [auto-ap.views.components.buttons :as buttons] - [auto-ap.views.components.grid :as grid])) + [auto-ap.views.components.grid :as grid] + [auto-ap.views.pages.admin.users.form :as form] + [auto-ap.views.utils + :refer [action-cell-width dispatch-event with-user]] + [clojure.string :as str] + [re-frame.core :as re-frame])) + +(re-frame/reg-event-fx + ::impersonated + (fn [_ [_ impersonate-jwt]] + (println "SUCCESED") + (.setItem js/localStorage "jwt" impersonate-jwt) + (.removeItem js/localStorage "last-client-id" nil) + (.removeItem js/localStorage "last-selected-clients" nil) + (.reload (.-location js/document ) true) + {})) + +(re-frame/reg-event-fx + ::impersonate + [with-user] + (fn [{:keys [db user]} [_ impersonate-jwt]] + (js/alert "HI") + + {:http {:method "GET" + :uri (str "/impersonate?jwt=" impersonate-jwt) + :on-success [::impersonated impersonate-jwt]}})) (re-frame/reg-event-fx ::params-changed @@ -50,11 +71,7 @@ [grid/cell {} role] [grid/cell {} (str/join ", " (map :name clients))] [grid/cell {} - [:a.button {:on-click (fn [] - (.setItem js/localStorage "jwt" (:impersonate-jwt c)) - (.removeItem js/localStorage "last-client-id" nil) - (.removeItem js/localStorage "last-selected-clients" nil) - (.reload (.-location js/document ) true))} + [:a.button {:on-click (dispatch-event [::impersonate (:impersonate-jwt c)])} "Impersonate"]