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/resources/input.css b/resources/input.css index cb8a07a5..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; } @@ -12,6 +16,9 @@ .htmx-added .slide-up { @apply translate-y-5 !important; } +.hidden .slide-up { + @apply translate-y-5 !important; +} .slide-up { @apply translate-y-0; } @@ -65,3 +72,7 @@ .fade-out { opacity: 1.0; } + +.min-h-content { + min-height: calc(100vh - 4em); +} 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..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; } @@ -1149,10 +1183,6 @@ input:checked + .toggle-bg { margin-bottom: 1rem; } -.ml-0 { - margin-left: 0px; -} - .ml-1 { margin-left: 0.25rem; } @@ -1165,10 +1195,6 @@ input:checked + .toggle-bg { margin-left: 0.75rem; } -.ml-8 { - margin-left: 2rem; -} - .mr-16 { margin-right: 4rem; } @@ -1201,6 +1227,10 @@ input:checked + .toggle-bg { margin-top: 1.25rem; } +.mt-8 { + margin-top: 2rem; +} + .block { display: block; } @@ -1253,6 +1283,10 @@ input:checked + .toggle-bg { height: 1rem; } +.h-48 { + height: 12rem; +} + .h-5 { height: 1.25rem; } @@ -1269,6 +1303,10 @@ input:checked + .toggle-bg { height: 2rem; } +.h-96 { + height: 24rem; +} + .h-\[calc\(100\%-1rem\)\] { height: calc(100% - 1rem); } @@ -1281,28 +1319,12 @@ input:checked + .toggle-bg { height: 100vh; } -.h-2 { - height: 0.5rem; +.h-16 { + height: 4rem; } -.h-48 { - height: 12rem; -} - -.h-64 { - height: 16rem; -} - -.h-80 { - height: 20rem; -} - -.h-1\/2 { - height: 50%; -} - -.h-96 { - height: 24rem; +.max-h-96 { + max-height: 24rem; } .max-h-full { @@ -1345,21 +1367,12 @@ 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; +.w-16 { + width: 4rem; } .max-w-2xl { @@ -1370,6 +1383,10 @@ input:checked + .toggle-bg { max-width: 1536px; } +.max-w-screen-lg { + max-width: 1024px; +} + .max-w-sm { max-width: 24rem; } @@ -1510,16 +1527,16 @@ input:checked + .toggle-bg { justify-content: space-between; } -.gap-4 { - gap: 1rem; +.gap-1 { + gap: 0.25rem; } .gap-2 { gap: 0.5rem; } -.gap-6 { - gap: 1.5rem; +.gap-4 { + gap: 1rem; } .gap-8 { @@ -1568,18 +1585,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 +1638,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; @@ -1706,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)); @@ -1746,6 +1752,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,14 +1767,9 @@ input:checked + .toggle-bg { background-color: rgb(242 248 234 / var(--tw-bg-opacity)); } -.bg-red-700 { +.bg-red-50 { --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)); + background-color: rgb(255 230 230 / var(--tw-bg-opacity)); } .bg-white { @@ -1780,19 +1786,9 @@ input:checked + .toggle-bg { background-color: rgb(253 246 178 / var(--tw-bg-opacity)); } -.bg-blue-200 { +.bg-green-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)); + background-color: rgb(201 225 171 / var(--tw-bg-opacity)); } .bg-opacity-50 { @@ -1823,14 +1819,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 +1854,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 +1879,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 +1895,10 @@ input:checked + .toggle-bg { padding-left: 2.75rem; } +.pl-2 { + padding-left: 0.5rem; +} + .pl-3 { padding-left: 0.75rem; } @@ -1912,6 +1907,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,16 +1927,8 @@ 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; +.pt-8 { + padding-top: 2rem; } .text-left { @@ -1978,10 +1973,6 @@ input:checked + .toggle-bg { font-weight: 700; } -.font-light { - font-weight: 300; -} - .font-medium { font-weight: 500; } @@ -2054,34 +2045,38 @@ 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)); } +.text-red-800 { + --tw-text-opacity: 1; + color: rgb(102 1 1 / var(--tw-text-opacity)); +} + .text-white { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity)); } -.text-yellow-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 { +.text-red-600 { --tw-text-opacity: 1; - color: rgb(229 231 235 / var(--tw-text-opacity)); + color: rgb(204 2 2 / var(--tw-text-opacity)); } -.text-green-800 { - --tw-text-opacity: 1; - color: rgb(48 72 18 / var(--tw-text-opacity)); +.underline { + text-decoration-line: underline; } .opacity-0 { @@ -2169,6 +2164,10 @@ input:checked + .toggle-bg { transition-duration: 300ms; } +.duration-500 { + transition-duration: 500ms; +} + .duration-75 { transition-duration: 75ms; } @@ -2185,6 +2184,10 @@ input:checked + .toggle-bg { opacity: 0.0 !important; } +.htmx-added.fade-in { + opacity: 0.0 !important; +} + .fade-in { opacity: 1.0; } @@ -2194,6 +2197,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)); @@ -2270,6 +2278,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; @@ -2296,6 +2308,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 +2338,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 +2463,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 +2526,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 +2555,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 +2570,21 @@ 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-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)); @@ -2558,16 +2595,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 +2629,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 +2674,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,14 +2714,14 @@ input:checked + .toggle-bg { display: block; } - .sm\:flex { - display: flex; - } - .sm\:rounded-lg { border-radius: 0.5rem; } + .sm\:p-6 { + padding: 1.5rem; + } + .sm\:py-5 { padding-top: 1.25rem; padding-bottom: 1.25rem; @@ -2742,10 +2779,6 @@ input:checked + .toggle-bg { display: block; } - .lg\:inline { - display: inline; - } - .lg\:flex { display: flex; } @@ -2804,10 +2837,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/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 843e2026..777b4d87 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 @@ -58,13 +59,9 @@ (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)] [(->> (graphql-results ids-to-retrieve db args)) matching-count])) - - - - - 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/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 b4b81d2b..948aa434 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) @@ -121,121 +119,101 @@ (->> 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))] - [: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-content" - :hx-swap "outerHTML"} - svg/pencil))))))] - (com/paginator))])) - -(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)) +(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]}] + (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))] + (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)))) @@ -246,114 +224,114 @@ (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"})) (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 (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"]) + [: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 new file mode 100644 index 00000000..1562dfd9 --- /dev/null +++ b/src/clj/auto_ap/ssr/company/reports.clj @@ -0,0 +1,83 @@ +(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-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.utils :refer [html-response]] + [auto-ap.time :as atime] + [bidi.bidi :as bidi] + [config.core :refer [env]] + [datomic.api :as dc])) + +(def grid-page {:id "report-table" + :nav (com/company-aside-nav) + :id-fn :db/id + :fetch-page (fn [user args] + (r/get-graphql (into args {:id user}))) + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"] + + [: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)}]}) + +(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* identity + report + {:flash? true + :delete-after-settle? true})))) + 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..10cfb061 --- /dev/null +++ b/src/clj/auto_ap/ssr/company/yodlee.clj @@ -0,0 +1,123 @@ +(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.grid-page-helper :as helper] + [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])) + +(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 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])))) + +(def grid-page {:id "yodlee-table" + :nav (com/company-aside-nav) + :id-fn :db/id + :fetch-page (fn [user args] + (yodlee2/get-graphql (assoc args :id user))) + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :company)} + "My Company"] + + [:a {:href (bidi/path-for ssr-routes/only-routes + :company-yodlee)} + "Yodlee"]] + :title "Yodlee Accounts" + :entity-name "Yodlee accounts" + :route :company-yodlee-table + :action-buttons (fn [user] + [(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)} + + {:key "last-updated" + :name "Last Updated" + :sort-key "last-updated" + :render #(atime/unparse-local (:yodlee-provider-account/last-updated %) + atime/normal-date)} + {:key "accounts" + :name "Accounts" + :show-starting "md" + :render (fn [e] + [:ul + (for [a (:yodlee-provider-account/accounts e)] + [:li (:yodlee-account/name a) " - " (:yodlee-account/number a)])])}]}) + +(def page (partial helper/page grid-page)) +(def table (partial helper/table grid-page)) + +;; 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/company_dropdown.clj b/src/clj/auto_ap/ssr/company_dropdown.clj index 4f73a8ea..c5b900c7 100644 --- a/src/clj/auto_ap/ssr/company_dropdown.clj +++ b/src/clj/auto_ap/ssr/company_dropdown.clj @@ -2,103 +2,120 @@ (: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" - :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) +(defn dropdown-search-results* [{:keys [options]}] + [:ul + (for [[id company-name]options] + [:li + [: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\" + [: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 id} + company-name]]])]) - }, - resultsList: { - tabSelect: true - }, - submit: true +(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 ""))) -}); -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-search-results [{:keys [identity] :as request}] + (html-response + (dropdown-search-results* {:options (get-clients identity (get (:query-params request) "search-text")) + :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" + :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"} -(defn dropdown [request] + [: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'); - (let [client (get-in request [:session :client])] + // set the element that trigger the dropdown menu on click + var $dropdownTriggerEl = document.getElementById('company-dropdown-button'); - (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 +126,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..0362c0a6 100644 --- a/src/clj/auto_ap/ssr/components.clj +++ b/src/clj/auto_ap/ssr/components.clj @@ -8,14 +8,17 @@ [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 dialog dialog/dialog-) +(def a-icon-button buttons/a-icon-button-) +(def modal dialog/modal-) +(def modal-card dialog/modal-card-) (def text-input inputs/text-input-) (def select inputs/select-) @@ -34,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-) @@ -43,29 +47,9 @@ :class (str "font-medium text-blue-600 dark:text-blue-500 hover:underline " class)}] children)) -(defn paginator [] - [: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"}]]]]]]) + + + +(def paginator paginator/paginator-) +(def data-grid-card data-grid/data-grid-card-) + diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index bf630990..cb470b81 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 @@ -186,17 +188,25 @@ (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 - (menu-button- {:icon svg/report} + [: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 - (menu-button- {:icon svg/government-building} - "1099 Vendor Info")]]) + [:li + (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..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..."]] @@ -16,6 +18,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..2f146f83 100644 --- a/src/clj/auto_ap/ssr/components/data_grid.clj +++ b/src/clj/auto_ap/ssr/components/data_grid.clj @@ -1,13 +1,26 @@ -(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] + [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 {: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)) @@ -23,12 +36,61 @@ [: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)] (into [:tbody] rest)]) + +(defn data-grid-card- [{:keys [id + route + title + action-buttons + total + subtitle + thead-params + 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 + (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 + + )] + (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/dialog.clj b/src/clj/auto_ap/ssr/components/dialog.clj index ed56d0b8..8a8bacae 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='