From e78c73e0936932fa2f65b2e68f27b942c1eb9978 Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Fri, 12 May 2023 12:27:48 -0700 Subject: [PATCH 1/6] Makes company 1099 page work better and faster --- resources/input.css | 3 + resources/public/js/htmx-disable.js | 16 ++ resources/public/output.css | 267 +++++++------------ src/clj/auto_ap/handler.clj | 11 +- src/clj/auto_ap/ssr/company/company_1099.clj | 213 ++++++++------- src/clj/auto_ap/ssr/company_dropdown.clj | 206 +++++++------- src/clj/auto_ap/ssr/components.clj | 84 ++++-- src/clj/auto_ap/ssr/components/dialog.clj | 43 ++- src/clj/auto_ap/ssr/components/navbar.clj | 34 ++- src/clj/auto_ap/ssr/components/page.clj | 19 +- src/clj/auto_ap/ssr/core.clj | 3 +- src/clj/auto_ap/ssr/search.clj | 44 +-- src/clj/auto_ap/ssr/svg.clj | 4 + src/clj/auto_ap/ssr/ui.clj | 14 +- src/cljc/auto_ap/ssr_routes.cljc | 2 +- 15 files changed, 519 insertions(+), 444 deletions(-) create mode 100644 resources/public/js/htmx-disable.js diff --git a/resources/input.css b/resources/input.css index cb8a07a5..9027f220 100644 --- a/resources/input.css +++ b/resources/input.css @@ -12,6 +12,9 @@ .htmx-added .slide-up { @apply translate-y-5 !important; } +.hidden .slide-up { + @apply translate-y-5 !important; +} .slide-up { @apply translate-y-0; } diff --git a/resources/public/js/htmx-disable.js b/resources/public/js/htmx-disable.js new file mode 100644 index 00000000..b1bbc156 --- /dev/null +++ b/resources/public/js/htmx-disable.js @@ -0,0 +1,16 @@ +htmx.defineExtension('disable-submit', { + onEvent: function (name, evt, data) { + let elt = evt.detail.elt; + let result = elt.querySelectorAll('.hx-disable'); + + if (name === 'htmx:beforeRequest') { + result.forEach(element => element.disabled = true); + if (elt.classList.contains('hx-disable')) { + elt.disabled = true;} + + } else if(name == 'htmx:afterRequest') { + result.forEach(element => element.disabled = false); + if (elt.classList.contains('hx-disable')) elt.disabled = false; + } + } +}) diff --git a/resources/public/output.css b/resources/public/output.css index 0d584506..ed0087b4 100644 --- a/resources/public/output.css +++ b/resources/public/output.css @@ -1149,10 +1149,6 @@ input:checked + .toggle-bg { margin-bottom: 1rem; } -.ml-0 { - margin-left: 0px; -} - .ml-1 { margin-left: 0.25rem; } @@ -1165,10 +1161,6 @@ input:checked + .toggle-bg { margin-left: 0.75rem; } -.ml-8 { - margin-left: 2rem; -} - .mr-16 { margin-right: 4rem; } @@ -1253,6 +1245,10 @@ input:checked + .toggle-bg { height: 1rem; } +.h-48 { + height: 12rem; +} + .h-5 { height: 1.25rem; } @@ -1269,6 +1265,10 @@ input:checked + .toggle-bg { height: 2rem; } +.h-96 { + height: 24rem; +} + .h-\[calc\(100\%-1rem\)\] { height: calc(100% - 1rem); } @@ -1281,30 +1281,6 @@ input:checked + .toggle-bg { height: 100vh; } -.h-2 { - height: 0.5rem; -} - -.h-48 { - height: 12rem; -} - -.h-64 { - height: 16rem; -} - -.h-80 { - height: 20rem; -} - -.h-1\/2 { - height: 50%; -} - -.h-96 { - height: 24rem; -} - .max-h-full { max-height: 100%; } @@ -1345,23 +1321,10 @@ input:checked + .toggle-bg { width: 2rem; } -.w-auto { - width: auto; -} - .w-full { width: 100%; } -.w-max { - width: -moz-max-content; - width: max-content; -} - -.w-2 { - width: 0.5rem; -} - .max-w-2xl { max-width: 42rem; } @@ -1510,16 +1473,12 @@ input:checked + .toggle-bg { justify-content: space-between; } -.gap-4 { - gap: 1rem; -} - .gap-2 { gap: 0.5rem; } -.gap-6 { - gap: 1.5rem; +.gap-4 { + gap: 1rem; } .gap-8 { @@ -1568,18 +1527,6 @@ input:checked + .toggle-bg { margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); } -.space-y-4 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); - margin-bottom: calc(1rem * var(--tw-space-y-reverse)); -} - -.space-y-8 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse))); - margin-bottom: calc(2rem * var(--tw-space-y-reverse)); -} - .divide-y > :not([hidden]) ~ :not([hidden]) { --tw-divide-y-reverse: 0; border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); @@ -1633,10 +1580,6 @@ input:checked + .toggle-bg { border-radius: 0.5rem; } -.rounded-xl { - border-radius: 0.75rem; -} - .rounded-l-lg { border-top-left-radius: 0.5rem; border-bottom-left-radius: 0.5rem; @@ -1746,6 +1689,11 @@ input:checked + .toggle-bg { background-color: rgb(17 24 39 / var(--tw-bg-opacity)); } +.bg-green-100 { + --tw-bg-opacity: 1; + background-color: rgb(228 240 213 / var(--tw-bg-opacity)); +} + .bg-green-500 { --tw-bg-opacity: 1; background-color: rgb(121 181 46 / var(--tw-bg-opacity)); @@ -1756,16 +1704,6 @@ input:checked + .toggle-bg { background-color: rgb(242 248 234 / var(--tw-bg-opacity)); } -.bg-red-700 { - --tw-bg-opacity: 1; - background-color: rgb(153 2 2 / var(--tw-bg-opacity)); -} - -.bg-slate-300 { - --tw-bg-opacity: 1; - background-color: rgb(203 213 225 / var(--tw-bg-opacity)); -} - .bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); @@ -1780,21 +1718,6 @@ input:checked + .toggle-bg { background-color: rgb(253 246 178 / var(--tw-bg-opacity)); } -.bg-blue-200 { - --tw-bg-opacity: 1; - background-color: rgb(153 215 247 / var(--tw-bg-opacity)); -} - -.bg-blue-300 { - --tw-bg-opacity: 1; - background-color: rgb(102 196 242 / var(--tw-bg-opacity)); -} - -.bg-green-100 { - --tw-bg-opacity: 1; - background-color: rgb(228 240 213 / var(--tw-bg-opacity)); -} - .bg-opacity-50 { --tw-bg-opacity: 0.5; } @@ -1823,14 +1746,6 @@ input:checked + .toggle-bg { padding: 1.5rem; } -.p-8 { - padding: 2rem; -} - -.p-5 { - padding: 1.25rem; -} - .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -1866,11 +1781,6 @@ input:checked + .toggle-bg { padding-bottom: 0.25rem; } -.py-1\.5 { - padding-top: 0.375rem; - padding-bottom: 0.375rem; -} - .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; @@ -1896,6 +1806,14 @@ input:checked + .toggle-bg { padding-bottom: 1.25rem; } +.pb-2 { + padding-bottom: 0.5rem; +} + +.pb-3 { + padding-bottom: 0.75rem; +} + .pl-10 { padding-left: 2.5rem; } @@ -1904,6 +1822,10 @@ input:checked + .toggle-bg { padding-left: 2.75rem; } +.pl-2 { + padding-left: 0.5rem; +} + .pl-3 { padding-left: 0.75rem; } @@ -1912,6 +1834,14 @@ input:checked + .toggle-bg { padding-left: 1rem; } +.pr-2 { + padding-right: 0.5rem; +} + +.pr-2\.5 { + padding-right: 0.625rem; +} + .pt-16 { padding-top: 4rem; } @@ -1924,18 +1854,6 @@ input:checked + .toggle-bg { padding-top: 1.25rem; } -.pb-2 { - padding-bottom: 0.5rem; -} - -.pr-2 { - padding-right: 0.5rem; -} - -.pr-2\.5 { - padding-right: 0.625rem; -} - .text-left { text-align: left; } @@ -1978,10 +1896,6 @@ input:checked + .toggle-bg { font-weight: 700; } -.font-light { - font-weight: 300; -} - .font-medium { font-weight: 500; } @@ -2054,6 +1968,11 @@ input:checked + .toggle-bg { color: rgb(17 24 39 / var(--tw-text-opacity)); } +.text-green-800 { + --tw-text-opacity: 1; + color: rgb(48 72 18 / var(--tw-text-opacity)); +} + .text-primary-600 { --tw-text-opacity: 1; color: rgb(97 145 37 / var(--tw-text-opacity)); @@ -2064,26 +1983,11 @@ input:checked + .toggle-bg { color: rgb(255 255 255 / var(--tw-text-opacity)); } -.text-yellow-400 { - --tw-text-opacity: 1; - color: rgb(227 160 8 / var(--tw-text-opacity)); -} - .text-yellow-800 { --tw-text-opacity: 1; color: rgb(114 59 19 / var(--tw-text-opacity)); } -.text-gray-200 { - --tw-text-opacity: 1; - color: rgb(229 231 235 / var(--tw-text-opacity)); -} - -.text-green-800 { - --tw-text-opacity: 1; - color: rgb(48 72 18 / var(--tw-text-opacity)); -} - .opacity-0 { opacity: 0; } @@ -2169,6 +2073,10 @@ input:checked + .toggle-bg { transition-duration: 300ms; } +.duration-500 { + transition-duration: 500ms; +} + .duration-75 { transition-duration: 75ms; } @@ -2194,6 +2102,11 @@ input:checked + .toggle-bg { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important; } +.hidden .slide-up { + --tw-translate-y: 1.25rem !important; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important; +} + .slide-up { --tw-translate-y: 0px; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); @@ -2296,6 +2209,16 @@ input:checked + .toggle-bg { background-color: rgb(243 244 246 / var(--tw-bg-opacity)); } +.hover\:bg-gray-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.hover\:bg-green-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(228 240 213 / var(--tw-bg-opacity)); +} + .hover\:bg-green-600:hover { --tw-bg-opacity: 1; background-color: rgb(97 145 37 / var(--tw-bg-opacity)); @@ -2316,11 +2239,6 @@ input:checked + .toggle-bg { background-color: rgb(255 255 255 / var(--tw-bg-opacity)); } -.hover\:bg-gray-200:hover { - --tw-bg-opacity: 1; - background-color: rgb(229 231 235 / var(--tw-bg-opacity)); -} - .hover\:text-blue-600:hover { --tw-text-opacity: 1; color: rgb(0 125 187 / var(--tw-text-opacity)); @@ -2446,6 +2364,11 @@ input:checked + .toggle-bg { border-color: rgb(0 156 234 / var(--tw-border-opacity)); } +.dark .dark\:border-gray-500 { + --tw-border-opacity: 1; + border-color: rgb(107 114 128 / var(--tw-border-opacity)); +} + .dark .dark\:border-gray-600 { --tw-border-opacity: 1; border-color: rgb(75 85 99 / var(--tw-border-opacity)); @@ -2504,21 +2427,16 @@ input:checked + .toggle-bg { background-color: rgb(97 145 37 / var(--tw-bg-opacity)); } -.dark .dark\:bg-yellow-900 { - --tw-bg-opacity: 1; - background-color: rgb(99 49 18 / var(--tw-bg-opacity)); -} - -.dark .dark\:bg-blue-700 { - --tw-bg-opacity: 1; - background-color: rgb(0 94 140 / var(--tw-bg-opacity)); -} - .dark .dark\:bg-green-900 { --tw-bg-opacity: 1; background-color: rgb(24 36 9 / var(--tw-bg-opacity)); } +.dark .dark\:bg-yellow-900 { + --tw-bg-opacity: 1; + background-color: rgb(99 49 18 / var(--tw-bg-opacity)); +} + .dark .dark\:bg-opacity-80 { --tw-bg-opacity: 0.8; } @@ -2538,6 +2456,11 @@ input:checked + .toggle-bg { color: rgb(243 244 246 / var(--tw-text-opacity)); } +.dark .dark\:text-gray-200 { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); +} + .dark .dark\:text-gray-300 { --tw-text-opacity: 1; color: rgb(209 213 219 / var(--tw-text-opacity)); @@ -2548,6 +2471,16 @@ input:checked + .toggle-bg { color: rgb(156 163 175 / var(--tw-text-opacity)); } +.dark .dark\:text-gray-50 { + --tw-text-opacity: 1; + color: rgb(249 250 251 / var(--tw-text-opacity)); +} + +.dark .dark\:text-green-300 { + --tw-text-opacity: 1; + color: rgb(175 211 130 / var(--tw-text-opacity)); +} + .dark .dark\:text-white { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); @@ -2558,16 +2491,6 @@ input:checked + .toggle-bg { color: rgb(250 202 21 / var(--tw-text-opacity)); } -.dark .dark\:text-green-300 { - --tw-text-opacity: 1; - color: rgb(175 211 130 / var(--tw-text-opacity)); -} - -.dark .dark\:text-gray-50 { - --tw-text-opacity: 1; - color: rgb(249 250 251 / var(--tw-text-opacity)); -} - .dark .dark\:placeholder-gray-400::-moz-placeholder { --tw-placeholder-opacity: 1; color: rgb(156 163 175 / var(--tw-placeholder-opacity)); @@ -2602,6 +2525,11 @@ input:checked + .toggle-bg { background-color: rgb(31 41 55 / var(--tw-bg-opacity)); } +.dark .dark\:hover\:bg-green-600:hover { + --tw-bg-opacity: 1; + background-color: rgb(97 145 37 / var(--tw-bg-opacity)); +} + .dark .dark\:hover\:bg-green-700:hover { --tw-bg-opacity: 1; background-color: rgb(73 109 28 / var(--tw-bg-opacity)); @@ -2642,6 +2570,11 @@ input:checked + .toggle-bg { --tw-ring-color: rgb(0 156 234 / var(--tw-ring-opacity)); } +.dark .dark\:focus\:ring-blue-800:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(0 62 94 / var(--tw-ring-opacity)); +} + .dark .dark\:focus\:ring-gray-600:focus { --tw-ring-opacity: 1; --tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity)); @@ -2677,10 +2610,6 @@ input:checked + .toggle-bg { display: block; } - .sm\:flex { - display: flex; - } - .sm\:rounded-lg { border-radius: 0.5rem; } @@ -2742,10 +2671,6 @@ input:checked + .toggle-bg { display: block; } - .lg\:inline { - display: inline; - } - .lg\:flex { display: flex; } @@ -2804,10 +2729,6 @@ input:checked + .toggle-bg { padding-left: 0.75rem; } - .lg\:pl-3\.5 { - padding-left: 0.875rem; - } - .lg\:pl-64 { padding-left: 16rem; } diff --git a/src/clj/auto_ap/handler.clj b/src/clj/auto_ap/handler.clj index 54643aa9..da43b0df 100644 --- a/src/clj/auto_ap/handler.clj +++ b/src/clj/auto_ap/handler.clj @@ -30,7 +30,8 @@ [ring.util.response :as response] [unilog.context :as lc] [clj-time.coerce :as coerce] - [clj-time.core :as time])) + [clj-time.core :as time] + [cemerick.url :as url])) (when (:aws-access-key-id env) (defcredential (:aws-access-key-id env) (:aws-secret-access-key env) (:aws-region env))) @@ -161,9 +162,17 @@ (let [end-time (time/plus (time/now) (time/days 14))] (assoc response :session (assoc session ::idle-timeout (coerce/to-date end-time))))))))))) +(defn wrap-hx-current-url-params + [handler ] + (fn [request] + (let [query-params (some-> (get-in request [:headers "hx-current-url"]) (url/url ) :query) + request (assoc request :hx-query-params query-params)] + (handler request)))) + #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (def app (-> route-handler + (wrap-hx-current-url-params) (wrap-guess-route) (wrap-authorization auth-backend ) diff --git a/src/clj/auto_ap/ssr/company/company_1099.clj b/src/clj/auto_ap/ssr/company/company_1099.clj index b4b81d2b..e487cde3 100644 --- a/src/clj/auto_ap/ssr/company/company_1099.clj +++ b/src/clj/auto_ap/ssr/company/company_1099.clj @@ -121,12 +121,19 @@ (->> results (filter (fn [[_ _ a]] (>= (or a 0.0) 600.0))) - (take 200) (sort-by (fn [[client _ amount]] - [(:client/code client ) amount]))))) + [(:client/code client ) amount])) + (into [])))) -(defn table* [{:keys [identity session]} & {:keys [flash-id]}] - (let [companies (take 30 (get-1099-companies identity session))] +(defn table* [{:keys [identity session query-params hx-query-params]} & {:keys [flash-id]}] + (println hx-query-params) + (let [start (or (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) + 0) + per-page (or (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) + 30) + companies (get-1099-companies identity session) + total (count companies) + companies (subvec companies (Math/min start total) (Math/min (+ start per-page) total))] [:div#vendor-table {:hx-get (bidi/path-for ssr-routes/only-routes :company-1099-vendor-table :request-method :get) @@ -210,10 +217,20 @@ (com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes :company-1099-vendor-dialog :vendor-id (:db/id vendor)) - :hx-target "#modal-content" + :hx-target "#modal-holder" :hx-swap "outerHTML"} svg/pencil))))))] - (com/paginator))])) + (com/paginator {:start start + :end (Math/min (+ start per-page) total) + :per-page per-page + :total total + :a-params (fn [page] + {:hx-get (str (bidi/path-for ssr-routes/only-routes + :company-1099-vendor-table + :request-method :get) + "?start=" (* page per-page)) + :hx-target "#vendor-table" + :hx-swap "outerHTML show:#app:top"})}))])) (defn form-data->map [form-data] (reduce-kv @@ -246,7 +263,7 @@ (update :vendor/legal-entity-tin-type #(some->> % not-empty (keyword "legal-entity-tin-type")))))])) (html-response (table* request :flash-id (Long/parseLong (:vendor-id route-params))) - :headers {"hx-trigger" "closeDialog"})) + :headers {"hx-trigger" "closeModal"})) @@ -254,104 +271,102 @@ (let [vendor (dc/pull (dc/db conn) '[* {:vendor/legal-entity-1099-type [:db/ident] :vendor/legal-entity-tin-type [:db/ident]}] (Long/parseLong (:vendor-id (:params request))))] ;; TODO perms (html-response - [:form {:hx-post (bidi/path-for ssr-routes/only-routes - :company-1099-vendor-save - :request-method :post - :vendor-id (Long/parseLong (:vendor-id (:params request)))) - :hx-target "#vendor-table" - :hx-swap "outerHTML swap:300ms"} - (com/dialog - [:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name vendor)]] - [:div.space-y-6 - [:div.grid.grid-cols-6.gap-4 - [:h4.text-xl.border-b.col-span-6 "Address"] - [:div.col-span-6 - (com/field {:label "Street 1"} - (com/text-input {:name (path->name [:vendor/address :address/street1]) - :value (-> vendor :vendor/address :address/street1) - :placeholder "1700 Pennsylvania Ave" - :autofocus true}))] - [:div.col-span-6 - (com/field {:label "Street 2"} - (com/text-input {:name (path->name [:vendor/address :address/street2]) - :value (-> vendor :vendor/address :address/street2) - :placeholder "Suite 200"}))] - [:div.col-span-3 - (com/field {:label "City"} - (com/text-input {:name (path->name [:vendor/address :address/city]) - :value (-> vendor :vendor/address :address/city) - :placeholder "Cupertino"}))] - [:div.col-span-1 - (com/field {:label "State"} - (com/text-input {:name (path->name [:vendor/address :address/state]) - :value (-> vendor :vendor/address :address/state) - :placeholder "CA"}))] - [:div.col-span-2 - (com/field {:label "Zip"} - (com/text-input {:name (path->name [:vendor/address :address/zip]) - :value (-> vendor :vendor/address :address/zip) - :placeholder "98102"}))] - [:h4.text-xl.border-b.col-span-6 "Legal Entity"] - [:div.col-span-6 - (com/field {:label "Legal Entity Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-name]) - :value (-> vendor :vendor/legal-entity-name) - :placeholder "Good Restaurant LLC"}))] - [:div.col-span-6.text-center " - OR -"] - [:div.col-span-2 - (com/field {:label "First Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-first-name]) - :value (-> vendor :vendor/legal-entity-first-name) - :placeholder "John"}))] - [:div.col-span-2 - (com/field {:label "Middle Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-middle-name]) - :value (-> vendor :vendor/legal-entity-middle-name) - :placeholder "C."}))] - [:div.col-span-2 - (com/field {:label "Last Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-last-name]) - :value (-> vendor :vendor/legal-entity-last-name) - :placeholder "Riley"}))] - [:div.col-span-2 - (com/field {:label "TIN"} - (com/text-input {:name (path->name [:vendor/legal-entity-tin]) - :value (-> vendor :vendor/legal-entity-tin) - :placeholder "John"}))] - [:div.col-span-2 - (com/field {:label "TIN Type"} - (com/select {:name (path->name [:vendor/legal-entity-tin-type]) - :allow-blank? true - :value (some-> vendor :vendor/legal-entity-tin-type :db/ident name) - :options [["ein" "EIN"] - ["ssn" "SSN"]]}))] - [:div.col-span-2 - (com/field {:label "1099 Type"} - (com/select {:name (path->name [:vendor/legal-entity-1099-type]) - :allow-blank? true - :value (some-> vendor :vendor/legal-entity-1099-type :db/ident name) - :options [["none" "None"] - ["misc" "Misc"] - ["landlord" "Landlord"]]}))] - [:div.col-span-6 - (com/button {:color :primary} - "Save")]]] - [:div])] - :headers {"hx-trigger" "openDialog"}))) + (com/modal {} + [:form {:hx-post (str (bidi/path-for ssr-routes/only-routes + :company-1099-vendor-save + :request-method :post + :vendor-id (Long/parseLong (:vendor-id (:params request))))) + :hx-target "#vendor-table" + :hx-swap "outerHTML swap:300ms"} + [:fieldset {:class "hx-disable"} + (com/modal-card {} + [:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name vendor)]] + [:div.space-y-6 + [:div.grid.grid-cols-6.gap-4 + [:h4.text-xl.border-b.col-span-6 "Address"] + [:div.col-span-6 + (com/field {:label "Street 1"} + (com/text-input {:name (path->name [:vendor/address :address/street1]) + :value (-> vendor :vendor/address :address/street1) + :placeholder "1700 Pennsylvania Ave" + :autofocus true}))] + [:div.col-span-6 + (com/field {:label "Street 2"} + (com/text-input {:name (path->name [:vendor/address :address/street2]) + :value (-> vendor :vendor/address :address/street2) + :placeholder "Suite 200"}))] + [:div.col-span-3 + (com/field {:label "City"} + (com/text-input {:name (path->name [:vendor/address :address/city]) + :value (-> vendor :vendor/address :address/city) + :placeholder "Cupertino"}))] + [:div.col-span-1 + (com/field {:label "State"} + (com/text-input {:name (path->name [:vendor/address :address/state]) + :value (-> vendor :vendor/address :address/state) + :placeholder "CA"}))] + [:div.col-span-2 + (com/field {:label "Zip"} + (com/text-input {:name (path->name [:vendor/address :address/zip]) + :value (-> vendor :vendor/address :address/zip) + :placeholder "98102"}))] + [:h4.text-xl.border-b.col-span-6 "Legal Entity"] + [:div.col-span-6 + (com/field {:label "Legal Entity Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-name]) + :value (-> vendor :vendor/legal-entity-name) + :placeholder "Good Restaurant LLC"}))] + [:div.col-span-6.text-center " - OR -"] + [:div.col-span-2 + (com/field {:label "First Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-first-name]) + :value (-> vendor :vendor/legal-entity-first-name) + :placeholder "John"}))] + [:div.col-span-2 + (com/field {:label "Middle Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-middle-name]) + :value (-> vendor :vendor/legal-entity-middle-name) + :placeholder "C."}))] + [:div.col-span-2 + (com/field {:label "Last Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-last-name]) + :value (-> vendor :vendor/legal-entity-last-name) + :placeholder "Riley"}))] + [:div.col-span-2 + (com/field {:label "TIN"} + (com/text-input {:name (path->name [:vendor/legal-entity-tin]) + :value (-> vendor :vendor/legal-entity-tin) + :placeholder "John"}))] + [:div.col-span-2 + (com/field {:label "TIN Type"} + (com/select {:name (path->name [:vendor/legal-entity-tin-type]) + :allow-blank? true + :value (some-> vendor :vendor/legal-entity-tin-type :db/ident name) + :options [["ein" "EIN"] + ["ssn" "SSN"]]}))] + [:div.col-span-2 + (com/field {:label "1099 Type"} + (com/select {:name (path->name [:vendor/legal-entity-1099-type]) + :allow-blank? true + :value (some-> vendor :vendor/legal-entity-1099-type :db/ident name) + :options [["none" "None"] + ["misc" "Misc"] + ["landlord" "Landlord"]]}))] + [:div.col-span-6 + (com/button {:color :primary} + "Save")]]] + [:div])]])))) (defn vendor-table [request] - (html-response (com/page - {:nav - (com/company-aside-nav)} - (com/breadcrumbs {} - [:a {:href "#"} "My Company"] - [:a {:href "#"} "1099 Vendor Info"]) - (table* request)))) + (html-response (table* request) + :headers {"hx-push-url" (str "?start=" (get (:query-params request) "start"))})) (defn page [{:keys [identity matched-route] :as request}] (base-page request - (com/page {:nav (com/company-aside-nav)} + (com/page {:nav (com/company-aside-nav) + :active-client (:client (:session request)) + :identity (:identity request)} (com/breadcrumbs {} [:a {:href "#"} "My Company"] [:a {:href "#"} "1099 Vendor Info"]) diff --git a/src/clj/auto_ap/ssr/company_dropdown.clj b/src/clj/auto_ap/ssr/company_dropdown.clj index 4f73a8ea..8e60eaba 100644 --- a/src/clj/auto_ap/ssr/company_dropdown.clj +++ b/src/clj/auto_ap/ssr/company_dropdown.clj @@ -2,103 +2,130 @@ (:require [auto-ap.datomic :refer [conn]] [auto-ap.graphql.utils :refer [assert-can-see-client]] - [iol-ion.query :refer [can-see-client?]] [auto-ap.ssr-routes :as ssr-routes] - [auto-ap.ssr.components.navbar-dropdown :refer [navbar-dropdown]] + [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils :refer [html-response]] [bidi.bidi :as bidi] [datomic.api :as dc] - [hiccup2.core :as hiccup])) + [hiccup2.core :as hiccup] + [iol-ion.query :refer [can-see-client?]])) -(defn dropdown-contents [{:keys [identity]}] - (let [options (->> (dc/q '[:find ?c ?n - :in $ ?user - :where [?c :client/name ?n] - [(iol-ion.query/can-see-client? ?user ?c)]] - (dc/db conn) - identity) - (map (fn [[k v]] - {"key" k - "value" v})))] - (html-response - [:div.navbar-dropdown {:style {:width "20em"}} - [:a.navbar-item {:hx-put (bidi/path-for ssr-routes/only-routes - :active-client - :request-method :put) - :hx-target "#company-dropdown" - :hx-swap "outerHTML" - :hx-trigger "click"} - "All"] - [:hr.navbar-divider] - [:input#company-search.input.navbar-item {:placeholder "Company name" - :name "search-text" - :autoFocus true} ] - [:input#company-search-value {:type "hidden" +(defn dropdown-search-results* [{:keys [options]}] + [:ul + (for [option options] + [:li + [:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"} + + [:a {:href "#" :class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300" + "_" (hiccup/raw "on click set value of <#company-search-value/> to @data-value then send selected to #company-dropdown") + :data-value (get option "key")} + (get option "value")]]])]) + +(defn get-clients [identity query] + (dc/q '[:find ?c ?n + :in $ ?user ?q + :where [?c :client/name ?n] + [(clojure.string/includes? ?n ?q)] + [(iol-ion.query/can-see-client? ?user ?c)]] + (dc/db conn) + identity + (or query ""))) + +(defn dropdown-search-results [{:keys [identity] :as request}] + (html-response + (dropdown-search-results* {:options (->> (get-clients identity (get (:query-params request) "search-text")) + (map (fn [[k v]] + {"key" k + "value" v}))) + :client (:client (:session request))}))) + +(defn dropdown [{:keys [client]}] + [:div#company-dropdown + {:hx-put (bidi/path-for ssr-routes/only-routes + :active-client + :request-method :put) + :hx-target "#company-dropdown" + :hx-include "#company-search-value" + :hx-swap "outerHTML" + :hx-trigger "selected"} + [:script + (hiccup/raw + "localStorage.setItem(\"last-client-id\", \""(:db/id client)"\")")] + [:div + [:button#company-dropdown-button { :class "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" + :type "button"} + (if client + (:client/name client) + "All Companies") + [:div.w-4.h-4.ml-2 + svg/drop-down]] + [:div#company-dropdown-list.hidden {"_" (hiccup/raw "init call initCompanyDropdown()") + } + [:div {:class "z-10 bg-white rounded-lg shadow w-64 dark:bg-gray-700 slide-up duration-500 transition-all"} + [:div {:class "p-3"} + [:label {:for "input-group-search", :class "sr-only"} "Search"] + [:div {:class "relative"} + [:div {:class "absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"} + [:div.w-5.h-5.text-gray-500.dark:text-gray-400 + svg/search]] + [:input#company-search {:placeholder "Company name" + :name "search-text" + :class "block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + :autoFocus true + :tab-index -1 + :hx-trigger "keyup changed delay:500ms, search" + :hx-get (bidi/path-for ssr-routes/only-routes + :company-dropdown-search-results) + :hx-target "#company-search-results" + :hx-swap "innerHTML"} ]] + [:input#company-search-value {:type "hidden" :autocomplete "off" - :name "search-client" - :hx-put (bidi/path-for ssr-routes/only-routes - :active-client - :request-method :put) - :hx-target "#company-dropdown" - :hx-swap "outerHTML" - :hx-trigger "change"} ] - [:script - (hiccup/raw - (str " -var z = new autoComplete({ - selector:\"#company-search\", - placeholder: \"Company Name....\", - data: { - keys: [\"value\"], - src: " (cheshire.core/encode - options) + :name "search-client" + :hx-put (bidi/path-for ssr-routes/only-routes + :active-client + :request-method :put) + :hx-target "#company-dropdown" + :hx-swap "outerHTML" + :hx-trigger "change changed"} ]] + [:div.divide-y.divide-gray-100 + [:div#company-search-results {:class "h-48 px-3 pb-3 overflow-y-auto text-sm text-gray-700 dark:text-gray-200"}] + [:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"} - " - }, - resultItem: { - highlight:true, - class: \"autocomplete-suggestion\", - selected: \"highlighted\" + [:button {:class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300" + :hx-put (bidi/path-for ssr-routes/only-routes + :active-client + :request-method :put) + :hx-target "#company-dropdown" + :hx-swap "outerHTML" + :hx-trigger "click"} + "All"]]] + ]] + [:script {:lang "text/javascript"} + (hiccup/raw + " +function initCompanyDropdown() { + var $dropdownTargetEl = document.getElementById('company-dropdown-list'); - }, - resultsList: { - tabSelect: true - }, - submit: true + // set the element that trigger the dropdown menu on click + var $dropdownTriggerEl = document.getElementById('company-dropdown-button'); -}); -z.input.addEventListener(\"selection\", function (event) { - z.input.blur(); - z.input.value = event.detail.selection.value.value; -document.getElementById(\"company-search-value\").value= event.detail.selection.value.key; -document.getElementById(\"company-search-value\").dispatchEvent(new Event('change')); - -}); -"))]]))) - -(defn dropdown [request] - - (let [client (get-in request [:session :client])] - - (navbar-dropdown - "company-dropdown" - [:span - (if client - (str "Company: " (:client/name client)) - "Company") - [:script - (hiccup/raw - "localStorage.setItem(\"last-client-id\", \""(:db/id client)"\")")]] - [:div {:hx-get - (bidi/path-for ssr-routes/only-routes - :company-dropdown-contents) - :hx-swap "outerHTML" - :hx-trigger "intersect delay:150ms" - :hx-target "closest .navbar-dropdown" - :style {:width "20em" - :height "80px"} - } - [:div.loader.is-loading.is-active.is-centered]]))) + var dropdownOptions = { + placement: 'bottom', + triggerType: 'click', + offsetSkidding: 0, + offsetDistance: 10, + delay: 300, + onHide: () => { + }, + onShow: () => { + document.getElementById('company-search').focus() + }, + onToggle: () => { + } + }; + var companyDrowdown = new Dropdown($dropdownTargetEl, $dropdownTriggerEl, dropdownOptions); +} +")]]]) (defn active-client [{:keys [identity params] :as request}] (let [client-id (some-> (or (:search-client params) (get params "search-client")) not-empty Long/parseLong)] @@ -109,7 +136,8 @@ document.getElementById(\"company-search-value\").dispatchEvent(new Event('chang (dc/pull (dc/db conn) [:db/id :client/name :client/code] client-id)))] (assoc (html-response - (dropdown (assoc request :session new-session))) + (dropdown {:client (:client new-session) + :identity identity})) :session new-session :headers diff --git a/src/clj/auto_ap/ssr/components.clj b/src/clj/auto_ap/ssr/components.clj index f4a7777f..f7d3fae5 100644 --- a/src/clj/auto_ap/ssr/components.clj +++ b/src/clj/auto_ap/ssr/components.clj @@ -15,7 +15,8 @@ (def button buttons/button-) (def button-icon buttons/button-icon-) (def icon-button buttons/icon-button-) -(def dialog dialog/dialog-) +(def modal dialog/modal-) +(def modal-card dialog/modal-card-) (def text-input inputs/text-input-) (def select inputs/select-) @@ -43,29 +44,62 @@ :class (str "font-medium text-blue-600 dark:text-blue-500 hover:underline " class)}] children)) -(defn paginator [] +(defn bound [x y z] + (cond + (< z x) + x + (< y x) + x + (> y z) + z + :else + y)) + +(def elipsis-button + [:p {:href "#", :class "flex items-center justify-center px-3 py-2 text-sm leading-tight text-gray-500 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400"} "..."]) + +(defn paginator- [{:keys [start per-page end total a-params]}] + (let [per-page (or per-page 20) + max-buttons 5 + buttons-before (Math/floor (/ max-buttons 2)) + total-pages (long (Math/max (long 1) (long (Math/ceil (/ total per-page))))) + current-page (long (Math/floor (/ start per-page))) + first-page-button (bound 0 (- current-page buttons-before) (- total-pages max-buttons)) + all-buttons (into [] (for [x (range total-pages)] + [:li + [:a (-> (a-params x) + (update + :class #(cond-> % + true (str " flex items-center justify-center px-3 py-2 text-sm leading-tight border ") + + (= current-page x) + (str " text-primary-600 bg-primary-50 border-primary-300 hover:bg-primary-100 hover:text-primary-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white") + + (not= current-page x) + (str " text-gray-500 bg-white border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"))) + (assoc :href "#")) + (inc x)]])) + + + last-page-button (Math/min (long total-pages) (long (+ max-buttons first-page-button))) + + extended-last-page-button (when (not= last-page-button total-pages) + (list + elipsis-button + (last all-buttons))) + + extended-first-page-button (when (not= first-page-button 0) + (list + (first all-buttons) + elipsis-button))] + [:nav + [:ul {:class "inline-flex items-stretch -space-x-px"} + extended-first-page-button + (apply list (subvec all-buttons first-page-button last-page-button)) + extended-last-page-button]])) + +(defn paginator [{:keys [start per-page end total a-params] :as params}] [:nav {:class "flex flex-col items-start justify-between p-4 space-y-3 md:flex-row md:items-center md:space-y-0", :aria-label "Table navigation"} [:span {:class "text-sm font-normal text-gray-500 dark:text-gray-400"} - [:span {:class "font-semibold text-gray-900 dark:text-white"} "1-10"] - [:span {:class "font-semibold text-gray-900 dark:text-white"} "1000"]] - [:ul {:class "inline-flex items-stretch -space-x-px"} - [:li - [:a {:href "#", :class "flex items-center justify-center h-full py-1.5 px-3 ml-0 text-gray-500 bg-white rounded-l-lg border 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"} - [:span {:class "sr-only"} "Previous"] - [:svg {:class "w-5 h-5", :aria-hidden "true", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} - [:path {:fill-rule "evenodd", :d "M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z", :clip-rule "evenodd"}]]]] - [:li - [:a {:href "#", :class "flex items-center justify-center px-3 py-2 text-sm leading-tight text-gray-500 bg-white border 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"} "1"]] - [:li - [:a {:href "#", :class "flex items-center justify-center px-3 py-2 text-sm leading-tight text-gray-500 bg-white border 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"} "2"]] - [:li - [:a {:href "#", :aria-current "page", :class "z-10 flex items-center justify-center px-3 py-2 text-sm leading-tight border text-primary-600 bg-primary-50 border-primary-300 hover:bg-primary-100 hover:text-primary-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white"} "3"]] - [:li - [:a {:href "#", :class "flex items-center justify-center px-3 py-2 text-sm leading-tight text-gray-500 bg-white border 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"} "..."]] - [:li - [:a {:href "#", :class "flex items-center justify-center px-3 py-2 text-sm leading-tight text-gray-500 bg-white border 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"} "100"]] - [:li - [:a {:href "#", :class "flex items-center justify-center h-full py-1.5 px-3 leading-tight text-gray-500 bg-white rounded-r-lg border 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"} - [:span {:class "sr-only"} "Next"] - [:svg {:class "w-5 h-5", :aria-hidden "true", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} - [:path {:fill-rule "evenodd", :d "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", :clip-rule "evenodd"}]]]]]]) + [:span {:class "font-semibold text-gray-900 dark:text-white"} (str (inc start)) "-" (str end) " of " (str total)]] + (paginator- params)]) diff --git a/src/clj/auto_ap/ssr/components/dialog.clj b/src/clj/auto_ap/ssr/components/dialog.clj index ed56d0b8..dc8e9635 100644 --- a/src/clj/auto_ap/ssr/components/dialog.clj +++ b/src/clj/auto_ap/ssr/components/dialog.clj @@ -1,9 +1,44 @@ -(ns auto-ap.ssr.components.dialog) +(ns auto-ap.ssr.components.dialog + (:require [hiccup2.core :as hiccup])) -(defn dialog- [header content footer] - [:div#modal-content +(defn modal- [params & children] + [:div + [:div#modal-holder { :tabindex "-1", :class "fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full flex justify-center hidden" :aria-hidden true + "_" (hiccup/raw "on closeModal transition <#modal-holder .modal-content /> opacity to 0.0 over 300ms then call hideModal()")} + [:div {:class "relative w-full max-w-2xl max-h-full"} + (into [:div#modal-content] + children)] + ] + [:script {:lang "text/javascript"} + (hiccup/raw " + var modal_element = document.getElementById('modal-holder'); + var modal_options = { + placement: 'center', + backdrop: 'dynamic', + backdropClasess: 'bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40', + closable: true, + onOpen: function() { + modal_element.dispatchEvent('openModal'); + + }, + onHide: function() { + modal_element.outerHTML='
'; + }, + }; + var curModal = new Modal(modal_element, modal_options); +curModal.show(); +function hideModal() { +curModal.hide(); +} +") + + ]]) + +(defn modal-card- [params header content footer] + [:div#modal-card [:div {:class "relative bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white fade-in slide-up duration-300 transition-all modal-content"} [:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600"} header] [:div {:class "p-6 space-y-6"} content] - [:div footer]]]) + [:div footer]] + ]) diff --git a/src/clj/auto_ap/ssr/components/navbar.clj b/src/clj/auto_ap/ssr/components/navbar.clj index 964924b6..21024527 100644 --- a/src/clj/auto_ap/ssr/components/navbar.clj +++ b/src/clj/auto_ap/ssr/components/navbar.clj @@ -3,9 +3,10 @@ [auto-ap.ssr.svg :as svg] [hiccup2.core :as hiccup] [bidi.bidi :as bidi] - [auto-ap.ssr-routes :as ssr-routes])) + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.company-dropdown :as cd])) -(defn navbar- [] +(defn navbar- [{:keys [client identity]}] [:nav {:class "fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700"} [:div {:class "px-3 py-3 lg:px-5 lg:pl-3"} [:div {:class "flex items-center justify-between"} @@ -17,36 +18,31 @@ [:a {:href "https://flowbite-admin-dashboard.vercel.app/", :class "flex ml-2 md:mr-24"} [:img {:src "/img/logo-big2.png", :class "h-10 mr-16", :alt "Integreat logo"}] ] - [:button.mt-1.lg:w-96.relative.hidden.lg:block {:class "bg-gray-50 hover:bg-gray-200 dark:hover:bg-gray-700 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 w-full pl-10 py-4 pr-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 gap-4 " - :hx-get (bidi/path-for ssr-routes/only-routes - :search) - :hx-target "#modal-content" - :hx-swap "innerHTML"} + ] + + [:div {:class "flex items-center gap-4"} + [:button.mt-1.lg:w-96.relative.hidden.lg:block {:class "bg-gray-50 hover:bg-gray-200 dark:hover:bg-gray-700 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 w-full pl-10 py-4 pr-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 gap-4 " + :hx-get (bidi/path-for ssr-routes/only-routes + :search) + :hx-target "#modal-holder" + :hx-swap "outerHTML"} [:div {:class "absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-gray-500"} [:div.w-4.h-4 svg/search] - [:span.ml-2 "Search"]]]] - - [:div {:class "flex items-center gap-4"} + [:span.ml-2 "Search"]]] [:div {:class "hidden mr-3 -mb-1 sm:block"} [:span]] (icon-button- {:id "toggleSidebarMobileSearch", :type "button", :class "p-2 text-gray-500 rounded-lg lg:hidden hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white" :hx-get (bidi/path-for ssr-routes/only-routes :search) - :hx-target "#modal-content" - :hx-swap "innerHTML"} + :hx-target "#modal-holder" + :hx-swap "outerHTML"} svg/search) #_[:button [:div.w-4.h-4 svg/search]] + (cd/dropdown {:client client :identity identity}) - - #_[:button {:type "button", :data-dropdown-toggle "apps-dropdown", :class "hidden p-2 text-gray-500 rounded-lg sm:flex hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-700"} - [:span {:class "sr-only"} "View notifications"] - [:svg {:class "w-6 h-6", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} - [:path {:d "M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"}]]] - #_(com/icon-button {} - [:div.w-4.h-4 svg/search]) [:div {:class "z-20 z-50 max-w-sm my-4 overflow-hidden text-base list-none bg-white divide-y divide-gray-100 rounded shadow-lg dark:bg-gray-700 dark:divide-gray-600 hidden", :id "apps-dropdown", :style "position: absolute; inset: 0px auto auto 0px; margin: 0px; transform: translate(1545px, 65px);", :data-popper-placement "bottom"} [:div {:class "block px-4 py-2 text-base font-medium text-center text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-400"} ] [:div {:class "grid grid-cols-3 gap-4 p-4"} diff --git a/src/clj/auto_ap/ssr/components/page.clj b/src/clj/auto_ap/ssr/components/page.clj index c1037992..1f312c4d 100644 --- a/src/clj/auto_ap/ssr/components/page.clj +++ b/src/clj/auto_ap/ssr/components/page.clj @@ -3,9 +3,10 @@ [auto-ap.ssr.components.aside :refer [left-aside-]] [hiccup2.core :as hiccup])) -(defn page- [{:keys [nav page-specific]} & children] +(defn page- [{:keys [nav page-specific active-client identity]} & children] [:div#app - (navbar-) + (navbar- {:client active-client + :identity identity}) [:div.flex.pt-16.overflow-hidden (left-aside- {:nav nav :page-specific page-specific}) @@ -14,11 +15,13 @@ (into [:div.p-4] children)]] - [:div#modal-holder.hidden + + + + + #_[:div#modal-holder.hidden {"_" (hiccup/raw "on click trigger closeDialog")} - [:div { :tabindex "-1", :class "fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full flex justify-center "} - [:div {:class "relative w-full max-w-2xl max-h-full" - "_" (hiccup/raw "on click halt the event")} - [:div#modal-content ]]] + [:div {:class "bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40" - }]]]) + }]] + [:div#modal-holder]]) diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index e5d344d4..32f1277b 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -18,7 +18,8 @@ :admin-history-search (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin admin/history-search))) :admin-history-inspect (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin admin/inspect))) :active-client (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin company-dropdown/active-client))) - :company-dropdown-contents (wrap-client-redirect-unauthenticated (wrap-secure company-dropdown/dropdown-contents)) + :company-dropdown-search-results + (wrap-client-redirect-unauthenticated (wrap-secure company-dropdown/dropdown-search-results)) :company-1099 (wrap-client-redirect-unauthenticated (wrap-secure company-1099/page)) :company-1099-vendor-table (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-table)) :company-1099-vendor-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-dialog)) diff --git a/src/clj/auto_ap/ssr/search.clj b/src/clj/auto_ap/ssr/search.clj index f50e34ee..cf8ea376 100644 --- a/src/clj/auto_ap/ssr/search.clj +++ b/src/clj/auto_ap/ssr/search.clj @@ -130,26 +130,26 @@ (if-let [q (get (:form-params request) "q")] (html-response (search-results* q (:identity request))) (html-response - (com/dialog - [:div.p-2 "Search"] - [:div#search.overflow-auto.space-y-6.p-2.h-96 - - (com/text-input {:id "search-input" - :type "search" - :placeholder "5/5/2034 Magheritas" - :name "q" - :hx-post "/search" - :hx-trigger "keyup changed delay:300ms, search" - :hx-target "#search-results" - :hx-indicator "#search" - :value (:q (:params request)) - :autofocus true}) - [:i.text-sm.text-gray-600.dark:text-gray-50 "Try dates, numbers, vendors. To filter to specific type, use 'invoice', 'transaction', 'journal-entry', 'payment'."] - #_[:style - ".htmx-request #search-results {display: none} .htmx-request .htmx-indicator { display: block !important; }"] - [:div#search-results - ] - [:div.loader.is-loading.big.htmx-indicator ]] - nil) - :headers {"hx-trigger" "openDialog"}))) + (com/modal {} + (com/modal-card {} + [:div.p-2 "Search"] + [:div#search.overflow-auto.space-y-6.p-2.h-96 + + (com/text-input {:id "search-input" + :type "search" + :placeholder "5/5/2034 Magheritas" + :name "q" + :hx-post "/search" + :hx-trigger "keyup changed delay:300ms, search" + :hx-target "#search-results" + :hx-indicator "#search" + :value (:q (:params request)) + :autofocus true}) + [:i.text-sm.text-gray-600.dark:text-gray-50 "Try dates, numbers, vendors. To filter to specific type, use 'invoice', 'transaction', 'journal-entry', 'payment'."] + #_[:style + ".htmx-request #search-results {display: none} .htmx-request .htmx-indicator { display: block !important; }"] + [:div#search-results + ] + [:div.loader.is-loading.big.htmx-indicator ]] + nil))))) diff --git a/src/clj/auto_ap/ssr/svg.clj b/src/clj/auto_ap/ssr/svg.clj index 9cd986b7..be39d49b 100644 --- a/src/clj/auto_ap/ssr/svg.clj +++ b/src/clj/auto_ap/ssr/svg.clj @@ -193,3 +193,7 @@ :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]]) + +(def drop-down + [:svg {:class "w-4 h-4 ml-2", :aria-hidden "true", :fill "none", :stroke "currentColor", :viewbox "0 0 24 24", :xmlns "http://www.w3.org/2000/svg"} + [:path {:stroke-linecap "round", :stroke-linejoin "round", :stroke-width "2", :d "M19 9l-7 7-7-7"}]]) diff --git a/src/clj/auto_ap/ssr/ui.clj b/src/clj/auto_ap/ssr/ui.clj index 619fc952..bf96c347 100644 --- a/src/clj/auto_ap/ssr/ui.clj +++ b/src/clj/auto_ap/ssr/ui.clj @@ -29,11 +29,12 @@ :crossorigin= "anonymous"}] [:script {:src "https://unpkg.com/htmx.org@1.9.0/dist/htmx.js" :crossorigin= "anonymous"}] + [: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.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.7/dist/autoComplete.min.js"}] [:script {:src "https://unpkg.com/dropzone@5/dist/min/dropzone.min.js"}] [:link {:rel "stylesheet" :href "https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" :type "text/css"}] - [:body {"_" (hiccup/raw "on closeDialog transition <#modal-holder .modal-content /> opacity to 0.0 over 300ms then add .hidden to <#modal-holder /> on openDialog remove .hidden from #modal-holder")} + [:body {:hx-ext "disable-submit"} contents [:script {:src "/js/flowbite.min.js"}] [:script {:lang "text/javascript"} @@ -53,4 +54,13 @@ } }; - const collapse = new Collapse($targetEl, $triggerEl, options); ")]]])) + const collapse = new Collapse($targetEl, $triggerEl, options); + + + ; + +" + + + + )]]])) diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index 9f4cdfba..07aa8b7d 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -13,7 +13,7 @@ ["/approve/" [#"\d+" :transaction-id]] {:post :transaction-insight-approve} ["/rows/" [#"\d+" :after]] {:get :transaction-insight-rows} ["/explain/" [#"\d+" :transaction-id]] {:get :transaction-insight-explain}}} - "company" {"/dropdown" :company-dropdown-contents + "company" {"/dropdown" :company-dropdown-search-results "/active" {:put :active-client} "/1099" :company-1099 "/1099/table" {:get :company-1099-vendor-table} From 8dca62294734b4d289a26b51ea05fe1acb5799b7 Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Fri, 12 May 2023 15:17:54 -0700 Subject: [PATCH 2/6] Makes reports page work with new tailwind --- src/clj/auto_ap/datomic/reports.clj | 10 +- src/clj/auto_ap/ssr/company/company_1099.clj | 375 +++++++++---------- src/clj/auto_ap/ssr/company/reports.clj | 120 ++++++ src/clj/auto_ap/ssr/company_dropdown.clj | 20 +- src/clj/auto_ap/ssr/components.clj | 61 +-- src/clj/auto_ap/ssr/components/aside.clj | 15 +- src/clj/auto_ap/ssr/components/buttons.clj | 7 + src/clj/auto_ap/ssr/components/data_grid.clj | 59 ++- src/clj/auto_ap/ssr/components/paginator.clj | 61 +++ src/clj/auto_ap/ssr/core.clj | 4 + src/clj/auto_ap/ssr/svg.clj | 74 ++++ src/clj/auto_ap/ssr/utils.clj | 24 +- src/cljc/auto_ap/ssr_routes.cljc | 6 +- 13 files changed, 540 insertions(+), 296 deletions(-) create mode 100644 src/clj/auto_ap/ssr/company/reports.clj create mode 100644 src/clj/auto_ap/ssr/components/paginator.clj diff --git a/src/clj/auto_ap/datomic/reports.clj b/src/clj/auto_ap/datomic/reports.clj index 843e2026..66ce0cf2 100644 --- a/src/clj/auto_ap/datomic/reports.clj +++ b/src/clj/auto_ap/datomic/reports.clj @@ -12,6 +12,8 @@ [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 [query (cond-> {:query {:find [] :in ['$ ] @@ -43,8 +45,7 @@ (apply-pagination args)))) (defn graphql-results [ids db args] - (let [results (->> (pull-many db '[:db/id :report/client :report/created :report/url :report/name :report/creator] - ids) + (let [results (->> (pull-many db default-read ids) (map #(update % :report/created c/from-date)) (group-by :db/id))] (->> ids @@ -63,8 +64,3 @@ [(->> (graphql-results ids-to-retrieve db args)) matching-count])) - - - - - diff --git a/src/clj/auto_ap/ssr/company/company_1099.clj b/src/clj/auto_ap/ssr/company/company_1099.clj index e487cde3..e580c6bb 100644 --- a/src/clj/auto_ap/ssr/company/company_1099.clj +++ b/src/clj/auto_ap/ssr/company/company_1099.clj @@ -6,7 +6,7 @@ [auto-ap.ssr.components :as com] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.ui :refer [base-page]] - [auto-ap.ssr.utils :refer [html-response]] + [auto-ap.ssr.utils :refer [html-response form-data->map path->name]] [bidi.bidi :as bidi] [clojure.string :as str] [datomic.api :as dc] @@ -53,7 +53,6 @@ [(>= ?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) @@ -113,7 +112,6 @@ [(>= ?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) @@ -125,134 +123,97 @@ [(:client/code client ) amount])) (into [])))) +(defn row* [{:keys [client vendor amount flash?]}] + (com/data-grid-row + {:class (when flash? + "live-added")} + (com/data-grid-cell {} (:client/code client)) + (com/data-grid-cell + {} + [:div.flex.whitespace-nowrap.items-center.gap-4 + [:div [:div (:vendor/name vendor)] + [:div.text-sm.text-gray-400 + (or (-> vendor :vendor/legal-entity-name not-empty) + (str (-> vendor :vendor/legal-entity-first-name) " " + (-> vendor :vendor/legal-entity-middle-name) " " + (-> vendor :vendor/legal-entity-last-name)))]] + (when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)] + (com/pill + {:class "text-xs font-medium" + :color :primary} + (str/capitalize t99-type)) + )]) + (com/data-grid-cell + {:class "hidden md:table-cell"} + [:div.flex.gap-4 + (when-let [tin (-> vendor :vendor/legal-entity-tin)] + [:span {:class "text-xs font-medium py-0.5 "} + tin]) + (when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)] + (com/pill {:class "text-xs font-medium" + :color :yellow} + (name tin-type)))]) + (com/data-grid-cell + {:class "hidden lg:table-cell"} + (if (-> vendor :vendor/address :address/street1) + [:div + [:div (-> vendor :vendor/address :address/street1)] " " + [:div + (-> vendor :vendor/address :address/street2)] " " + [:div + (-> vendor :vendor/address :address/city) " " + (-> vendor :vendor/address :address/state) "," + (-> vendor :vendor/address :address/zip)]] + [:p.text-sm.italic.text-gray-400 "No address"])) + (com/data-grid-cell {} + (com/pill {:class "text-xs font-medium" + :color :primary} + "Paid $" (Math/round amount))) + (com/data-grid-right-stack-cell + {} + (if (cannot-overwrite? vendor) + [:div (com/link {:href "mailto:ben@integreatconsult.com"} "Contact Integreat")] + (com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes + :company-1099-vendor-dialog + :vendor-id (:db/id vendor)) + :hx-target "#modal-holder" + :hx-swap "outerHTML"} + svg/pencil))))) (defn table* [{:keys [identity session query-params hx-query-params]} & {:keys [flash-id]}] - (println hx-query-params) - (let [start (or (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) + (let [start (or (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) 0) - per-page (or (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) + per-page (or (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) 30) companies (get-1099-companies identity session) - total (count companies) + total (count companies) companies (subvec companies (Math/min start total) (Math/min (+ start per-page) total))] - [:div#vendor-table {:hx-get (bidi/path-for ssr-routes/only-routes - :company-1099-vendor-table - :request-method :get) - :hx-trigger "clientSelected from:body" - :hx-swap "outerHTML swap:300ms"} - (com/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 - [:h1.text-2xl.mb-3.font-bold "1099 Vendor Info"] - [:div {:class "flex items-center flex-1 space-x-4"} - [:h5 - [:span "Total Vendors:"] - [:span {:class "dark:text-white pl-4"} (count companies)]]]] - [:div {:class "flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"} - (com/button {:color :primary} - (com/button-icon {} svg/refresh) - "Add new product") - (com/button {:color :secondary} - (com/button-icon {} svg/refresh) - "Update stocks 1/250") - (com/icon-button {} - svg/upload)]] - [:div {:class "overflow-x-auto"} - (apply com/data-grid {:headers [(com/data-grid-header {} "Client") - (com/data-grid-header {} "Vendor Name") - (com/data-grid-header {:class "hidden md:table-cell"} "TIN") - (com/data-grid-header {:class "hidden lg:table-cell"} "Address") - (com/data-grid-header {}) - (com/data-grid-header {})]} - (for [[client vendor amount] companies] - (com/data-grid-row - {:class (when (= flash-id - (:db/id vendor)) - "live-added")} - (com/data-grid-cell {} (:client/code client)) - (com/data-grid-cell - {} - [:div.flex.whitespace-nowrap.items-center.gap-4 - [:div [:div (:vendor/name vendor)] - [:div.text-sm.text-gray-400 - (or (-> vendor :vendor/legal-entity-name not-empty) - (str (-> vendor :vendor/legal-entity-first-name) " " - (-> vendor :vendor/legal-entity-middle-name) " " - (-> vendor :vendor/legal-entity-last-name)))]] - (when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)] - (com/pill - {:class "text-xs font-medium" - :color :primary} - (str/capitalize t99-type)) - )]) - (com/data-grid-cell - {:class "hidden md:table-cell"} - [:div.flex.gap-4 - (when-let [tin (-> vendor :vendor/legal-entity-tin)] - [:span {:class "text-xs font-medium py-0.5 "} - tin]) - (when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)] - (com/pill {:class "text-xs font-medium" - :color :yellow} - (name tin-type)))]) - (com/data-grid-cell - {:class "hidden lg:table-cell"} - (if (-> vendor :vendor/address :address/street1) - [:div - [:div (-> vendor :vendor/address :address/street1)] " " - [:div - (-> vendor :vendor/address :address/street2)] " " - [:div - (-> vendor :vendor/address :address/city) " " - (-> vendor :vendor/address :address/state) "," - (-> vendor :vendor/address :address/zip)]] - [:p.text-sm.italic.text-gray-400 "No address"])) - (com/data-grid-cell {} - (com/pill {:class "text-xs font-medium" - :color :primary} - "Paid $" (Math/round amount))) - (com/data-grid-right-stack-cell - {} - (if (cannot-overwrite? vendor) - [:div (com/link {:href "mailto:ben@integreatconsult.com"} "Contact Integreat")] - (com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes - :company-1099-vendor-dialog - :vendor-id (:db/id vendor)) - :hx-target "#modal-holder" - :hx-swap "outerHTML"} - svg/pencil))))))] - (com/paginator {:start start - :end (Math/min (+ start per-page) total) - :per-page per-page - :total total - :a-params (fn [page] - {:hx-get (str (bidi/path-for ssr-routes/only-routes - :company-1099-vendor-table - :request-method :get) - "?start=" (* page per-page)) - :hx-target "#vendor-table" - :hx-swap "outerHTML show:#app:top"})}))])) - -(defn form-data->map [form-data] - (reduce-kv - (fn [acc k v] - (cond (and (string? v) - (empty? v)) - acc - - :else - (assoc-in acc (->> (str/split k #"_") - (mapv #(apply keyword (str/split % #"/")))) - v))) - {} - form-data)) - -(defn path->name [k] - (cond (keyword? k) - (str (namespace k) "/" (name k)) - - (seq k) - (str/join "_" (map path->name k)) - :else k)) + (com/data-grid-card {:id "vendor-table" + :title "1099 Vendor Info" + :entity-name "vendors" + :route :company-1099-vendor-table + :start start + :per-page per-page + :total total + :action-buttons [(com/button {:color :primary} + (com/button-icon {} svg/refresh) + "Add new product") + (com/button {:color :secondary} + (com/button-icon {} svg/refresh) + "Update stocks 1/250") + (com/icon-button {} + svg/upload)] + :rows (for [[client vendor amount] companies] + (row* {:client client + :vendor vendor + :amount amount + :flash? (= flash-id + (:db/id vendor))})) + :headers [(com/data-grid-header {} "Client") + (com/data-grid-header {} "Vendor Name") + (com/data-grid-header {:class "hidden md:table-cell"} "TIN") + (com/data-grid-header {:class "hidden lg:table-cell"} "Address") + (com/data-grid-header {}) + (com/data-grid-header {})]}))) (defn vendor-save [{:keys [form-params identity route-params] :as request}] (when-not (cannot-overwrite? (dc/pull (dc/db conn) '[*] (Long/parseLong (:vendor-id route-params)))) @@ -265,8 +226,6 @@ (table* request :flash-id (Long/parseLong (:vendor-id route-params))) :headers {"hx-trigger" "closeModal"})) - - (defn vendor-dialog [request] (let [vendor (dc/pull (dc/db conn) '[* {:vendor/legal-entity-1099-type [:db/ident] :vendor/legal-entity-tin-type [:db/ident]}] (Long/parseLong (:vendor-id (:params request))))] ;; TODO perms @@ -276,86 +235,86 @@ :company-1099-vendor-save :request-method :post :vendor-id (Long/parseLong (:vendor-id (:params request))))) - :hx-target "#vendor-table" - :hx-swap "outerHTML swap:300ms"} - [:fieldset {:class "hx-disable"} - (com/modal-card {} - [:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name vendor)]] - [:div.space-y-6 - [:div.grid.grid-cols-6.gap-4 - [:h4.text-xl.border-b.col-span-6 "Address"] - [:div.col-span-6 - (com/field {:label "Street 1"} - (com/text-input {:name (path->name [:vendor/address :address/street1]) - :value (-> vendor :vendor/address :address/street1) - :placeholder "1700 Pennsylvania Ave" - :autofocus true}))] - [:div.col-span-6 - (com/field {:label "Street 2"} - (com/text-input {:name (path->name [:vendor/address :address/street2]) - :value (-> vendor :vendor/address :address/street2) - :placeholder "Suite 200"}))] - [:div.col-span-3 - (com/field {:label "City"} - (com/text-input {:name (path->name [:vendor/address :address/city]) - :value (-> vendor :vendor/address :address/city) - :placeholder "Cupertino"}))] - [:div.col-span-1 - (com/field {:label "State"} - (com/text-input {:name (path->name [:vendor/address :address/state]) - :value (-> vendor :vendor/address :address/state) - :placeholder "CA"}))] - [:div.col-span-2 - (com/field {:label "Zip"} - (com/text-input {:name (path->name [:vendor/address :address/zip]) - :value (-> vendor :vendor/address :address/zip) - :placeholder "98102"}))] - [:h4.text-xl.border-b.col-span-6 "Legal Entity"] - [:div.col-span-6 - (com/field {:label "Legal Entity Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-name]) - :value (-> vendor :vendor/legal-entity-name) - :placeholder "Good Restaurant LLC"}))] - [:div.col-span-6.text-center " - OR -"] - [:div.col-span-2 - (com/field {:label "First Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-first-name]) - :value (-> vendor :vendor/legal-entity-first-name) - :placeholder "John"}))] - [:div.col-span-2 - (com/field {:label "Middle Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-middle-name]) - :value (-> vendor :vendor/legal-entity-middle-name) - :placeholder "C."}))] - [:div.col-span-2 - (com/field {:label "Last Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-last-name]) - :value (-> vendor :vendor/legal-entity-last-name) - :placeholder "Riley"}))] - [:div.col-span-2 - (com/field {:label "TIN"} - (com/text-input {:name (path->name [:vendor/legal-entity-tin]) - :value (-> vendor :vendor/legal-entity-tin) - :placeholder "John"}))] - [:div.col-span-2 - (com/field {:label "TIN Type"} - (com/select {:name (path->name [:vendor/legal-entity-tin-type]) - :allow-blank? true - :value (some-> vendor :vendor/legal-entity-tin-type :db/ident name) - :options [["ein" "EIN"] - ["ssn" "SSN"]]}))] - [:div.col-span-2 - (com/field {:label "1099 Type"} - (com/select {:name (path->name [:vendor/legal-entity-1099-type]) - :allow-blank? true - :value (some-> vendor :vendor/legal-entity-1099-type :db/ident name) - :options [["none" "None"] - ["misc" "Misc"] - ["landlord" "Landlord"]]}))] - [:div.col-span-6 - (com/button {:color :primary} - "Save")]]] - [:div])]])))) + :hx-target "#vendor-table" + :hx-swap "outerHTML swap:300ms"} + [:fieldset {:class "hx-disable"} + (com/modal-card {} + [:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name vendor)]] + [:div.space-y-6 + [:div.grid.grid-cols-6.gap-4 + [:h4.text-xl.border-b.col-span-6 "Address"] + [:div.col-span-6 + (com/field {:label "Street 1"} + (com/text-input {:name (path->name [:vendor/address :address/street1]) + :value (-> vendor :vendor/address :address/street1) + :placeholder "1700 Pennsylvania Ave" + :autofocus true}))] + [:div.col-span-6 + (com/field {:label "Street 2"} + (com/text-input {:name (path->name [:vendor/address :address/street2]) + :value (-> vendor :vendor/address :address/street2) + :placeholder "Suite 200"}))] + [:div.col-span-3 + (com/field {:label "City"} + (com/text-input {:name (path->name [:vendor/address :address/city]) + :value (-> vendor :vendor/address :address/city) + :placeholder "Cupertino"}))] + [:div.col-span-1 + (com/field {:label "State"} + (com/text-input {:name (path->name [:vendor/address :address/state]) + :value (-> vendor :vendor/address :address/state) + :placeholder "CA"}))] + [:div.col-span-2 + (com/field {:label "Zip"} + (com/text-input {:name (path->name [:vendor/address :address/zip]) + :value (-> vendor :vendor/address :address/zip) + :placeholder "98102"}))] + [:h4.text-xl.border-b.col-span-6 "Legal Entity"] + [:div.col-span-6 + (com/field {:label "Legal Entity Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-name]) + :value (-> vendor :vendor/legal-entity-name) + :placeholder "Good Restaurant LLC"}))] + [:div.col-span-6.text-center " - OR -"] + [:div.col-span-2 + (com/field {:label "First Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-first-name]) + :value (-> vendor :vendor/legal-entity-first-name) + :placeholder "John"}))] + [:div.col-span-2 + (com/field {:label "Middle Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-middle-name]) + :value (-> vendor :vendor/legal-entity-middle-name) + :placeholder "C."}))] + [:div.col-span-2 + (com/field {:label "Last Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-last-name]) + :value (-> vendor :vendor/legal-entity-last-name) + :placeholder "Riley"}))] + [:div.col-span-2 + (com/field {:label "TIN"} + (com/text-input {:name (path->name [:vendor/legal-entity-tin]) + :value (-> vendor :vendor/legal-entity-tin) + :placeholder "John"}))] + [:div.col-span-2 + (com/field {:label "TIN Type"} + (com/select {:name (path->name [:vendor/legal-entity-tin-type]) + :allow-blank? true + :value (some-> vendor :vendor/legal-entity-tin-type :db/ident name) + :options [["ein" "EIN"] + ["ssn" "SSN"]]}))] + [:div.col-span-2 + (com/field {:label "1099 Type"} + (com/select {:name (path->name [:vendor/legal-entity-1099-type]) + :allow-blank? true + :value (some-> vendor :vendor/legal-entity-1099-type :db/ident name) + :options [["none" "None"] + ["misc" "Misc"] + ["landlord" "Landlord"]]}))] + [:div.col-span-6 + (com/button {:color :primary} + "Save")]]] + [:div])]])))) (defn vendor-table [request] (html-response (table* request) diff --git a/src/clj/auto_ap/ssr/company/reports.clj b/src/clj/auto_ap/ssr/company/reports.clj new file mode 100644 index 00000000..62a502fa --- /dev/null +++ b/src/clj/auto_ap/ssr/company/reports.clj @@ -0,0 +1,120 @@ +(ns auto-ap.ssr.company.reports + (:require + [amazonica.aws.s3 :as s3] + [auto-ap.datomic :refer [conn]] + [auto-ap.datomic.reports :as r] + [auto-ap.graphql.utils :refer [assert-admin is-admin?]] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.ui :refer [base-page]] + [auto-ap.ssr.utils :refer [html-response]] + [auto-ap.time :as atime] + [bidi.bidi :as bidi] + [config.core :refer [env]] + [datomic.api :as dc] + [hiccup2.core :as hiccup])) + +(defn row* [{:keys [flash? report identity delete-after-settle?]}] + (com/data-grid-row + {:class (when flash? + "live-added") + "_" (hiccup/raw (when delete-after-settle?" on htmx:afterSettle wait 400ms then remove me"))} + (com/data-grid-cell + {} + (:report/name report)) + (com/data-grid-cell + {} + (when (:report/creator report) + (com/pill {:color :primary } + (:report/creator report)))) + (com/data-grid-cell + {} + (atime/unparse-local (:report/created report) + atime/normal-date)) + (com/data-grid-right-stack-cell + {} + (com/a-icon-button {:href (:report/url report)} + svg/download) + (when (is-admin? identity) + [:form + [:input {:type :hidden :name "id" :value (:db/id report)}] + (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)])))) + +(defn table* [{:keys [client start per-page identity session flash-id]}] + (let [start (or start 0) + per-page (or per-page 30) + [reports total] (r/get-graphql {:id identity + :start start + :per-page per-page + :client-id (:db/id client) + :sort nil})] + (com/data-grid-card {:id "report-table" + :title "Reports" + :entity-name "reports" + :route :company-reports-table + :start start + :per-page per-page + :total total + :action-buttons [(com/button {:color :primary} + (com/button-icon {} svg/refresh) + "Add new product") + (com/button {:color :secondary} + (com/button-icon {} svg/refresh) + "Update stocks 1/250") + (com/icon-button {} + svg/upload)] + :rows (for [report reports] + (row* {:report report + :flash? (= flash-id + (:db/id report)) + :identity identity})) + :headers [(com/data-grid-header {} "Name") + (com/data-grid-header {:class "hidden md:table-cell"} "Created by") + (com/data-grid-header {:class "hidden md:table-cell"} "Created") + (com/data-grid-header {})]}))) + +(defn delete-report [{:keys [query-params hx-query-params form-params identity session] :as request}] + (let [[id-to-delete key] (first (dc/q '[:find ?i ?k + :in $ ?i + :where [?i :report/key ?k]] + (dc/db conn) + (some-> (get form-params "id") not-empty Long/parseLong))) + report (dc/pull (dc/db conn) r/default-read id-to-delete)] + (when id-to-delete + (s3/delete-object :bucket-name (:data-bucket env) + :key key) + @(dc/transact conn [[:db/retractEntity id-to-delete]])) + (html-response + (row* {:report report + :flash? true + :identity identity + :delete-after-settle? true})))) + +(defn reports-table [{:keys [query-params hx-query-params identity session] :as request}] + (html-response (table* {:client (:client (:session request)) + :start (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) + :per-page (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) + :identity identity + :session session}) + :headers {"hx-push-url" (str "?start=" (get (:query-params request) "start"))})) + +(defn page [{:keys [query-params hx-query-params identity session] :as request}] + (base-page + request + (com/page {:nav (com/company-aside-nav) + :active-client (:client (:session request)) + :identity (:identity request)} + (com/breadcrumbs {} + [:a {:href "#"} "My Company"] + [:a {:href "#"} "Reports"]) + (table* {:client (:client (:session request)) + :start (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) + :per-page (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) + :identity identity + :session session})) + nil)) diff --git a/src/clj/auto_ap/ssr/company_dropdown.clj b/src/clj/auto_ap/ssr/company_dropdown.clj index 8e60eaba..c5b900c7 100644 --- a/src/clj/auto_ap/ssr/company_dropdown.clj +++ b/src/clj/auto_ap/ssr/company_dropdown.clj @@ -12,14 +12,14 @@ (defn dropdown-search-results* [{:keys [options]}] [:ul - (for [option options] + (for [[id company-name]options] [:li [:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"} [:a {:href "#" :class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300" "_" (hiccup/raw "on click set value of <#company-search-value/> to @data-value then send selected to #company-dropdown") - :data-value (get option "key")} - (get option "value")]]])]) + :data-value id} + company-name]]])]) (defn get-clients [identity query] (dc/q '[:find ?c ?n @@ -33,10 +33,7 @@ (defn dropdown-search-results [{:keys [identity] :as request}] (html-response - (dropdown-search-results* {:options (->> (get-clients identity (get (:query-params request) "search-text")) - (map (fn [[k v]] - {"key" k - "value" v}))) + (dropdown-search-results* {:options (get-clients identity (get (:query-params request) "search-text")) :client (:client (:session request))}))) (defn dropdown [{:keys [client]}] @@ -79,14 +76,7 @@ :hx-target "#company-search-results" :hx-swap "innerHTML"} ]] [:input#company-search-value {:type "hidden" - :autocomplete "off" - :name "search-client" - :hx-put (bidi/path-for ssr-routes/only-routes - :active-client - :request-method :put) - :hx-target "#company-dropdown" - :hx-swap "outerHTML" - :hx-trigger "change changed"} ]] + :name "search-client"}]] [:div.divide-y.divide-gray-100 [:div#company-search-results {:class "h-48 px-3 pb-3 overflow-y-auto text-sm text-gray-700 dark:text-gray-200"}] [:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"} diff --git a/src/clj/auto_ap/ssr/components.clj b/src/clj/auto_ap/ssr/components.clj index f7d3fae5..75233aa2 100644 --- a/src/clj/auto_ap/ssr/components.clj +++ b/src/clj/auto_ap/ssr/components.clj @@ -8,13 +8,15 @@ [auto-ap.ssr.components.navbar :as navbar] [auto-ap.ssr.components.page :as page] [auto-ap.ssr.components.data-grid :as data-grid] - [auto-ap.ssr.components.tags :as tags])) + [auto-ap.ssr.components.tags :as tags] + [auto-ap.ssr.components.paginator :as paginator])) (def breadcrumbs breadcrumbs/breadcrumbs-) (def button buttons/button-) (def button-icon buttons/button-icon-) (def icon-button buttons/icon-button-) +(def a-icon-button buttons/a-icon-button-) (def modal dialog/modal-) (def modal-card dialog/modal-card-) @@ -44,62 +46,9 @@ :class (str "font-medium text-blue-600 dark:text-blue-500 hover:underline " class)}] children)) -(defn bound [x y z] - (cond - (< z x) - x - (< y x) - x - (> y z) - z - :else - y)) - -(def elipsis-button - [:p {:href "#", :class "flex items-center justify-center px-3 py-2 text-sm leading-tight text-gray-500 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400"} "..."]) - -(defn paginator- [{:keys [start per-page end total a-params]}] - (let [per-page (or per-page 20) - max-buttons 5 - buttons-before (Math/floor (/ max-buttons 2)) - total-pages (long (Math/max (long 1) (long (Math/ceil (/ total per-page))))) - current-page (long (Math/floor (/ start per-page))) - first-page-button (bound 0 (- current-page buttons-before) (- total-pages max-buttons)) - all-buttons (into [] (for [x (range total-pages)] - [:li - [:a (-> (a-params x) - (update - :class #(cond-> % - true (str " flex items-center justify-center px-3 py-2 text-sm leading-tight border ") - - (= current-page x) - (str " text-primary-600 bg-primary-50 border-primary-300 hover:bg-primary-100 hover:text-primary-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white") - - (not= current-page x) - (str " text-gray-500 bg-white border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"))) - (assoc :href "#")) - (inc x)]])) - last-page-button (Math/min (long total-pages) (long (+ max-buttons first-page-button))) - extended-last-page-button (when (not= last-page-button total-pages) - (list - elipsis-button - (last all-buttons))) +(def paginator paginator/paginator-) +(def data-grid-card data-grid/data-grid-card-) - extended-first-page-button (when (not= first-page-button 0) - (list - (first all-buttons) - elipsis-button))] - [:nav - [:ul {:class "inline-flex items-stretch -space-x-px"} - extended-first-page-button - (apply list (subvec all-buttons first-page-button last-page-button)) - extended-last-page-button]])) - -(defn paginator [{:keys [start per-page end total a-params] :as params}] - [:nav {:class "flex flex-col items-start justify-between p-4 space-y-3 md:flex-row md:items-center md:space-y-0", :aria-label "Table navigation"} - [:span {:class "text-sm font-normal text-gray-500 dark:text-gray-400"} - [:span {:class "font-semibold text-gray-900 dark:text-white"} (str (inc start)) "-" (str end) " of " (str total)]] - (paginator- params)]) diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index bf630990..f486f88c 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -1,6 +1,8 @@ (ns auto-ap.ssr.components.aside (:require [auto-ap.ssr.svg :as svg] - [hiccup2.core :as hiccup])) + [hiccup2.core :as hiccup] + [bidi.bidi :as bidi] + [auto-ap.ssr-routes :as ssr-routes])) (defn menu-button- [params & children] [:a (-> params @@ -188,7 +190,9 @@ [:ul {:class "space-y-2"} [:li - (menu-button- {:icon svg/report} + (menu-button- {:icon svg/report + :href (bidi/path-for ssr-routes/only-routes + :company-reports)} "Reports")] [:li (menu-button- {:icon svg/bank} @@ -198,5 +202,8 @@ "Vendors")] [:li - (menu-button- {:icon svg/government-building} - "1099 Vendor Info")]]) + (menu-button- {:icon svg/government-building + :href (bidi/path-for ssr-routes/only-routes + :company-1099)} + "1099 Vendor Info" + )]]) diff --git a/src/clj/auto_ap/ssr/components/buttons.clj b/src/clj/auto_ap/ssr/components/buttons.clj index 36f83a12..17b5f90e 100644 --- a/src/clj/auto_ap/ssr/components/buttons.clj +++ b/src/clj/auto_ap/ssr/components/buttons.clj @@ -16,6 +16,13 @@ (defn icon-button- [params & children] (into [:button (update params :class str " inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center p-3 text-sm font-medium border border-gray-300 dark:border-gray-700 text-center text-gray-500 hover:text-gray-800 rounded-lg dark:text-gray-400 dark:hover:text-gray-100") + [:div.htmx-indicator.flex.items-center + (svg/spinner {:class "inline w-4 h-4 text-white"})] + [:div.htmx-indicator-hidden.inline-flex.gap-2.items-center.justify-center (into [:div.h-4.w-4] children)]])) + +(defn a-icon-button- [params & children] + (into + [:a (update params :class str " inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center p-3 text-sm font-medium border border-gray-300 dark:border-gray-700 text-center text-gray-500 hover:text-gray-800 rounded-lg dark:text-gray-400 dark:hover:text-gray-100") [:div.h-4.w-4 children]])) (defn save-button- [params & children] diff --git a/src/clj/auto_ap/ssr/components/data_grid.clj b/src/clj/auto_ap/ssr/components/data_grid.clj index f6e33b9e..7a0dbf73 100644 --- a/src/clj/auto_ap/ssr/components/data_grid.clj +++ b/src/clj/auto_ap/ssr/components/data_grid.clj @@ -1,13 +1,17 @@ -(ns auto-ap.ssr.components.data-grid) +(ns auto-ap.ssr.components.data-grid + (:require + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components.card :refer [content-card-]] + [auto-ap.ssr.components.paginator :refer [paginator-]] + [bidi.bidi :as bidi])) (defn header- [params & rest] (into [:th.px-4.py-3 {:scope "col" :class (:class params)} ] rest)) (defn row- [params & rest] - (into [:tr {:class (cond-> "border-b dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700" - - (:class params) (str " " (:class params)))}] rest)) + (into [:tr (update params + :class str " border-b dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700")] rest)) (defn cell- [params & rest] (into [:td.px-4.py-2 {:class (:class params)}] rest)) @@ -32,3 +36,50 @@ (into [:tbody] rest)]) + +(defn data-grid-card- [{:keys [id + route + title + entity-name + action-buttons + total + start + per-page + flash-id + headers + rows] :as params}] + [:div {:hx-get (bidi/path-for ssr-routes/only-routes + route + :request-method :get) + :hx-trigger "clientSelected from:body" + :hx-swap "outerHTML swap:300ms" + :id id} + (content-card- + {} + [:div {:class "flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"} + [:div + [:h1.text-2xl.mb-3.font-bold title] + [:div {:class "flex items-center flex-1 space-x-4"} + [:h5 + [:span (format "Total %s:" entity-name)] + [:span {:class "dark:text-white pl-4"} total]]]] + (into [:div {:class "flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"} + ] + action-buttons)] + [:div {:class "overflow-x-auto"} + (data-grid- {:headers headers + } + rows + + )] + (paginator- {:start start + :end (Math/min (+ start per-page) total) + :per-page per-page + :total total + :a-params (fn [page] + {:hx-get (str (bidi/path-for ssr-routes/only-routes + route + :request-method :get) + "?start=" (* page per-page)) + :hx-target (str "#" id) + :hx-swap "outerHTML show:#app:top"})}))]) diff --git a/src/clj/auto_ap/ssr/components/paginator.clj b/src/clj/auto_ap/ssr/components/paginator.clj new file mode 100644 index 00000000..c89caffb --- /dev/null +++ b/src/clj/auto_ap/ssr/components/paginator.clj @@ -0,0 +1,61 @@ +(ns auto-ap.ssr.components.paginator) + +(defn bound [x y z] + (cond + (< z x) + x + (< y x) + x + (> y z) + z + :else + y)) + +(def elipsis-button + [:p {:href "#", :class "flex items-center justify-center px-3 py-2 text-sm leading-tight text-gray-500 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400"} "..."]) + +(defn paginator-internal- [{:keys [start per-page end total a-params]}] + (let [per-page (or per-page 20) + max-buttons 5 + buttons-before (Math/floor (/ max-buttons 2)) + total-pages (long (Math/max (long 1) (long (Math/ceil (/ total per-page))))) + current-page (long (Math/floor (/ start per-page))) + first-page-button (bound 0 (- current-page buttons-before) (- total-pages max-buttons)) + all-buttons (into [] (for [x (range total-pages)] + [:li + [:a (-> (a-params x) + (update + :class #(cond-> % + true (str " flex items-center justify-center px-3 py-2 text-sm leading-tight border ") + + (= current-page x) + (str " text-primary-600 bg-primary-50 border-primary-300 hover:bg-primary-100 hover:text-primary-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white") + + (not= current-page x) + (str " text-gray-500 bg-white border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"))) + (assoc :href "#")) + (inc x)]])) + + + last-page-button (Math/min (long total-pages) (long (+ max-buttons first-page-button))) + + extended-last-page-button (when (not= last-page-button total-pages) + (list + elipsis-button + (last all-buttons))) + + extended-first-page-button (when (not= first-page-button 0) + (list + (first all-buttons) + elipsis-button))] + [:nav + [:ul {:class "inline-flex items-stretch -space-x-px"} + extended-first-page-button + (apply list (subvec all-buttons first-page-button last-page-button)) + extended-last-page-button]])) + +(defn paginator- [{:keys [start per-page end total a-params] :as params}] + [:nav {:class "flex flex-col items-start justify-between p-4 space-y-3 md:flex-row md:items-center md:space-y-0", :aria-label "Table navigation"} + [:span {:class "text-sm font-normal text-gray-500 dark:text-gray-400"} + [:span {:class "font-semibold text-gray-900 dark:text-white"} (str (inc start)) "-" (str end) " of " (str total)]] + (paginator-internal- params)]) diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index 32f1277b..70725b2a 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -8,6 +8,7 @@ [auto-ap.ssr.company.company-1099 :as company-1099] [auto-ap.ssr.search :as search] [auto-ap.ssr.company-dropdown :as company-dropdown] + [auto-ap.ssr.company.reports :as company-reports] [auto-ap.routes.ezcater-xls :as ezcater-xls])) ;; from auto-ap.ssr-routes, because they're shared @@ -24,6 +25,9 @@ :company-1099-vendor-table (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-table)) :company-1099-vendor-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-dialog)) :company-1099-vendor-save (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-save)) + :company-reports (wrap-client-redirect-unauthenticated (wrap-secure company-reports/page)) + :company-reports-table (wrap-client-redirect-unauthenticated (wrap-secure company-reports/reports-table)) + :company-reports-delete (wrap-client-redirect-unauthenticated (wrap-admin company-reports/delete-report)) :transaction-insights (wrap-client-redirect-unauthenticated (wrap-secure insights/page)) :transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-secure insights/insight-table)) :transaction-insight-rows (wrap-client-redirect-unauthenticated (wrap-secure insights/transaction-rows)) diff --git a/src/clj/auto_ap/ssr/svg.clj b/src/clj/auto_ap/ssr/svg.clj index be39d49b..c3a5d88f 100644 --- a/src/clj/auto_ap/ssr/svg.clj +++ b/src/clj/auto_ap/ssr/svg.clj @@ -197,3 +197,77 @@ (def drop-down [:svg {:class "w-4 h-4 ml-2", :aria-hidden "true", :fill "none", :stroke "currentColor", :viewbox "0 0 24 24", :xmlns "http://www.w3.org/2000/svg"} [:path {:stroke-linecap "round", :stroke-linejoin "round", :stroke-width "2", :d "M19 9l-7 7-7-7"}]]) + +(def download + [:svg + {:xmlns "http://www.w3.org/2000/svg", :viewBox "0 0 24 24"} + [:defs] + [:title "download-thick-bottom"] + [:path + {:d + "M5.5,11.5c-.275,0-.341.159-.146.354l6.292,6.293a.5.5,0,0,0,.709,0l6.311-6.275c.2-.193.13-.353-.145-.355L15.5,11.5V1.5a1,1,0,0,0-1-1h-5a1,1,0,0,0-1,1V11a.5.5,0,0,1-.5.5Z", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:path + {:d "M23.5,18.5v4a1,1,0,0,1-1,1H1.5a1,1,0,0,1-1-1v-4", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}]]) + +(def trash + [:svg + {:xmlns "http://www.w3.org/2000/svg", :viewBox "0 0 24 24"} + [:defs] + [:title "bin-1"] + [:path + {:d + "M21,4.5,19.188,21.709A2,2,0,0,1,17.2,23.5H6.8a2,2,0,0,1-1.989-1.791L3,4.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:line + {:x1 "0.5", + :y1 "4.5", + :x2 "23.5", + :y2 "4.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:path + {:d "M7.5,4.5v-3a1,1,0,0,1,1-1h7a1,1,0,0,1,1,1v3", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:line + {:x1 "12", + :y1 "9", + :x2 "12", + :y2 "19.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:line + {:x1 "16.5", + :y1 "9", + :x2 "16", + :y2 "19.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:line + {:x1 "7.5", + :y1 "9", + :x2 "8", + :y2 "19.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}]]) diff --git a/src/clj/auto_ap/ssr/utils.clj b/src/clj/auto_ap/ssr/utils.clj index 189762b8..929d5d64 100644 --- a/src/clj/auto_ap/ssr/utils.clj +++ b/src/clj/auto_ap/ssr/utils.clj @@ -2,7 +2,8 @@ (:require [auto-ap.logging :as alog] [config.core :refer [env]] - [hiccup2.core :as hiccup])) + [hiccup2.core :as hiccup] + [clojure.string :as str])) (defn html-response [hiccup & {:keys [status headers] :or {status 200 headers {}}}] {:status status @@ -39,3 +40,24 @@ (ex-message e)] :status 500))))))) +(defn form-data->map [form-data] + (reduce-kv + (fn [acc k v] + (cond (and (string? v) + (empty? v)) + acc + + :else + (assoc-in acc (->> (str/split k #"_") + (mapv #(apply keyword (str/split % #"/")))) + v))) + {} + form-data)) + +(defn path->name [k] + (cond (keyword? k) + (str (namespace k) "/" (name k)) + + (seq k) + (str/join "_" (map path->name k)) + :else k)) diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index 07aa8b7d..83db9fbb 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -18,7 +18,11 @@ "/1099" :company-1099 "/1099/table" {:get :company-1099-vendor-table} "/1099/vendor-dialog" {["/" [#"\d+" :vendor-id]] {:get :company-1099-vendor-dialog - :post :company-1099-vendor-save}}}}) + :post :company-1099-vendor-save}} + "/reports" {"" {:get :company-reports + :delete :company-reports-delete}} + "/reports/table" :company-reports-table + }}) (def only-routes ["/" routes]) From b9ae20125bf6b1b92deef5b51faeb983c3cef13c Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Sat, 13 May 2023 22:51:03 -0700 Subject: [PATCH 3/6] Yodlee page in ssr. --- config/dev.edn | 16 +- config/prod-cloud.edn | 1 + config/prod.edn | 2 +- deps.edn | 8 - resources/input.css | 4 + resources/public/output.css | 12 ++ src/clj/auto_ap/routes/exports.clj | 7 +- src/clj/auto_ap/ssr/company.clj | 66 +++++++ src/clj/auto_ap/ssr/company/company_1099.clj | 182 +++++++++--------- src/clj/auto_ap/ssr/company/reports.clj | 6 +- src/clj/auto_ap/ssr/company/yodlee.clj | 168 ++++++++++++++++ src/clj/auto_ap/ssr/components/aside.clj | 19 +- src/clj/auto_ap/ssr/components/buttons.clj | 8 +- src/clj/auto_ap/ssr/components/dialog.clj | 4 +- src/clj/auto_ap/ssr/components/page.clj | 22 +-- src/clj/auto_ap/ssr/core.clj | 9 +- src/clj/auto_ap/yodlee/core2.clj | 19 +- src/cljc/auto_ap/ssr_routes.cljc | 21 +- test/clj/auto_ap/import/transactions_test.clj | 3 +- 19 files changed, 417 insertions(+), 160 deletions(-) delete mode 100644 deps.edn create mode 100644 src/clj/auto_ap/ssr/company.clj create mode 100644 src/clj/auto_ap/ssr/company/yodlee.clj diff --git a/config/dev.edn b/config/dev.edn index 9a3c54dd..f79b29ef 100644 --- a/config/dev.edn +++ b/config/dev.edn @@ -1,5 +1,6 @@ {:db {:server "localhost"} :scheme "http" + :base-url "http://localhost:3449" :solr-uri "http://localhost:8983" :solr-impl :solr :client-config {:server-type :dev-local @@ -24,21 +25,22 @@ :yodlee-cobrand-name "restserver" :yodlee-cobrand-login "sbCobda48aa19712a83c3ca4e935dd5e5d46b1a" :yodlee-cobrand-password "0a07ea32-1b5d-461b-ad0f-2752cdd77602" - :yodlee-user-login "sbMemda48aa19712a83c3ca4e935dd5e5d46b1a4" + :yodlee-user-login "G7T9kiwaG8rMiykdV4pckmQnfj4OM2pf" :yodlee-user-password "sbMemda48aa19712a83c3ca4e935dd5e5d46b1a4#123" :yodlee-base-url "https://developer.api.yodlee.com/ysl" :yodlee-app "10003600" - :yodlee-fastlink "https://node.developer.yodlee.com/authenticate/restserver/?channelAppName=restserver" + :yodlee-fastlink "https://fl4.sandbox.yodlee.com/authenticate/restserver/fastlink" :run-web? true :run-background? true :dd-env "dev" :dd-service "integreat-app" - :yodlee2-admin-user "e02b38f9-9865-4264-8e4f-6a5ac2c500b0_ADMIN" + :yodlee2-admin-user "50ec7e57-297d-4970-941e-1cb07b8dcb4e_ADMIN" :yodlee2-integreat-user "integreat-main" - :yodlee2-client-id "l6sUyK2NEq3mwopISHlFGWUcJ1U8OUQd" - :yodlee2-client-secret "wZQHoGEkv5AGG2ZH" - :yodlee2-base-url "https://development.api.yodlee.com/ysl" - :yodlee2-fastlink "https://fl4.preprod.yodlee.com/authenticate/USDevexPreProd2-195/fastlink/?channelAppName=usdevexpreprod2" + :yodlee2-test-user "sbMem5f61421aba2161" + :yodlee2-client-id "G7T9kiwaG8rMiykdV4pckmQnfj4OM2pf" + :yodlee2-client-secret "8I0mmq1wmAWSSpr9" + :yodlee2-base-url "https://sandbox.api.yodlee.com/ysl" + :yodlee2-fastlink "https://fl4.sandbox.yodlee.com/authenticate/restserver/fastlink" :square-config {"NGE1" {:square-location "SCX0Y8CTGM1S0", diff --git a/config/prod-cloud.edn b/config/prod-cloud.edn index 44b742d7..db5449f8 100644 --- a/config/prod-cloud.edn +++ b/config/prod-cloud.edn @@ -1,5 +1,6 @@ {:scheme "https" :db-name "prod-mirror2" + :base-url "https://prod-cloud.app.integreatconsult.com" :solr-uri "http://solr-prod-cloud.local:8983" :solr-impl :solr :datomic-url "datomic:ddb://us-east-1/iol-dev/dev" diff --git a/config/prod.edn b/config/prod.edn index ed829225..6629a8cd 100644 --- a/config/prod.edn +++ b/config/prod.edn @@ -1,5 +1,6 @@ {:db {:server "database"} :datomic-url "datomic:ddb://us-east-1/integreat/integreat-prod" + :base-url "https://app.integreatconsult.com" :solr-uri "http://solr-prod.local:8983" :solr-impl :solr :scheme "https" @@ -23,7 +24,6 @@ :yodlee-proxy-port 8888 :run-background? false :run-web? true - :yodlee2-admin-user "93398522-412b-470d-8400-3691392b12fb_ADMIN" :yodlee2-integreat-user "integreat-main" :yodlee2-client-id "3AATcwfPsWP1rP9oDoo4HvZhtaroGVcA" :yodlee2-client-secret "cXTBmKbGfkaBFIpM" diff --git a/deps.edn b/deps.edn deleted file mode 100644 index f8b3bdca..00000000 --- a/deps.edn +++ /dev/null @@ -1,8 +0,0 @@ -{:paths ["iol_ion/src/" "resources"] - :deps {com.cognitect/anomalies {:mvn/version "0.1.12"} - com.datomic/client-cloud {:mvn/version "1.0.123"} - com.datomic/ion {:mvn/version "1.0.62"} - clj-time/clj-time {:mvn/version "0.15.2"} - org.clojure/clojure {:mvn/version "1.10.1"} - org.clojure/data.json {:mvn/version "0.2.6"}} - :mvn/repos {"datomic-cloud" {:url "s3://datomic-releases-1fc2183a/maven/releases"}}} diff --git a/resources/input.css b/resources/input.css index 9027f220..be241fc7 100644 --- a/resources/input.css +++ b/resources/input.css @@ -68,3 +68,7 @@ .fade-out { opacity: 1.0; } + +.min-h-content { + min-height: calc(100vh - 4em); +} diff --git a/resources/public/output.css b/resources/public/output.css index ed0087b4..4015199c 100644 --- a/resources/public/output.css +++ b/resources/public/output.css @@ -1473,6 +1473,10 @@ input:checked + .toggle-bg { justify-content: space-between; } +.gap-1 { + gap: 0.25rem; +} + .gap-2 { gap: 0.5rem; } @@ -2183,6 +2187,10 @@ input:checked + .toggle-bg { opacity: 1.0; } +.min-h-content { + min-height: calc(100vh - 4em); +} + .hover\:scale-105:hover { --tw-scale-x: 1.05; --tw-scale-y: 1.05; @@ -2614,6 +2622,10 @@ input:checked + .toggle-bg { border-radius: 0.5rem; } + .sm\:p-6 { + padding: 1.5rem; + } + .sm\:py-5 { padding-top: 1.25rem; padding-bottom: 1.25rem; diff --git a/src/clj/auto_ap/routes/exports.clj b/src/clj/auto_ap/routes/exports.clj index 441f2e2a..994bbe4a 100644 --- a/src/clj/auto_ap/routes/exports.clj +++ b/src/clj/auto_ap/routes/exports.clj @@ -267,8 +267,9 @@ )) (into [["Vendor Name" "Address" "City" "State" "Zip" "Terms" "Account" "Account Code"]]))] {:body - (into (list) - data)}))) + (into [] + data) + :headers {"content-disposition" "attachment; filename=\"vendors.csv\""}}))) (defn export-ledger [{:keys [identity query-params]}] (let [start-date (or (some-> (query-params "start-date") @@ -426,7 +427,7 @@ "expected-deposit/" {#"export/?" {:get :export-expected-deposits}} "clients/" {#"export/?" {:get :export-clients}} "vendors/" {#"export/?" {:get :export-vendors} - "/company" {#"export/?" {:get :export-company-vendors}}} + "company/" {#"export" {:get :export-company-vendors}}} "ledger/" {#"export/?" {:get :export-ledger}} "accounts/" {#"export/?" {:get :export-accounts}} "transactions/" {#"export/?" {:get :export-transactions} diff --git a/src/clj/auto_ap/ssr/company.clj b/src/clj/auto_ap/ssr/company.clj new file mode 100644 index 00000000..1b9dd19d --- /dev/null +++ b/src/clj/auto_ap/ssr/company.clj @@ -0,0 +1,66 @@ +(ns auto-ap.ssr.company + (:require + [auto-ap.datomic :refer [conn]] + [auto-ap.datomic.clients :refer [full-read]] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.ui :refer [base-page]] + [cemerick.url :as url] + [config.core :refer [env]] + [datomic.api :as dc] + [auto-ap.ssr-routes :as ssr-routes] + [bidi.bidi :as bidi])) + +(defn please-select-client-screen* [] + [:div.grid.grid-cols-3 + (com/content-card {} + [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} + [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} + "Please select a company"] + ])]) + +(defn main-content* [{:keys [client]}] + (if-not client + (please-select-client-screen*) + (let [client (dc/pull (dc/db conn) full-read (:db/id client))] + [:div.grid.grid-cols-3.gap-4 + (com/content-card {} + [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} + [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} + (:client/name client)] + (when-let [address (-> client :client/address)] + [:div.flex.flex-col.gap-1.text-lg.dark:text-white.text-gray-700 + [:p (-> address :address/street1)] + [:p (-> address :address/street2)] + [:p (-> address :address/city) " " + (-> address :address/state) ", " + (-> address :address/zip)]])] + ) + (com/content-card {} + [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} + [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} + "Downloads"] + [:a {:href (str (assoc (url/url (str (:base-url env) "/api/vendors/company/export")) + :query {"client" (:client/code client)}))} + (com/button {:color :primary} + "Download vendor list" + (com/button-icon {} svg/download))]])]))) + +(defn page [{:keys [identity matched-route] :as request}] + (base-page + request + (com/page {:nav (com/company-aside-nav) + :active-client (:client (:session request)) + :identity (:identity request) + :app-params { + :hx-get (bidi/path-for ssr-routes/only-routes + :company) + :hx-trigger "clientSelected from:body" + :hx-swap "outerHTML swap:300ms"}} + (com/breadcrumbs {} + [:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"]) + (main-content* {:client (:client (:session request))})) + nil)) + diff --git a/src/clj/auto_ap/ssr/company/company_1099.clj b/src/clj/auto_ap/ssr/company/company_1099.clj index e580c6bb..948aa434 100644 --- a/src/clj/auto_ap/ssr/company/company_1099.clj +++ b/src/clj/auto_ap/ssr/company/company_1099.clj @@ -230,91 +230,93 @@ (let [vendor (dc/pull (dc/db conn) '[* {:vendor/legal-entity-1099-type [:db/ident] :vendor/legal-entity-tin-type [:db/ident]}] (Long/parseLong (:vendor-id (:params request))))] ;; TODO perms (html-response - (com/modal {} - [:form {:hx-post (str (bidi/path-for ssr-routes/only-routes - :company-1099-vendor-save - :request-method :post - :vendor-id (Long/parseLong (:vendor-id (:params request))))) - :hx-target "#vendor-table" - :hx-swap "outerHTML swap:300ms"} - [:fieldset {:class "hx-disable"} - (com/modal-card {} - [:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name vendor)]] - [:div.space-y-6 - [:div.grid.grid-cols-6.gap-4 - [:h4.text-xl.border-b.col-span-6 "Address"] - [:div.col-span-6 - (com/field {:label "Street 1"} - (com/text-input {:name (path->name [:vendor/address :address/street1]) - :value (-> vendor :vendor/address :address/street1) - :placeholder "1700 Pennsylvania Ave" - :autofocus true}))] - [:div.col-span-6 - (com/field {:label "Street 2"} - (com/text-input {:name (path->name [:vendor/address :address/street2]) - :value (-> vendor :vendor/address :address/street2) - :placeholder "Suite 200"}))] - [:div.col-span-3 - (com/field {:label "City"} - (com/text-input {:name (path->name [:vendor/address :address/city]) - :value (-> vendor :vendor/address :address/city) - :placeholder "Cupertino"}))] - [:div.col-span-1 - (com/field {:label "State"} - (com/text-input {:name (path->name [:vendor/address :address/state]) - :value (-> vendor :vendor/address :address/state) - :placeholder "CA"}))] - [:div.col-span-2 - (com/field {:label "Zip"} - (com/text-input {:name (path->name [:vendor/address :address/zip]) - :value (-> vendor :vendor/address :address/zip) - :placeholder "98102"}))] - [:h4.text-xl.border-b.col-span-6 "Legal Entity"] - [:div.col-span-6 - (com/field {:label "Legal Entity Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-name]) - :value (-> vendor :vendor/legal-entity-name) - :placeholder "Good Restaurant LLC"}))] - [:div.col-span-6.text-center " - OR -"] - [:div.col-span-2 - (com/field {:label "First Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-first-name]) - :value (-> vendor :vendor/legal-entity-first-name) - :placeholder "John"}))] - [:div.col-span-2 - (com/field {:label "Middle Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-middle-name]) - :value (-> vendor :vendor/legal-entity-middle-name) - :placeholder "C."}))] - [:div.col-span-2 - (com/field {:label "Last Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-last-name]) - :value (-> vendor :vendor/legal-entity-last-name) - :placeholder "Riley"}))] - [:div.col-span-2 - (com/field {:label "TIN"} - (com/text-input {:name (path->name [:vendor/legal-entity-tin]) - :value (-> vendor :vendor/legal-entity-tin) - :placeholder "John"}))] - [:div.col-span-2 - (com/field {:label "TIN Type"} - (com/select {:name (path->name [:vendor/legal-entity-tin-type]) - :allow-blank? true - :value (some-> vendor :vendor/legal-entity-tin-type :db/ident name) - :options [["ein" "EIN"] - ["ssn" "SSN"]]}))] - [:div.col-span-2 - (com/field {:label "1099 Type"} - (com/select {:name (path->name [:vendor/legal-entity-1099-type]) - :allow-blank? true - :value (some-> vendor :vendor/legal-entity-1099-type :db/ident name) - :options [["none" "None"] - ["misc" "Misc"] - ["landlord" "Landlord"]]}))] - [:div.col-span-6 - (com/button {:color :primary} - "Save")]]] - [:div])]])))) + (com/modal + {} + [:form {:hx-post (str (bidi/path-for ssr-routes/only-routes + :company-1099-vendor-save + :request-method :post + :vendor-id (Long/parseLong (:vendor-id (:params request))))) + :hx-target "#vendor-table" + :hx-swap "outerHTML swap:300ms"} + [:fieldset {:class "hx-disable"} + (com/modal-card + {} + [:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name vendor)]] + [:div.space-y-6 + [:div.grid.grid-cols-6.gap-4 + [:h4.text-xl.border-b.col-span-6 "Address"] + [:div.col-span-6 + (com/field {:label "Street 1"} + (com/text-input {:name (path->name [:vendor/address :address/street1]) + :value (-> vendor :vendor/address :address/street1) + :placeholder "1700 Pennsylvania Ave" + :autofocus true}))] + [:div.col-span-6 + (com/field {:label "Street 2"} + (com/text-input {:name (path->name [:vendor/address :address/street2]) + :value (-> vendor :vendor/address :address/street2) + :placeholder "Suite 200"}))] + [:div.col-span-3 + (com/field {:label "City"} + (com/text-input {:name (path->name [:vendor/address :address/city]) + :value (-> vendor :vendor/address :address/city) + :placeholder "Cupertino"}))] + [:div.col-span-1 + (com/field {:label "State"} + (com/text-input {:name (path->name [:vendor/address :address/state]) + :value (-> vendor :vendor/address :address/state) + :placeholder "CA"}))] + [:div.col-span-2 + (com/field {:label "Zip"} + (com/text-input {:name (path->name [:vendor/address :address/zip]) + :value (-> vendor :vendor/address :address/zip) + :placeholder "98102"}))] + [:h4.text-xl.border-b.col-span-6 "Legal Entity"] + [:div.col-span-6 + (com/field {:label "Legal Entity Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-name]) + :value (-> vendor :vendor/legal-entity-name) + :placeholder "Good Restaurant LLC"}))] + [:div.col-span-6.text-center " - OR -"] + [:div.col-span-2 + (com/field {:label "First Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-first-name]) + :value (-> vendor :vendor/legal-entity-first-name) + :placeholder "John"}))] + [:div.col-span-2 + (com/field {:label "Middle Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-middle-name]) + :value (-> vendor :vendor/legal-entity-middle-name) + :placeholder "C."}))] + [:div.col-span-2 + (com/field {:label "Last Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-last-name]) + :value (-> vendor :vendor/legal-entity-last-name) + :placeholder "Riley"}))] + [:div.col-span-2 + (com/field {:label "TIN"} + (com/text-input {:name (path->name [:vendor/legal-entity-tin]) + :value (-> vendor :vendor/legal-entity-tin) + :placeholder "John"}))] + [:div.col-span-2 + (com/field {:label "TIN Type"} + (com/select {:name (path->name [:vendor/legal-entity-tin-type]) + :allow-blank? true + :value (some-> vendor :vendor/legal-entity-tin-type :db/ident name) + :options [["ein" "EIN"] + ["ssn" "SSN"]]}))] + [:div.col-span-2 + (com/field {:label "1099 Type"} + (com/select {:name (path->name [:vendor/legal-entity-1099-type]) + :allow-blank? true + :value (some-> vendor :vendor/legal-entity-1099-type :db/ident name) + :options [["none" "None"] + ["misc" "Misc"] + ["landlord" "Landlord"]]}))] + [:div.col-span-6 + (com/button {:color :primary} + "Save")]]] + [:div])]])))) (defn vendor-table [request] (html-response (table* request) @@ -323,11 +325,13 @@ (defn page [{:keys [identity matched-route] :as request}] (base-page request - (com/page {:nav (com/company-aside-nav) + (com/page {:nav (com/company-aside-nav) :active-client (:client (:session request)) - :identity (:identity request)} + :identity (:identity request)} (com/breadcrumbs {} - [:a {:href "#"} "My Company"] - [:a {:href "#"} "1099 Vendor Info"]) + [:a {:href (bidi/path-for ssr-routes/only-routes + :company)} "My Company"] + [:a {:href (bidi/path-for ssr-routes/only-routes + :company-1099)} "1099 Vendor Info"]) (table* request)) nil)) diff --git a/src/clj/auto_ap/ssr/company/reports.clj b/src/clj/auto_ap/ssr/company/reports.clj index 62a502fa..9c0a93e4 100644 --- a/src/clj/auto_ap/ssr/company/reports.clj +++ b/src/clj/auto_ap/ssr/company/reports.clj @@ -110,8 +110,10 @@ :active-client (:client (:session request)) :identity (:identity request)} (com/breadcrumbs {} - [:a {:href "#"} "My Company"] - [:a {:href "#"} "Reports"]) + [:a {:href (bidi/path-for ssr-routes/only-routes + :company)} "My Company"] + [:a {:href {:href (bidi/path-for ssr-routes/only-routes + :company-reports)}} "Reports"]) (table* {:client (:client (:session request)) :start (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) :per-page (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) diff --git a/src/clj/auto_ap/ssr/company/yodlee.clj b/src/clj/auto_ap/ssr/company/yodlee.clj new file mode 100644 index 00000000..0e736793 --- /dev/null +++ b/src/clj/auto_ap/ssr/company/yodlee.clj @@ -0,0 +1,168 @@ +(ns auto-ap.ssr.company.yodlee + (:require + [auto-ap.datomic :refer [conn]] + [auto-ap.datomic.yodlee2 :as yodlee2] + [auto-ap.graphql.utils :refer [is-admin?]] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.ui :refer [base-page]] + [auto-ap.ssr.utils :refer [html-response]] + [auto-ap.time :as atime] + [auto-ap.yodlee.core2 :as yodlee] + [bidi.bidi :as bidi] + [config.core :refer [env]] + [datomic.api :as dc] + [hiccup2.core :as hiccup])) + +(defn row* [{:keys [flash? yodlee-provider-account identity delete-after-settle?]}] + (com/data-grid-row + {:class (when flash? + "live-added") + "_" (hiccup/raw (when delete-after-settle?" on htmx:afterSettle wait 400ms then remove me"))} + (com/data-grid-cell + {} + (:yodlee-provider-account/id yodlee-provider-account)) + (com/data-grid-cell + {} + (when-let [status (:yodlee-provider-account/status yodlee-provider-account)] + (com/pill {:color (if (not= status "SUCCESS") + :yellow + :primary) } + status))) + (com/data-grid-cell + {} + (when-let [status (:yodlee-provider-account/detailed-status yodlee-provider-account)] + status) + ) + + (com/data-grid-cell + {} + (atime/unparse-local (:yodlee-provider-account/last-updated yodlee-provider-account) + atime/normal-date)) + (com/data-grid-cell + {} + [:ul + (for [a (:yodlee-provider-account/accounts yodlee-provider-account)] + [:li (:yodlee-account/name a) " - " (:yodlee-account/number a) #_[:div.tag (->$ (:available-balance a))]])]) + (com/data-grid-right-stack-cell + {} + (when (is-admin? identity) + [:form + [:input {:type :hidden :name "id" :value (:db/id yodlee-provider-account)}] + (com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes + :company-yodlee-provider-account-refresh) + :hx-target "closest tr"} + svg/refresh)]) + #_(when (is-admin? identity) + [:form + [:input {:type :hidden :name "id" :value (:db/id report)}] + (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)])))) + +(defn table* [{:keys [identity start per-page client flash-id]}] + (let [start (or start 0) + per-page (or per-page 30) + [yodlee-provider-accounts total] (yodlee2/get-graphql {:id identity + :start start + :per-page per-page + :client-id (:db/id client) + :sort nil})] + [:div + (com/data-grid-card {:id "yodlee-table" + :title "Yodlee Accounts" + :entity-name "Yodlee accounts" + :route :company-yodlee-table + :start start + :per-page per-page + :total total + :action-buttons [(com/button {:color :primary + :on-click "openFastlink()" + :hx-get (bidi/path-for ssr-routes/only-routes + :company-yodlee-fastlink-dialog) + :hx-target "#modal-holder"} + (com/button-icon {} svg/refresh) + "Link new account")] + :rows (for [yodlee-provider-account yodlee-provider-accounts] + (row* {:yodlee-provider-account yodlee-provider-account + :flash? (= flash-id + (:db/id yodlee-provider-account)) + :identity identity})) + :headers [(com/data-grid-header {} "Provider Account") + (com/data-grid-header {} "Status") + (com/data-grid-header {} "Detailed Status") + (com/data-grid-header {} "Last Updated") + (com/data-grid-header {:class "hidden md:table-cell"} "Accounts") + (com/data-grid-header {})]})])) + +(def default-read '[:db/id + :yodlee-provider-account/last-updated + :yodlee-provider-account/status + :yodlee-provider-account/id + :yodlee-provider-account/detailed-status + {:yodlee-provider-account/accounts [:yodlee-account/name :yodlee-account/number] + :yodlee-provider-account/client [:client/code]}]) + +(defn refresh-provider-account [{:keys [form-params identity]}] + (let [provider-account (dc/pull (dc/db conn) default-read (some-> (get form-params "id") not-empty Long/parseLong))] + (yodlee/refresh-provider-account (:client/code (:yodlee-provider-account/client provider-account)) + (:yodlee-provider-account/id provider-account)) + (html-response + (row* {:yodlee-provider-account provider-account + :flash? true + :identity identity})))) + +(defn fastlink-dialog [{:keys [session]}] + (html-response + (com/modal + {} + (com/modal-card + {} + [:div.flex [:div.p-2 "Yodlee Fastlink"] ] + [:div + [:div#fa-spot] + [:script {:lang "text/javascript"} + (hiccup/raw + (format " +fastlink.open({fastLinkURL: '%s', + accessToken: '%s', + params: {'configName': 'Aggregation'}}, + 'fa-spot'); + +" (:yodlee2-fastlink env) (yodlee/get-access-token (:client/code (:client session)))))] + ] + [:div])))) + + +(defn table [{:keys [query-params hx-query-params identity session] :as request}] + (html-response (table* {:client (:client (:session request)) + :start (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) + :per-page (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) + :identity identity + :session session}) + :headers {"hx-push-url" (str "?start=" (get (:query-params request) "start"))})) + +(defn page [{:keys [identity matched-route query-params :hx-query-params session] :as request}] + (base-page + request + (com/page {:nav (com/company-aside-nav) + :active-client (:client (:session request)) + :identity (:identity request)} + (com/breadcrumbs {} + [:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"] + + [:a {:href (bidi/path-for ssr-routes/only-routes + :company-yodlee)} + "Yodlee"] + ) + (table* {:client (:client session) + :start (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) + :per-page (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) + :identity identity + :session session})) + nil)) diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index f486f88c..cb470b81 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -188,20 +188,23 @@ (defn company-aside-nav- [] [:ul {:class "space-y-2"} + [:li + (menu-button- {:icon svg/vendors + :href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company")] - [:li + [:li (menu-button- {:icon svg/report :href (bidi/path-for ssr-routes/only-routes :company-reports)} "Reports")] - [:li - (menu-button- {:icon svg/bank} + [:li + (menu-button- {:icon svg/bank + :href (bidi/path-for ssr-routes/only-routes + :company-yodlee)} "Yodlee Link")] - [:li - (menu-button- {:icon svg/vendors} - "Vendors")] - - [:li + [:li (menu-button- {:icon svg/government-building :href (bidi/path-for ssr-routes/only-routes :company-1099)} diff --git a/src/clj/auto_ap/ssr/components/buttons.clj b/src/clj/auto_ap/ssr/components/buttons.clj index 17b5f90e..40198207 100644 --- a/src/clj/auto_ap/ssr/components/buttons.clj +++ b/src/clj/auto_ap/ssr/components/buttons.clj @@ -5,9 +5,11 @@ [:div.h-4.w-4 i]) (defn button- [params & children] - [:button { :class (cond-> "text-white focus:ring-4 font-bold rounded-lg text-sm px-5 py-2.5 text-center mr-2 inline-flex items-center hover:scale-105 transition duration-100 justify-center" - (= :secondary (:color params)) (str " bg-blue-500 hover:bg-blue-600 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700") - (= :primary (:color params)) (str " bg-green-500 hover:bg-green-600 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 "))} + [:button (update params + :class #(cond-> % + true (str " text-white focus:ring-4 font-bold rounded-lg text-sm px-5 py-2.5 text-center mr-2 inline-flex items-center hover:scale-105 transition duration-100 justify-center") + (= :secondary (:color params)) (str " bg-blue-500 hover:bg-blue-600 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700") + (= :primary (:color params)) (str " bg-green-500 hover:bg-green-600 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 "))) [:div.htmx-indicator.flex.items-center (svg/spinner {:class "inline w-4 h-4 text-white"}) [:div.ml-3 "Loading..."]] diff --git a/src/clj/auto_ap/ssr/components/dialog.clj b/src/clj/auto_ap/ssr/components/dialog.clj index dc8e9635..8a8bacae 100644 --- a/src/clj/auto_ap/ssr/components/dialog.clj +++ b/src/clj/auto_ap/ssr/components/dialog.clj @@ -4,7 +4,7 @@ (defn modal- [params & children] [:div [:div#modal-holder { :tabindex "-1", :class "fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full flex justify-center hidden" :aria-hidden true - "_" (hiccup/raw "on closeModal transition <#modal-holder .modal-content /> opacity to 0.0 over 300ms then call hideModal()")} + "_" (hiccup/raw "on closeModal transition <#modal-holder .modal-content /> opacity to 0.0 over 300ms then call hideModal() ")} [:div {:class "relative w-full max-w-2xl max-h-full"} (into [:div#modal-content] children)] @@ -22,7 +22,7 @@ }, onHide: function() { - modal_element.outerHTML='
'; + modal_element.outerHTML='
'; }, }; var curModal = new Modal(modal_element, modal_options); diff --git a/src/clj/auto_ap/ssr/components/page.clj b/src/clj/auto_ap/ssr/components/page.clj index 1f312c4d..232691e6 100644 --- a/src/clj/auto_ap/ssr/components/page.clj +++ b/src/clj/auto_ap/ssr/components/page.clj @@ -1,27 +1,17 @@ (ns auto-ap.ssr.components.page - (:require [auto-ap.ssr.components.navbar :refer [navbar-]] - [auto-ap.ssr.components.aside :refer [left-aside-]] - [hiccup2.core :as hiccup])) + (:require + [auto-ap.ssr.components.aside :refer [left-aside-]] + [auto-ap.ssr.components.navbar :refer [navbar-]])) -(defn page- [{:keys [nav page-specific active-client identity]} & children] - [:div#app +(defn page- [{:keys [nav page-specific active-client identity app-params] :or {app-params {}}} & children] + [:div#app app-params (navbar- {:client active-client :identity identity}) [:div.flex.pt-16.overflow-hidden (left-aside- {:nav nav :page-specific page-specific}) - - [:div#main-content {:class "relative w-full h-full lg:pl-64 overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900"} + [: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 "} (into [:div.p-4] children)]] - - - - - #_[:div#modal-holder.hidden - {"_" (hiccup/raw "on click trigger closeDialog")} - - [:div {:class "bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40" - }]] [:div#modal-holder]]) diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index 70725b2a..b3447b53 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -6,10 +6,12 @@ [auto-ap.ssr.auth :as auth] [auto-ap.ssr.transaction.insights :as insights] [auto-ap.ssr.company.company-1099 :as company-1099] + [auto-ap.ssr.company.yodlee :as company-yodlee] [auto-ap.ssr.search :as search] [auto-ap.ssr.company-dropdown :as company-dropdown] [auto-ap.ssr.company.reports :as company-reports] - [auto-ap.routes.ezcater-xls :as ezcater-xls])) + [auto-ap.routes.ezcater-xls :as ezcater-xls] + [auto-ap.ssr.company :as company])) ;; from auto-ap.ssr-routes, because they're shared @@ -21,10 +23,15 @@ :active-client (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin company-dropdown/active-client))) :company-dropdown-search-results (wrap-client-redirect-unauthenticated (wrap-secure company-dropdown/dropdown-search-results)) + :company (wrap-client-redirect-unauthenticated (wrap-secure company/page)) :company-1099 (wrap-client-redirect-unauthenticated (wrap-secure company-1099/page)) :company-1099-vendor-table (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-table)) :company-1099-vendor-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-dialog)) :company-1099-vendor-save (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-save)) + :company-yodlee (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/page)) + :company-yodlee-table (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/table)) + :company-yodlee-fastlink-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/fastlink-dialog)) + :company-yodlee-provider-account-refresh (wrap-client-redirect-unauthenticated (wrap-admin company-yodlee/refresh-provider-account)) :company-reports (wrap-client-redirect-unauthenticated (wrap-secure company-reports/page)) :company-reports-table (wrap-client-redirect-unauthenticated (wrap-secure company-reports/reports-table)) :company-reports-delete (wrap-client-redirect-unauthenticated (wrap-admin company-reports/delete-report)) diff --git a/src/clj/auto_ap/yodlee/core2.clj b/src/clj/auto_ap/yodlee/core2.clj index 9437647c..04dd1ce5 100644 --- a/src/clj/auto_ap/yodlee/core2.clj +++ b/src/clj/auto_ap/yodlee/core2.clj @@ -13,7 +13,8 @@ [clj-time.coerce :as coerce] [datomic.api :as dc] [auto-ap.datomic :refer [conn]] - [auto-ap.datomic.clients :as d-clients])) + [auto-ap.datomic.clients :as d-clients] + [clojure.string :as str])) ;; switch all of this to use tokens instead of passing around client codes, particularly because the codes ;; need to be tweaked for repeats (defn client-code->login [client-code] @@ -61,7 +62,7 @@ (client/post (merge {:headers (assoc base-headers "loginName" (:yodlee2-admin-user env) "Content-Type" "application/x-www-form-urlencoded") - :body (str "clientId=" (:yodlee2-client-id env) " &secret=" (:yodlee2-client-secret env)) + :body (str "clientId=" (:yodlee2-client-id env) "&secret=" (:yodlee2-client-secret env)) :as :json} other-config) ) @@ -75,11 +76,13 @@ (log/info "logging in as " client-code) (-> (str (:yodlee2-base-url env) "/auth/token") (client/post (merge {:headers (assoc base-headers - "loginName" (if (<= (count client-code) 3) - (str client-code client-code) - client-code) + "loginName" (if (:yodlee2-test-user env) + (:yodlee2-test-user env) + (if (<= (count client-code) 3) + (str client-code client-code) + client-code)) "Content-Type" "application/x-www-form-urlencoded") - :body (str "clientId=" (:yodlee2-client-id env) " &secret=" (:yodlee2-client-secret env)) + :body (str "clientId=" (:yodlee2-client-id env) "&secret=" (:yodlee2-client-secret env)) :as :json} other-config) ) @@ -278,10 +281,6 @@ provider-accounts) vals))) - - - - (defn delete-provider-account [client-code id] (let [cob-session (login-user (client-code->login client-code))] diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index 83db9fbb..cbfa37e4 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -13,15 +13,20 @@ ["/approve/" [#"\d+" :transaction-id]] {:post :transaction-insight-approve} ["/rows/" [#"\d+" :after]] {:get :transaction-insight-rows} ["/explain/" [#"\d+" :transaction-id]] {:get :transaction-insight-explain}}} - "company" {"/dropdown" :company-dropdown-search-results - "/active" {:put :active-client} - "/1099" :company-1099 - "/1099/table" {:get :company-1099-vendor-table} - "/1099/vendor-dialog" {["/" [#"\d+" :vendor-id]] {:get :company-1099-vendor-dialog + "company" {"" :company + "/dropdown" :company-dropdown-search-results + "/active" {:put :active-client} + "/1099" :company-1099 + "/1099/table" {:get :company-1099-vendor-table} + "/1099/vendor-dialog" {["/" [#"\d+" :vendor-id]] {:get :company-1099-vendor-dialog :post :company-1099-vendor-save}} - "/reports" {"" {:get :company-reports - :delete :company-reports-delete}} - "/reports/table" :company-reports-table + "/reports" {"" {:get :company-reports + :delete :company-reports-delete} + "/table" :company-reports-table} + "/yodlee" {"" {:get :company-yodlee} + "/table" {:get :company-yodlee-table} + "/fastlink" {:get :company-yodlee-fastlink-dialog} + "/refresh" {:put :company-yodlee-provider-account-refresh}} }}) diff --git a/test/clj/auto_ap/import/transactions_test.clj b/test/clj/auto_ap/import/transactions_test.clj index 64b9536f..87f123ce 100644 --- a/test/clj/auto_ap/import/transactions_test.clj +++ b/test/clj/auto_ap/import/transactions_test.clj @@ -16,8 +16,7 @@ (def base-transaction #:transaction {:date #inst "2020-01-02T00:00:00-08:00" :raw-id "1" - :id #_{:clj-kondo/ignore [:unresolved-var]} - (di/sha-256 "1") + :id (di/sha-256 "1") :amount 12.0 :description-original "original-description" :status "POSTED" From fb50f5727b0cce09345612f351406c64a377e640 Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Mon, 15 May 2023 11:51:16 -0700 Subject: [PATCH 4/6] Makes error popup work --- resources/input.css | 4 ++ resources/public/output.css | 96 +++++++++++++++++++++++++ src/clj/auto_ap/ssr/components/page.clj | 25 ++++++- src/clj/auto_ap/ssr/svg.clj | 21 ++++++ src/clj/auto_ap/ssr/ui.clj | 3 +- 5 files changed, 146 insertions(+), 3 deletions(-) diff --git a/resources/input.css b/resources/input.css index be241fc7..3ed74314 100644 --- a/resources/input.css +++ b/resources/input.css @@ -5,6 +5,10 @@ .htmx-added .fade-in { opacity: 0.0 !important; } + +.htmx-added.fade-in { + opacity: 0.0 !important; +} .fade-in { opacity: 1.0; } diff --git a/resources/public/output.css b/resources/public/output.css index 4015199c..0db99f1e 100644 --- a/resources/public/output.css +++ b/resources/public/output.css @@ -1073,6 +1073,18 @@ input:checked + .toggle-bg { top: 1.25rem; } +.right-4 { + right: 1rem; +} + +.right-2 { + right: 0.5rem; +} + +.top-2 { + top: 0.5rem; +} + .z-10 { z-index: 10; } @@ -1109,6 +1121,18 @@ input:checked + .toggle-bg { grid-column: span 6 / span 6; } +.m-2 { + margin: 0.5rem; +} + +.m-8 { + margin: 2rem; +} + +.m-4 { + margin: 1rem; +} + .mx-2 { margin-left: 0.5rem; margin-right: 0.5rem; @@ -1124,11 +1148,21 @@ input:checked + .toggle-bg { margin-right: auto; } +.my-20 { + margin-top: 5rem; + margin-bottom: 5rem; +} + .my-4 { margin-top: 1rem; margin-bottom: 1rem; } +.my-0 { + margin-top: 0px; + margin-bottom: 0px; +} + .-mb-1 { margin-bottom: -0.25rem; } @@ -1193,6 +1227,10 @@ input:checked + .toggle-bg { margin-top: 1.25rem; } +.mt-8 { + margin-top: 2rem; +} + .block { display: block; } @@ -1281,6 +1319,14 @@ input:checked + .toggle-bg { height: 100vh; } +.h-16 { + height: 4rem; +} + +.max-h-96 { + max-height: 24rem; +} + .max-h-full { max-height: 100%; } @@ -1325,6 +1371,10 @@ input:checked + .toggle-bg { width: 100%; } +.w-16 { + width: 4rem; +} + .max-w-2xl { max-width: 42rem; } @@ -1333,6 +1383,10 @@ input:checked + .toggle-bg { max-width: 1536px; } +.max-w-screen-lg { + max-width: 1024px; +} + .max-w-sm { max-width: 24rem; } @@ -1653,6 +1707,11 @@ input:checked + .toggle-bg { border-color: rgb(175 211 130 / var(--tw-border-opacity)); } +.border-red-300 { + --tw-border-opacity: 1; + border-color: rgb(255 104 104 / var(--tw-border-opacity)); +} + .bg-blue-100 { --tw-bg-opacity: 1; background-color: rgb(204 235 251 / var(--tw-bg-opacity)); @@ -1708,6 +1767,11 @@ input:checked + .toggle-bg { background-color: rgb(242 248 234 / var(--tw-bg-opacity)); } +.bg-red-50 { + --tw-bg-opacity: 1; + background-color: rgb(255 230 230 / var(--tw-bg-opacity)); +} + .bg-white { --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); @@ -1722,6 +1786,11 @@ input:checked + .toggle-bg { background-color: rgb(253 246 178 / var(--tw-bg-opacity)); } +.bg-green-200 { + --tw-bg-opacity: 1; + background-color: rgb(201 225 171 / var(--tw-bg-opacity)); +} + .bg-opacity-50 { --tw-bg-opacity: 0.5; } @@ -1858,6 +1927,10 @@ input:checked + .toggle-bg { padding-top: 1.25rem; } +.pt-8 { + padding-top: 2rem; +} + .text-left { text-align: left; } @@ -1982,6 +2055,11 @@ input:checked + .toggle-bg { color: rgb(97 145 37 / var(--tw-text-opacity)); } +.text-red-800 { + --tw-text-opacity: 1; + color: rgb(102 1 1 / var(--tw-text-opacity)); +} + .text-white { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); @@ -1992,6 +2070,15 @@ input:checked + .toggle-bg { color: rgb(114 59 19 / var(--tw-text-opacity)); } +.text-red-600 { + --tw-text-opacity: 1; + color: rgb(204 2 2 / var(--tw-text-opacity)); +} + +.underline { + text-decoration-line: underline; +} + .opacity-0 { opacity: 0; } @@ -2097,6 +2184,10 @@ input:checked + .toggle-bg { opacity: 0.0 !important; } +.htmx-added.fade-in { + opacity: 0.0 !important; +} + .fade-in { opacity: 1.0; } @@ -2489,6 +2580,11 @@ input:checked + .toggle-bg { color: rgb(175 211 130 / var(--tw-text-opacity)); } +.dark .dark\:text-red-400 { + --tw-text-opacity: 1; + color: rgb(255 53 53 / var(--tw-text-opacity)); +} + .dark .dark\:text-white { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); diff --git a/src/clj/auto_ap/ssr/components/page.clj b/src/clj/auto_ap/ssr/components/page.clj index 232691e6..5ead824e 100644 --- a/src/clj/auto_ap/ssr/components/page.clj +++ b/src/clj/auto_ap/ssr/components/page.clj @@ -1,7 +1,9 @@ (ns auto-ap.ssr.components.page (:require [auto-ap.ssr.components.aside :refer [left-aside-]] - [auto-ap.ssr.components.navbar :refer [navbar-]])) + [auto-ap.ssr.components.navbar :refer [navbar-]] + [hiccup2.core :as hiccup] + [auto-ap.ssr.svg :as svg])) (defn page- [{:keys [nav page-specific active-client identity app-params] :or {app-params {}}} & children] [:div#app app-params @@ -10,7 +12,26 @@ [:div.flex.pt-16.overflow-hidden (left-aside- {:nav nav :page-specific page-specific}) - [:div#main-content {:class "relative w-full h-full lg:pl-64 overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content "} + [: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 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 10ms then remove .htmx-added from #error-holder")} + [:div#error-holder.hidden + [:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg + [:div.relative + [:button.absolute.right-2.top-2.w-6.h-6.z-50.text-red-600 + {"_" (hiccup/raw "on click add .hidden to #error-holder")} + svg/filled-x + ] + ] + [:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-red-800.bg-red-50.dark:bg-gray-800.dark:text-red-400.border-red-300.rounded-lg.border.transition-all.duration-500.fade-in.slide-up.max-h-96 + + + [:div {:class "p-4 mb-4 text-lg w-full" :role "alert"} + [:div.inline-block.w-8.h-8.mr-2 svg/alert] + [:span.font-medium "Oh, drat! An unexpected error has occurred."] + [:div.text-sm + [:p "Integreat staff have been notified and are looking into it. " ] + [:p "To see error details, " [:a.underline {:href "#" :data-collapse-toggle "error-details"} "click here"] "."] + [:pre#error-details.text-xs.hidden]]]]]] (into [:div.p-4] children)]] diff --git a/src/clj/auto_ap/ssr/svg.clj b/src/clj/auto_ap/ssr/svg.clj index c3a5d88f..048d2677 100644 --- a/src/clj/auto_ap/ssr/svg.clj +++ b/src/clj/auto_ap/ssr/svg.clj @@ -271,3 +271,24 @@ :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]]) + +(def alert + [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} + [:defs] + [:title "alert-triangle"] + [:path {:d "M22.553,22.581a.569.569,0,0,1-.553.894H2a.569.569,0,0,1-.553-.894L11.553,2.37c.246-.492.648-.492.894,0Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}] + [:line {:x1 "12", :y1 "16.979", :x2 "12", :y2 "9.979", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}] + [:path {:d "M11.991,18.979a.246.246,0,0,0-.241.255.255.255,0,0,0,.254.245h.005a.246.246,0,0,0,.241-.255A.255.255,0,0,0,12,18.979h-.005", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]]) + +(def x + [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} + [:defs] + [:title "delete-2"] + [:circle {:cx "12", :cy "12", :r "11.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}] + [:line {:x1 "7", :y1 "7", :x2 "17", :y2 "17", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}] + [:line {:x1 "17", :y1 "7", :x2 "7", :y2 "17", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]]) + + +(def filled-x + [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} + [:path {:d "M24,12A12,12,0,1,0,12,24,12,12,0,0,0,24,12Zm-7.29,3.28a1,1,0,0,1,0,1.41,1,1,0,0,1-1.42,0l-3.11-3.11a.26.26,0,0,0-.35,0L8.72,16.69a1,1,0,0,1-1.41-1.41l3.11-3.11a.26.26,0,0,0,0-.35L7.31,8.71a1,1,0,0,1,0-1.42,1,1,0,0,1,1.41,0l3.11,3.11a.24.24,0,0,0,.35,0l3.11-3.11a1,1,0,1,1,1.42,1.42L13.6,11.82a.24.24,0,0,0,0,.35Z", :fill "currentColor"}]]) diff --git a/src/clj/auto_ap/ssr/ui.clj b/src/clj/auto_ap/ssr/ui.clj index bf96c347..4521e0ba 100644 --- a/src/clj/auto_ap/ssr/ui.clj +++ b/src/clj/auto_ap/ssr/ui.clj @@ -34,7 +34,8 @@ [: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/dist/min/dropzone.min.js"}] [:link {:rel "stylesheet" :href "https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" :type "text/css"}] - [:body {:hx-ext "disable-submit"} + [:body {:hx-ext "disable-submit" + } contents [:script {:src "/js/flowbite.min.js"}] [:script {:lang "text/javascript"} From 33fc70295bd0f02225717ed40b2d4d8e932543f6 Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Mon, 15 May 2023 12:36:43 -0700 Subject: [PATCH 5/6] Error thing --- src/clj/auto_ap/ssr/components/page.clj | 2 +- src/clj/auto_ap/ssr/svg.clj | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/clj/auto_ap/ssr/components/page.clj b/src/clj/auto_ap/ssr/components/page.clj index 5ead824e..1d1c5ae4 100644 --- a/src/clj/auto_ap/ssr/components/page.clj +++ b/src/clj/auto_ap/ssr/components/page.clj @@ -13,7 +13,7 @@ (left-aside- {:nav nav :page-specific page-specific}) [:div#main-content {:class "relative w-full h-full lg:pl-64 overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content " - "_" (hiccup/raw "on 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 10ms then remove .htmx-added from #error-holder")} + "_" (hiccup/raw "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#error-holder.hidden [:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg [:div.relative diff --git a/src/clj/auto_ap/ssr/svg.clj b/src/clj/auto_ap/ssr/svg.clj index 048d2677..e1dc3681 100644 --- a/src/clj/auto_ap/ssr/svg.clj +++ b/src/clj/auto_ap/ssr/svg.clj @@ -291,4 +291,5 @@ (def filled-x [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} + [:circle {:cx "12", :cy "12", :r "11.5", :fill "#FFF", :stroke-linecap "round", :stroke-linejoin "round"}] [:path {:d "M24,12A12,12,0,1,0,12,24,12,12,0,0,0,24,12Zm-7.29,3.28a1,1,0,0,1,0,1.41,1,1,0,0,1-1.42,0l-3.11-3.11a.26.26,0,0,0-.35,0L8.72,16.69a1,1,0,0,1-1.41-1.41l3.11-3.11a.26.26,0,0,0,0-.35L7.31,8.71a1,1,0,0,1,0-1.42,1,1,0,0,1,1.41,0l3.11,3.11a.24.24,0,0,0,.35,0l3.11-3.11a1,1,0,1,1,1.42,1.42L13.6,11.82a.24.24,0,0,0,0,.35Z", :fill "currentColor"}]]) From 33cae26cc10610ffce3ae2e958c7844cf36d75d3 Mon Sep 17 00:00:00 2001 From: Bryce Date: Thu, 18 May 2023 22:14:23 -0700 Subject: [PATCH 6/6] other pages --- src/clj/auto_ap/datomic.clj | 6 +- src/clj/auto_ap/datomic/reports.clj | 1 + src/clj/auto_ap/ssr/company/reports.clj | 149 ++++++--------- src/clj/auto_ap/ssr/company/yodlee.clj | 191 +++++++------------ src/clj/auto_ap/ssr/components.clj | 1 + src/clj/auto_ap/ssr/components/data_grid.clj | 25 ++- src/clj/auto_ap/ssr/core.clj | 2 +- src/clj/auto_ap/ssr/grid_page_helper.clj | 182 ++++++++++++++++++ src/clj/auto_ap/ssr/svg.clj | 14 ++ 9 files changed, 349 insertions(+), 222 deletions(-) create mode 100644 src/clj/auto_ap/ssr/grid_page_helper.clj diff --git a/src/clj/auto_ap/datomic.clj b/src/clj/auto_ap/datomic.clj index 30da1c3d..5a007006 100644 --- a/src/clj/auto_ap/datomic.clj +++ b/src/clj/auto_ap/datomic.clj @@ -579,12 +579,14 @@ (defn add-sorter-fields [q sort-map args] (reduce - (fn [q {:keys [sort-key]}] + (fn [q {:keys [sort-key] :as z}] + (prn z) + (println (class sort-key)) (merge-query q {:query {:find [(symbol (str "?sort-" sort-key))] :where (sort-map sort-key - (println "Warning, trying to sort by unsupported field" sort-key))}})) + (println "Warning, trying to sort by unsupported field" sort-key, "sort map" (pr-str sort-map)))}})) q (:sort args))) diff --git a/src/clj/auto_ap/datomic/reports.clj b/src/clj/auto_ap/datomic/reports.clj index 66ce0cf2..777b4d87 100644 --- a/src/clj/auto_ap/datomic/reports.clj +++ b/src/clj/auto_ap/datomic/reports.clj @@ -59,6 +59,7 @@ (map :db/id (:report/client r)))))))) (defn get-graphql [args] + (clojure.pprint/pprint args) (let [db (dc/db conn) {ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args)] diff --git a/src/clj/auto_ap/ssr/company/reports.clj b/src/clj/auto_ap/ssr/company/reports.clj index 9c0a93e4..1562dfd9 100644 --- a/src/clj/auto_ap/ssr/company/reports.clj +++ b/src/clj/auto_ap/ssr/company/reports.clj @@ -3,120 +3,81 @@ [amazonica.aws.s3 :as s3] [auto-ap.datomic :refer [conn]] [auto-ap.datomic.reports :as r] - [auto-ap.graphql.utils :refer [assert-admin is-admin?]] + [auto-ap.graphql.utils :refer [assert-can-see-client is-admin?]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] + [auto-ap.ssr.grid-page-helper :as helper] [auto-ap.ssr.svg :as svg] - [auto-ap.ssr.ui :refer [base-page]] [auto-ap.ssr.utils :refer [html-response]] [auto-ap.time :as atime] [bidi.bidi :as bidi] [config.core :refer [env]] - [datomic.api :as dc] - [hiccup2.core :as hiccup])) + [datomic.api :as dc])) -(defn row* [{:keys [flash? report identity delete-after-settle?]}] - (com/data-grid-row - {:class (when flash? - "live-added") - "_" (hiccup/raw (when delete-after-settle?" on htmx:afterSettle wait 400ms then remove me"))} - (com/data-grid-cell - {} - (:report/name report)) - (com/data-grid-cell - {} - (when (:report/creator report) - (com/pill {:color :primary } - (:report/creator report)))) - (com/data-grid-cell - {} - (atime/unparse-local (:report/created report) - atime/normal-date)) - (com/data-grid-right-stack-cell - {} - (com/a-icon-button {:href (:report/url report)} - svg/download) - (when (is-admin? identity) - [:form - [:input {:type :hidden :name "id" :value (:db/id report)}] - (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)])))) +(def grid-page {:id "report-table" + :nav (com/company-aside-nav) + :id-fn :db/id + :fetch-page (fn [user args] + (r/get-graphql (into args {:id user}))) + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"] -(defn table* [{:keys [client start per-page identity session flash-id]}] - (let [start (or start 0) - per-page (or per-page 30) - [reports total] (r/get-graphql {:id identity - :start start - :per-page per-page - :client-id (:db/id client) - :sort nil})] - (com/data-grid-card {:id "report-table" - :title "Reports" - :entity-name "reports" - :route :company-reports-table - :start start - :per-page per-page - :total total - :action-buttons [(com/button {:color :primary} - (com/button-icon {} svg/refresh) - "Add new product") - (com/button {:color :secondary} - (com/button-icon {} svg/refresh) - "Update stocks 1/250") - (com/icon-button {} - svg/upload)] - :rows (for [report reports] - (row* {:report report - :flash? (= flash-id - (:db/id report)) - :identity identity})) - :headers [(com/data-grid-header {} "Name") - (com/data-grid-header {:class "hidden md:table-cell"} "Created by") - (com/data-grid-header {:class "hidden md:table-cell"} "Created") - (com/data-grid-header {})]}))) + [:a {:href (bidi/path-for ssr-routes/only-routes + :company-reports)} + "Reports"]] + :title "Reports" + :entity-name "Reports" + :route :company-reports-table + :action-buttons (fn [user] + nil) + :row-buttons (fn [user e] + (com/a-icon-button {:href (:report/url e)} + svg/download)[ + (when (is-admin? user) + (com/icon-button {:hx-delete (str (bidi/path-for ssr-routes/only-routes + :company-reports-delete + :request-method :delete)) + :hx-target "closest tr"} + svg/trash))]) + :headers [{:key "name" + :name "Name" + :sort-key "name" + :render :report/name} + {:key "created-by" + :name "Created by" + :sort-key "creator" + :render (fn [report] + (when (:report/creator report) + (com/pill {:color :primary } + (:report/creator report))))} + {:key "created" + :name "Created" + :sort-key "created" + :render #(atime/unparse-local (:report/created %) + atime/normal-date)}]}) -(defn delete-report [{:keys [query-params hx-query-params form-params identity session] :as request}] +(def row* (partial helper/row* grid-page)) +(def table* (partial helper/table* grid-page)) +(def table (partial helper/table grid-page)) +(def page (partial helper/page grid-page)) + +(defn delete-report [{:keys [form-params identity]}] + (let [[id-to-delete key] (first (dc/q '[:find ?i ?k :in $ ?i :where [?i :report/key ?k]] (dc/db conn) (some-> (get form-params "id") not-empty Long/parseLong))) report (dc/pull (dc/db conn) r/default-read id-to-delete)] + (assert-can-see-client identity (:report/client report)) (when id-to-delete (s3/delete-object :bucket-name (:data-bucket env) :key key) @(dc/transact conn [[:db/retractEntity id-to-delete]])) (html-response - (row* {:report report - :flash? true - :identity identity + (row* identity + report + {:flash? true :delete-after-settle? true})))) -(defn reports-table [{:keys [query-params hx-query-params identity session] :as request}] - (html-response (table* {:client (:client (:session request)) - :start (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) - :per-page (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) - :identity identity - :session session}) - :headers {"hx-push-url" (str "?start=" (get (:query-params request) "start"))})) - -(defn page [{:keys [query-params hx-query-params identity session] :as request}] - (base-page - request - (com/page {:nav (com/company-aside-nav) - :active-client (:client (:session request)) - :identity (:identity request)} - (com/breadcrumbs {} - [:a {:href (bidi/path-for ssr-routes/only-routes - :company)} "My Company"] - [:a {:href {:href (bidi/path-for ssr-routes/only-routes - :company-reports)}} "Reports"]) - (table* {:client (:client (:session request)) - :start (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) - :per-page (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) - :identity identity - :session session})) - nil)) diff --git a/src/clj/auto_ap/ssr/company/yodlee.clj b/src/clj/auto_ap/ssr/company/yodlee.clj index 0e736793..10cfb061 100644 --- a/src/clj/auto_ap/ssr/company/yodlee.clj +++ b/src/clj/auto_ap/ssr/company/yodlee.clj @@ -7,6 +7,7 @@ [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.utils :refer [html-response]] [auto-ap.time :as atime] [auto-ap.yodlee.core2 :as yodlee] @@ -15,89 +16,6 @@ [datomic.api :as dc] [hiccup2.core :as hiccup])) -(defn row* [{:keys [flash? yodlee-provider-account identity delete-after-settle?]}] - (com/data-grid-row - {:class (when flash? - "live-added") - "_" (hiccup/raw (when delete-after-settle?" on htmx:afterSettle wait 400ms then remove me"))} - (com/data-grid-cell - {} - (:yodlee-provider-account/id yodlee-provider-account)) - (com/data-grid-cell - {} - (when-let [status (:yodlee-provider-account/status yodlee-provider-account)] - (com/pill {:color (if (not= status "SUCCESS") - :yellow - :primary) } - status))) - (com/data-grid-cell - {} - (when-let [status (:yodlee-provider-account/detailed-status yodlee-provider-account)] - status) - ) - - (com/data-grid-cell - {} - (atime/unparse-local (:yodlee-provider-account/last-updated yodlee-provider-account) - atime/normal-date)) - (com/data-grid-cell - {} - [:ul - (for [a (:yodlee-provider-account/accounts yodlee-provider-account)] - [:li (:yodlee-account/name a) " - " (:yodlee-account/number a) #_[:div.tag (->$ (:available-balance a))]])]) - (com/data-grid-right-stack-cell - {} - (when (is-admin? identity) - [:form - [:input {:type :hidden :name "id" :value (:db/id yodlee-provider-account)}] - (com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes - :company-yodlee-provider-account-refresh) - :hx-target "closest tr"} - svg/refresh)]) - #_(when (is-admin? identity) - [:form - [:input {:type :hidden :name "id" :value (:db/id report)}] - (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)])))) - -(defn table* [{:keys [identity start per-page client flash-id]}] - (let [start (or start 0) - per-page (or per-page 30) - [yodlee-provider-accounts total] (yodlee2/get-graphql {:id identity - :start start - :per-page per-page - :client-id (:db/id client) - :sort nil})] - [:div - (com/data-grid-card {:id "yodlee-table" - :title "Yodlee Accounts" - :entity-name "Yodlee accounts" - :route :company-yodlee-table - :start start - :per-page per-page - :total total - :action-buttons [(com/button {:color :primary - :on-click "openFastlink()" - :hx-get (bidi/path-for ssr-routes/only-routes - :company-yodlee-fastlink-dialog) - :hx-target "#modal-holder"} - (com/button-icon {} svg/refresh) - "Link new account")] - :rows (for [yodlee-provider-account yodlee-provider-accounts] - (row* {:yodlee-provider-account yodlee-provider-account - :flash? (= flash-id - (:db/id yodlee-provider-account)) - :identity identity})) - :headers [(com/data-grid-header {} "Provider Account") - (com/data-grid-header {} "Status") - (com/data-grid-header {} "Detailed Status") - (com/data-grid-header {} "Last Updated") - (com/data-grid-header {:class "hidden md:table-cell"} "Accounts") - (com/data-grid-header {})]})])) - (def default-read '[:db/id :yodlee-provider-account/last-updated :yodlee-provider-account/status @@ -106,14 +24,7 @@ {:yodlee-provider-account/accounts [:yodlee-account/name :yodlee-account/number] :yodlee-provider-account/client [:client/code]}]) -(defn refresh-provider-account [{:keys [form-params identity]}] - (let [provider-account (dc/pull (dc/db conn) default-read (some-> (get form-params "id") not-empty Long/parseLong))] - (yodlee/refresh-provider-account (:client/code (:yodlee-provider-account/client provider-account)) - (:yodlee-provider-account/id provider-account)) - (html-response - (row* {:yodlee-provider-account provider-account - :flash? true - :identity identity})))) + (defn fastlink-dialog [{:keys [session]}] (html-response @@ -136,33 +47,77 @@ fastlink.open({fastLinkURL: '%s', ] [:div])))) +(def grid-page {:id "yodlee-table" + :nav (com/company-aside-nav) + :id-fn :db/id + :fetch-page (fn [user args] + (yodlee2/get-graphql (assoc args :id user))) + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"] -(defn table [{:keys [query-params hx-query-params identity session] :as request}] - (html-response (table* {:client (:client (:session request)) - :start (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) - :per-page (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) - :identity identity - :session session}) - :headers {"hx-push-url" (str "?start=" (get (:query-params request) "start"))})) + [:a {:href (bidi/path-for ssr-routes/only-routes + :company-yodlee)} + "Yodlee"]] + :title "Yodlee Accounts" + :entity-name "Yodlee accounts" + :route :company-yodlee-table + :action-buttons (fn [user] + [(com/button {:color :primary + :on-click "openFastlink()" + :hx-get (bidi/path-for ssr-routes/only-routes + :company-yodlee-fastlink-dialog) + :hx-target "#modal-holder"} + (com/button-icon {} svg/refresh) + "Link new account")]) + :row-buttons (fn [user e] + [(when (is-admin? user) + (com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes + :company-yodlee-provider-account-refresh) + :hx-target "closest tr"} + svg/refresh))]) + :headers [{:key "provider-account" + :name "Provider Account" + :sort-key "provider-account" + :render :yodlee-provider-account/id} + {:key "status" + :name "Status" + :sort-key "status" + :render #(when-let [status (:yodlee-provider-account/status %)] + (com/pill {:color (if (not= status "SUCCESS") + :yellow + :primary) } + status))} + {:key "detailed-status" + :name "Detailed Status" + :sort-key "detailed-status" + :render #(when-let [status (:yodlee-provider-account/detailed-status %)] + status)} -(defn page [{:keys [identity matched-route query-params :hx-query-params session] :as request}] - (base-page - request - (com/page {:nav (com/company-aside-nav) - :active-client (:client (:session request)) - :identity (:identity request)} - (com/breadcrumbs {} - [:a {:href (bidi/path-for ssr-routes/only-routes - :company)} - "My Company"] + {:key "last-updated" + :name "Last Updated" + :sort-key "last-updated" + :render #(atime/unparse-local (:yodlee-provider-account/last-updated %) + atime/normal-date)} + {:key "accounts" + :name "Accounts" + :show-starting "md" + :render (fn [e] + [:ul + (for [a (:yodlee-provider-account/accounts e)] + [:li (:yodlee-account/name a) " - " (:yodlee-account/number a)])])}]}) - [:a {:href (bidi/path-for ssr-routes/only-routes - :company-yodlee)} - "Yodlee"] - ) - (table* {:client (:client session) - :start (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) - :per-page (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) - :identity identity - :session session})) - nil)) +(def page (partial helper/page grid-page)) +(def table (partial helper/table grid-page)) + +;; TODO delete-after-settle +(defn refresh-provider-account [{:keys [form-params identity]}] + (let [provider-account (dc/pull (dc/db conn) default-read (some-> (get form-params "id") not-empty Long/parseLong))] + (yodlee/refresh-provider-account (:client/code (:yodlee-provider-account/client provider-account)) + (:yodlee-provider-account/id provider-account)) + (html-response + (helper/row* + grid-page + identity + provider-account + {:flash? true})))) diff --git a/src/clj/auto_ap/ssr/components.clj b/src/clj/auto_ap/ssr/components.clj index 75233aa2..0362c0a6 100644 --- a/src/clj/auto_ap/ssr/components.clj +++ b/src/clj/auto_ap/ssr/components.clj @@ -37,6 +37,7 @@ (def data-grid data-grid/data-grid-) (def data-grid-header data-grid/header-) +(def data-grid-sort-header data-grid/sort-header-) (def data-grid-row data-grid/row-) (def data-grid-cell data-grid/cell-) (def data-grid-right-stack-cell data-grid/right-stack-cell-) diff --git a/src/clj/auto_ap/ssr/components/data_grid.clj b/src/clj/auto_ap/ssr/components/data_grid.clj index 7a0dbf73..2f146f83 100644 --- a/src/clj/auto_ap/ssr/components/data_grid.clj +++ b/src/clj/auto_ap/ssr/components/data_grid.clj @@ -3,11 +3,20 @@ [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components.card :refer [content-card-]] [auto-ap.ssr.components.paginator :refer [paginator-]] - [bidi.bidi :as bidi])) + [bidi.bidi :as bidi] + [hiccup2.core :as hiccup])) (defn header- [params & rest] - (into [:th.px-4.py-3 {:scope "col" :class (:class params)} ] rest)) + (into [:th.px-4.py-3 {:scope "col" :class (:class params) + "_" (hiccup/raw (when (:sort-key params ) (format "on click trigger sorted(key:\"%s\")", (:sort-key params))))}] + (if (:sort-key params) + [(into [:a {:href "#"} ] rest)] + rest))) +(defn sort-header- [params & rest] + [:th.px-4.py-3 {:scope "col" :class (:class params) + "_" (hiccup/raw (format "on click trigger sorted(key:\"%s\")", (:sort-key params)))} + (into [:a {:href "#"} ] rest)]) (defn row- [params & rest] (into [:tr (update params @@ -27,9 +36,9 @@ [:input {:id "checkbox-all", :type "checkbox", :class "w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"}] [:label {:for "checkbox-all", :class "sr-only"} "checkbox"]]]) -(defn data-grid- [{:keys [headers]} & rest] +(defn data-grid- [{:keys [headers thead-params]} & rest] [:table {:class "w-full text-sm text-left text-gray-500 dark:text-gray-400"} - [:thead {:class "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"} + [:thead (assoc thead-params :class "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400") (into [:tr] headers)] @@ -40,9 +49,10 @@ (defn data-grid-card- [{:keys [id route title - entity-name action-buttons total + subtitle + thead-params start per-page flash-id @@ -61,13 +71,14 @@ [:h1.text-2xl.mb-3.font-bold title] [:div {:class "flex items-center flex-1 space-x-4"} [:h5 - [:span (format "Total %s:" entity-name)] - [:span {:class "dark:text-white pl-4"} total]]]] + (when subtitle + [:span subtitle])]]] (into [:div {:class "flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"} ] action-buttons)] [:div {:class "overflow-x-auto"} (data-grid- {:headers headers + :thead-params thead-params } rows diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index b3447b53..76328117 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -33,7 +33,7 @@ :company-yodlee-fastlink-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/fastlink-dialog)) :company-yodlee-provider-account-refresh (wrap-client-redirect-unauthenticated (wrap-admin company-yodlee/refresh-provider-account)) :company-reports (wrap-client-redirect-unauthenticated (wrap-secure company-reports/page)) - :company-reports-table (wrap-client-redirect-unauthenticated (wrap-secure company-reports/reports-table)) + :company-reports-table (wrap-client-redirect-unauthenticated (wrap-secure company-reports/table)) :company-reports-delete (wrap-client-redirect-unauthenticated (wrap-admin company-reports/delete-report)) :transaction-insights (wrap-client-redirect-unauthenticated (wrap-secure insights/page)) :transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-secure insights/insight-table)) diff --git a/src/clj/auto_ap/ssr/grid_page_helper.clj b/src/clj/auto_ap/ssr/grid_page_helper.clj new file mode 100644 index 00000000..1c2c1d90 --- /dev/null +++ b/src/clj/auto_ap/ssr/grid_page_helper.clj @@ -0,0 +1,182 @@ +(ns auto-ap.ssr.grid-page-helper + (:require + [auto-ap.ssr.components :as com] + [auto-ap.ssr.ui :refer [base-page]] + [auto-ap.ssr.utils :refer [html-response]] + [hiccup2.core :as hiccup] + [bidi.bidi :as bidi] + [auto-ap.ssr-routes :as ssr-routes] + [cemerick.url :as url] + [clojure.string :as str] + [auto-ap.ssr.svg :as svg])) + +(defn row* [gridspec user entity {:keys [flash? delete-after-settle?] :as options}] + (let [cells (mapv (fn [header] + (com/data-grid-cell {} + ((:render header) entity))) + (:headers gridspec)) + cells (conj cells (com/data-grid-right-stack-cell {} + (into [:form + [:input {:type :hidden :name "id" :value ((:id-fn gridspec) entity)}]] + ((:row-buttons gridspec) user entity))))] + (apply com/data-grid-row + {:class (when flash? + "live-added") + "_" (hiccup/raw (when delete-after-settle? + " on htmx:afterSettle wait 400ms then remove me")) + } + cells))) + +(defn sort-icon [sort key] + (->> sort + (filter (comp #(= key %) :sort-key)) + first + :sort-icon)) + +(defn sort-by-list [sort] + (if (seq sort) + (into + [:div.flex.gap-2.items-center + "sorted by" + + ] + (for [{:keys [name sort-icon ]} sort] + [:div.py-1.px-3.text-sm.rounded.bg-gray-100.dark:bg-gray-600.flex.items-center.gap-2.relative name [:div.h-4.w-4.mr-3 sort-icon] + [:div {:class "absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white hover:scale-110 transition-all duration-300 bg-gray-400 border-2 border-white rounded-full -top-2 -right-2 dark:border-gray-900"} + [:div.h-4.w-4 svg/x] + ]] + )) + "default sort")) + +(defn table* [grid-spec user {:keys [start per-page client flash-id sort]}] + (let [start (or start 0) + per-page (or per-page 30) + [entities total] ((:fetch-page grid-spec) + user + {:start start + :per-page per-page + :client-id (:db/id client) + :sort sort})] + (com/data-grid-card {:id (:id grid-spec) + :title (:title grid-spec) + :route (:route grid-spec) + :start start + :per-page per-page + :total total + :subtitle [:div.flex.items-center.gap-2 + [:span (format "Total %s: %d, " (:entity-name grid-spec) total)] + (sort-by-list sort)] + :action-buttons ((:action-buttons grid-spec) user) + :rows (for [entity entities] + (row* grid-spec user entity {:flash? (= flash-id ((:id-fn grid-spec) entity))})) + :thead-params {:hx-get (bidi/path-for ssr-routes/only-routes + (:route grid-spec)) + :hx-target (str "#" (:id grid-spec)) + :hx-trigger "sorted once" + :hx-vals "js:{\"toggle-sort\": event.detail.key || \"\"}"} + :headers + (conj + (mapv + (fn [h] + (if (:sort-key h) + (com/data-grid-sort-header {:class (if-let [show-starting (:show-starting h)] + (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))]]) + (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 {}))}))) + + + +(defn parse-sort [grid-spec q] + (if (not-empty q) + (into [] + (map (fn [k] + (let [[k v] (str/split k #":")] + {:sort-key (str k) + :asc (boolean (= "asc" v)) + :name (:name (first (filter #(= (str k) (:sort-key %)) (:headers grid-spec)))) + :sort-icon (if (= (boolean (= "asc" v)) true) + svg/sort-down + svg/sort-up)})) + (str/split q #","))) + [])) + +(defn toggle-sort [grid-spec q k] + (if ((set (map :sort-key q)) k) + (mapv + (fn [s] + (if (= (:sort-key s) + k) + (-> s + (update :asc + #(boolean (not %))) + (update :sort-icon (fn [x] + (if (= x svg/sort-down) + svg/sort-up + svg/sort-down)))) + s)) + q) + (conj q {:sort-key k + :asc true + :name (:name (first (filter #(= (str k) (:sort-key %)) (:headers grid-spec)))) + :sort-icon svg/sort-down}))) + +(defn sort->query [s] + (str/join "," (map (fn [k] (format "%s:%s" (:sort-key k) (if (= true (:asc k)) + "asc" + "desc"))) + s))) + +(defn params->query-string [q] + (-> q + (dissoc :client :session) + (update :sort sort->query) + (url/map->query))) + +(defn extract-params [grid-spec {:keys [query-params hx-query-params identity session] :as request}] + (let [{hx-start "start" hx-per-page "per-page" hx-sort "sort" } hx-query-params + {q-start "start" q-per-page "per-page" q-sort "sort" q-toggle-sort "toggle-sort"} query-params] + (cond-> {} + hx-start (assoc :start (some-> hx-start not-empty (Long/parseLong ))) + q-start (assoc :start (some-> q-start not-empty (Long/parseLong ))) + hx-per-page (assoc :per-page (some-> hx-per-page not-empty (Long/parseLong ))) + q-per-page (assoc :per-page (some-> q-per-page not-empty (Long/parseLong ))) + hx-sort (assoc :sort (parse-sort grid-spec hx-sort)) + q-sort (assoc :sort (parse-sort grid-spec q-sort)) + (not-empty q-toggle-sort) (update :sort #(toggle-sort grid-spec % q-toggle-sort) ) + (:session request) (assoc :session (:session request)) + (:client (:session request)) (assoc :client (:client (:session request)))))) + +(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) + :active-client (:client (:session request)) + :identity (:identity request)} + (apply com/breadcrumbs {} (:breadcrumbs grid-spec)) + (table* grid-spec + identity + (extract-params grid-spec request))) + nil)) diff --git a/src/clj/auto_ap/ssr/svg.clj b/src/clj/auto_ap/ssr/svg.clj index e1dc3681..feb60375 100644 --- a/src/clj/auto_ap/ssr/svg.clj +++ b/src/clj/auto_ap/ssr/svg.clj @@ -293,3 +293,17 @@ [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:circle {:cx "12", :cy "12", :r "11.5", :fill "#FFF", :stroke-linecap "round", :stroke-linejoin "round"}] [:path {:d "M24,12A12,12,0,1,0,12,24,12,12,0,0,0,24,12Zm-7.29,3.28a1,1,0,0,1,0,1.41,1,1,0,0,1-1.42,0l-3.11-3.11a.26.26,0,0,0-.35,0L8.72,16.69a1,1,0,0,1-1.41-1.41l3.11-3.11a.26.26,0,0,0,0-.35L7.31,8.71a1,1,0,0,1,0-1.42,1,1,0,0,1,1.41,0l3.11,3.11a.24.24,0,0,0,.35,0l3.11-3.11a1,1,0,1,1,1.42,1.42L13.6,11.82a.24.24,0,0,0,0,.35Z", :fill "currentColor"}]]) + +(def sort-down + [:svg {:id "Regular", :xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} + [:defs] + [:title "arrow-thick-down-4"] + [:rect {:y "0.75", :rx "3", :stroke "currentColor", :transform "translate(0 24) rotate(-90)", :fill "none", :stroke-linejoin "round", :width "22.5", :stroke-linecap "round", :stroke-width "1.5px", :x "0.75", :ry "3", :height "22.5"}] + [:path {:d "M9.75,6v7.5L6.53,10.28a.75.75,0,0,0-1.28.531v2.068a1.5,1.5,0,0,0,.439,1.06L11.47,19.72a.749.749,0,0,0,1.06,0l5.781-5.781a1.5,1.5,0,0,0,.439-1.06V10.811a.75.75,0,0,0-1.28-.531L14.25,13.5V6a.75.75,0,0,0-.75-.75h-3A.75.75,0,0,0,9.75,6Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]]) + +(def sort-up + [:svg {:id "Regular", :xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} + [:defs] + [:title "arrow-thick-up-4"] + [:rect {:y "0.75", :rx "3", :stroke "currentColor", :transform "translate(24 0) rotate(90)", :fill "none", :stroke-linejoin "round", :width "22.5", :stroke-linecap "round", :stroke-width "1.5px", :x "0.75", :ry "3", :height "22.5"}] + [:path {:d "M14.25,18V10.5l3.22,3.22a.75.75,0,0,0,1.28-.531V11.121a1.5,1.5,0,0,0-.439-1.06L12.53,4.28a.749.749,0,0,0-1.06,0L5.689,10.061a1.5,1.5,0,0,0-.439,1.06v2.068a.75.75,0,0,0,1.28.531L9.75,10.5V18a.75.75,0,0,0,.75.75h3A.75.75,0,0,0,14.25,18Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]])