This commit is contained in:
Bryce
2023-05-18 22:15:37 -07:00
32 changed files with 1461 additions and 606 deletions

View File

@@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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='<div id=\"modal-holder\"><div class=\"relative w-full max-w-2xl max-h-full\"><div id=\"modal-content\"></div></div></div>';
},
};
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]]
])

View File

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

View File

@@ -1,24 +1,38 @@
(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-]]
[hiccup2.core :as hiccup]
[auto-ap.ssr.svg :as svg]))
(defn page- [{:keys [nav page-specific]} & children]
[:div#app
(navbar-)
(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 "
"_" (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
[: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)]]
[: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]])

View File

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

View File

@@ -6,9 +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.routes.ezcater-xls :as ezcater-xls]))
[auto-ap.ssr.company.reports :as company-reports]
[auto-ap.routes.ezcater-xls :as ezcater-xls]
[auto-ap.ssr.company :as company]))
;; from auto-ap.ssr-routes, because they're shared
@@ -18,11 +21,20 @@
: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 (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/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))

View File

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

View File

@@ -131,26 +131,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)))))

View File

@@ -193,3 +193,117 @@
: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"}]])
(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"}]])
(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"}
[: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"}]])

View File

@@ -29,11 +29,13 @@
: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 +55,13 @@
}
};
const collapse = new Collapse($targetEl, $triggerEl, options); ")]]]))
const collapse = new Collapse($targetEl, $triggerEl, options);
;
"
)]]]))

View File

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

View File

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

View File

@@ -13,12 +13,21 @@
["/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
"/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}}}})
"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}
"/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}}
}})
(def only-routes ["/" routes])

View File

@@ -16,8 +16,7 @@
(def base-transaction #:transaction {:date #inst "2020-01-02T00:00:00-08:00"
:raw-id "1"
:id
(di/sha-256 "1")
:id (di/sha-256 "1")
:amount 12.0
:description-original "original-description"
:status "POSTED"