Merge branch 'ssr-sales'

This commit is contained in:
2023-09-28 23:22:53 -07:00
37 changed files with 2279 additions and 697 deletions

View File

@@ -48,6 +48,8 @@
[client end])))) [client end]))))
(defn can-see-client? [identity client] (defn can-see-client? [identity client]
(when (not client) (when (not client)
(println "WARNING - permission checking for null client")) (println "WARNING - permission checking for null client"))
@@ -72,6 +74,51 @@
(time/plus (time/days 1)) (time/plus (time/days 1))
coerce/to-date)) 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] (defn scan-invoices [db clients start end]
(for [c clients (for [c clients
:let [c (entid db c)] :let [c (entid db c)]
@@ -107,3 +154,6 @@
[c (or start #inst "2001-01-01T08:00:00.000-00:00") ] [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") ]))] [c (or (next-day end) #inst "2030-03-05T08:00:00.000-00:00") ]))]
[(:e r) (first (:v r)) (second (:v r))])) [(:e r) (first (:v r)) (second (:v r))]))
(defn ident [x]
(:db/ident x))

View File

@@ -0,0 +1 @@
{:xforms [clj-time.coerce/to-date clj-time.coerce/from-date iol-ion.query/ident ]}

View File

@@ -16,3 +16,7 @@ htmx.defineExtension('disable-submit', {
*/ */
} }
}) })
initDatepicker = function(elem) {
elem.dp = new Datepicker(elem, {format: "mm/dd/yyyy", autohide: true});
}

View File

@@ -1075,6 +1075,10 @@ input:checked + .toggle-bg {
left: 0px; left: 0px;
} }
.left-1\/2 {
left: 50%;
}
.right-0 { .right-0 {
right: 0px; right: 0px;
} }
@@ -1091,6 +1095,10 @@ input:checked + .toggle-bg {
top: 0.5rem; top: 0.5rem;
} }
.top-2\/4 {
top: 50%;
}
.top-5 { .top-5 {
top: 1.25rem; top: 1.25rem;
} }
@@ -1348,6 +1356,10 @@ input:checked + .toggle-bg {
width: 1rem; width: 1rem;
} }
.w-48 {
width: 12rem;
}
.w-5 { .w-5 {
width: 1.25rem; width: 1.25rem;
} }
@@ -1408,11 +1420,21 @@ input:checked + .toggle-bg {
flex-basis: 25%; 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 { .-translate-x-full {
--tw-translate-x: -100%; --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)); 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 { .-translate-y-full {
--tw-translate-y: -100%; --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)); 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; transform: none;
} }
@keyframes pulse {
50% {
opacity: .5;
}
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes spin { @keyframes spin {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
@@ -1476,6 +1508,12 @@ input:checked + .toggle-bg {
list-style-type: none; list-style-type: none;
} }
.appearance-none {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.grid-cols-3 { .grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
@@ -1520,6 +1558,10 @@ input:checked + .toggle-bg {
align-items: center; align-items: center;
} }
.items-baseline {
align-items: baseline;
}
.items-stretch { .items-stretch {
align-items: stretch; align-items: stretch;
} }
@@ -1585,6 +1627,18 @@ input:checked + .toggle-bg {
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); 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]) { .space-y-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0; --tw-space-y-reverse: 0;
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
@@ -1662,6 +1716,10 @@ input:checked + .toggle-bg {
border-radius: 0.5rem; border-radius: 0.5rem;
} }
.rounded-md {
border-radius: 0.375rem;
}
.rounded-l-lg { .rounded-l-lg {
border-top-left-radius: 0.5rem; border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem; border-bottom-left-radius: 0.5rem;
@@ -1677,6 +1735,11 @@ input:checked + .toggle-bg {
border-top-right-radius: 0.25rem; border-top-right-radius: 0.25rem;
} }
.rounded-t-lg {
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
.border { .border {
border-width: 1px; border-width: 1px;
} }
@@ -2052,6 +2115,10 @@ input:checked + .toggle-bg {
text-align: right; text-align: right;
} }
.align-baseline {
vertical-align: baseline;
}
.text-2xl { .text-2xl {
font-size: 1.5rem; font-size: 1.5rem;
line-height: 2rem; line-height: 2rem;
@@ -2114,10 +2181,19 @@ input:checked + .toggle-bg {
line-height: 2.25rem; line-height: 2.25rem;
} }
.leading-none {
line-height: 1;
}
.leading-tight { .leading-tight {
line-height: 1.25; line-height: 1.25;
} }
.text-black {
--tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity));
}
.text-blue-400 { .text-blue-400 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(51 176 238 / var(--tw-text-opacity)); color: rgb(51 176 238 / var(--tw-text-opacity));
@@ -2511,6 +2587,10 @@ input:checked + .toggle-bg {
text-decoration-line: underline; text-decoration-line: underline;
} }
.focus\:z-10:focus {
z-index: 10;
}
.focus\:border-blue-500:focus { .focus\:border-blue-500:focus {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(0 156 234 / var(--tw-border-opacity)); 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)); 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 { .focus\:outline-none:focus {
outline: 2px solid transparent; outline: 2px solid transparent;
outline-offset: 2px; outline-offset: 2px;
@@ -2573,6 +2658,11 @@ input:checked + .toggle-bg {
--tw-ring-color: rgb(175 211 130 / var(--tw-ring-opacity)); --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 { .focus\:ring-primary-500:focus {
--tw-ring-opacity: 1; --tw-ring-opacity: 1;
--tw-ring-color: rgb(121 181 46 / var(--tw-ring-opacity)); --tw-ring-color: rgb(121 181 46 / var(--tw-ring-opacity));
@@ -2714,6 +2804,11 @@ input:checked + .toggle-bg {
--tw-bg-opacity: 0.8; --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) { :is(.dark .dark\:text-blue-300) {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(102 196 242 / var(--tw-text-opacity)); 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)); 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) { :is(.dark .dark\:ring-offset-gray-800) {
--tw-ring-offset-color: #1F2937; --tw-ring-offset-color: #1F2937;
} }
@@ -2868,11 +2967,21 @@ input:checked + .toggle-bg {
border-color: rgb(121 181 46 / var(--tw-border-opacity)); 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) { :is(.dark .dark\:focus\:ring-blue-500:focus) {
--tw-ring-opacity: 1; --tw-ring-opacity: 1;
--tw-ring-color: rgb(0 156 234 / var(--tw-ring-opacity)); --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) { :is(.dark .dark\:focus\:ring-blue-800:focus) {
--tw-ring-opacity: 1; --tw-ring-opacity: 1;
--tw-ring-color: rgb(0 62 94 / var(--tw-ring-opacity)); --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)); --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) { :is(.dark .dark\:focus\:ring-green-800:focus) {
--tw-ring-opacity: 1; --tw-ring-opacity: 1;
--tw-ring-color: rgb(48 72 18 / var(--tw-ring-opacity)); --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)); --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) { :is(.dark .group:hover .dark\:group-hover\:text-white) {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity)); color: rgb(255 255 255 / var(--tw-text-opacity));

View File

@@ -1806,6 +1806,28 @@
:db/valueType :db.type/tuple :db/valueType :db.type/tuple
:db/tupleAttrs [ :payment/client :payment/date] :db/tupleAttrs [ :payment/client :payment/date]
:db/cardinality :db.cardinality/one :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}] :db/index true}]

View File

@@ -180,4 +180,48 @@
{:user/name "hydrate-tuples"}) {: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"})
) )

View File

@@ -623,14 +623,16 @@
(defn apply-pagination-raw [args results] (defn apply-pagination-raw [args results]
{:entries (->> results {:entries (->> results
(drop (:start args 0)) (drop (or (:start args) 0))
(take (:count args (or (:per-page args) default-pagination-size)))) (take (or (:count args) (:per-page args) default-pagination-size)))
:count (count results)}) :count (count results)})
(defn apply-pagination [args results] (defn apply-pagination [args results]
{:ids (->> results {:ids (->> results
(drop (:start args 0)) (drop (or (:start args) 0))
(take (:count args (or (:per-page args) default-pagination-size))) (take (or (:count args )
(:per-page args)
default-pagination-size))
(map last)) (map last))
:count (count results)}) :count (count results)})

View File

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

View File

@@ -70,22 +70,17 @@
:else :else
visible-clients) visible-clients)
(take 3) (take 10)
set) set)
_ (mu/log ::selected-clients _ (mu/log ::selected-clients
:selected-clients selected-clients) :selected-clients selected-clients)
query (cond-> {:query {:find [] query (cond-> {:query {:find []
:in ['$ '[?c ...]] :in ['$ '[?clients ?start-date ?end-date]]
:where []} :where '[[(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]]}
:args [db selected-clients]} :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] (:sort args) (add-sorter-fields-2 {"client" ['[?e :sales-order/client ?c]
'[?c :client/name ?sort-client]] '[?c :client/name ?sort-client]]
"location" ['[?e :sales-order/location ?sort-location]] "location" ['[?e :sales-order/location ?sort-location]]
@@ -169,3 +164,8 @@
matching-count matching-count
(summarize-orders ids-to-retrieve)])) (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)))

View File

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

View File

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

View File

@@ -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] (defn logout [request]
{:status 301 {:status 301
:headers {"Location" "/login"} :headers {"Location" "/login"}
:session {}}) :session {}})
(defn impersonate [request]
{:status 200
:session {:identity (dissoc (jwt/unsign (get-in request [:query-params "jwt"])
(:jwt-secret env)
{:alg :hs512})
:exp)}})

View File

@@ -44,161 +44,117 @@
client-id client-id
vendor-id))) vendor-id)))
(defn get-1099-companies [user {:keys [clients] :as args}] (defn fetch-page [{:keys [trimmed-clients parsed-query-params] :as request user :identity}]
(let [valid-clients (extract-client-ids (:clients args) (let [results (dc/q '[:find
(:client-id args) (pull ?c [:client/code :db/id])
(when (:client-code args) (pull ?v vendor-read)
[:client/code (:client-code args)])) (sum ?a)
results (cond :with ?d
clients :in $ [?c ...] vendor-read
(dc/q '[:find :where
(pull ?c [:client/code :db/id]) [?p :payment/client ?c]
(pull ?v vendor-read) [?p :payment/date ?d ]
(sum ?a) [(>= ?d #inst "2022-01-01T08:00")]
:with ?d [(< ?d #inst "2023-01-01T08:00")]
:in $ [?c ...] vendor-read [?p :payment/type :payment-type/check]
:where [?p :payment/amount ?a]
[?p :payment/client ?c] [?p :payment/vendor ?v]]
[?p :payment/date ?d ] (dc/db conn)
[(>= ?d #inst "2022-01-01T08:00")] trimmed-clients
[(< ?d #inst "2023-01-01T08:00")] vendor-read)
[?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))
all (->> results all (->> results
(filter (fn [[_ _ a]] (filter (fn [[_ _ a]]
(>= (or a 0.0) 600.0))) (>= (or a 0.0) 600.0)))
(sort-by (fn [[client _ amount]] (sort-by (fn [[client _ amount]]
[(:client/code client ) amount])) [(:client/code client ) amount]))
(into [])) (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)])) [(:entries paginated) (:count paginated)]))
(def grid-page {:id "vendor-table" (def grid-page
:nav (com/company-aside-nav) (helper/build
:id-fn (comp :db/id second) {:id "vendor-table"
:fetch-page (fn [user args] :nav (com/company-aside-nav)
(get-1099-companies user args) :id-fn (comp :db/id second)
#_(r/get-graphql (into args {:id user}))) :fetch-page fetch-page
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:company)} :company)}
"My Company"] "My Company"]
[:a {:href (bidi/path-for ssr-routes/only-routes [:a {:href (bidi/path-for ssr-routes/only-routes
:company-1099)} :company-1099)}
"1099 Vendor Info"]] "1099 Vendor Info"]]
:title "1099 Vendors" :title "1099 Vendors"
:entity-name "Vendors" :entity-name "Vendors"
:route :company-1099-vendor-table :route :company-1099-vendor-table
:action-buttons (fn [user _] :row-buttons (fn [request e]
nil) [(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes
:row-buttons (fn [user e] :company-1099-vendor-dialog
[(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes :vendor-id (:db/id (second e)))
:company-1099-vendor-dialog "?"
:vendor-id (:db/id (second e))) (url/map->query {:client-id (:db/id (first e))}))
"?" :hx-ext "debug"
(url/map->query {:client-id (:db/id (first e))})) :hx-target "#modal-holder"
:hx-ext "debug" :hx-swap "outerHTML"}
:hx-target "#modal-holder" svg/pencil)])
:hx-swap "outerHTML"} :headers [{:key "Client"
svg/pencil)]) :name "Client"
:headers [{:key "Client" :sort-key "client"
:name "Client" :render (comp :client/code first)}
:sort-key "client" {:key "vendor-name"
:render (comp :client/code first)} :name "Vendor Name"
{:key "vendor-name" :sort-key "vendor"
:name "Vendor Name" :render (fn [[_ vendor]]
:sort-key "vendor" [:div.flex.whitespace-nowrap.items-center.gap-4
:render (fn [[_ vendor]] [:div [:div (:vendor/name vendor)]
[:div.flex.whitespace-nowrap.items-center.gap-4 [:div.text-sm.text-gray-400
[:div [:div (:vendor/name vendor)] (or (-> vendor :vendor/legal-entity-name not-empty)
[:div.text-sm.text-gray-400 (str (-> vendor :vendor/legal-entity-first-name) " "
(or (-> vendor :vendor/legal-entity-name not-empty) (-> vendor :vendor/legal-entity-middle-name) " "
(str (-> vendor :vendor/legal-entity-first-name) " " (-> vendor :vendor/legal-entity-last-name)))]]
(-> vendor :vendor/legal-entity-middle-name) " " (when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)]
(-> vendor :vendor/legal-entity-last-name)))]] (com/pill
(when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)] {:class "text-xs font-medium"
(com/pill :color :primary}
{:class "text-xs font-medium" (str/capitalize t99-type))
:color :primary} )])}
(str/capitalize t99-type)) {:key "tin"
)])} :name "TIN"
{:key "tin" :sort-key "tin"
:name "TIN" :show-starting "md"
:sort-key "tin" :render (fn [[_ vendor]]
:show-starting "md" [:div.flex.gap-4
:render (fn [[_ vendor]] (when-let [tin (-> vendor :vendor/legal-entity-tin)]
[:div.flex.gap-4 [:span {:class "text-xs font-medium py-0.5 "}
(when-let [tin (-> vendor :vendor/legal-entity-tin)] tin])
[:span {:class "text-xs font-medium py-0.5 "} (when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)]
tin]) (com/pill {:class "text-xs font-medium"
(when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)] :color :yellow}
(com/pill {:class "text-xs font-medium" (name tin-type)))]
:color :yellow} )}
(name tin-type)))] {:key "address"
)} :name "Address"
{:key "address" :sort-key "address"
:name "Address" :show-starting "lg"
:sort-key "address" :render (fn [[_ vendor]]
:show-starting "lg" (if (-> vendor :vendor/address :address/street1)
:render (fn [[_ vendor]] [:div
(if (-> vendor :vendor/address :address/street1) [:div (-> vendor :vendor/address :address/street1)] " "
[:div [:div
[:div (-> vendor :vendor/address :address/street1)] " " (-> vendor :vendor/address :address/street2)] " "
[:div [:div
(-> vendor :vendor/address :address/street2)] " " (-> vendor :vendor/address :address/city) " "
[:div (-> vendor :vendor/address :address/state) ","
(-> vendor :vendor/address :address/city) " " (-> vendor :vendor/address :address/zip)]]
(-> vendor :vendor/address :address/state) "," [:p.text-sm.italic.text-gray-400 "No address"]))}
(-> vendor :vendor/address :address/zip)]] {:key "paid"
[:p.text-sm.italic.text-gray-400 "No address"]))} :name "Paid"
{:key "paid" :sort-key "paid"
:name "Paid" :render (fn [[_ _ paid]]
:sort-key "paid" (com/pill {:class "text-xs font-medium"
:render (fn [[_ _ paid]] :color :primary}
(com/pill {:class "text-xs font-medium" "Paid $" (Math/round paid)))}]}))
:color :primary}
"Paid $" (Math/round paid)))}]})
@@ -317,5 +273,5 @@
"Save")]]] "Save")]]]
[:div])]])))) [:div])]]))))
(def vendor-table (partial helper/table grid-page)) (def vendor-table (helper/table-route grid-page))
(def page (partial helper/page grid-page)) (def page (helper/page-route grid-page))

View File

@@ -9,8 +9,7 @@
pull-attr pull-attr
pull-many-by-id pull-many-by-id
query2]] query2]]
[auto-ap.graphql.utils [auto-ap.graphql.utils :refer [assert-can-see-client]]
:refer [assert-can-see-client extract-client-ids]]
[auto-ap.logging :as alog] [auto-ap.logging :as alog]
[auto-ap.plaid.core :as p] [auto-ap.plaid.core :as p]
[auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr-routes :as ssr-routes]
@@ -36,43 +35,36 @@
:plaid-account/balance :plaid-account/balance
:plaid-account/name]}]) :plaid-account/name]}])
(defn raw-graphql-ids [db args] (defn fetch-ids [db request]
(let [valid-clients (extract-client-ids (:clients args) (let [query-params (:parsed-query-params request)
(:client-id args)
(when (:client-code args)
[:client/code (:client-code args)]))
query (cond-> {:query {:find [] query (cond-> {:query {:find []
:in ['$ '[?xx ...]] :in ['$ '[?xx ...]]
:where ['[?e :plaid-item/client ?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]]} "status" ['[?e :plaid-item/status ?sort-status]]}
args) query-params)
(:client-id args)
(merge-query {:query {:in '[?client-id]
:where ['[?e :plaid-item/client ?client-id]]}
:args [(:client-id args)]})
true true
(merge-query {:query {:find ['?e] (merge-query {:query {:find ['?e]
:where ['[?e :plaid-item/external-id]]}}))] :where ['[?e :plaid-item/external-id]]}}))]
(clojure.pprint/pprint query-params)
(cond->> (query2 query) (cond->> (query2 query)
true (apply-sort-3 args) true (apply-sort-3 query-params)
true (apply-pagination args)))) 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)] (let [results (pull-many-by-id db default-read ids)]
(->> ids (->> ids
(map results)))) (map results))))
(defn get-page [args] (defn fetch-page [request]
(let [db (dc/db conn) (let [db (dc/db conn)
{ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args)] {ids-to-retrieve :ids matching-count :count} (fetch-ids db request)]
[(graphql-results ids-to-retrieve db args) [(hydrate-results ids-to-retrieve db request)
matching-count])) matching-count]))
@@ -138,60 +130,61 @@
"Start relink")]))) "Start relink")])))
(def grid-page {:id "plaid-table" (def grid-page
:nav (com/company-aside-nav) (helper/build
:id-fn :db/id {:id "plaid-table"
:fetch-page (fn [user args] :nav (com/company-aside-nav)
(get-page (assoc args :id user))) :fetch-page fetch-page
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:company)} :company)}
"My Company"] "My Company"]
[:a {:href (bidi/path-for ssr-routes/only-routes [:a {:href (bidi/path-for ssr-routes/only-routes
:company-plaid)} :company-plaid)}
"Plaid"]] "Plaid"]]
:title "Plaid Accounts" :title "Plaid Accounts"
:entity-name "Plaid accounts" :entity-name "Plaid accounts"
:route :company-plaid-table :route :company-plaid-table
:action-buttons (fn [user params] :action-buttons (fn [request]
(when-let [client-code (:client/code (:client params))] (when-let [client-code (:client/code (:client request))]
[[:div {:hx-post (str (bidi/path-for ssr-routes/only-routes [[:div {:hx-post (str (bidi/path-for ssr-routes/only-routes
:company-plaid-link :company-plaid-link
:request-method :post)) :request-method :post))
:hx-vals (hiccup/raw (format "js:{client_code: \"%s\", public_token: event.detail.public_token}", client-code)) :hx-vals (hiccup/raw (format "js:{client_code: \"%s\", public_token: event.detail.public_token}", client-code))
:hx-trigger "linked"} :hx-trigger "linked"}
[:script (hiccup/raw (plaid-link-script (p/get-link-token client-code)))] [:script (hiccup/raw (plaid-link-script (p/get-link-token client-code)))]
(com/button {:color :primary (com/button {:color :primary
:id "link-account" :id "link-account"
:onClick "window.plaid.open()"} :onClick "window.plaid.open()"}
(com/button-icon {} svg/refresh) (com/button-icon {} svg/refresh)
"Link new account")]])) (format "Link %s account" client-code))]]))
:row-buttons (fn [user e] :row-buttons (fn [request e]
[[:div (com/button {:hx-put (str (bidi/path-for ssr-routes/only-routes [[:div (com/button {:hx-put (str (bidi/path-for ssr-routes/only-routes
:company-plaid-relink) :company-plaid-relink)
"?plaid-item-id=" (:db/id e)) "?plaid-item-id=" (:db/id e))
:color :primary :color :primary
:hx-target "closest div"} :hx-target "closest div"}
"Reauthenticate")]]) "Reauthenticate")]])
:headers [{:key "plaid-item" :headers [{:key "plaid-item"
:name "Plaid Item" :name "Plaid Item"
:sort-key "id" :sort-key "external-id"
:render :db/id} :render :plaid-item/external-id}
{:key "status" {:key "status"
:name "Status" :name "Status"
:sort-key "status" :sort-key "status"
:render #(when-let [status (:plaid-item/status %)] :render #(when-let [status (:plaid-item/status %)]
[:div [:div (com/pill {:color :primary} [:div [:div (com/pill {:color :primary}
status)] status)]
[:div (atime/unparse-local (coerce/to-date-time (:plaid-item/last-updated %)) atime/normal-date)]])} [:div (atime/unparse-local (coerce/to-date-time (:plaid-item/last-updated %)) atime/normal-date)]])}
{:key "accounts" {:key "accounts"
:name "Accounts" :name "Accounts"
:show-starting "md" :show-starting "md"
:render (fn [e] :render (fn [e]
[:ul [:ul
(for [a (:plaid-item/accounts e)] (for [a (:plaid-item/accounts e)]
[:li (:plaid-account/name a) " - " (:plaid-account/number a)])])}]}) [: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))

View File

@@ -1,7 +1,14 @@
(ns auto-ap.ssr.company.reports (ns auto-ap.ssr.company.reports
(:require (:require
[amazonica.aws.s3 :as s3] [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.datomic.reports :as r]
[auto-ap.graphql.utils :refer [assert-can-see-client is-admin?]] [auto-ap.graphql.utils :refer [assert-can-see-client is-admin?]]
[auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr-routes :as ssr-routes]
@@ -11,57 +18,101 @@
[auto-ap.ssr.utils :refer [html-response]] [auto-ap.ssr.utils :refer [html-response]]
[auto-ap.time :as atime] [auto-ap.time :as atime]
[bidi.bidi :as bidi] [bidi.bidi :as bidi]
[clojure.set :as set]
[config.core :refer [env]] [config.core :refer [env]]
[datomic.api :as dc] [datomic.api :as dc]))
[com.brunobonacci.mulog :as mu]))
(def grid-page {:id "report-table" (def default-read '[:db/id :report/client [:report/created :xform clj-time.coerce/from-date] :report/url :report/name :report/creator])
: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"]
[:a {:href (bidi/path-for ssr-routes/only-routes (defn fetch-ids [db request]
:company-reports)}
"Reports"]] (let [query-params (:parsed-query-params request)
:title "Reports" query (cond-> {:query {:find []
:entity-name "Reports" :in '[$ [?c ...]]
:route :company-reports-table :where '[[?e :report/client ?c]]}
:action-buttons (fn [user _] :args [db (:trimmed-clients request)]}
nil)
:row-buttons (fn [user e]
(com/a-icon-button {:href (:report/url e)} (:sort query-params) (add-sorter-fields {"client" ['[?e :report/client ?c]
svg/download)[ '[?c :client/name ?sort-client]]
(when (is-admin? user) "created" ['[?e :report/created ?sort-created]]
(com/icon-button {:hx-delete (str (bidi/path-for ssr-routes/only-routes "creator" ['[?e :report/creator ?sort-creator]]
:company-reports-delete "name" ['[?e :report/name ?sort-name]
:request-method :delete)) ]}
:hx-target "closest tr"} query-params)
svg/trash))])
:headers [{:key "name" true
:name "Name" (merge-query {:query {:find ['?sort-default '?e] :where ['[?e :report/created ?sort-default]]}}))]
:sort-key "name" (->> (query2 query)
:render :report/name} (apply-sort-3 (update query-params :sort conj {:sort-key "default-2" :asc true}))
{:key "created-by" (apply-pagination query-params))))
:name "Created by"
:sort-key "creator" (defn hydrate-results [ids db request]
:render (fn [report] (let [results (->> (pull-many db default-read ids)
(when (:report/creator report) (group-by :db/id))
(com/pill {:color :primary } valid-clients (:trimmed-clients request)]
(:report/creator report))))} (->> ids
{:key "created" (map results)
:name "Created" (filter identity)
:sort-key "created"
:render #(atime/unparse-local (:report/created %) (map first)
atime/normal-date)}]}) (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 row* (partial helper/row* grid-page))
(def table* (partial helper/table* grid-page)) (def table* (partial helper/table* grid-page))
(def table (partial helper/table grid-page)) (def table (helper/table-route grid-page))
(def page (partial helper/page grid-page)) (def page (helper/page-route grid-page))
(defn delete-report [{:keys [form-params identity]}] (defn delete-report [{:keys [form-params identity]}]
@@ -70,7 +121,7 @@
:where [?i :report/key ?k]] :where [?i :report/key ?k]]
(dc/db conn) (dc/db conn)
(some-> (get form-params "id") not-empty Long/parseLong))) (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)) (assert-can-see-client identity (:report/client report))
(when id-to-delete (when id-to-delete
(s3/delete-object :bucket-name (:data-bucket env) (s3/delete-object :bucket-name (:data-bucket env)

View File

@@ -1,32 +1,73 @@
(ns auto-ap.ssr.company.yodlee (ns auto-ap.ssr.company.yodlee
(:require (:require
[auto-ap.datomic :refer [conn]] [auto-ap.datomic
[auto-ap.datomic.yodlee2 :as yodlee2] :refer [add-sorter-fields
[auto-ap.graphql.utils :refer [is-admin?]] 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-routes :as ssr-routes]
[auto-ap.ssr.components :as com] [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.grid-page-helper :as helper]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils :refer [html-response]] [auto-ap.ssr.utils :refer [html-response]]
[auto-ap.time :as atime] [auto-ap.time :as atime]
[auto-ap.yodlee.core2 :as yodlee] [auto-ap.yodlee.core2 :as yodlee]
[bidi.bidi :as bidi] [bidi.bidi :as bidi]
[config.core :refer [env]] [config.core :refer [env]]
[datomic.api :as dc] [datomic.api :as dc]
[hiccup2.core :as hiccup] [hiccup2.core :as hiccup]))
[auto-ap.datomic :refer [pull-attr]]))
(def default-read '[:db/id (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/status
:yodlee-provider-account/id :yodlee-provider-account/id
:yodlee-provider-account/detailed-status :yodlee-provider-account/detailed-status
{:yodlee-provider-account/accounts [:yodlee-account/name :yodlee-account/number] {: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]}] (defn fastlink-dialog [{:keys [client]}]
(html-response (html-response
(com/modal (com/modal
@@ -53,7 +94,10 @@ fastlink.open({fastLinkURL: '%s',
] ]
[:div])))) [: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 (html-response
(com/modal (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")))))]] (pull-attr (dc/db conn) :yodlee-provider-account/id (Long/parseLong (get form-params "id")))))]]
[:div])))) [:div]))))
(def grid-page {:id "yodlee-table" (def grid-page
:nav (com/company-aside-nav) (helper/build
:id-fn :db/id {:id "yodlee-table"
:fetch-page (fn [user args] :nav (com/company-aside-nav)
(yodlee2/get-graphql (assoc args :id user))) :id-fn :db/id
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :fetch-page fetch-page
:company)} :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
"My Company"] :company)}
"My Company"]
[:a {:href (bidi/path-for ssr-routes/only-routes [:a {:href (bidi/path-for ssr-routes/only-routes
:company-yodlee)} :company-yodlee)}
"Yodlee"]] "Yodlee"]]
:title "Yodlee Accounts" :title "Yodlee Accounts"
:entity-name "Yodlee accounts" :entity-name "Yodlee accounts"
:route :company-yodlee-table :route :company-yodlee-table
:action-buttons (fn [user args] :action-buttons (fn [request]
[[:div.flex.flex-col.flex-shrink [[:div.flex.flex-col.flex-shrink
[:div.flex-shrink [:div.flex-shrink
(com/button {:color :primary (com/button {:color :primary
:on-click "openFastlink()" :on-click "openFastlink()"
:disabled (if (:client args) :disabled (if (:client request)
false false
true) true)
:hx-get (bidi/path-for ssr-routes/only-routes :hx-get (bidi/path-for ssr-routes/only-routes
@@ -110,53 +154,59 @@ fastlink.open({fastLinkURL: '%s',
:hx-target "#modal-holder"} :hx-target "#modal-holder"}
(com/button-icon {} svg/refresh) (com/button-icon {} svg/refresh)
"Link new account")] "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."])]]) [:div.text-xs "Note: please select a specific customer to link a new account."])]])
:row-buttons (fn [user e] :row-buttons (fn [request _]
[ [
(com/button {:hx-put (bidi/path-for ssr-routes/only-routes (com/button {:hx-put (bidi/path-for ssr-routes/only-routes
:company-yodlee-provider-account-reauthenticate) :company-yodlee-provider-account-reauthenticate)
:color :primary :color :primary
:hx-target "#modal-holder"} :hx-target "#modal-holder"}
"Reauthenticate") "Reauthenticate")
(when (is-admin? user) (when (is-admin? (:identity request))
(com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes (com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes
:company-yodlee-provider-account-refresh) :company-yodlee-provider-account-refresh)
:hx-target "closest tr"} :hx-target "closest tr"}
svg/refresh))]) svg/refresh))])
:headers [{:key "provider-account" :headers [{:key "client"
:name "Provider Account" :name "Client"
:sort-key "provider-account" :sort-key "client"
:render :yodlee-provider-account/id} :hide? (fn [args]
{:key "status" (= (count (:clients args)) 1))
:name "Status" :render #(-> % :yodlee-provider-account/client :client/code)}
:sort-key "status" {:key "provider-account"
:render #(when-let [status (:yodlee-provider-account/status %)] :name "Provider Account"
(com/pill {:color (if (not= status "SUCCESS") :sort-key "provider-account"
:yellow :render :yodlee-provider-account/id}
:primary) } {:key "status"
status))} :name "Status"
{:key "detailed-status" :sort-key "status"
:name "Detailed Status" :render #(when-let [status (:yodlee-provider-account/status %)]
:sort-key "detailed-status" (com/pill {:color (if (not= status "SUCCESS")
:render #(when-let [status (:yodlee-provider-account/detailed-status %)] :yellow
status)} :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" {:key "last-updated"
:name "Last Updated" :name "Last Updated"
:sort-key "last-updated" :sort-key "last-updated"
:render #(atime/unparse-local (:yodlee-provider-account/last-updated %) :render #(atime/unparse-local (:yodlee-provider-account/last-updated %)
atime/normal-date)} atime/normal-date)}
{:key "accounts" {:key "accounts"
:name "Accounts" :name "Accounts"
:show-starting "md" :show-starting "md"
:render (fn [e] :render (fn [e]
[:ul [:ul
(for [a (:yodlee-provider-account/accounts e)] (for [a (:yodlee-provider-account/accounts e)]
[:li (:yodlee-account/name a) " - " (:yodlee-account/number a)])])}]}) [:li (:yodlee-account/name a) " - " (:yodlee-account/number a)])])}]}))
(def page (partial helper/page grid-page)) (def page (helper/page-route grid-page))
(def table (partial helper/table grid-page)) (def table (helper/table-route grid-page))
;; TODO delete-after-settle ;; TODO delete-after-settle
(defn refresh-provider-account [{:keys [form-params identity]}] (defn refresh-provider-account [{:keys [form-params identity]}]

View File

@@ -9,14 +9,18 @@
[auto-ap.ssr.components.page :as page] [auto-ap.ssr.components.page :as page]
[auto-ap.ssr.components.data-grid :as data-grid] [auto-ap.ssr.components.data-grid :as data-grid]
[auto-ap.ssr.components.tags :as tags] [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 breadcrumbs breadcrumbs/breadcrumbs-)
(def button buttons/button-) (def button buttons/button-)
(def a-button buttons/a-button-)
(def button-icon buttons/button-icon-) (def button-icon buttons/button-icon-)
(def icon-button buttons/icon-button-) (def icon-button buttons/icon-button-)
(def a-icon-button buttons/a-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 dialog/modal-)
(def modal-card dialog/modal-card-) (def modal-card dialog/modal-card-)
@@ -36,6 +40,7 @@
(def navbar navbar/navbar-) (def navbar navbar/navbar-)
(def page page/page-) (def page page/page-)
(def radio radio/radio-)
(def pill tags/pill-) (def pill tags/pill-)
(def badge tags/badge-) (def badge tags/badge-)

View File

@@ -10,8 +10,9 @@
[:a (-> params [:a (-> params
(dissoc :icon) (dissoc :icon)
(assoc :type "button") (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-indicator "find .htmx-indicator")
(assoc :hx-boost "true")
(assoc :hx-select "#app-contents") (assoc :hx-select "#app-contents")
(assoc :hx-target "#app-contents") (assoc :hx-target "#app-contents")
(assoc :hx-swap "outerHTML")) (assoc :hx-swap "outerHTML"))
@@ -28,7 +29,7 @@
(svg/spinner-primary {:class "inline w-4 h-4 text-white"})]]]) (svg/spinner-primary {:class "inline w-4 h-4 text-white"})]]])
(defn sub-menu- [params & children] (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] (for [c children]
[:li [: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")])]) (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- [] (defn main-aside-nav- []
[:ul {:class "space-y-2"} [:ul {:class "space-y-1"}
[:li [:li
(menu-button- {:icon svg/pie (menu-button- {:icon svg/pie
@@ -178,10 +179,22 @@
:icon svg/receipt-register-1} :icon svg/receipt-register-1}
"Sales") "Sales")
(sub-menu- {:id "dropdown-sales"} (sub-menu- {:id "dropdown-sales"}
(menu-button- {:href (bidi/path-for client-routes/routes (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:sales-orders)} "Sales") :pos-sales)
(menu-button- {:href (bidi/path-for client-routes/routes "?date-range=week")} "Sales")
:expected-deposits)} "Expected Deposits") (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"} "Cash Shifts")
#_(menu-button- {:href "Sales"} "Tenders"))] #_(menu-button- {:href "Sales"} "Tenders"))]
[:li [:li

View File

@@ -96,7 +96,20 @@
(svg/spinner {:class "inline w-4 h-4 text-white"}) (svg/spinner {:class "inline w-4 h-4 text-white"})
[:div.ml-3 "Loading..."]] [:div.ml-3 "Loading..."]]
(into [:div.htmx-indicator-hidden.inline-flex.gap-2.items-center.justify-center] children)]) (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] (defn icon-button- [params & children]
(into (into
[:button [:button
@@ -126,3 +139,31 @@
(svg/spinner {:class "inline w-4 h-4 text-white"}) (svg/spinner {:class "inline w-4 h-4 text-white"})
[:div.ml-3 "Loading..."]] [:div.ml-3 "Loading..."]]
(into [:div.htmx-indicator-hidden ] children)]) (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)))

View File

@@ -69,6 +69,7 @@
:hx-trigger "clientSelected from:body" :hx-trigger "clientSelected from:body"
:hx-swap "outerHTML swap:300ms" :hx-swap "outerHTML swap:300ms"
:id id} :id id}
(content-card- (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"} [: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) :request-method :get)
"?start=" (* page per-page)) "?start=" (* page per-page))
:hx-target (str "#" id) :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..."]]])])

View File

@@ -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] (defn select- [params & children]
(into (into
@@ -11,28 +12,43 @@
(:allow-blank? params) (conj [:option {:value "" :selected (not (:value params))} ""]))] (:allow-blank? params) (conj [:option {:value "" :selected (not (:value params))} ""]))]
children)) 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 [:input
(update params (-> 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") (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 [:input
(-> params (-> params
(update (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" (assoc :type "number"
:step "0.01")) :step "0.01")
(dissoc :size))
]) ])
(defn date-input- [params] (defn date-input- [{:keys [size] :as params}]
[:input [:div
(-> 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") (update
(assoc :type "date"))]) :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] (defn field- [params & rest]
(into (into

View File

@@ -6,7 +6,10 @@
[auto-ap.ssr.svg :as svg])) [auto-ap.ssr.svg :as svg]))
(defn page- [{:keys [nav page-specific client client-selection identity app-params] :or {app-params {}}} & children] (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 (navbar- {:client-selection client-selection
:client client :client client
:identity identity}) :identity identity})
@@ -14,10 +17,7 @@
(left-aside- {:nav nav (left-aside- {:nav nav
:page-specific page-specific}) :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 " [: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#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.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 [:div.relative

View File

@@ -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] (defn bound [x y z]
(cond (cond
@@ -34,7 +35,10 @@
(not= current-page x) (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"))) (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 "#")) (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))) last-page-button (Math/min (long total-pages) (long (+ max-buttons first-page-button)))

View File

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

View File

@@ -12,47 +12,59 @@
[auto-ap.ssr.company-dropdown :as company-dropdown] [auto-ap.ssr.company-dropdown :as company-dropdown]
[auto-ap.ssr.company.reports :as company-reports] [auto-ap.ssr.company.reports :as company-reports]
[auto-ap.ssr.invoice.glimpse :as invoice-glimpse] [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.routes.ezcater-xls :as ezcater-xls]
[auto-ap.ssr.company :as company])) [auto-ap.ssr.company :as company]))
;; from auto-ap.ssr-routes, because they're shared ;; from auto-ap.ssr-routes, because they're shared
(def key->handler {:logout auth/logout (def key->handler
:admin-history (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page))) (-> {:logout auth/logout
:admin-history-search (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page))) :impersonate (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin auth/impersonate)))
:admin-history-inspect (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/inspect))) :admin-history (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page)))
:active-client (wrap-client-redirect-unauthenticated (wrap-secure (wrap-secure company-dropdown/active-client))) :admin-history-search (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page)))
:company-dropdown-search-results :admin-history-inspect (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/inspect)))
(wrap-client-redirect-unauthenticated (wrap-secure company-dropdown/dropdown-search-results)) :active-client (wrap-client-redirect-unauthenticated (wrap-secure (wrap-secure company-dropdown/active-client)))
:company (wrap-client-redirect-unauthenticated (wrap-secure company/page)) :company-dropdown-search-results
:company-1099 (wrap-client-redirect-unauthenticated (wrap-secure company-1099/page)) (wrap-client-redirect-unauthenticated (wrap-secure company-dropdown/dropdown-search-results))
:company-1099-vendor-table (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-table)) :company (wrap-client-redirect-unauthenticated (wrap-secure company/page))
:company-1099-vendor-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-dialog)) :company-1099 (wrap-client-redirect-unauthenticated (wrap-secure company-1099/page))
:company-1099-vendor-save (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-save)) :company-1099-vendor-table (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-table))
:company-plaid (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/page)) :company-1099-vendor-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-dialog))
:company-plaid-table (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/table)) :company-1099-vendor-save (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-save))
:company-plaid-link (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/link)) :company-plaid (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/page))
:company-plaid-relink (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/relink)) :company-plaid-table (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/table))
:company-yodlee (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/page)) :company-plaid-link (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/link))
:company-yodlee-table (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/table)) :company-plaid-relink (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/relink))
:company-yodlee-fastlink-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/fastlink-dialog)) :company-yodlee (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/page))
:company-yodlee-provider-account-refresh (wrap-client-redirect-unauthenticated (wrap-admin company-yodlee/refresh-provider-account)) :company-yodlee-table (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/table))
:company-yodlee-provider-account-reauthenticate (wrap-client-redirect-unauthenticated (wrap-admin company-yodlee/reauthenticate)) :company-yodlee-fastlink-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/fastlink-dialog))
:company-reports (wrap-client-redirect-unauthenticated (wrap-secure company-reports/page)) :company-yodlee-provider-account-refresh (wrap-client-redirect-unauthenticated (wrap-admin company-yodlee/refresh-provider-account))
:company-reports-table (wrap-client-redirect-unauthenticated (wrap-secure company-reports/table)) :company-yodlee-provider-account-reauthenticate (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/reauthenticate))
:company-reports-delete (wrap-client-redirect-unauthenticated (wrap-admin company-reports/delete-report)) :company-reports (wrap-client-redirect-unauthenticated (wrap-secure company-reports/page))
:invoice-glimpse (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/page)) :company-reports-table (wrap-client-redirect-unauthenticated (wrap-secure company-reports/table))
:invoice-glimpse-upload (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/upload)) :company-reports-delete (wrap-client-redirect-unauthenticated (wrap-admin company-reports/delete-report))
:invoice-glimpse-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/textract-invoice)) :invoice-glimpse (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/page))
:invoice-glimpse-create-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/create-invoice)) :invoice-glimpse-upload (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/upload))
:invoice-glimpse-update-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/update-textract-invoice)) :invoice-glimpse-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/textract-invoice))
:transaction-insights (wrap-client-redirect-unauthenticated (wrap-admin insights/page)) :invoice-glimpse-create-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/create-invoice))
:transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-admin insights/insight-table)) :invoice-glimpse-update-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/update-textract-invoice))
:transaction-insight-rows (wrap-client-redirect-unauthenticated (wrap-admin insights/transaction-rows)) :transaction-insights (wrap-client-redirect-unauthenticated (wrap-admin insights/page))
:transaction-insight-code (wrap-client-redirect-unauthenticated (wrap-admin insights/code)) :transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-admin insights/insight-table))
:transaction-insight-disapprove (wrap-client-redirect-unauthenticated (wrap-admin insights/disapprove)) :transaction-insight-rows (wrap-client-redirect-unauthenticated (wrap-admin insights/transaction-rows))
:transaction-insight-explain (wrap-client-redirect-unauthenticated (wrap-admin insights/explain)) :transaction-insight-code (wrap-client-redirect-unauthenticated (wrap-admin insights/code))
:admin-ezcater-xls (wrap-client-redirect-unauthenticated (wrap-admin ezcater-xls/page)) :transaction-insight-disapprove (wrap-client-redirect-unauthenticated (wrap-admin insights/disapprove))
:search (wrap-client-redirect-unauthenticated (wrap-secure search/dialog-contents))}) :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)))

View File

@@ -1,28 +1,39 @@
(ns auto-ap.ssr.grid-page-helper (ns auto-ap.ssr.grid-page-helper
(:require (: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.components :as com]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.ui :refer [base-page]] [auto-ap.ssr.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [html-response]] [auto-ap.ssr.utils :refer [html-response]]
[hiccup2.core :as hiccup] [auto-ap.time :as atime]
[malli.core :as m]
[bidi.bidi :as bidi] [bidi.bidi :as bidi]
[auto-ap.ssr-routes :as ssr-routes]
[cemerick.url :as url] [cemerick.url :as url]
[clojure.string :as str] [clojure.string :as str]
[auto-ap.ssr.svg :as svg] [hiccup2.core :as hiccup]
[unilog.context :as lc] [malli.transform :as mt2]))
[com.brunobonacci.mulog :as mu]))
(defn row* [gridspec user entity {:keys [flash? delete-after-settle?] :as options}] (defn row* [gridspec user entity {:keys [flash? delete-after-settle? request] :as options}]
(let [cells (mapv (fn [header] (let [cells (->> gridspec
(com/data-grid-cell {:class (if-let [show-starting (:show-starting header)] :headers
(format "hidden %s:table-cell" show-starting) (filter (fn [h]
(:class header))} (if (and (:hide? h)
((:render header) entity))) ((:hide? h) request))
(:headers gridspec)) 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 {} cells (conj cells (com/data-grid-right-stack-cell {}
(into [:form (into [:form
[:input {:type :hidden :name "id" :value ((:id-fn gridspec) entity)}]] [: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 (apply com/data-grid-row
{:class (when flash? {:class (when flash?
"live-added") "live-added")
@@ -38,154 +49,241 @@
first first
:sort-icon)) :sort-icon))
(defn sort-by-list [sort] (defn sort-by-list [grid-spec sort]
(if (seq sort) (if (seq sort)
(into (into
[:div.flex.gap-2.items-center [:div.flex.gap-2.items-center
"sorted by" "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.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 {: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")) "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) (let [start (or start 0)
per-page (or per-page 30) per-page (or per-page 30)
[entities total] ((:fetch-page grid-spec) [entities total] ((:fetch-page grid-spec)
user request)]
{:start start
:per-page per-page
:clients clients
:sort sort})]
(com/data-grid-card {:id (:id grid-spec) (com/data-grid-card {:id (:id grid-spec)
:title (:title grid-spec) :title (:title grid-spec)
:route (:route grid-spec) :route (:route grid-spec)
:start start :start start
:per-page per-page :per-page per-page
:total total :total total
:subtitle [:div.flex.items-center.gap-2 :subtitle [:div.flex.items-center.gap-2
[:span (format "Total %s: %d, " (:entity-name grid-spec) total)] [:span (format "Total %s: %d, " (:entity-name grid-spec) total)]
(sort-by-list sort)] (sort-by-list grid-spec sort)]
:action-buttons ((:action-buttons grid-spec) user params) :action-buttons ((:action-buttons grid-spec) request)
:rows (for [entity entities] :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 :thead-params {:hx-get (bidi/path-for ssr-routes/only-routes
(:route grid-spec)) (:route grid-spec))
:hx-target (str "#" (:id grid-spec)) :hx-target (str "#" (:id grid-spec))
:hx-indicator (str "#" (:id grid-spec))
:hx-trigger "sorted once" :hx-trigger "sorted once"
:hx-vals "js:{\"toggle-sort\": event.detail.key || \"\"}"} :hx-vals "js:{\"toggle-sort\": event.detail.key || \"\"}"}
:headers :headers
(conj (conj
(mapv (->> grid-spec
(fn [h] :headers
(if (:sort-key h) (map
(com/data-grid-sort-header {:class (if-let [show-starting (:show-starting h)] (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) (format "hidden %s:table-cell" show-starting)
(:class h)) (:class h))
:sort-key (:sort-key h)} :sort-key (:sort-key h)}
(:name h))
[:div.flex.gap-4.items-center )))
(:name h) (filter identity)
[:div.h-6.w-6.text-gray-400.dark:text-gray-500 (sort-icon sort (:sort-key h))]]) (into []))
(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))
(com/data-grid-header {}))}))) (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] (defn sort->query [s]
(str/join "," (map (fn [k] (format "%s:%s" (:sort-key k) (if (= true (:asc k)) (str/join "," (map (fn [k] (format "%s:%s" (:sort-key k) (if (= true (:asc k))
"asc" "asc"
"desc"))) "desc")))
s))) s)))
(defn params->query-string [q] (defn default-unparse-query-params [query-params]
(-> q (reduce
(dissoc :client :session) (fn [query-params [k value]]
(update :sort sort->query) (assoc query-params k
(url/map->query))) (cond (= k :sort)
(sort->query value)
(defn extract-params [grid-spec {:keys [query-params hx-query-params identity session] :as request}] (instance? org.joda.time.base.AbstractInstant value)
(let [{hx-start "start" hx-per-page "per-page" hx-sort "sort" } hx-query-params (atime/unparse-local value atime/normal-date)
{q-start "start" q-per-page "per-page" q-sort "sort" q-toggle-sort "toggle-sort"} query-params]
(cond-> {} (instance? Long value)
hx-start (assoc :start (some-> hx-start not-empty (Long/parseLong ))) (str value)
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 ))) (instance? Double value)
q-per-page (assoc :per-page (some-> q-per-page not-empty (Long/parseLong ))) (format "%.2f" value)
hx-sort (assoc :sort (parse-sort grid-spec hx-sort))
q-sort (assoc :sort (parse-sort grid-spec q-sort)) (instance? Float value)
(not-empty q-toggle-sort) (update :sort #(toggle-sort grid-spec % q-toggle-sort) ) (format "%.2f" value)
(:session request) (assoc :session (:session request))
(:client-selection (:session request)) (assoc :client-selection (:client-selection (:session request))) (keyword? value)
(:clients request) (assoc :clients (:clients request)) (name value)
(:client request) (assoc :client (:client request)))))
: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)))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,7 @@
[:title (str "Integreat | " page-name)] [:title (str "Integreat | " page-name)]
[:link {:href "/css/font.min.css", :rel "stylesheet"}] [:link {:href "/css/font.min.css", :rel "stylesheet"}]
[:link {:rel "icon" :type "image/png" :href "/favicon.png"}] [: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"}] [:link {:rel "stylesheet", :href "/output.css"}]
[:script {:src "https://unpkg.com/hyperscript.org@0.9.7/dist/_hyperscript.min.js"}] [: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" #_[:script {:src "https://unpkg.com/htmx.org@1.8.4"
:integrity "sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV" :integrity "sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV"
:crossorigin= "anonymous"}] :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"}] :crossorigin= "anonymous"}]
[:script {:src "https://unpkg.com/htmx.org/dist/ext/debug.js"}] [:script {:src "https://unpkg.com/htmx.org/dist/ext/debug.js"}]
[:script {:src "/js/htmx-disable.js"}] [:script {:src "/js/htmx-disable.js"}]
[:script {:type "text/javascript", :src "https://cdn.yodlee.com/fastlink/v4/initialize.js", :async "async"}]] [: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 {: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"}] [: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"}] [: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"} [:body {:hx-ext "disable-submit"}
contents contents
[:script {:src "/js/flowbite.min.js"}]]])) [:script {:src "/js/flowbite.min.js"}]]]))

View File

@@ -5,14 +5,21 @@
[hiccup2.core :as hiccup] [hiccup2.core :as hiccup]
[clojure.string :as str])) [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 {:status status
:headers (into {"Content-Type" "text/html"} :headers (into {"Content-Type" "text/html"}
headers) headers)
:body (str :body (str
(hiccup/html (hiccup/html
{} {}
hiccup))}) hiccup)
"\n"
(str/join "\n"
(map (fn [o]
(hiccup/html
{}
o))
oob)))})
(defn wrap-error-response [handler] (defn wrap-error-response [handler]
(fn [request] (fn [request]

View File

@@ -1,6 +1,7 @@
(ns auto-ap.ssr-routes) (ns auto-ap.ssr-routes)
(def routes {"logout" :logout (def routes {"impersonate" :impersonate
"logout" :logout
"search" :search "search" :search
"invoice" {"/glimpse" {"" {:get :invoice-glimpse "invoice" {"/glimpse" {"" {:get :invoice-glimpse
:post :invoice-glimpse-upload :post :invoice-glimpse-upload
@@ -19,6 +20,17 @@
["/disapprove/" [#"\d+" :transaction-id]] {:delete :transaction-insight-disapprove} ["/disapprove/" [#"\d+" :transaction-id]] {:delete :transaction-insight-disapprove}
["/rows/" [#"\d+" :after]] {:get :transaction-insight-rows} ["/rows/" [#"\d+" :after]] {:get :transaction-insight-rows}
["/explain/" [#"\d+" :transaction-id]] {:get :transaction-insight-explain}}} ["/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 "company" {"" :company
"/dropdown" :company-dropdown-search-results "/dropdown" :company-dropdown-search-results
"/active" {:put :active-client} "/active" {:put :active-client}

View File

@@ -173,8 +173,8 @@
:href (bidi/path-for routes/routes :payments)} :href (bidi/path-for routes/routes :payments)}
"Payments" ] "Payments" ]
(when (= "admin" (:user/role @user)) (when (= "admin" (:user/role @user))
[:a.navbar-item {:class [(active-when ap = :sales-orders)] [:a.navbar-item {:class [(active-when ap = :pos-sales)]
:href (bidi/path-for routes/routes :sales-orders)} :href (str (bidi/path-for ssr-routes/only-routes :pos-sales) "?date-range=week")}
"POS" ]) "POS" ])
[:a.navbar-item {:class [(active-when ap = :transactions)] [:a.navbar-item {:class [(active-when ap = :transactions)]
:href (bidi/path-for routes/routes :transactions)} :href (bidi/path-for routes/routes :transactions)}

View File

@@ -1,11 +1,32 @@
(ns auto-ap.views.pages.admin.users.table (ns auto-ap.views.pages.admin.users.table
(:require (: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]
[auto-ap.views.components.buttons :as buttons] [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 (re-frame/reg-event-fx
::params-changed ::params-changed
@@ -50,11 +71,7 @@
[grid/cell {} role] [grid/cell {} role]
[grid/cell {} (str/join ", " (map :name clients))] [grid/cell {} (str/join ", " (map :name clients))]
[grid/cell {} [grid/cell {}
[:a.button {:on-click (fn [] [:a.button {:on-click (dispatch-event [::impersonate (:impersonate-jwt c)])}
(.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))}
"Impersonate"] "Impersonate"]