Merge branch 'try-tailwind'

This commit is contained in:
Bryce
2023-07-13 20:50:20 -07:00
75 changed files with 10406 additions and 1823 deletions

2
.gitignore vendored
View File

@@ -8,7 +8,7 @@ pom.xml.asc
*.class *.class
/.lein-* /.lein-*
/.nrepl-port /.nrepl-port
/resources/public/js/compiled resources/public/js/compiled
*.log *.log
examples/ examples/
\#*\# \#*\#

View File

@@ -1,5 +1,6 @@
{:db {:server "localhost"} {:db {:server "localhost"}
:scheme "http" :scheme "http"
:base-url "http://localhost:3449"
:solr-uri "http://localhost:8983" :solr-uri "http://localhost:8983"
:solr-impl :solr :solr-impl :solr
:client-config {:server-type :dev-local :client-config {:server-type :dev-local
@@ -24,103 +25,22 @@
:yodlee-cobrand-name "restserver" :yodlee-cobrand-name "restserver"
:yodlee-cobrand-login "sbCobda48aa19712a83c3ca4e935dd5e5d46b1a" :yodlee-cobrand-login "sbCobda48aa19712a83c3ca4e935dd5e5d46b1a"
:yodlee-cobrand-password "0a07ea32-1b5d-461b-ad0f-2752cdd77602" :yodlee-cobrand-password "0a07ea32-1b5d-461b-ad0f-2752cdd77602"
:yodlee-user-login "sbMemda48aa19712a83c3ca4e935dd5e5d46b1a4" :yodlee-user-login "G7T9kiwaG8rMiykdV4pckmQnfj4OM2pf"
:yodlee-user-password "sbMemda48aa19712a83c3ca4e935dd5e5d46b1a4#123" :yodlee-user-password "sbMemda48aa19712a83c3ca4e935dd5e5d46b1a4#123"
:yodlee-base-url "https://developer.api.yodlee.com/ysl" :yodlee-base-url "https://developer.api.yodlee.com/ysl"
:yodlee-app "10003600" :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-web? true
:run-background? true :run-background? true
:dd-env "dev" :dd-env "dev"
:dd-service "integreat-app" :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-integreat-user "integreat-main"
:yodlee2-client-id "l6sUyK2NEq3mwopISHlFGWUcJ1U8OUQd" :yodlee2-test-user "sbMem5f61421aba2161"
:yodlee2-client-secret "wZQHoGEkv5AGG2ZH" :yodlee2-client-id "G7T9kiwaG8rMiykdV4pckmQnfj4OM2pf"
:yodlee2-base-url "https://development.api.yodlee.com/ysl" :yodlee2-client-secret "8I0mmq1wmAWSSpr9"
:yodlee2-fastlink "https://fl4.preprod.yodlee.com/authenticate/USDevexPreProd2-195/fastlink/?channelAppName=usdevexpreprod2" :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",
:location "UC",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGOP"
{:square-location "L3GMNBFARX9GG",
:location "OP",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGDG"
{:square-location "LQTHXQY69MYB6",
:location "DB",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGPG"
{:square-location "AMQ0NPA8FGDEF",
:location "SZ",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGEZ"
{:square-location "L0Z167T2T7W7F",
:location "JS",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGLK"
{:square-location "LRC7WVD77ZM81",
:location "SM",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGA1"
{:square-location "FZ3ZYC77T3W1T",
:location "KA",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGKG"
{:square-location "8JT71V8XGYAT3",
:location "NB",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGEB"
{:square-location "LEV4GM1JPJS6R",
:location "CV",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGGG"
{:square-location "L7S9MXZBJ00HY",
:location "LM",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGRV"
{:square-location "L0J45VZKHWXVR",
:location "RV",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGSM"
{:square-location "LG5X0MHA4NZDM",
:location "SM",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGZO"
{:square-location "KMVFQ9CRCXJ10",
:location "VT",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGWL"
{:square-location "LSWNP14T0YKD9",
:location "WL",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGMJ"
{:square-location "FNH5VRT890WK8",
:location "SC",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGHG"
{:square-location "LXJCAHYGZVNEJ",
:location "DC",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGVZ"
{:square-location "ACNTYY8WVZ6DV",
:location "NP",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGAK"
{:square-location "2RVBYER6QSV7W",
:location "MH",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"},
"NGGH"
{:square-location "LT322EK0S2TRD",
:location "FM",
:token "EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7"}
"20TY"
{:square-location "L2579ATQ0X1ET",
:location "WG",
:token "EAAAEEr749Ea6AdPTdngsmUPwIM3ETbPwcx3QQl_NS0KWuIL-JNzAg4f3W9DGQhb"}}
:intuit {:client-id "ABBAQI0qeck149vEC1e8tV6b3YJNujOCdwsUMkJ1ZoptzumyYu" :intuit {:client-id "ABBAQI0qeck149vEC1e8tV6b3YJNujOCdwsUMkJ1ZoptzumyYu"
:client-secret "7DriIEend1K9RHlzhupIxPFQozXHELLfeFW2GfTR" :client-secret "7DriIEend1K9RHlzhupIxPFQozXHELLfeFW2GfTR"
:redirect-uri "https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl"}} :redirect-uri "https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl"}}

View File

@@ -1,5 +1,6 @@
{:scheme "https" {:scheme "https"
:db-name "prod-mirror2" :db-name "prod-mirror2"
:base-url "https://prod-cloud.app.integreatconsult.com"
:solr-uri "http://solr-prod-cloud.local:8983" :solr-uri "http://solr-prod-cloud.local:8983"
:solr-impl :solr :solr-impl :solr
:datomic-url "datomic:ddb://us-east-1/iol-dev/dev" :datomic-url "datomic:ddb://us-east-1/iol-dev/dev"

View File

@@ -1,5 +1,6 @@
{:db {:server "database"} {:db {:server "database"}
:datomic-url "datomic:ddb://us-east-1/integreat/integreat-prod" :datomic-url "datomic:ddb://us-east-1/integreat/integreat-prod"
:base-url "https://app.integreatconsult.com"
:solr-uri "http://solr-prod.local:8983" :solr-uri "http://solr-prod.local:8983"
:solr-impl :solr :solr-impl :solr
:scheme "https" :scheme "https"
@@ -23,7 +24,6 @@
:yodlee-proxy-port 8888 :yodlee-proxy-port 8888
:run-background? false :run-background? false
:run-web? true :run-web? true
:yodlee2-admin-user "93398522-412b-470d-8400-3691392b12fb_ADMIN"
:yodlee2-integreat-user "integreat-main" :yodlee2-integreat-user "integreat-main"
:yodlee2-client-id "3AATcwfPsWP1rP9oDoo4HvZhtaroGVcA" :yodlee2-client-id "3AATcwfPsWP1rP9oDoo4HvZhtaroGVcA"
:yodlee2-client-secret "cXTBmKbGfkaBFIpM" :yodlee2-client-secret "cXTBmKbGfkaBFIpM"

1871
data/inference-outcome.csv Normal file

File diff suppressed because it is too large Load Diff

0
data/solr/data/log4j2.xml Normal file → Executable file
View File

0
data/solr/log4j2.xml Normal file → Executable file
View File

View File

@@ -1,31 +1,31 @@
version: '3' version: '3.8'
services: services:
nginx-proxy: # nginx-proxy:
restart: "always" # restart: "always"
ports: # ports:
- "80:80" # - "80:80"
- "443:443" # - "443:443"
labels: # labels:
- com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true # - com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true
volumes: # volumes:
# - ./data/var/run/docker.sock:/tmp/docker.sock:ro # # - ./data/var/run/docker.sock:/tmp/docker.sock:ro
- ./data/opt/integreat/dev/usr/share/nginx/html:/usr/share/nginx/html # - ./data/opt/integreat/dev/usr/share/nginx/html:/usr/share/nginx/html
database: # database:
restart: "always" # restart: "always"
ports: # ports:
- "5432:5432" # - "5432:5432"
datomic-transactor: # datomic-transactor:
restart: "always" # restart: "always"
environment: # environment:
- DATOMIC_PORT=4334 # - DATOMIC_PORT=4334
- XMS=-Xms3g # - XMS=-Xms3g
- XMX=-Xmx6g # - XMX=-Xmx6g
ulimits: # ulimits:
nofile: # nofile:
soft: 65536 # soft: 65536
hard: 65536 # hard: 65536
ports: # ports:
- 4334:4334 # - 4334:4334
solr: solr:
volumes: volumes:
- ./data/solr:/var/solr - ./data/solr:/var/solr

View File

@@ -1,55 +1,70 @@
version: '3' version: '3.8'
services: services:
nginx-proxy: # nginx-proxy:
image: jwilder/nginx-proxy:alpine # image: jwilder/nginx-proxy:alpine
labels: # labels:
- com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true # - com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true
volumes: # volumes:
- /var/run/docker.sock:/tmp/docker.sock:ro # - /var/run/docker.sock:/tmp/docker.sock:ro
- ./data/certs/:/etc/nginx/certs:ro # - ./data/certs/:/etc/nginx/certs:ro
- ./data/etc/nginx/vhost.d:/etc/nginx/vhost.d # - ./data/etc/nginx/vhost.d:/etc/nginx/vhost.d
- ./data/usr/share/nginx/html:/usr/share/nginx/html # - ./data/usr/share/nginx/html:/usr/share/nginx/html
- ./data/etc/nginx/conf.d:/etc/nginx/conf.d # - ./data/etc/nginx/conf.d:/etc/nginx/conf.d
letsencrypt: # letsencrypt:
image: jrcs/letsencrypt-nginx-proxy-companion:stable # image: jrcs/letsencrypt-nginx-proxy-companion:stable
volumes: # volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro # - /var/run/docker.sock:/var/run/docker.sock:ro
- ./data/certs/:/etc/nginx/certs # - ./data/certs/:/etc/nginx/certs
- ./data/etc/nginx/vhost.d:/etc/nginx/vhost.d # - ./data/etc/nginx/vhost.d:/etc/nginx/vhost.d
- ./data/usr/share/nginx/html:/usr/share/nginx/html # - ./data/usr/share/nginx/html:/usr/share/nginx/html
app: # app:
build: . # build: .
expose: # expose:
- 3000 # - 3000
depends_on: # depends_on:
- database # - database
- datomic-transactor # - datomic-transactor
environment: # environment:
config: /usr/local/config/local.edn # config: /usr/local/config/local.edn
VIRTUAL_HOST: local.app.integreatconsult.com # VIRTUAL_HOST: local.app.integreatconsult.com
VIRTUAL_PORT: 3000 # VIRTUAL_PORT: 3000
database: # database:
image: postgres:9-alpine # image: postgres:9-alpine
environment: # environment:
POSTGRES_USER: ap # POSTGRES_USER: ap
POSTGRES_PASSWORD: fifteen-invoices-imported! # POSTGRES_PASSWORD: fifteen-invoices-imported!
POSTGRES_DB: autoap # POSTGRES_DB: autoap
volumes: # volumes:
- ./data/var/lib/postgresql/data:/var/lib/postgresql/data # - ./data/var/lib/postgresql/data:/var/lib/postgresql/data
solr: solr:
image: solr image: solr
user: solr
# network_mode: bridge
ports: ports:
- "8983:8983" - 8983:8983
datomic-transactor: ulimits:
depends_on: nofile:
- database soft: "65536"
image: 679918342773.dkr.ecr.us-east-1.amazonaws.com/datomic:latest hard: "65536"
environment: # datomic-transactor:
- DATOMIC_HOST=datomic-transactor # depends_on:
- DATOMIC_PORT=4334 # - database
- DATOMIC_SQL_HOST=database # image: 679918342773.dkr.ecr.us-east-1.amazonaws.com/datomic:latest
ports: # environment:
- 4334:4334 # - DATOMIC_HOST=datomic-transactor
restart: on-failure # - DATOMIC_PORT=4334
# - DATOMIC_SQL_HOST=database
# ports:
# - 4334:4334
# restart: on-failure
networks:
default:
driver: bridge
attachable: false
driver_opts:
com.docker.network.bridge.default_bridge: "true"
com.docker.network.bridge.enable_icc: "true"
com.docker.network.bridge.enable_ip_masquerade: "true"
com.docker.network.bridge.host_binding_ipv4: "0.0.0.0"
com.docker.network.bridge.name: "docker0"
com.docker.network.driver.mtu: "1500"

View File

@@ -1,25 +1 @@
1) Aligning checks 4. Maybe make dark mode persist
2) popup blocker -- disable X
3) Splitting accounts
4) Permissions per location
5) List of payments
6) add payment
7) add invoice - X
--
Rules priority:
Match check
Match debit
Match rule (assign vendor and account)
+ Memo
+ Wild card matches
+ Match memo and amount
+ Match for specificclient
+ Match for specifo
Transfers
Splits

2382
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@
"@popperjs/core": "^2.11.5", "@popperjs/core": "^2.11.5",
"downshift": "^6.1.3", "downshift": "^6.1.3",
"dropzone": "^4.3.0", "dropzone": "^4.3.0",
"flowbite": "^1.6.5",
"minisearch": "^3.0.2", "minisearch": "^3.0.2",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^17.0.1", "react": "^17.0.1",
@@ -17,12 +18,12 @@
"react-plaid-link": "^3.3.2", "react-plaid-link": "^3.3.2",
"react-popper": "^2.3.0", "react-popper": "^2.3.0",
"react-prop-types": "^0.4.0", "react-prop-types": "^0.4.0",
"react-signature-canvas": "^1.0.3", "react-signature-canvas": "^1.0.6",
"react-signature-pad": "0.0.6",
"react-transition-group": "^4.4.2", "react-transition-group": "^4.4.2",
"recharts": "^1.4.2" "recharts": "^2.5.0"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"webpack": "^5.75.0", "webpack": "^5.75.0",
"webpack-cli": "^5.0.1" "webpack-cli": "^5.0.1"
}, },

View File

@@ -146,7 +146,6 @@
:plugins [[lein-pdo "0.1.1"]] :plugins [[lein-pdo "0.1.1"]]
:jvm-opts ["-Dconfig=config/dev.edn" "-Dlogback.configurationFile=logback.xml" "-Xms4G" "-Xmx20G" ]} :jvm-opts ["-Dconfig=config/dev.edn" "-Dlogback.configurationFile=logback.xml" "-Xms4G" "-Xmx20G" ]}
:uberjar :uberjar
{:java-cmd "/usr/lib/jvm/java-11-openjdk/bin/java" {:java-cmd "/usr/lib/jvm/java-11-openjdk/bin/java"
:prep-tasks ["fig:min" ] :prep-tasks ["fig:min" ]

78
resources/input.css Normal file
View File

@@ -0,0 +1,78 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.htmx-added .fade-in {
opacity: 0.0 !important;
}
.htmx-added.fade-in {
opacity: 0.0 !important;
}
.fade-in {
opacity: 1.0;
}
.htmx-added .slide-up {
@apply translate-y-5 !important;
}
.hidden .slide-up {
@apply translate-y-5 !important;
}
.slide-up {
@apply translate-y-0;
}
.live-added {
animation: pulse-green 300ms 2;
animation-direction: alternate;
animation-timing-function: ease-in-out;
}
.dark .live-added {
animation: pulse-dark-green 300ms 2 !important;
animation-direction: alternate;
animation-timing-function: ease-in-out;
}
/*
.live-added {
@apply bg-white;
}
*/
@keyframes pulse-green {
0% { @apply bg-white; }
100% { @apply bg-green-300 dark:bg-red-700;}
}
@keyframes pulse-dark-green {
0% { @apply dark:bg-gray-700; }
100% { @apply bg-green-700;}
}
.htmx-request .htmx-indicator, .htmx-request.htmx-indicator {
display: inherit !important;
}
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator-hidden {
display: none !important;
}
.htmx-indicator-hidden {
display: inherit;
}
.htmx-swapping .fade-out {
opacity: 0.0 !important;
}
.fade-out {
opacity: 1.0;
}
.min-h-content {
min-height: calc(100vh - 4em);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="48" width="48"><g><path d="M23.74,11.06a.47.47,0,0,0-.5,0h0l-2.35,1.47a.25.25,0,0,1-.25,0,.24.24,0,0,1-.13-.22V2a2,2,0,0,0-2-2H5.5a2,2,0,0,0-2,2V12.33a.24.24,0,0,1-.13.22.25.25,0,0,1-.25,0L.76,11.08a.5.5,0,0,0-.5,0A.51.51,0,0,0,0,11.5V22a2,2,0,0,0,2,2H22a2,2,0,0,0,2-2V11.5A.51.51,0,0,0,23.74,11.06ZM5.5,13.9V3a1,1,0,0,1,1-1l11,0a1,1,0,0,1,1,1V13.9a.25.25,0,0,1-.12.21l-2,1.25a.25.25,0,0,0,0,.4l4.15,3.68a.75.75,0,1,1-1,1.12l-4.21-3.75a.27.27,0,0,0-.17-.06H8.88a.27.27,0,0,0-.17.06L4.5,20.56a.76.76,0,0,1-1.06-.06.75.75,0,0,1,.06-1.06l4.15-3.68a.25.25,0,0,0,0-.4l-2-1.25A.25.25,0,0,1,5.5,13.9Z" fill="currentColor"></path><path d="M10.35,9.22l2.31.92a.5.5,0,0,1-.19,1H10.2a1,1,0,0,0,0,2h.42a.26.26,0,0,1,.26.25v.15a1,1,0,0,0,2,0v-.23a.25.25,0,0,1,.19-.25,2.49,2.49,0,0,0,.33-4.74l-2.31-.92a.5.5,0,0,1,.19-1h2.27a1,1,0,0,0,0-2h-.43a.25.25,0,0,1-.24-.25V4a1,1,0,0,0-2,0v.23a.26.26,0,0,1-.2.25,2.49,2.49,0,0,0-.33,4.74Z" fill="currentColor"></path></g></svg>

After

Width:  |  Height:  |  Size: 1023 B

1
resources/public/img/cas Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="48" width="48"><g><path d="M24,9.33a1.5,1.5,0,0,0-1.5-1.5H15.93a.31.31,0,0,0-.18.07L9.41,14.24a2,2,0,0,1-1,.55,19.76,19.76,0,0,1-2.89.54,2,2,0,0,1-2-2.39l.5-2.5a2,2,0,0,1,.55-1L5.75,8.26a.26.26,0,0,0-.18-.43H1.5A1.5,1.5,0,0,0,0,9.33v11a1.5,1.5,0,0,0,1.5,1.5h21a1.5,1.5,0,0,0,1.5-1.5ZM12.5,17.08h8a.75.75,0,0,1,0,1.5h-8a.75.75,0,0,1,0-1.5Z" fill="currentColor"></path><path d="M5.65,10.48a.52.52,0,0,0-.14.25L5,13.23a.49.49,0,0,0,.14.45.48.48,0,0,0,.45.14l2.5-.5a.48.48,0,0,0,.25-.14L16.1,5.43a1.91,1.91,0,0,0-2.7-2.7Z" fill="currentColor"></path></g></svg>

After

Width:  |  Height:  |  Size: 624 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="48" width="48"><g><path d="M24,9.33a1.5,1.5,0,0,0-1.5-1.5H15.93a.31.31,0,0,0-.18.07L9.41,14.24a2,2,0,0,1-1,.55,19.76,19.76,0,0,1-2.89.54,2,2,0,0,1-2-2.39l.5-2.5a2,2,0,0,1,.55-1L5.75,8.26a.26.26,0,0,0-.18-.43H1.5A1.5,1.5,0,0,0,0,9.33v11a1.5,1.5,0,0,0,1.5,1.5h21a1.5,1.5,0,0,0,1.5-1.5ZM12.5,17.08h8a.75.75,0,0,1,0,1.5h-8a.75.75,0,0,1,0-1.5Z" fill="currentColor"></path><path d="M5.65,10.48a.52.52,0,0,0-.14.25L5,13.23a.49.49,0,0,0,.14.45.48.48,0,0,0,.45.14l2.5-.5a.48.48,0,0,0,.25-.14L16.1,5.43a1.91,1.91,0,0,0-2.7-2.7Z" fill="currentColor"></path></g></svg>

After

Width:  |  Height:  |  Size: 624 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs><style>.a{fill:currentColor;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;}</style></defs><title>loading-circle</title><line class="a" x1="12" y1="20.498" x2="12" y2="22.498"/><line class="a" x1="21.5" y1="12.998" x2="19.5" y2="12.998"/><line class="a" x1="12" y1="1.498" x2="12" y2="5.498"/><line class="a" x1="8.94" y1="19.889" x2="8.173" y2="21.736"/><line class="a" x1="15.064" y1="19.889" x2="15.826" y2="21.736"/><line class="a" x1="4.5" y1="12.998" x2="2.5" y2="12.998"/><line class="a" x1="20.776" y1="9.362" x2="18.929" y2="10.128"/><line class="a" x1="5.07" y1="15.868" x2="3.223" y2="16.634"/><line class="a" x1="17.303" y1="18.302" x2="18.717" y2="19.716"/><line class="a" x1="18.717" y1="6.28" x2="17.303" y2="7.694"/><line class="a" x1="15.635" y1="4.221" x2="14.869" y2="6.069"/><line class="a" x1="8.363" y1="4.222" x2="9.13" y2="6.069"/><line class="a" x1="5.282" y1="6.28" x2="6.696" y2="7.694"/><line class="a" x1="6.696" y1="18.302" x2="5.282" y2="19.716"/><line class="a" x1="18.929" y1="15.868" x2="20.776" y2="16.634"/><line class="a" x1="3.223" y1="9.362" x2="5.07" y2="10.128"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

2
resources/public/js/flowbite.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,18 @@
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;
}
*/
}
})

2953
resources/public/output.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
;; You can also press C-u C-j to evaluate the expression and pretty-print its result. ;; You can also press C-u C-j to evaluate the expression and pretty-print its result.
(ns build-ml (ns build-ml
(:require [datomic.client.api :as dc] (:require [datomic.api :as dc]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.data.csv :as csv] [clojure.data.csv :as csv]
[auto-ap.datomic :refer [conn]])) [auto-ap.datomic :refer [conn]]))

View File

@@ -41,7 +41,7 @@
(drop 1) (drop 1)
(map (fn [[a n]] (map (fn [[a n]]
[(Long/parseLong a) (numeric-code->account (Long/parseLong n))]))) [(Long/parseLong a) (numeric-code->account (Long/parseLong n))])))
(slurp-csv "/home/notid/dev/transaction-training/input/accounts.csv")) (slurp-csv "/mnt/data/dev2/ml-test/input/accounts.csv"))
vendor-name->vendor (into {} (dc/q '[:find ?vn ?v vendor-name->vendor (into {} (dc/q '[:find ?vn ?v
:in $ :in $
:where [?v :vendor/name ?vn]] :where [?v :vendor/name ?vn]]
@@ -51,18 +51,18 @@
(drop 1) (drop 1)
(map (fn [[a n]] (map (fn [[a n]]
[(Long/parseLong a) (vendor-name->vendor (Long/parseLong n))]))) [(Long/parseLong a) (vendor-name->vendor (Long/parseLong n))])))
(slurp-csv "/home/notid/dev/transaction-training/input/vendors.csv"))] (slurp-csv "/mnt/data/dev2/ml-test/input/vendors.csv"))]
inference-vendor->vendor #_inference-vendor->vendor
#_(with-open [reader (io/reader "/home/notid/dev/transaction-training/inference-outcome.csv")] (with-open [reader (io/reader "/mnt/data/dev2/ml-test/inference-outcome.csv")]
(->> (csv/read-csv reader) (->> (csv/read-csv reader)
(into [] (into []
(comp (comp
(drop 1) (drop 1)
(map (fn [[_ transaction best-vendor best-account account-confidence]] (map (fn [[_ transaction best-vendor best-account account-confidence]]
{:db/id (Long/parseLong transaction) {:db/id (Long/parseLong transaction)
:transaction/recommended-account (Long/parseLong best-account) :transaction/recommended-account (inference-account->account best-account)
:transaction/account-confidence (Double/parseDouble account-confidence) :transaction/account-confidence (Double/parseDouble account-confidence)
:transaction/recommended-vendor (Long/parseLong best-vendor)})))))))) :transaction/recommended-vendor (inference-vendor->vendor best-vendor)}))))))))
(defn apply-inference [inference] (defn apply-inference [inference]
(doseq [p (->> inference (partition-all 100))] (doseq [p (->> inference (partition-all 100))]

View File

@@ -0,0 +1,83 @@
;; This buffer is for Clojure experiments and evaluation.
;; Press C-j to evaluate the last expression.
;; You can also press C-u C-j to evaluate the expression and pretty-print its result.
(dc/pull (dc/db conn) [:db/id] [:client/code "NGAK"])
(user/init-repl)
(require '[auto-ap.datomic :refer [pull-attr pull-many]])
(csv/write-csv *out*
(let [clients (pull-many (dc/db conn) '[:db/id :client/code :client/locations] [[:client/code "NGAK"] [:client/code "NGOP"] #_#_[:client/code "NGRV"] [:client/code "NGE1"]])
account->numeric-code (into {} (seq (dc/q '[:find ?i ?n
:in $ [?a ...]
:where [?i ?a ?n]]
(dc/db conn) [:account/numeric-code :bank-account/numeric-code])))
account->name (let [lookup (into {} (seq (dc/q '[:find ?i ?n
:in $
:where [?i :account/name ?n]]
(dc/db conn))))]
(fn [x]
(or (lookup x) "Bank Account")))
account->location (into {} (seq (dc/q '[:find ?i ?l
:in $
:where [?i :account/location ?l]]
(dc/db conn))))
used-accounts (into #{} (for [c clients
d (dc/index-range (dc/db conn)
:journal-entry-line/client+account+location+date
[(:db/id c)]
[(inc (:db/id c))])
:let [[_ account] (:v d)]]
account))]
(for [client clients
:let [all-entries (->> (dc/index-pull (dc/db conn)
{:index :avet
:selector [:db/id :journal-entry-line/running-balance :journal-entry-line/client+account+location+date]
:start [:journal-entry-line/client+account+location+date [(:db/id client)]]
:reverse false
:limit 1})
(take-while (fn [curr]
(= (first (:journal-entry-line/client+account+location+date curr))
(:db/id client)))))
account-lookup (->> all-entries
(reduce
(fn [acc curr]
(let [[client account location date] (:journal-entry-line/client+account+location+date curr)
numeric-code (account->numeric-code account)]
(assoc acc (format "%d-%d-%s-%s" client numeric-code location (atime/unparse-local (clj-time.coerce/to-date-time date) atime/iso-date)) (:journal-entry-line/running-balance curr))))
(sorted-map)))]
a (sort-by account->numeric-code used-accounts)
:let [numeric-code (account->numeric-code a)]
l (or (account->location a) (:client/locations client))
delta (range -60 0)
:let [date (t/plus (c/to-date-time #inst "2023-06-12T08:00:00") (t/days delta))
date-str (atime/unparse-local date atime/iso-date)]
:when (and numeric-code (>= numeric-code 40000))]
(conj [(:client/code client) l numeric-code (account->name a) date-str] (with-precision 2
(double (.setScale (bigdec (or
(some->> (subseq account-lookup >= (format "%d-%d-%s-%s" (:db/id client) numeric-code l "2000-01-01") <= (format "%d-%d-%s-%s" (:db/id client) numeric-code l date-str))
last
last)
0.0))
2 java.math.RoundingMode/HALF_UP))))))
)
(dc/q '[:find (count ?x)
:where [?x :journal-entry-line/dirty true]]
(dc/db conn))
(let [clients (map first
(dc/q '[:find (pull ?c [:client/code :db/id])
:where [?c :client/code]]
(dc/db conn)))]
(doseq [[c i] (map vector clients (range))]
(let [db (dc/db conn)
accounts-needing-rebuild (auto-ap.ledger/accounts-needing-rebuild db (:db/id c))]
(when (seq accounts-needing-rebuild)
(println "C" c "needs" (count accounts-needing-rebuild) "built")
#_(refresh-running-balance-accounts accounts-needing-rebuild clients c i db)
#_(mu/log ::client-completed)))))
,

View File

@@ -578,12 +578,14 @@
(defn add-sorter-fields [q sort-map args] (defn add-sorter-fields [q sort-map args]
(reduce (reduce
(fn [q {:keys [sort-key]}] (fn [q {:keys [sort-key] :as z}]
(prn z)
(println (class sort-key))
(merge-query q (merge-query q
{:query {:find [(symbol (str "?sort-" sort-key))] {:query {:find [(symbol (str "?sort-" sort-key))]
:where (sort-map :where (sort-map
sort-key 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 q
(:sort args))) (:sort args)))
@@ -620,9 +622,13 @@
(range length)))] (range length)))]
(sort comparator results ))) (sort comparator results )))
(defn apply-pagination [args results] (defn apply-pagination-raw [args results]
(log/info (take 4 results)) {:entries (->> results
(drop (:start args 0))
(take (:count args (or (:per-page args) default-pagination-size))))
:count (count results)})
(defn apply-pagination [args results]
{:ids (->> results {:ids (->> results
(drop (:start args 0)) (drop (:start args 0))
(take (:count args (or (:per-page args) default-pagination-size))) (take (:count args (or (:per-page args) default-pagination-size)))

View File

@@ -12,6 +12,8 @@
[clj-time.coerce :as c] [clj-time.coerce :as c]
[datomic.api :as dc])) [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] (defn raw-graphql-ids [db args]
(let [query (cond-> {:query {:find [] (let [query (cond-> {:query {:find []
:in ['$ ] :in ['$ ]
@@ -43,8 +45,7 @@
(apply-pagination args)))) (apply-pagination args))))
(defn graphql-results [ids db args] (defn graphql-results [ids db args]
(let [results (->> (pull-many db '[:db/id :report/client :report/created :report/url :report/name :report/creator] (let [results (->> (pull-many db default-read ids)
ids)
(map #(update % :report/created c/from-date)) (map #(update % :report/created c/from-date))
(group-by :db/id))] (group-by :db/id))]
(->> ids (->> ids
@@ -63,8 +64,3 @@
[(->> (graphql-results ids-to-retrieve db args)) [(->> (graphql-results ids-to-retrieve db args))
matching-count])) matching-count]))

View File

@@ -30,7 +30,8 @@
[ring.util.response :as response] [ring.util.response :as response]
[unilog.context :as lc] [unilog.context :as lc]
[clj-time.coerce :as coerce] [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) (when (:aws-access-key-id env)
(defcredential (:aws-access-key-id env) (:aws-secret-access-key env) (:aws-region 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))] (let [end-time (time/plus (time/now) (time/days 14))]
(assoc response :session (assoc session ::idle-timeout (coerce/to-date end-time))))))))))) (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]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(def app (def app
(-> route-handler (-> route-handler
(wrap-hx-current-url-params)
(wrap-guess-route) (wrap-guess-route)
(wrap-authorization auth-backend (wrap-authorization auth-backend
) )

View File

@@ -44,12 +44,14 @@
extract)])) extract)]))
(defn extract-sheet-details [bucket object] (defn extract-sheet-details [bucket object]
(-> (lambda/invoke {:function-name "xls-extractor" :payload (doto
(json/write-str (-> (lambda/invoke {:function-name "xls-extractor" :payload
(json/write-str
{"s3_url" object "s3_bucket" bucket})}) {"s3_url" object "s3_bucket" bucket})})
:payload :payload
slurp slurp
json/read-str)) json/read-str)
println))
(defn parse-file (defn parse-file
[file _] [file _]

View File

@@ -20,16 +20,16 @@
(:jwt-secret env) (:jwt-secret env)
{:alg :hs512})) {:alg :hs512}))
(defn oauth [{{:strs [code]} :query-params {:strs [host]} :headers}] (defn oauth [{{:strs [code state]} :query-params {:strs [host]} :headers :as request}]
(try (try
(let [auth (-> "https://accounts.google.com/o/oauth2/token" (let [auth (-> "https://accounts.google.com/o/oauth2/token"
(http/post (http/post
{:form-params {"client_id" google-client-id {:form-params {"client_id" google-client-id
"client_secret" google-client-secret "client_secret" google-client-secret
"code" code "code" code
"redirect_uri" (str (:scheme env) "://" host "/api/oauth") "redirect_uri" (str (:scheme env) "://" host "/api/oauth")
"grant_type" "authorization_code"} "grant_type" "authorization_code"}
:as :json}) :as :json})
:body) :body)
token (:access_token auth) token (:access_token auth)
@@ -45,6 +45,7 @@
:user/name (:name profile)}) :user/name (:name profile)})
auth {:user (:name profile) auth {:user (:name profile)
:exp (time/plus (time/now) (time/days 30)) :exp (time/plus (time/now) (time/days 30))
:db/id (:db/id user)
:user/clients (map (fn [c] :user/clients (map (fn [c]
(select-keys c [:client/code :db/id :client/locations])) (select-keys c [:client/code :db/id :client/locations]))
(:user/clients user)) (:user/clients user))
@@ -58,14 +59,13 @@
(let [jwt (jwt/sign auth (let [jwt (jwt/sign auth
(:jwt-secret env) (:jwt-secret env)
{:alg :hs512})] {:alg :hs512})]
{:status 301 {:status 301
:headers {"Location" (str "/?jwt=" jwt)} :headers {"Location" (str (or (not-empty state) "/") "?jwt=" jwt)}
:session {:identity (dissoc auth :exp)}}) :session {:identity (dissoc auth :exp)}})
{:status 401 {:status 401
:body "Couldn't authenticate"})) :body "Couldn't authenticate"}))
(catch Exception e (catch Exception e
(log/warn e ) (log/warn e)
{:status 401 {:status 401
:body (str "Couldn't authenticate " (.toString e))}))) :body (str "Couldn't authenticate " (.toString e))})))

View File

@@ -267,8 +267,9 @@
)) ))
(into [["Vendor Name" "Address" "City" "State" "Zip" "Terms" "Account" "Account Code"]]))] (into [["Vendor Name" "Address" "City" "State" "Zip" "Terms" "Account" "Account Code"]]))]
{:body {:body
(into (list) (into []
data)}))) data)
:headers {"content-disposition" "attachment; filename=\"vendors.csv\""}})))
(defn export-ledger [{:keys [identity query-params]}] (defn export-ledger [{:keys [identity query-params]}]
(let [start-date (or (some-> (query-params "start-date") (let [start-date (or (some-> (query-params "start-date")
@@ -426,7 +427,7 @@
"expected-deposit/" {#"export/?" {:get :export-expected-deposits}} "expected-deposit/" {#"export/?" {:get :export-expected-deposits}}
"clients/" {#"export/?" {:get :export-clients}} "clients/" {#"export/?" {:get :export-clients}}
"vendors/" {#"export/?" {:get :export-vendors} "vendors/" {#"export/?" {:get :export-vendors}
"/company" {#"export/?" {:get :export-company-vendors}}} "company/" {#"export" {:get :export-company-vendors}}}
"ledger/" {#"export/?" {:get :export-ledger}} "ledger/" {#"export/?" {:get :export-ledger}}
"accounts/" {#"export/?" {:get :export-accounts}} "accounts/" {#"export/?" {:get :export-accounts}}
"transactions/" {#"export/?" {:get :export-transactions} "transactions/" {#"export/?" {:get :export-transactions}

View File

@@ -18,7 +18,8 @@
[com.brunobonacci.mulog :as mu] [com.brunobonacci.mulog :as mu]
[datomic.api :as dc] [datomic.api :as dc]
[hiccup2.core :as hiccup] [hiccup2.core :as hiccup]
[amazonica.aws.s3 :as s3])) [amazonica.aws.s3 :as s3]
[auto-ap.ssr.components :as com]))
(defn fmt-amount [a] (defn fmt-amount [a]
(with-precision 2 (with-precision 2
@@ -27,19 +28,14 @@
(.setScale 2 java.math.RoundingMode/HALF_UP) (.setScale 2 java.math.RoundingMode/HALF_UP)
(double)))) (double))))
(defn rows->maps [rows] (defn rows->maps [rows]
(let [[headers & rows] rows] (let [[headers & rows] rows]
(for [r rows] (for [r rows]
(into {} (into {}
(map vector headers r))))) (map vector headers r)))))
(defn map->sales-order [r clients] (defn map->sales-order [r clients]
(println r)
(let [order-number (get r "Order Number") (let [order-number (get r "Order Number")
event-date (get r "Event Date") event-date (get r "Event Date")
store-name (get r "Store Name") store-name (get r "Store Name")
@@ -135,30 +131,29 @@
(stream->sales-orders s))) (stream->sales-orders s)))
(defn page* [] (defn page* []
[:div [:div.mt-4
[:h1.title "EZCater XLS Import"] (com/card {}
[:div.card.block {:style {:width "500px"}} [:div.px-4.py-3.space-y-4.flex.flex-col
[:div.card-content [:h1.text-2xl.mb-3.font-bold "EZCater XLS Import"]
"Please go to " [:p.text-sm.italic
[:a {:href "https://www.ezcater.com/ez_manage/reports/new" :target "_blank"} "EZCater's report page"] "Please go to "
" to generate a new report. Then drop it below."]] (com/link {:href "https://www.ezcater.com/ez_manage/reports/new" :target "_blank"} "EZCater's report page")
[:div#page-notification.notification.block {:style {:display "none"}}] " to generate a new report. Then drop it below."]
[:div.card.block [:div#page-notification.notification.block {:style {:display "none"}}]
[:div.card-content [:form.bg-blue-300 {:action (bidi/path-for ssr-routes/only-routes
[:form {:action (bidi/path-for ssr-routes/only-routes :admin-ezcater-xls)
:admin-ezcater-xls) :method "POST"
:method "POST" :class "dropzone"
:class "dropzone" :id "ezcater"}]
:id "ezcater"}]]] [:script
[:script (hiccup/raw
(hiccup/raw "
"
Dropzone.options.ezcater = { Dropzone.options.ezcater = {
success: function (file, response) { success: function (file, response) {
document.getElementById(\"page-notification\").innerHTML = response; document.getElementById(\"page-notification\").innerHTML = response;
document.getElementById(\"page-notification\").style[\"display\"] = \"block\"; document.getElementById(\"page-notification\").style[\"display\"] = \"block\";
} }
}")]]) }")]])])
(defn upload-xls [{:keys [identity] :as request}] (defn upload-xls [{:keys [identity] :as request}]
@@ -194,8 +189,23 @@
(if (= :post request-method) (if (= :post request-method)
(upload-xls request) (upload-xls request)
(base-page (base-page
request request
(page*) (com/page {:nav (com/admin-aside-nav)
:active-client (:client (:session request))
:identity (:identity request)
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
:admin-ezcater-xls)
:hx-trigger "clientSelected from:body"
:hx-select "#app-contents"
:hx-swap "outerHTML swap:300ms"}}
(com/breadcrumbs {}
[:a {:href (bidi/path-for ssr-routes/only-routes
:admin)}
"Admin"]
[:a {:href (bidi/path-for ssr-routes/only-routes
:admin-ezcater-xls)}
"EZCater XLS Import"])
(page*))
(admin-side-bar matched-route)))) "EZCater upload")))

View File

@@ -8,16 +8,17 @@
(defn wrap-secure [handler] (defn wrap-secure [handler]
(fn [request] (fn [request]
(cond (authenticated? request) (cond (authenticated? request)
(handler request) (handler request)
(get (:headers request) "hx-request")
{:status 401
:headers {"hx-redirect" (str "/login?"
(url/map->query {"redirect-to" (:uri request)}))}}
(get (:headers request) "hx-request") :else
{:status 401 {:status 302
:headers {"hx-redirect" "/login"}} :headers {"Location" (str "/login?"
(url/map->query {"redirect-to" (:uri request)}))}})))
:else
{:status 302
:headers {"Location" "/login" }})))
(defn wrap-admin [handler] (defn wrap-admin [handler]
(fn [request] (fn [request]
@@ -26,12 +27,14 @@
(do (do
(alog/warn ::unauthenticated) (alog/warn ::unauthenticated)
{:status 302 {:status 302
:headers {"Location" "/login"}})))) :headers {"Location" (str "/login?"
(url/map->query {"redirect-to" (:uri request)}))}}))))
(defn wrap-client-redirect-unauthenticated [handler] (defn wrap-client-redirect-unauthenticated [handler]
(fn [request] (fn [request]
(let [response (handler request)] (let [response (handler request)]
(if (= 401 (get response :status)) (if (= 401 (get response :status))
(-> response (-> response
(assoc-in [:headers "hx-redirect"] "/login/")) (assoc-in [:headers "hx-redirect"] (str "/login?"
(url/map->query {"redirect-to" (:uri request)}))))
response)))) response))))

View File

@@ -206,3 +206,34 @@
(index-documents impl index [i]))) (index-documents impl index [i])))
(defrecord InMemSolrClient [data-set-atom]
SolrClient
(index-documents [this index xs]
(swap! data-set-atom
(fn [data-set]
(reduce
(fn [data-set x]
(let [thing (datomic->solr x)]
(update data-set index conj [(str/join " " (vals x)) thing])))
data-set
xs)))
nil)
(index-documents-raw [this index xs]
(swap! data-set-atom
(fn [data-set]
(reduce
(fn [data-set x]
(update data-set index conj [(str/join " " (vals x)) x]))
data-set
xs))))
(query [this index q]
(filter
(fn [[x e]]
(str/includes? x (get q "query")))
(get @data-set-atom index)))
(delete [this index]
(swap! data-set-atom dissoc index)))

View File

@@ -1,200 +0,0 @@
(ns auto-ap.ssr.admin
(:require
[auto-ap.datomic :refer [conn]]
[auto-ap.logging :as alog]
[auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]]
[auto-ap.ssr.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [html-response]]
[auto-ap.time :as atime]
[clj-time.coerce :as coerce]
[clojure.string :as str]
[datomic.api :as dc]
[hiccup2.core :as hiccup]))
(defn tx-rows->changes [history]
(->> history
(group-by (fn [[a _ t]]
[a t]))
(map (fn [[[a t] changes]]
(let [changes (-> (reduce
(fn [acc [_ v _ added]]
(if added
(assoc acc :added v)
(assoc acc :removed v)))
{}
changes))]
[t a changes])))))
(def error-script
(hiccup/raw "on htmx:responseError from me set event.detail.target's innerHTML to event.detail.xhr.responseText end"))
(defn format-value [v]
(cond (inst? v)
(-> v
coerce/to-date-time
atime/localize
(atime/unparse atime/normal-date))
(nil? v)
[:em "(none)"]
(and (integer? v)
(> v 1000000))
[:span
[:a {:hx-get (str "/admin/history/" v)
:hx-swap "innerHTML"
:hx-push-url "true"
:hx-target "#history-table"}
v]
" [" [:a
{:hx-get (str "/admin/history/inspect/" v)
:hx-swap "innerHTML"
:hx-target "#inspector"
:hx-trigger "click"
"_" error-script}
"snapshot"] "]"]
:else
(pr-str v)))
(defn page-template [& {:keys [table entity-id]}]
[:div
[:div.columns
[:div.column.is-4
[:form {"hx-target" "#history-table"
"hx-post" "/admin/history/search"
"hx-swap" "innerHTML"
"_" (hiccup/raw "on htmx:beforeRequest toggle @disabled on me then toggle .is-loading on <#dig/> end
on htmx:afterRequest toggle @disabled on me then toggle .is-loading on <#dig /> end")
}
[:div.field.is-grouped
[:p.control {}
[:input.input {:type "text" :name "entity-id" :placeholder "Entity id" :value entity-id}]]
[:p.control
[:button#dig.button.is-primary {}
"Dig"]]]]]]
[:div#history-table
table]])
(defn table [entity-id best-guess-entity history]
[:div [:h1.title "History for "
(str/capitalize best-guess-entity)
" "
entity-id]
[:div.columns
[:div.column.is-9
[:table.table.compact.grid {:style "width: 100%"}
[:thead
[:tr
[:td {:style "width: 14em"} "Date"]
[:td {:style "width: 14em"} "User"]
[:td {:style "width: 18em"} "Field"]
[:td "From"]
[:td "To"]]]
[:tbody
(for [[tx a c] history]
[:tr
[:td [:div [:div (some-> (:db/txInstant tx)
coerce/to-date-time
atime/localize
(atime/unparse atime/standard-time))
]
[:div.tag (:db/id tx)]]]
[:td (str (:audit/user tx))]
[:td (namespace a) ": " (name a)]
[:td
[:div.tag.is-danger.is-light
[:span
(format-value (:removed c))]]]
[:td
[:div.tag.is-primary.is-light
[:span
(format-value (:added c))]]]])]
]]
[:div.column.is-3
[:div#inspector]]]])
(defn history-search [{:keys [form-params params] :as request}]
(try
(let [entity-id (Long/parseLong (or (some-> (:entity-id form-params) not-empty)
(:entity-id params)
(get params "entity-id")
(get form-params "entity-id")))
history (->>
(dc/q '[:find ?a2 ?v (pull ?tx [:db/txInstant :audit/user :db/id]) ?ad
:in $ $$ ?i
:where
[$$ ?i ?a ?v ?tx ?ad]
[$ ?a :db/ident ?a2]]
(dc/db conn)
(dc/history (dc/db conn))
entity-id )
tx-rows->changes
(sort-by (comp :db/id first))
vec)
best-guess-entity (or (->> history
(group-by
(comp
namespace
second)
)
(map (fn [[k v]]
[k v]))
(sort-by second)
last
first)
"?")]
(if (get (:headers request) "hx-request")
(html-response
(table entity-id best-guess-entity history))
(base-page request (page-template :table (table entity-id best-guess-entity history)
:entity-id entity-id)
(admin-side-bar :admin-history))))
(catch NumberFormatException _
(html-response
[:div.notification.is-danger.is-light
"Cannot parse the entity-id " (or (:entity-id form-params)
(:entity-id params))
". It should be a number."]))))
(defn inspect [{{:keys [entity-id]} :params :as request}]
(alog/info ::inspect
:request request)
(try
(let [entity-id (Long/parseLong entity-id)
data (dc/pull (dc/db conn)
'[*]
entity-id)]
(html-response
[:div.box {:style {:position "sticky"
:display "inline-block"
:vertical-align "top"
:overflow-y "auto"
:max-height "100vh"
:top "0px"
:bottom "0px"}}
[:div {:style {:display "inline-block"}}
[:h1.title "Snapshot of "
entity-id]
[:ul
(for [[k v] data]
[:li [:strong k] ":" (format-value v)]
)]]]))
(catch NumberFormatException _
(html-response
[:div.notification.is-danger.is-light
"Cannot parse the entity-id " entity-id ". It should be a number."]))))
(defn history [{:keys [matched-route] :as request}]
(base-page request
(page-template )
(admin-side-bar matched-route)))

View File

@@ -0,0 +1,190 @@
(ns auto-ap.ssr.admin.history
(:require
[auto-ap.datomic :refer [conn]]
[auto-ap.logging :as alog]
[auto-ap.ssr.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [html-response]]
[auto-ap.time :as atime]
[clj-time.coerce :as coerce]
[clojure.string :as str]
[datomic.api :as dc]
[auto-ap.ssr.components :as com]
[auto-ap.ssr-routes :as ssr-routes]
[bidi.bidi :as bidi]))
(defn tx-rows->changes [history]
(->> history
(group-by (fn [[a _ t]]
[a t]))
(map (fn [[[a t] changes]]
(let [changes (-> (reduce
(fn [acc [_ v _ added]]
(if added
(assoc acc :added v)
(assoc acc :removed v)))
{}
changes))]
[t a changes])))))
(defn format-value [v]
(cond (inst? v)
(-> v
coerce/to-date-time
atime/localize
(atime/unparse atime/normal-date))
(nil? v)
[:em "(none)"]
(and (integer? v)
(> v 1000000))
[:span
(com/link
{:href "#"
:hx-get (str "/admin/history/" v)
:hx-swap "innerHTML"
:hx-push-url "true"
:hx-select "#history-table"
:hx-target "#history-table"}
v)
" ["
(com/link
{:href "#"
:hx-get (str "/admin/history/inspect/" v)
:hx-swap "innerHTML"
:hx-target "#inspector"
:hx-trigger "click"}
"snapshot") "]"]
:else
(pr-str v)))
(defn inspect [{{:keys [entity-id]} :params :as request}]
(alog/info ::inspect
:request request)
(try
(let [entity-id (Long/parseLong entity-id)
data (dc/pull (dc/db conn)
'[*]
entity-id)]
(html-response
[:section.py-3.sm:py-5.max-w-lg
(com/card {:class "p-2"}
[:div {:style {:display "inline-block"}}
[:h1.title "Snapshot of "
entity-id]
[:ul
(for [[k v] data]
[:li [:strong k] ":" (format-value v)])]])]))
(catch NumberFormatException _
(html-response
[:div.notification.is-danger.is-light
"Cannot parse the entity-id " entity-id ". It should be a number."]))))
(defn result-table [{:keys [entity-id]}]
(try
(let [history (->>
(dc/q '[:find ?a2 ?v (pull ?tx [:db/txInstant :audit/user :db/id]) ?ad
:in $ $$ ?i
:where
[$$ ?i ?a ?v ?tx ?ad]
[$ ?a :db/ident ?a2]]
(dc/db conn)
(dc/history (dc/db conn))
entity-id)
tx-rows->changes
(sort-by (comp :db/id first))
vec)
best-guess-entity (or (->> history
(group-by
(comp
namespace
second))
(map (fn [[k v]]
[k v]))
(sort-by second)
last
first)
"?")]
(com/data-grid-card {:id "history-table"
:title (format "History for %s: %d" (str/capitalize best-guess-entity) entity-id)
:route :history-table
:paginate? false
:total (count history)
:subtitle nil
:action-buttons nil
:rows (for [[tx a c] history]
(com/data-grid-row
{}
(com/data-grid-cell {} [:div [:div (some-> (:db/txInstant tx)
coerce/to-date-time
atime/localize
(atime/unparse atime/standard-time))]
[:div.tag (:db/id tx)]])
(com/data-grid-cell {} (str (:audit/user tx)))
(com/data-grid-cell {} (namespace a) ": " (name a))
(com/data-grid-cell {}
(com/pill {:color :red}
(format-value (:removed c))))
(com/data-grid-cell {}
[:div.tag.is-primary.is-light
[:span
(format-value (:added c))]])))
:headers
[(com/data-grid-header {}
"Date")
(com/data-grid-header {}
"User")
(com/data-grid-header {}
"Field")
(com/data-grid-header {}
"From")
(com/data-grid-header {}
"To")]}))
(catch NumberFormatException e
(throw e))))
(defn search-box [{:keys [entity-id]}]
[:div.mt-4
[:form.flex.gap-2 {"hx-target" "#history-table"
"hx-get" (bidi/path-for ssr-routes/only-routes
:admin-history)
"hx-select" "#history-table"
"hx-swap" "innerHTML"
"hx-ext" "debug"
"hx-push-url" "true"}
(com/text-input {:name "entity-id" :placeholder "Entity Id" :value entity-id
:style {:width "300px"}})
(com/button {:color :primary}
"DIG")]])
(defn page [{:keys [matched-route route-params query-params] :as request}]
(let [entity-id (or (some-> query-params (get "entity-id") Long/parseLong)
(some-> route-params (get :entity-id) Long/parseLong))]
(base-page request
(com/page {:nav (com/admin-aside-nav)
:active-client (:client (:session request))
:identity (:identity request)
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
:admin-history)
:hx-trigger "clientSelected from:body"
:hx-select "#app-contents"
:hx-swap "outerHTML swap:300ms"}}
(com/breadcrumbs {}
[:a {:href (bidi/path-for ssr-routes/only-routes
:admin)}
"Admin"]
[:a {:href (bidi/path-for ssr-routes/only-routes
:admin-history)}
"History"])
(search-box {:entity-id entity-id})
[:div.flex.gap-4.flex-col.lg:flex-row
(if entity-id
(result-table {:entity-id entity-id})
[:div#history-table])
[:div#inspector]
])
"History")))

View File

@@ -0,0 +1,67 @@
(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-select "#app-contents"
: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))}))
"My Company"))

View File

@@ -1,367 +1,325 @@
(ns auto-ap.ssr.company.company-1099 (ns auto-ap.ssr.company.company-1099
(:require (:require
[auto-ap.datomic :refer [conn remove-nils]] [auto-ap.datomic :refer [apply-pagination-raw conn remove-nils]]
[auto-ap.graphql.utils :refer [is-admin?]] [auto-ap.graphql.utils :refer [assert-can-see-client is-admin?]]
[iol-ion.query :refer [can-see-client?]]
[auto-ap.shared-views.company.sidebar :refer [company-side-bar]]
[auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.ui :refer [base-page]] [auto-ap.ssr.components :as com]
[auto-ap.ssr.utils :refer [html-response]] [auto-ap.ssr.grid-page-helper :as helper]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils :refer [form-data->map html-response path->name]]
[bidi.bidi :as bidi] [bidi.bidi :as bidi]
[cemerick.url :as url]
[clojure.string :as str] [clojure.string :as str]
[datomic.api :as dc] [datomic.api :as dc]
[hiccup2.core :as hiccup])) [iol-ion.query :refer [can-see-client?]]))
(def vendor-read '[:db/id
:vendor/name
{:vendor/legal-entity-1099-type [:db/ident]}
{:vendor/legal-entity-tin-type [:db/ident]}
{:vendor/address [:address/street1
:address/city
:address/state
:address/zip]}
:vendor/legal-entity-tin
:vendor/legal-entity-name
:vendor/legal-entity-first-name
:vendor/legal-entity-middle-name
:vendor/legal-entity-last-name])
(defn sum-for-client-vendor [client-id vendor-id]
(ffirst (dc/q '[:find
(sum ?a)
:with ?d
:in $ ?c ?v
:where
[?p :payment/client ?c]
[?p :payment/date ?d ]
[(>= ?d #inst "2022-01-01T08:00")]
[(< ?d #inst "2023-01-01T08:00")]
[?p :payment/type :payment-type/check]
[?p :payment/amount ?a]
[?p :payment/vendor ?v]]
(dc/db conn)
client-id
vendor-id)))
(defn get-1099-companies [user session] (defn get-1099-companies [user {:keys [client-id] :as args}]
(let [clients (->> (dc/q '[:find ?c (let [clients (->> (dc/q '[:find ?c
:in $ ?user :in $ ?user
:where [?c :client/code] :where [?c :client/code]
[(iol-ion.query/can-see-client? ?user ?c)]] [(iol-ion.query/can-see-client? ?user ?c)]]
(dc/db conn) user) (dc/db conn) user)
(map first) (map first)
set) set)
results (cond results (cond
(and (some-> session :client :db/id) (and client-id
(can-see-client? user (can-see-client? user client-id))
(some-> session :client :db/id)))
(dc/q '[:find (dc/q '[:find
(pull ?c [:client/code :db/id]) (pull ?c [:client/code :db/id])
(pull ?v [:db/id (pull ?v vendor-read)
:vendor/name (sum ?a)
{:vendor/legal-entity-1099-type [:db/ident]} :with ?d
{:vendor/legal-entity-tin-type [:db/ident]} :in $ ?c vendor-read
{:vendor/address [:address/street1 :where
:address/city [?p :payment/client ?c]
:address/state [?p :payment/date ?d ]
:address/zip]} [(>= ?d #inst "2022-01-01T08:00")]
:vendor/legal-entity-tin [(< ?d #inst "2023-01-01T08:00")]
:vendor/legal-entity-name [?p :payment/type :payment-type/check]
:vendor/legal-entity-first-name [?p :payment/amount ?a]
:vendor/legal-entity-middle-name [?p :payment/vendor ?v]]
:vendor/legal-entity-last-name]) (dc/db conn)
(sum ?a) client-id
:with ?d vendor-read)
:in $ ?c
:where
[?p :payment/client ?c]
[?p :payment/date ?d ]
[(>= ?d #inst "2022-01-01T08:00")]
[(< ?d #inst "2023-01-01T08:00")]
[?p :payment/type :payment-type/check]
[?p :payment/amount ?a]
[?p :payment/vendor ?v]]
(dc/db conn)
(some-> session :client :db/id))
(is-admin? user) (is-admin? user)
(dc/q '[:find (dc/q '[:find
(pull ?c [:client/code :db/id]) (pull ?c [:client/code :db/id])
(pull ?v [:db/id (pull ?v vendor-read)
:vendor/name (sum ?a)
{:vendor/legal-entity-1099-type [:db/ident]} :with ?d
{:vendor/legal-entity-tin-type [:db/ident]} :in $ vendor-read
{:vendor/address [:address/street1 :where
:address/city [?p :payment/date ?d ]
:address/state [(>= ?d #inst "2022-01-01T08:00")]
:address/zip]} [(< ?d #inst "2023-01-01T08:00")]
:vendor/legal-entity-tin [?p :payment/type :payment-type/check]
:vendor/legal-entity-name [?p :payment/client ?c]
:vendor/legal-entity-first-name [?p :payment/amount ?a]
:vendor/legal-entity-middle-name [?p :payment/vendor ?v]]
:vendor/legal-entity-last-name]) (dc/db conn)
(sum ?a) vendor-read)
:with ?d
:in $
:where
[?p :payment/date ?d ]
[(>= ?d #inst "2022-01-01T08:00")]
[(< ?d #inst "2023-01-01T08:00")]
[?p :payment/type :payment-type/check]
[?p :payment/client ?c]
[?p :payment/amount ?a]
[?p :payment/vendor ?v]]
(dc/db conn))
:else :else
(dc/q '[:find (dc/q '[:find
(pull ?c [:client/code :db/id]) (pull ?c [:client/code :db/id])
(pull ?v [:db/id (pull ?v vendor-read)
:vendor/name (sum ?a)
{:vendor/legal-entity-1099-type [:db/ident]} :with ?d
{:vendor/legal-entity-tin-type [:db/ident]} :in $ [?c ...] vendor-read
{:vendor/address [:address/street1 :where
:address/city [?p :payment/client ?c]
:address/state [?p :payment/date ?d ]
:address/zip]} [(>= ?d #inst "2022-01-01T08:00")]
:vendor/legal-entity-tin [(< ?d #inst "2023-01-01T08:00")]
:vendor/legal-entity-name [?p :payment/type :payment-type/check]
:vendor/legal-entity-first-name [?p :payment/amount ?a]
:vendor/legal-entity-middle-name [?p :payment/vendor ?v]]
:vendor/legal-entity-last-name]) (dc/db conn)
(sum ?a) clients
:with ?d vendor-read))
:in $ [?c ...] all (->> results
:where (filter (fn [[_ _ a]]
[?p :payment/client ?c] (>= (or a 0.0) 600.0)))
[?p :payment/date ?d ] (sort-by (fn [[client _ amount]]
[(>= ?d #inst "2022-01-01T08:00")] [(:client/code client ) amount]))
[(< ?d #inst "2023-01-01T08:00")] (into []))
[?p :payment/type :payment-type/check] paginated (apply-pagination-raw args all)]
[(:entries paginated) (:count paginated)]))
[?p :payment/amount ?a] (def grid-page {:id "vendor-table"
[?p :payment/vendor ?v]] :nav (com/company-aside-nav)
(dc/db conn) :id-fn (comp :db/id second)
clients))] :fetch-page (fn [user args]
(->> results (get-1099-companies user args)
(filter (fn [[_ _ a]] #_(r/get-graphql (into args {:id user})))
(>= (or a 0.0) 600.0))) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
(take 200) :company)}
(sort-by (fn [[client _ amount]] "My Company"]
[(:client/code client ) amount])))))
(defn dialog [header content footer] [:a {:href (bidi/path-for ssr-routes/only-routes
[:div.modal.is-active :company-1099)}
[:div.modal-background {"_" (hiccup/raw "on click remove <#modal-holder div/>")}] "1099 Vendor Info"]]
[:div.modal-card :title "1099 Vendors"
[:div.modal-card-head :entity-name "Vendors"
header] :route :company-1099-vendor-table
[:div.modal-card-body :action-buttons (fn [user]
content] nil)
[:div.modal-card-foot :row-buttons (fn [user e]
footer]] [(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes
[:button.modal-close.is-large {"_" (hiccup/raw "on click remove <#modal-holder div/>")}]]) :company-1099-vendor-dialog
:vendor-id (:db/id (second e)))
"?"
(url/map->query {:client-id (:db/id (first e))}))
:hx-ext "debug"
:hx-target "#modal-holder"
:hx-swap "outerHTML"}
svg/pencil)])
:headers [{:key "Client"
:name "Client"
:sort-key "client"
:render (comp :client/code first)}
{:key "vendor-name"
:name "Vendor Name"
:sort-key "vendor"
:render (fn [[_ vendor]]
[:div.flex.whitespace-nowrap.items-center.gap-4
[:div [:div (:vendor/name vendor)]
[:div.text-sm.text-gray-400
(or (-> vendor :vendor/legal-entity-name not-empty)
(str (-> vendor :vendor/legal-entity-first-name) " "
(-> vendor :vendor/legal-entity-middle-name) " "
(-> vendor :vendor/legal-entity-last-name)))]]
(when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)]
(com/pill
{:class "text-xs font-medium"
:color :primary}
(str/capitalize t99-type))
)])}
{:key "tin"
:name "TIN"
:sort-key "tin"
:show-starting "md"
:render (fn [[_ vendor]]
[:div.flex.gap-4
(when-let [tin (-> vendor :vendor/legal-entity-tin)]
[:span {:class "text-xs font-medium py-0.5 "}
tin])
(when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)]
(com/pill {:class "text-xs font-medium"
:color :yellow}
(name tin-type)))]
)}
{:key "address"
:name "Address"
:sort-key "address"
:show-starting "lg"
:render (fn [[_ vendor]]
(if (-> vendor :vendor/address :address/street1)
[:div
[:div (-> vendor :vendor/address :address/street1)] " "
[:div
(-> vendor :vendor/address :address/street2)] " "
[:div
(-> vendor :vendor/address :address/city) " "
(-> vendor :vendor/address :address/state) ","
(-> vendor :vendor/address :address/zip)]]
[:p.text-sm.italic.text-gray-400 "No address"]))}
{:key "paid"
:name "Paid"
:sort-key "paid"
:render (fn [[_ _ paid]]
(com/pill {:class "text-xs font-medium"
:color :primary}
"Paid $" (Math/round paid)))}]})
(defn table [{:keys [identity session]} & {:keys [flash-id]}]
[:div#vendor-table {:hx-get (bidi/path-for ssr-routes/only-routes
:company-1099-vendor-table
:request-method :get)
:hx-trigger "clientSelected from:body"
"_" (hiccup/raw "on htmx:beforeRequest remove <table/> in me")
:hx-swap "outerHTML swap:300ms"}
[:table.table.grid.compact.is-fullwidth
[:thead
[:tr
[:th {:style {:width "5em"}}"Client"]
[:th "Vendor Name"]
[:th "Name"]
[:th {:style {:width "9em"}} "1099 Type"]
[:th {:style {:width "8em"}} "TIN"]
[:th "Address"]
[:th "Amount Paid"]
[:th {:style {:width "10em"}}]
]]
[:tbody
(for [[client vendor amount] (get-1099-companies identity session)]
[:tr (when (= flash-id
(:db/id vendor))
{:class "live-added"})
[:td (:client/code client)]
[:td (:vendor/name vendor)]
[:td (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)))]
[:td (some-> vendor :vendor/legal-entity-1099-type :db/ident name)] " "
[:td
(some-> vendor :vendor/legal-entity-tin-type :db/ident name) " "
(-> vendor :vendor/legal-entity-tin)]
[:td
(-> vendor :vendor/address :address/street1) " "
(-> vendor :vendor/address :address/street2) " "
(-> vendor :vendor/address :address/city) " "
(-> vendor :vendor/address :address/state) " "
(-> vendor :vendor/address :address/zip)
[:td "$" (Math/round amount)]
[:td
[:button.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 "innerHTML"}
[:span.icon [:i.fa.fa-pencil ]]]]]])]]
[:div.container.htmx-indicator
[:div.column.is-4.is-offset-4.has-text-centered
[:div.loader.is-loading.is-active.big.is-centered]]]])
(defn form-data->map [form-data]
(reduce-kv
(fn [acc k v]
(cond (and (string? v)
(empty? v))
acc
:else (def table* (partial helper/table* grid-page))
(assoc-in acc (->> (str/split k #"_") (def row* (partial helper/row* grid-page))
(mapv #(apply keyword (str/split % #"/"))))
v)))
{}
form-data))
(defn path->name [k] (defn vendor-save [{:keys [form-params identity route-params query-params] :as request}]
(cond (keyword? k) (let [client-id (Long/parseLong (get query-params "client-id"))
(str (namespace k) "/" (name k)) vendor-id (Long/parseLong (:vendor-id route-params))]
(assert-can-see-client identity client-id)
(seq k) @(dc/transact conn [(remove-nils
(str/join "_" (map path->name k)) (-> (form-data->map form-params)
:else k)) (assoc :db/id (Long/parseLong (:vendor-id route-params)))
(update :vendor/legal-entity-1099-type #(some->> % not-empty (keyword "legal-entity-1099-type")))
(defn vendor-save [{:keys [form-params identity route-params] :as request}] (update :vendor/legal-entity-tin-type #(some->> % not-empty (keyword "legal-entity-tin-type")))))])
@(dc/transact conn [(remove-nils (html-response
(-> (form-data->map form-params)
(assoc :db/id (Long/parseLong (:vendor-id route-params)))
(update :vendor/legal-entity-1099-type #(some->> % not-empty (keyword "legal-entity-1099-type")))
(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)))))
(row* identity [(dc/pull (dc/db conn) [:db/id :client/code] client-id)
(dc/pull (dc/db conn) vendor-read vendor-id)
(sum-for-client-vendor client-id vendor-id)
] {:flash? true})
:headers {"hx-trigger" "closeModal"})))
(defn vendor-dialog [request] (defn vendor-dialog [request]
(let [vendor (dc/pull (dc/db conn) '[* {:vendor/legal-entity-1099-type [:db/ident] (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 :vendor/legal-entity-tin-type [:db/ident]}] (Long/parseLong (:vendor-id (:params request))))] ;; TODO perms
(html-response (html-response
[:form {:hx-post (bidi/path-for ssr-routes/only-routes (com/modal
:company-1099-vendor-save {}
:request-method :post [:form {:hx-post (str (bidi/path-for ssr-routes/only-routes
:vendor-id (Long/parseLong (:vendor-id (:params request)))) :company-1099-vendor-save
:hx-target "#vendor-table" :request-method :post
:hx-swap "outerHTML swap:0.2s" :vendor-id (Long/parseLong (:vendor-id (:params request))))
"_" (hiccup/raw "on htmx:afterRequest transition <#modal-holder .modal-background, #modal-holder .modal-card />'s opacity from 1.0 to 0 over 100ms then remove <#modal-holder */> ")} "?"
(dialog (url/map->query {:client-id (:client-id (:params request))}))
[:h4.is-4.title "Vendor 1099 Info"] :hx-target (format "#vendor-table tr[data-id=\"%d\"]" (:db/id vendor))
[:div :hx-swap "outerHTML swap:300ms"}
[:h3.is-3.title (:vendor/name vendor)] [: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.is-4.title "Address"] [:h4.text-xl.border-b.col-span-6 "Address"]
[:hr] [:div.col-span-6
[:div.field (com/field {:label "Street 1"}
[:p.help "Street1"] (com/text-input {:name (path->name [:vendor/address :address/street1])
[:div.control :value (-> vendor :vendor/address :address/street1)
[:input.input.is-expanded {:type "text" :placeholder "1700 Pennsylvania Ave"
:autofocus true :autofocus true}))]
:name (path->name [:vendor/address :address/street1]) [:div.col-span-6
:placeholder "1700 Pennsylvania Ave" (com/field {:label "Street 2"}
:value (-> vendor :vendor/address :address/street1)}]]] (com/text-input {:name (path->name [:vendor/address :address/street2])
[:div.field :value (-> vendor :vendor/address :address/street2)
[:p.help "Street 2"] :placeholder "Suite 200"}))]
[:div.control [:div.col-span-3
[:input.input.is-expanded {:type "text" (com/field {:label "City"}
:name (path->name [:vendor/address :address/street2]) (com/text-input {:name (path->name [:vendor/address :address/city])
:placeholder "SUite 400" :value (-> vendor :vendor/address :address/city)
:value (-> vendor :vendor/address :address/street2)}]]] :placeholder "Cupertino"}))]
[:div.level [:div.col-span-1
[:div.level-left (com/field {:label "State"}
[:div.level-item (com/text-input {:name (path->name [:vendor/address :address/state])
[:div.field :value (-> vendor :vendor/address :address/state)
[:p.help "City"] :placeholder "CA"}))]
[:div.control [:div.col-span-2
[:input.input.is-expanded {:type "text" (com/field {:label "Zip"}
:placeholder "Cupertino" (com/text-input {:name (path->name [:vendor/address :address/zip])
:name (path->name [:vendor/address :address/city]) :value (-> vendor :vendor/address :address/zip)
:value (-> vendor :vendor/address :address/city)}]]]] :placeholder "98102"}))]
[:div.level-item [:h4.text-xl.border-b.col-span-6 "Legal Entity"]
[:div.field [:div.col-span-6
[:p.help "State"] (com/field {:label "Legal Entity Name"}
[:div.control (com/text-input {:name (path->name [:vendor/legal-entity-name])
[:input.input.is-expanded {:type "text" :value (-> vendor :vendor/legal-entity-name)
:style {:width "3em"} :placeholder "Good Restaurant LLC"}))]
:placeholder "CA" [:div.col-span-6.text-center " - OR -"]
:name (path->name [:vendor/address :address/state]) [:div.col-span-2
:value (-> vendor :vendor/address :address/state)}]]]] (com/field {:label "First Name"}
[:div.level-item (com/text-input {:name (path->name [:vendor/legal-entity-first-name])
[:div.field :value (-> vendor :vendor/legal-entity-first-name)
[:p.help "Zip"] :placeholder "John"}))]
[:div.control [:div.col-span-2
[:input.input.is-expanded {:type "text" (com/field {:label "Middle Name"}
:placeholder "95014" (com/text-input {:name (path->name [:vendor/legal-entity-middle-name])
:name (path->name [:vendor/address :address/zip]) :value (-> vendor :vendor/legal-entity-middle-name)
:value (-> vendor :vendor/address :address/zip)}]]]]]] :placeholder "C."}))]
[:h4.is-4.title "Legal Entity"] [:div.col-span-2
[:hr] (com/field {:label "Last Name"}
[:div.field (com/text-input {:name (path->name [:vendor/legal-entity-last-name])
[:p.help "Legal Entity Name"] :value (-> vendor :vendor/legal-entity-last-name)
[:div.control :placeholder "Riley"}))]
[:input.input.is-expanded {:type "text" [:div.col-span-2
:placeholder "Good Restaurant LLC" (com/field {:label "TIN"}
:name (path->name [:vendor/legal-entity-name]) (com/text-input {:name (path->name [:vendor/legal-entity-tin])
:value (-> vendor :vendor/legal-entity-name)}]]] :value (-> vendor :vendor/legal-entity-tin)
"OR" :placeholder "John"}))]
[:div.level [:div.col-span-2
[:div.level-left (com/field {:label "TIN Type"}
[:div.level-item (com/select {:name (path->name [:vendor/legal-entity-tin-type])
[:div.field :allow-blank? true
[:p.help "First Name"] :value (some-> vendor :vendor/legal-entity-tin-type :db/ident name)
[:div.control :options [["ein" "EIN"]
[:input.input.is-expanded {:type "text" ["ssn" "SSN"]]}))]
:placeholder "Josh" [:div.col-span-2
:name (path->name [:vendor/legal-entity-first-name]) (com/field {:label "1099 Type"}
:value (-> vendor :vendor/legal-entity-first-name)}]]]] (com/select {:name (path->name [:vendor/legal-entity-1099-type])
[:div.level-item :allow-blank? true
[:div.field :value (some-> vendor :vendor/legal-entity-1099-type :db/ident name)
[:p.help "Middle Name"] :options [["none" "None"]
[:div.control ["misc" "Misc"]
[:input.input.is-expanded {:type "text" ["landlord" "Landlord"]]}))]
:placeholder "Caleb" [:div.col-span-6
:name (path->name [:vendor/legal-entity-middle-name]) (com/button {:color :primary}
:value (-> vendor :vendor/legal-entity-middle-name)}]]]] "Save")]]]
[:div.level-item [:div])]]))))
[:div.field
[:p.help "Last Name"]
[:div.control
[:input.input.is-expanded {:type "text"
:placeholder "Smith"
:name (path->name [:vendor/legal-entity-last-name])
:value (-> vendor :vendor/legal-entity-last-name)}]]]]]]
[:div.level
[:div.level-left
[:div.level-item
[:div.field
[:p.help "TIN"]
[:div.control
[:input.input {:type "text"
:name (path->name [:vendor/legal-entity-tin])
:placeholder "SSN or EIN"
:size "12"
:value (-> vendor :vendor/legal-entity-tin)}]]]]
[:div.level-item
[:div.field
[:p.help "TIN Type"]
[:div.control
[:div.select
[:select {:name (path->name [:vendor/legal-entity-tin-type])}
[:option {:value ""} ""]
[:option {:value "ein" :selected (= (-> vendor :vendor/legal-entity-tin-type :db/ident) :legal-entity-tin-type/ein)} "EIN"]
[:option {:value "ssn" :selected (= (-> vendor :vendor/legal-entity-tin-type :db/ident) :legal-entity-tin-type/ssn)} "SSN"]]]]]]
[:div.level-item
[:div.field
[:p.help "1099 Type"]
[:div.control
[:div.select
[:select {:name (path->name [:vendor/legal-entity-1099-type])}
[:option {:value ""} ""]
[:option {:value "none" :selected (= (-> vendor :vendor/legal-entity-1099-type :db/ident) :legal-entity-1099-type/none)} "None"]
[:option {:value "misc" :selected (= (-> vendor :vendor/legal-entity-1099-type :db/ident) :legal-entity-1099-type/misc)} "Misc"]
[:option {:value "landlord" :selected (= (-> vendor :vendor/legal-entity-1099-type :db/ident) :legal-entity-1099-type/landlord)} "Landlord"]]]]]]]]]
[:button.button.is-primary.htmx-indicator "Save"])])))
(defn vendor-table [request]
(html-response (table request)))
(defn page [{:keys [identity matched-route] :as request}]
(base-page
request
[:div
[:div#vendor-table {:hx-get (bidi/path-for ssr-routes/only-routes
:company-1099-vendor-table
:request-method :get)
:hx-trigger "load, clientSelected from:body"
:hx-swap "outerHTML swap:400ms"}
[:div.container.htmx-indicator
[:div.column.is-4.is-offset-4.has-text-centered
[:div.loader.is-loading.is-active.big.is-centered]]]]]
[:div
(company-side-bar matched-route)]))
(def vendor-table (partial helper/table grid-page))
(def page (partial helper/page grid-page))

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

@@ -1,116 +1,142 @@
(ns auto-ap.ssr.company-dropdown (ns auto-ap.ssr.company-dropdown
(:require (:require
[auto-ap.datomic :refer [conn]] [auto-ap.datomic :refer [conn pull-many]]
[auto-ap.graphql.utils :refer [assert-can-see-client]] [auto-ap.graphql.utils :refer [assert-can-see-client cleanse-query]]
[iol-ion.query :refer [can-see-client?]] [auto-ap.solr :as solr]
[auto-ap.ssr-routes :as ssr-routes] [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]] [auto-ap.ssr.utils :refer [html-response]]
[bidi.bidi :as bidi] [bidi.bidi :as bidi]
[datomic.api :as dc] [datomic.api :as dc]
[hiccup2.core :as hiccup])) [hiccup2.core :as hiccup]
[iol-ion.query :refer [can-see-client?]]))
(defn dropdown-contents [{:keys [identity]}] (defn dropdown-search-results* [{:keys [options]}]
(let [options (->> (dc/q '[:find ?c ?n [:ul
:in $ ?user (for [{:keys [id name]} options]
:where [?c :client/name ?n] [:li
[(iol-ion.query/can-see-client? ?user ?c)]] [:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"}
(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)
" [: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")
resultItem: { :data-value id}
highlight:true, name]]])])
class: \"autocomplete-suggestion\",
selected: \"highlighted\"
},
resultsList: {
tabSelect: true
},
submit: true
});
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 get-clients [identity query]
"))]]))) (if-let [query (not-empty (cleanse-query query))]
(let [search-query (str "name:(" query ")")]
(defn dropdown [request] (for [n (pull-many (dc/db conn) [:client/name :db/id]
(for [{:keys [id name]} (solr/query solr/impl "clients" {"query" search-query
"fields" "id, name"})
:let [client-id (Long/parseLong id)]
:when (can-see-client? identity client-id)]
client-id))]
{:id (:db/id n)
:name (:client/name n)}))
[]))
(let [client (get-in request [:session :client])] (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))})))
(navbar-dropdown (defn dropdown [{:keys [client]}]
"company-dropdown" [:div#company-dropdown
[:span {:hx-put (bidi/path-for ssr-routes/only-routes
(if client :active-client
(str "Company: " (:client/name client)) :request-method :put)
"Company") :hx-target "#company-dropdown"
[:script :hx-include "#company-search-value"
(hiccup/raw :hx-swap "outerHTML"
"localStorage.setItem(\"last-client-id\", \""(:db/id client)"\")")]] :hx-trigger "selected"}
[:div {:hx-get [:script
(bidi/path-for ssr-routes/only-routes (hiccup/raw
:company-dropdown-contents) "localStorage.setItem(\"last-client-id\", \""(:db/id client)"\")")]
:hx-swap "outerHTML" [:div
:hx-trigger "intersect delay:150ms" [: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"
:hx-target "closest .navbar-dropdown" :type "button"}
:style {:width "20em" (if client
:height "80px"} (:client/name client)
} "All Companies")
[:div.loader.is-loading.is-active.is-centered]]))) [: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"}
[: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');
// set the element that trigger the dropdown menu on click
var $dropdownTriggerEl = document.getElementById('company-dropdown-button');
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}] (defn active-client [{:keys [identity params] :as request}]
(let [client-id (some-> (or (:search-client params) (get params "search-client")) not-empty Long/parseLong)] (let [client-id (some-> (or (:search-client params) (get params "search-client")) not-empty Long/parseLong)]
(println (format "HERE CLIENT ID '%s'" client-id))
(when client-id (when client-id
(assert-can-see-client identity client-id)) (assert-can-see-client identity client-id))
(let [new-session (assoc (:session request) :client (let [new-session (assoc (:session request) :client
(when client-id (when client-id
(dc/pull (dc/db conn) [:db/id :client/name :client/code] client-id)))] (dc/pull (dc/db conn) [:db/id :client/name :client/code] client-id)))]
(assoc (assoc
(html-response (html-response
(dropdown (assoc request :session new-session))) (dropdown {:client (:client new-session)
:session :identity identity}))
new-session :session
:headers new-session
{"hx-trigger" "clientSelected"})))) :headers
{"hx-trigger" "clientSelected"}))))

View File

@@ -0,0 +1,56 @@
(ns auto-ap.ssr.components
(:require [auto-ap.ssr.components.breadcrumbs :as breadcrumbs]
[auto-ap.ssr.components.buttons :as buttons]
[auto-ap.ssr.components.dialog :as dialog]
[auto-ap.ssr.components.inputs :as inputs]
[auto-ap.ssr.components.aside :as aside]
[auto-ap.ssr.components.card :as card]
[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.paginator :as paginator]))
(def breadcrumbs breadcrumbs/breadcrumbs-)
(def button buttons/button-)
(def button-icon buttons/button-icon-)
(def icon-button buttons/icon-button-)
(def a-icon-button buttons/a-icon-button-)
(def modal dialog/modal-)
(def modal-card dialog/modal-card-)
(def text-input inputs/text-input-)
(def select inputs/select-)
(def field inputs/field-)
(def left-aside aside/left-aside-)
(def company-aside-nav aside/company-aside-nav-)
(def admin-aside-nav aside/admin-aside-nav-)
(def main-aside-nav aside/main-aside-nav-)
(def content-card card/content-card-)
(def card card/card-)
(def navbar navbar/navbar-)
(def page page/page-)
(def pill tags/pill-)
(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-)
(defn link [params & children]
(into [:a (update params :class str " font-medium text-blue-600 dark:text-blue-500 hover:underline ")]
children))
(def paginator paginator/paginator-)
(def data-grid-card data-grid/data-grid-card-)

View File

@@ -0,0 +1,326 @@
(ns auto-ap.ssr.components.aside
(:require [auto-ap.ssr.svg :as svg]
[hiccup2.core :as hiccup]
[bidi.bidi :as bidi]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.client-routes :as client-routes]))
(defn menu-button- [params & children]
[:div
[:a (-> params
(dissoc :icon)
(assoc :type "button")
(update :class str " cursor-pointer flex items-center p-2 w-full text-sm text-gray-600 rounded-lg transition duration-75 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700")
(assoc :hx-indicator "find .htmx-indicator")
(assoc :hx-select "#app-contents")
(assoc :hx-target "#app-contents")
(assoc :hx-swap "outerHTML"))
(when (:icon params)
[:span {:class "flex-shrink-0 w-6 h-6 text-gray-400 transition duration-75 group-hover:text-blue-500 dark:text-gray-400 group-hover:scale-110 dark:group-hover:text-white mr-3"}
(:icon params)])
(into [:span {:class "flex-1 text-left whitespace-nowrap text-gray-600 dark:text-white"}] children)
(when (:data-collapse-toggle params)
[:svg {:aria-hidden "true", :class "w-6 h-6", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:fill-rule "evenodd", :d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z", :clip-rule "evenodd"}]])
[:div.htmx-indicator.flex.items-center
(svg/spinner-primary {:class "inline w-4 h-4 text-white"})]]])
(defn sub-menu- [params & children]
[:ul {:id (:id params) :class "hidden py-2 space-y-2"}
(for [c children]
[:li
(update-in c [1 1 :class ] str " flex items-center p-2 pl-11 w-full text-base font-normal text-gray-900 rounded-lg transition duration-75 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700")])])
(defn left-aside- [{:keys [nav page-specific]} & children]
[:aside {:id "left-nav", :class "fixed top-0 left-0 pt-16 z-20 w-64 h-screen transition-transform -translate-x-full lg:translate-x-0", :aria-labelledby "left-nav" :aria-hidden "true"
"_" (hiccup/raw "init call initSidebarToggle()")}
[:div {:class "overflow-y-auto py-5 px-3 h-full bg-gray-50 border-r border-gray-200 dark:bg-gray-800 dark:border-gray-700"}
nav
[:ul {:class "pt-5 mt-5 space-y-2 border-t border-gray-200 dark:border-gray-700"}
#_[:li
[:a {:href "#", :class "flex items-center p-2 text-base font-normal text-gray-900 rounded-lg transition duration-75 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white group"}
[:svg {:aria-hidden "true", :class "flex-shrink-0 w-6 h-6 text-gray-400 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:d "M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"}]
[:path {:fill-rule "evenodd", :d "M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z", :clip-rule "evenodd"}]]
[:span {:class "ml-3"} "Docs"]]]
#_[:li
[:a {:href "#", :class "flex items-center p-2 text-base font-normal text-gray-900 rounded-lg transition duration-75 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white group"}
[:svg {:aria-hidden "true", :class "flex-shrink-0 w-6 h-6 text-gray-400 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:d "M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"}]]
[:span {:class "ml-3"} "Components"]]]
#_[:li
[:a {:href "#", :class "flex items-center p-2 text-base font-normal text-gray-900 rounded-lg transition duration-75 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white group"}
[:svg {:aria-hidden "true", :class "flex-shrink-0 w-6 h-6 text-gray-400 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:fill-rule "evenodd", :d "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-2 0c0 .993-.241 1.929-.668 2.754l-1.524-1.525a3.997 3.997 0 00.078-2.183l1.562-1.562C15.802 8.249 16 9.1 16 10zm-5.165 3.913l1.58 1.58A5.98 5.98 0 0110 16a5.976 5.976 0 01-2.516-.552l1.562-1.562a4.006 4.006 0 001.789.027zm-4.677-2.796a4.002 4.002 0 01-.041-2.08l-.08.08-1.53-1.533A5.98 5.98 0 004 10c0 .954.223 1.856.619 2.657l1.54-1.54zm1.088-6.45A5.974 5.974 0 0110 4c.954 0 1.856.223 2.657.619l-1.54 1.54a4.002 4.002 0 00-2.346.033L7.246 4.668zM12 10a2 2 0 11-4 0 2 2 0 014 0z", :clip-rule "evenodd"}]]
[:span {:class "ml-3"} "Help"]]]]
page-specific]
#_[:div {:class "hidden absolute bottom-0 left-0 justify-center p-4 space-x-4 w-full lg:flex bg-white dark:bg-gray-800 z-20 border-r border-gray-200 dark:border-gray-700"}
[:a {:href "#", :class "inline-flex justify-center p-2 text-gray-500 rounded cursor-pointer dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600"}
[:svg {:aria-hidden "true", :class "w-6 h-6", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:d "M5 4a1 1 0 00-2 0v7.268a2 2 0 000 3.464V16a1 1 0 102 0v-1.268a2 2 0 000-3.464V4zM11 4a1 1 0 10-2 0v1.268a2 2 0 000 3.464V16a1 1 0 102 0V8.732a2 2 0 000-3.464V4zM16 3a1 1 0 011 1v7.268a2 2 0 010 3.464V16a1 1 0 11-2 0v-1.268a2 2 0 010-3.464V4a1 1 0 011-1z"}]]]
[:a {:href "#", :data-tooltip-target "tooltip-settings", :class "inline-flex justify-center p-2 text-gray-500 rounded cursor-pointer dark:text-gray-400 dark:hover:text-white hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600"}
[:svg {:aria-hidden "true", :class "w-6 h-6", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:fill-rule "evenodd", :d "M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z", :clip-rule "evenodd"}]]]
[:div {:id "tooltip-settings", :role "tooltip", :class "inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip"} "Settings page"]
[:button {:type "button", :data-dropdown-toggle "language-dropdown", :class "inline-flex justify-center p-2 text-gray-500 rounded cursor-pointer dark:hover:text-white dark:text-gray-400 hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600"}
[:svg {:aria-hidden "true", :class "h-5 w-5 rounded-full mt-0.5", :xmlns "http://www.w3.org/2000/svg", :xmlns:xlink "http://www.w3.org/1999/xlink", :viewbox "0 0 3900 3900"}
[:path {:fill "#b22234", :d "M0 0h7410v3900H0z"}]
[:path {:d "M0 450h7410m0 600H0m0 600h7410m0 600H0m0 600h7410m0 600H0", :stroke "#fff", :stroke-width "300"}]
[:path {:fill "#3c3b6e", :d "M0 0h2964v2100H0z"}]
[:g {:fill "#fff"}
[:g {:id "d"}
[:g {:id "c"}
[:g {:id "e"}
[:g {:id "b"}
[:path {:id "a", :d "M247 90l70.534 217.082-184.66-134.164h228.253L176.466 307.082z"}]
[:use {:xlink:href "#a", :y "420"}]
[:use {:xlink:href "#a", :y "840"}]
[:use {:xlink:href "#a", :y "1260"}]]
[:use {:xlink:href "#a", :y "1680"}]]
[:use {:xlink:href "#b", :x "247", :y "210"}]]
[:use {:xlink:href "#c", :x "494"}]]
[:use {:xlink:href "#d", :x "988"}]
[:use {:xlink:href "#c", :x "1976"}]
[:use {:xlink:href "#e", :x "2470"}]]]]
[:div {:class "hidden z-50 my-4 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700", :id "language-dropdown"}
[:ul {:class "py-1", :role "none"}
[:li
[:a {:href "#", :class "block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600", :role "menuitem"}
[:div {:class "inline-flex items-center"}
[:svg {:aria-hidden "true", :class "h-3.5 w-3.5 rounded-full mr-2", :xmlns "http://www.w3.org/2000/svg", :id "flag-icon-css-us", :viewbox "0 0 512 512"}
[:g {:fill-rule "evenodd"}
[:g {:stroke-width "1pt"}
[:path {:fill "#bd3d44", :d "M0 0h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0z", :transform "scale(3.9385)"}]
[:path {:fill "#fff", :d "M0 10h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0z", :transform "scale(3.9385)"}]]
[:path {:fill "#192f5d", :d "M0 0h98.8v70H0z", :transform "scale(3.9385)"}]
[:path {:fill "#fff", :d "M8.2 3l1 2.8H12L9.7 7.5l.9 2.7-2.4-1.7L6 10.2l.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7L74 8.5l-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 7.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm-74.1 7l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7H65zm16.4 0l1 2.8H86l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm-74 7l.8 2.8h3l-2.4 1.7.9 2.7-2.4-1.7L6 24.2l.9-2.7-2.4-1.7h3zm16.4 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 21.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm-74.1 7l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7H65zm16.4 0l1 2.8H86l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm-74 7l.8 2.8h3l-2.4 1.7.9 2.7-2.4-1.7L6 38.2l.9-2.7-2.4-1.7h3zm16.4 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 35.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm-74.1 7l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7H65zm16.4 0l1 2.8H86l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm-74 7l.8 2.8h3l-2.4 1.7.9 2.7-2.4-1.7L6 52.2l.9-2.7-2.4-1.7h3zm16.4 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 49.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm-74.1 7l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7H65zm16.4 0l1 2.8H86l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm-74 7l.8 2.8h3l-2.4 1.7.9 2.7-2.4-1.7L6 66.2l.9-2.7-2.4-1.7h3zm16.4 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 63.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9z", :transform "scale(3.9385)"}]]] " \n English (US)"]]]
[:li
[:a {:href "#", :class "block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-gray-600", :role "menuitem"}
[:div {:class "inline-flex items-center"}
[:svg {:aria-hidden "true", :class "h-3.5 w-3.5 rounded-full mr-2", :xmlns "http://www.w3.org/2000/svg", :id "flag-icon-css-de", :viewbox "0 0 512 512"}
[:path {:fill "#ffce00", :d "M0 341.3h512V512H0z"}]
[:path {:d "M0 0h512v170.7H0z"}]
[:path {:fill "#d00", :d "M0 170.7h512v170.6H0z"}]]]]]
[:li
[:a {:href "#", :class "block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-gray-600", :role "menuitem"}
[:div {:class "inline-flex items-center"}
[:svg {:aria-hidden "true", :class "h-3.5 w-3.5 rounded-full mr-2", :xmlns "http://www.w3.org/2000/svg", :id "flag-icon-css-it", :viewbox "0 0 512 512"}
[:g {:fill-rule "evenodd", :stroke-width "1pt"}
[:path {:fill "#fff", :d "M0 0h512v512H0z"}]
[:path {:fill "#009246", :d "M0 0h170.7v512H0z"}]
[:path {:fill "#ce2b37", :d "M341.3 0H512v512H341.3z"}]]]]]]
[:li
[:a {:href "#", :class "block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600", :role "menuitem"}
[:div {:class "inline-flex items-center"}
[:svg {:aria-hidden "true", :class "h-3.5 w-3.5 rounded-full mr-2", :xmlns "http://www.w3.org/2000/svg", :xmlns:xlink "http://www.w3.org/1999/xlink", :id "flag-icon-css-cn", :viewbox "0 0 512 512"}
[:defs
[:path {:id "a", :fill "#ffde00", :d "M1-.3L-.7.8 0-1 .6.8-1-.3z"}]]
[:path {:fill "#de2910", :d "M0 0h512v512H0z"}]
[:use {:width "30", :height "20", :transform "matrix(76.8 0 0 76.8 128 128)", :xlink:href "#a"}]
[:use {:width "30", :height "20", :transform "rotate(-121 142.6 -47) scale(25.5827)", :xlink:href "#a"}]
[:use {:width "30", :height "20", :transform "rotate(-98.1 198 -82) scale(25.6)", :xlink:href "#a"}]
[:use {:width "30", :height "20", :transform "rotate(-74 272.4 -114) scale(25.6137)", :xlink:href "#a"}]
[:use {:width "30", :height "20", :transform "matrix(16 -19.968 19.968 16 256 230.4)", :xlink:href "#a"}]] "中文 (繁體)"]]]]]]
[:script {:lang "text/javascript"}
(hiccup/raw "
function initSidebarToggle() {
var $targetEl = document.getElementById('left-nav');
var $triggerEl = document.getElementById('left-nav-toggle');
var options = {
onCollapse: () => {
document.getElementById('main-content').classList.remove('lg:pl-64')
},
onExpand: () => {
document.getElementById('main-content').classList.add('lg:pl-64')
},
onToggle: () => {
}
};
var collapse = new Collapse($targetEl, $triggerEl, options);
}
")]])
(defn main-aside-nav- []
[:ul {:class "space-y-2"}
[:li
(menu-button- {:icon svg/pie
:href "/"}
"Dashboard")]
[:li
(menu-button- {:aria-controls "dropdown-invoices",
:data-collapse-toggle "dropdown-invoices"
:icon svg/accounting-invoice-mail}
"Invoices")
(sub-menu- {:id "dropdown-invoices"}
(menu-button- {:href (bidi/path-for client-routes/routes
:invoices)}
"All")
(menu-button- {:href (bidi/path-for client-routes/routes
:paid-invoices)}
"Paid")
(menu-button- {:href (bidi/path-for client-routes/routes
:unpaid-invoices)}
"Unpaid")
(menu-button- {:href (bidi/path-for client-routes/routes
:voided-invoices)}
"Voided"))]
[:li
(menu-button- {:aria-controls "dropdown-sales",
:data-collapse-toggle "dropdown-sales"
:icon svg/receipt-register-1}
"Sales")
(sub-menu- {:id "dropdown-sales"}
(menu-button- {:href (bidi/path-for client-routes/routes
:sales-orders)} "Sales")
(menu-button- {:href (bidi/path-for client-routes/routes
:expected-deposits)} "Expected Deposits")
#_(menu-button- {:href "Sales"} "Cash Shifts")
#_(menu-button- {:href "Sales"} "Tenders"))]
[:li
(menu-button- {:aria-controls "dropdown-payments"
:data-collapse-toggle "dropdown-payments"
:icon svg/payments}
"Payments")
(sub-menu- {:id "dropdown-payments"}
(menu-button- {:href (bidi/path-for client-routes/routes
:payments)} "All")
(menu-button- {:href (bidi/path-for client-routes/routes
:payments)} "Pending")
(menu-button- {:href (bidi/path-for client-routes/routes
:payments)} "Cleared")
(menu-button- {:href (bidi/path-for client-routes/routes
:payments)} "Voided"))]
[:li
(menu-button- {:aria-controls "dropdown-transactions"
:data-collapse-toggle "dropdown-transactions"
:icon svg/bank}
"Transactions")
(sub-menu- {:id "dropdown-transactions"}
(menu-button- {:href (bidi/path-for client-routes/routes
:transactions)} "All")
(menu-button- {:href (bidi/path-for client-routes/routes
:unapproved-transactions)} "Unapproved")
(menu-button- {:href (bidi/path-for client-routes/routes
:requires-feedback-transactions)} "Client Review")
(menu-button- {:href (bidi/path-for client-routes/routes
:approved-transactions)} "Approved")
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
:transaction-insights)} "Insights"))]
[:li
(menu-button- {:aria-controls "dropdown-ledger"
:data-collapse-toggle "dropdown-ledger"
:icon svg/receipt}
"Ledger")
(sub-menu- {:id "dropdown-ledger"}
(menu-button- {:href (bidi/path-for client-routes/routes
:ledger)} "Register")
(menu-button- {:href (bidi/path-for client-routes/routes
:profit-and-loss)} "Profit & Loss")
(menu-button- {:href (bidi/path-for client-routes/routes
:profit-and-loss-detail)} "Profit & Loss Detail")
(menu-button- {:href (bidi/path-for client-routes/routes
:cash-flows)} "Cash Flows")
(menu-button- {:href (bidi/path-for client-routes/routes
:balance-sheet)} "Balance Sheet")
(menu-button- {:href (bidi/path-for client-routes/routes
:external-import-ledger)} "External Ledger Import"))]])
(defn company-aside-nav- []
[:ul {:class "space-y-2" :hx-boost "true"}
[:li
(menu-button- {:icon svg/vendors
:href (bidi/path-for ssr-routes/only-routes
:company)}
"My Company")]
[:li
(menu-button- {:icon svg/report
:href (bidi/path-for ssr-routes/only-routes
:company-reports)}
"Reports")]
[:li
(menu-button- {:icon svg/bank
:href (bidi/path-for ssr-routes/only-routes
:company-yodlee)}
"Yodlee Link")]
[:li
(menu-button- {:icon svg/government-building
:href (bidi/path-for ssr-routes/only-routes
:company-1099)}
"1099 Vendor Info"
)]])
(defn admin-aside-nav- []
[:ul {:class "space-y-2"}
[:li
(menu-button- {:icon svg/dashboard
:href (bidi/path-for client-routes/routes
:admin)}
"Dashboard")]
[:li
(menu-button- {:icon svg/restaurant
:href (bidi/path-for client-routes/routes
:admin-clients)}
"Clients")]
[:li
(menu-button- {:icon svg/vendors
:href (bidi/path-for client-routes/routes
:admin-vendors)}
"Vendors")]
[:li
(menu-button- {:icon svg/user
:href (bidi/path-for client-routes/routes
:admin-users)}
"Users")]
[:li
(menu-button- {:icon svg/accounts
:href (bidi/path-for client-routes/routes
:admin-accounts)}
"Accounts")]
[:li
(menu-button- {:icon svg/cog
:href (bidi/path-for client-routes/routes
:admin-rules)}
"Rules")]
[:li
(menu-button- {:icon svg/question
:href (bidi/path-for ssr-routes/only-routes
:admin-history)
:hx-boost "true"}
"History")]
[:li
(menu-button- {:icon svg/rabbit
:href (bidi/path-for client-routes/routes
:admin-jobs)}
"Background Jobs")]
[:li
(menu-button- {:aria-controls "dropdown-import"
:data-collapse-toggle "dropdown-import"
:icon svg/arrow-in}
"Import")
(sub-menu- {:id "dropdown-import"}
(menu-button- {:href (bidi/path-for client-routes/routes
:admin-excel-import)} "Excel Invoices")
(menu-button- {:href (bidi/path-for client-routes/routes
:admin-import-batches)} "Import Batches")
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
:admin-ezcater-xls)
:hx-boost "true"} "EZCater XLS Import"))]])

View File

@@ -0,0 +1,22 @@
(ns auto-ap.ssr.components.breadcrumbs
(:require [auto-ap.ssr.svg :as svg]))
(defn breadcrumbs- [params & steps]
[:div {:class "inline-flex px-5 py-3 text-gray-700 border border-gray-200 rounded-lg bg-white dark:bg-gray-800 dark:border-gray-700"}
[:ul {:class "inline-flex items-center space-x-1 md:space-x-3"}
[:li {:class "inline-flex items-center"}
[:a {:href "#", :class "inline-flex w-4 h-4 mr-2 items-center text-sm font-medium text-gray-700 hover:text-blue-600 dark:text-gray-400 dark:hover:text-white"}
[:div.w-4.h-4 svg/home]]]
(for [p steps]
[:li
[:div {:class "flex items-center"}
[:div {:class "w-6 h-6 text-gray-400",}
svg/breadcrumb-component]
(update-in p [1 :class] str " ml-1 text-sm font-medium text-gray-700 hover:text-blue-600 md:ml-2 dark:text-gray-400 dark:hover:text-white")]])
#_[:li {:aria-current "page"}
[:div {:class "flex items-center"}
[:svg {:aria-hidden "true", :class "w-6 h-6 text-gray-400", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:fill-rule "evenodd", :d "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", :clip-rule "evenodd"}]]
[:span {:class "ml-1 text-sm font-medium text-gray-500 md:ml-2 dark:text-gray-400"} "Flowbite"]]]]])

View File

@@ -0,0 +1,46 @@
(ns auto-ap.ssr.components.buttons
(:require [auto-ap.ssr.svg :as svg]))
(defn button-icon- [_ i]
[:div.h-4.w-4 i])
(defn button- [params & children]
[: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..."]]
(into [:div.htmx-indicator-hidden.inline-flex.gap-2.items-center.justify-center ] children)])
(defn icon-button- [params & children]
(into
[:button
(update params :class
#(cond-> %
true (str " inline-flex items-center justify-center items-center p-3 text-sm font-medium border border-gray-300 dark:border-gray-700 text-center rounded-lg ")
(= :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 ")
(= :primary-light (:color params)) (str " bg-green-200 hover:bg-green-300 focus:ring-green-200 dark:bg-green-700 dark:hover:bg-green-600 text-gray-800 dark:text-gray-200")
(= :secondary-light (:color params)) (str " bg-blue-200 hover:bg-blue-300 focus:ring-blue-200 dark:bg-blue-700 dark:hover:bg-blue-600 text-gray-800 dark:text-gray-200")
(= :danger-light (:color params)) (str " bg-red-200 hover:bg-red-300 focus:ring-red-200 dark:bg-red-700 dark:hover:bg-red-600 text-gray-800 dark:text-gray-200")
(nil? (:color params))
(str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100")))
[: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]
[:button { :class "text-white bg-green-500 hover:bg-green-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800 inline-flex items-center hover:scale-105 transition duration-300"}
[:div.htmx-indicator.flex.items-center
(svg/spinner {:class "inline w-4 h-4 text-white"})
[:div.ml-3 "Loading..."]]
(into [:div.htmx-indicator-hidden ] children)])

View File

@@ -0,0 +1,12 @@
(ns auto-ap.ssr.components.card)
(defn card- [params & children]
(into [:div (update params :class str " shadow-md dark:bg-gray-800 sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 bg-white overflow-hidden")]
children))
(defn content-card- [params & children]
[:section {:class " py-3 sm:py-5"}
[:div {:class "max-w-screen-2xl"}
(into
[:div {:class "relative overflow-hidden shadow-md dark:bg-gray-800 sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 bg-white"}]
children)]])

View File

@@ -0,0 +1,100 @@
(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)
"_" (hiccup/raw (when (:sort-key params ) (format "on click trigger sorted(key:\"%s\")", (:sort-key params))))
:style (:style params)}]
(if (:sort-key params)
[(into [:a {:href "#"} ] rest)]
rest)))
(defn sort-header- [params & rest]
[:th.px-4.py-3 {:scope "col" :class (:class params)
"_" (hiccup/raw (format "on click trigger sorted(key:\"%s\")", (:sort-key params)))}
(into [:a {:href "#"} ] rest)])
(defn row- [params & rest]
(into [:tr (update params
: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 params ] rest))
(defn right-stack-cell- [params & rest]
(cell- params (into [:div.flex.flex-row-reverse.items-center.justify-between
rest]))
)
(defn checkbox-header- [params & rest]
[:th {:scope "col", :class "p-4"}
[:div {:class "flex items-center"}
[: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 thead-params]} & rest]
[:table {:class "w-full text-sm text-left text-gray-500 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)])
;; needed for tailwind
;; lg:table-cell md:table-cell
(defn data-grid-card- [{:keys [id
route
title
paginate?
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)]
(when (or paginate?
(nil? paginate?))
(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

@@ -0,0 +1,44 @@
(ns auto-ap.ssr.components.dialog
(:require [hiccup2.core :as hiccup]))
(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 params
[: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]]
])

View File

@@ -0,0 +1,24 @@
(ns auto-ap.ssr.components.inputs)
(defn select- [params & children]
(into
[:select {:class "bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
:name (:name params)}
(cond->>
(map (fn [[k v]]
[:option {:value k :selected (= v (:value params))} v])
(:options params))
(:allow-blank? params) (conj [:option {:value "" :selected (not (:value params))} ""]))]
children))
(defn text-input- [params]
[:input
(update params
:class str " bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500")
])
(defn field- [params & rest]
(into
[:div
[:label {:class "block mb-2 text-sm font-medium text-gray-900 dark:text-white"} (:label params)]]
rest))

View File

@@ -0,0 +1,45 @@
(ns auto-ap.ssr.components.navbar
(:require
[auto-ap.graphql.utils :refer [is-admin?]]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.company-dropdown :as cd]
[auto-ap.ssr.components.buttons :refer [icon-button-]]
[auto-ap.ssr.components.user-dropdown :as user-dropdown]
[auto-ap.ssr.svg :as svg]
[bidi.bidi :as bidi]))
(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"}
[:div {:class "flex items-center justify-start"}
[:button {:aria-controls "left-nav", :id "left-nav-toggle" :type "button", :class "inline-flex items-center p-2 mt-2 ml-2 mr-2 text-sm text-gray-500 rounded-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"}
[:span {:class "sr-only"} "Open sidebar"]
[:svg {:class "w-6 h-6", :aria-hidden "true", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:clip-rule "evenodd", :fill-rule "evenodd", :d "M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z"}]]]
[:a {:href "/" :class "flex ml-2 md:mr-24"}
[:img {:src "/img/logo-big2.png", :class "h-10 mr-16", :alt "Integreat logo"}]]]
[:div {:class "flex items-center gap-4"}
(when (is-admin? identity)
[: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 "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-holder"
:hx-swap "outerHTML"}
svg/search)
(cd/dropdown {:client client :identity identity})
(user-dropdown/dropdown {:identity identity})]]]])

View File

@@ -1,14 +0,0 @@
(ns auto-ap.ssr.components.navbar-dropdown
(:require [hiccup2.core :as hiccup]))
(defn navbar-dropdown [id header children]
[:div {
:id id
:class (str "navbar-item has-dropdown")
"_" (hiccup/raw "on click elsewhere remove .is-active from me")
}
[:a {:class "navbar-link login"
"_" (hiccup/raw "on click toggle .is-active on the parentElement of me then add .appear to next <.navbar-dropdown />")} header]
(into [:div {:class "navbar-dropdown"}
children])])

View File

@@ -0,0 +1,36 @@
(ns auto-ap.ssr.components.page
(: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 active-client identity app-params] :or {app-params {}}} & children]
[:div#app
(navbar- {:client active-client
:identity identity})
[:div#app-contents.flex.pt-16.overflow-hidden (assoc app-params :hx-disinherit "*")
(left-aside- {:nav nav
:page-specific page-specific})
[:div#main-content {:class "relative w-full h-full lg:pl-64 overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content "
"_" (hiccup/raw "on htmx:responseError put event.detail.xhr.response into #error-details then add .htmx-added to #error-holder then remove .hidden from #error-holder then wait 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]])

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

@@ -0,0 +1,20 @@
(ns auto-ap.ssr.components.tags)
(defn pill- [params & children]
(into
[:span (cond-> params
true (update :class str " text-xs font-medium px-2 py-0.5 rounded whitespace-nowrap")
(= :primary (:color params))
(update :class str " bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300 ")
(= :secondary (:color params))
(update :class str " bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300 ")
(= :yellow (:color params))
(update :class str " bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300")
(= :red (:color params))
(update :class str " bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300"))]
children))

View File

@@ -0,0 +1,61 @@
(ns auto-ap.ssr.components.user-dropdown
(:require
[auto-ap.client-routes :as client-routes2]
[auto-ap.datomic :refer [conn pull-attr]]
[auto-ap.ssr-routes :as ssr-routes]
[bidi.bidi :as bidi]
[datomic.api :as dc]
[hiccup2.core :as hiccup]))
(defn dropdown [{:keys [identity]}]
[:div {:class "flex items-center ml-3 mr-10"}
[:div
[:button#user-menu-button {:type "button", :class "flex text-sm bg-gray-800 rounded-full focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600", :aria-expanded "false"
"_" (hiccup/raw "init call initUserDropdown()")}
[:span {:class "sr-only"} "Open user menu"]
[:img {:class "w-8 h-8 rounded-full", :src (pull-attr (dc/db conn) :user/profile-image-url (:db/id identity)) :alt "user photo" :referrerpolicy "no-referrer"}]]]
[:div#user-menu {:class "z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded shadow dark:bg-gray-700 dark:divide-gray-600 mr-10"}
[:div {:class "px-4 py-3", :role "none"}
[:p {:class "text-sm text-gray-900 dark:text-white", :role "none"} (:user/name identity)]
[:p {:class "text-sm font-medium text-gray-900 truncate dark:text-gray-300", :role "none"} (pull-attr (dc/db conn) :user/email (:db/id identity))]
#_(icon-button-
{"_" (hiccup/raw "on click toggle .dark on <body />")}
[:div.h-4.w-4
[:div.hidden.dark:block svg/sun]
[:div.dark:hidden svg/moon]])]
[:ul {:class "py-1", :role "none"}
[:li
[:a {:href (bidi/path-for ssr-routes/only-routes :company), :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "My Company"]]
(when (= "admin" (:user/role identity))
[:a {:href (bidi/path-for client-routes2/routes :admin), :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "Admin"])
[:li
[:a {:href "#", :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"
"_" (hiccup/raw "on click toggle .dark on <body />")}
"Night Mode"]]
[:li
[:a {:href "/logout", :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "Sign out"]]]]
[:script {:lang "text/javascript"}
(hiccup/raw
"
function initUserDropdown() {
var $dropdownTargetEl = document.getElementById('user-menu');
// set the element that trigger the dropdown menu on click
var $dropdownTriggerEl = document.getElementById('user-menu-button');
var userDrowdown = new Dropdown($dropdownTargetEl, $dropdownTriggerEl, {
placement: 'bottom',
triggerType: 'click',
offsetSkidding: 0,
offsetDistance: 10,
delay: 5000,
onHide: () => {
},
onShow: () => {
},
onToggle: () => {
}
});
}
")]])

View File

@@ -2,32 +2,45 @@
(:require (:require
[auto-ap.routes.utils [auto-ap.routes.utils
:refer [wrap-admin wrap-client-redirect-unauthenticated wrap-secure]] :refer [wrap-admin wrap-client-redirect-unauthenticated wrap-secure]]
[auto-ap.ssr.admin :as admin] [auto-ap.ssr.admin.history :as history]
[auto-ap.ssr.auth :as auth] [auto-ap.ssr.auth :as auth]
[auto-ap.ssr.transaction.insights :as insights] [auto-ap.ssr.transaction.insights :as insights]
[auto-ap.ssr.company.company-1099 :as company-1099] [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.search :as search]
[auto-ap.ssr.company-dropdown :as company-dropdown] [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 ;; from auto-ap.ssr-routes, because they're shared
(def key->handler {:logout auth/logout (def key->handler {:logout auth/logout
:admin-history (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin admin/history))) :admin-history (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page)))
:admin-history-search (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin admin/history-search))) :admin-history-search (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page)))
:admin-history-inspect (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin admin/inspect))) :admin-history-inspect (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/inspect)))
:active-client (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin company-dropdown/active-client))) :active-client (wrap-client-redirect-unauthenticated (wrap-secure (wrap-secure 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 (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-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-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-1099-vendor-save (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-save))
:transaction-insights (wrap-client-redirect-unauthenticated (wrap-secure insights/page)) :company-yodlee (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/page))
:transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-secure insights/insight-table)) :company-yodlee-table (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/table))
:transaction-insight-rows (wrap-client-redirect-unauthenticated (wrap-secure insights/transaction-rows)) :company-yodlee-fastlink-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/fastlink-dialog))
:transaction-insight-approve (wrap-client-redirect-unauthenticated (wrap-secure insights/approve)) :company-yodlee-provider-account-refresh (wrap-client-redirect-unauthenticated (wrap-admin company-yodlee/refresh-provider-account))
:transaction-insight-explain (wrap-client-redirect-unauthenticated (wrap-secure insights/explain)) :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-admin insights/page))
:transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-admin insights/insight-table))
:transaction-insight-rows (wrap-client-redirect-unauthenticated (wrap-admin insights/transaction-rows))
:transaction-insight-approve (wrap-client-redirect-unauthenticated (wrap-admin insights/approve))
:transaction-insight-disapprove (wrap-client-redirect-unauthenticated (wrap-admin insights/disapprove))
:transaction-insight-explain (wrap-client-redirect-unauthenticated (wrap-admin insights/explain))
:admin-ezcater-xls (wrap-client-redirect-unauthenticated (wrap-admin ezcater-xls/page)) :admin-ezcater-xls (wrap-client-redirect-unauthenticated (wrap-admin ezcater-xls/page))
:search (wrap-client-redirect-unauthenticated (wrap-secure search/dialog-contents))}) :search (wrap-client-redirect-unauthenticated (wrap-secure search/dialog-contents))})

View File

@@ -0,0 +1,186 @@
(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 {:class (if-let [show-starting (:show-starting header)]
(format "hidden %s:table-cell" show-starting)
(:class header))}
((: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"))
:data-id ((:id-fn gridspec) entity)}
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 request]}]
(let [start (or start 0)
per-page (or per-page 30)
[entities total] ((:fetch-page grid-spec)
user
{:start start
:per-page per-page
:client-id (:db/id client)
:sort sort
:request request})]
(com/data-grid-card {:id (:id grid-spec)
:title (:title grid-spec)
:route (:route grid-spec)
:start start
:per-page per-page
:total total
:subtitle [:div.flex.items-center.gap-2
[:span (format "Total %s: %d, " (:entity-name grid-spec) total)]
(sort-by-list sort)]
:action-buttons ((:action-buttons grid-spec) user)
: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)))
(:title grid-spec)))

View File

@@ -1,26 +0,0 @@
(ns auto-ap.ssr.login-dropdown
(:require
[auto-ap.client-routes :as client-routes]
[auto-ap.ssr.components.navbar-dropdown :refer [navbar-dropdown]]
[bidi.bidi :as bidi]
[hiccup2.core :as hiccup]
[auto-ap.ssr-routes :as ssr-routes]))
(defn dropdown [{:keys [identity]}]
(if identity
(navbar-dropdown
"login-dropdown"
[:span [:span.icon [:i.fa.fa-user] ]
[:span (:user/name identity)]]
[:div
[:a {:class "navbar-item"
:href (bidi/path-for client-routes/routes :reports)} "My company"]
#_[:a.dropdown-item {:on-click (dispatch-event-with-propagation [:vendor-dialog/started {}])} "New Vendor"] ;; double colorn
#_[:a.dropdown-item {:on-click (dispatch-event-with-propagation [:vendor-dialog/edit {}])} "Edit Vendor"]
(when (= "admin" (:user/role identity))
[:a {:class "navbar-item" :href (bidi/path-for client-routes/routes :admin)} "Administration"])
[:hr {:class "navbar-divider"}]
[:a.navbar-item {"_" (hiccup/raw "on click call localStorage.removeItem(\"jwt\")")
:href (bidi/path-for ssr-routes/only-routes :logout)}
"Logout"]])
[:a.navbar-item {:href (bidi/path-for client-routes/routes :login )} "Login"]))

View File

@@ -5,7 +5,9 @@
[auto-ap.ssr.utils :refer [html-response]] [auto-ap.ssr.utils :refer [html-response]]
[auto-ap.time :as atime] [auto-ap.time :as atime]
[clojure.string :as str] [clojure.string :as str]
[com.brunobonacci.mulog :as mu])) [com.brunobonacci.mulog :as mu]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.svg :as svg]))
(defn try-cleanse-date [d] (defn try-cleanse-date [d]
(try (try
@@ -61,58 +63,65 @@
(let [results (search-results q id)] (let [results (search-results q id)]
[:div [:div
(if (seq results) (if (seq results)
(for [doc results] [:div.flex.gap-8.flex-col
[:div.block (for [doc results]
[:div.card (com/card {}
[:div.card-header.has-background-info-light [:div.flex.flex-col.gap-4
(cond (= "transaction" (:type doc)) [:div.flex.items-center.p-2.gap-4.bg-gray-50.dark:bg-gray-800
[:div.card-header-icon.icon-task-list-text-1] [:div.h-8.w-8.p-2
(cond (= "transaction" (:type doc))
svg/bank
(= "invoice" (:type doc))
[:div.card-header-icon.icon-accounting-invoice-mail]
(= "payment" (:type doc)) (= "invoice" (:type doc))
[:div.card-header-icon.icon-check-payment-sign] svg/accounting-invoice-mail
(= "journal-entry" (:type doc))
[:div.card-header-icon.icon-receipt]
:else (= "payment" (:type doc))
nil) svg/payments
[:div.card-header-title (clojure.string/capitalize (:type doc))
" " (= "journal-entry" (:type doc))
" " svg/receipt
[:span.tags.ml-3
[:span.tag.is-warning "client: " (:client_code doc)] :else
[:span.tag.is-info "amount: $" (first (:amount doc))] nil)]
(clojure.string/capitalize (:type doc))
(com/pill {:color :primary}
"client: " (:client_code doc))
(com/pill {:color :secondary}
"amount: $" (first (:amount doc)))
(when-let [vendor-name (first (:vendor_name doc))] (when-let [vendor-name (first (:vendor_name doc))]
[:span.tag.is-primary "vendor: " vendor-name])]] (com/pill {:color :yellow}
[:a.card-header-icon.fa.fa-external-link {:href (str "/" (cond (= "invoice" "vendor: " vendor-name))
(:type doc)) [:div
"invoices" (com/link {:href (str "/" (cond (= "invoice"
(:type doc))
"invoices"
(= "transaction" (= "transaction"
(:type doc)) (:type doc))
"transactions" "transactions"
(= "journal-entry" (= "journal-entry"
(:type doc)) (:type doc))
"ledger" "ledger"
:else :else
"payments") "/?exact-match-id=" (:id doc)) "payments") "/?exact-match-id=" (:id doc))
:target "_blank"}] :target "_blank"}
] [:div.h-8.w-8.p-2
svg/external-link])]
]
[:div.card-content [:div.px-4.pb-2
[:span [:span
[:strong (atime/unparse (atime/parse (:date doc) atime/solr-date) atime/normal-date)] [:strong (atime/unparse (atime/parse (:date doc) atime/solr-date) atime/normal-date)]
": " ": "
(str (or (first (:description doc)) (str (or (first (:description doc))
(first (:number doc))))]]]] (first (:number doc))))]]])
) )]
[:div.block "No results found."])])) [:div.block "No results found."])]))
(defn dialog-contents [request] (defn dialog-contents [request]
@@ -122,22 +131,26 @@
(if-let [q (get (:form-params request) "q")] (if-let [q (get (:form-params request) "q")]
(html-response (search-results* q (:identity request))) (html-response (search-results* q (:identity request)))
(html-response (html-response
[:div#search {:style {:height "400px" :overflow "auto"}} (com/modal {}
(com/modal-card {}
[:div.p-2 "Search"]
[:div#search.overflow-auto.space-y-6.p-2.h-96
[:div.block (com/text-input {:id "search-input"
[:input#search-input.input {:type "search" :type "search"
:placeholder "5/5/2034 Magheritas" :placeholder "5/5/2034 Magheritas"
:name "q" :name "q"
:hx-post "/search" :hx-post "/search"
:hx-trigger "keyup changed delay:300ms, search" :hx-trigger "keyup changed delay:300ms, search"
:hx-target "#search-results" :hx-target "#search-results"
:hx-indicator "#search" :hx-indicator "#search"
:value (:q (:params request)) :value (:q (:params request))
:autofocus true}]] :autofocus true})
[:i "Try dates, numbers, vendors. To filter to specific entity type, use 'invoice', 'transaction', 'journal-entry', 'payment'."] [: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 #_[:style
".htmx-request #search-results {display: none} .htmx-request .htmx-indicator { display: block !important; }"] ".htmx-request #search-results {display: none} .htmx-request .htmx-indicator { display: block !important; }"]
[:div#search-results [:div#search-results
] ]
[:div.loader.is-loading.big.htmx-indicator {:style {:display "none"}}]]))) [:div.loader.is-loading.big.htmx-indicator ]]
nil)))))

433
src/clj/auto_ap/ssr/svg.clj Normal file
View File

@@ -0,0 +1,433 @@
(ns auto-ap.ssr.svg)
(def pie
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "analytics-pie-2"]
[:path {:d "M13.5.75v9h9A9,9,0,0,0,13.5.75Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:path {:d "M10.5,14.25v-9a9,9,0,1,0,5.561,16.077Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:path {:d "M22.5,12.75h-9l5.561,7.077A8.986,8.986,0,0,0,22.5,12.75Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]])
(def accounting-invoice-mail
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "accounting-document"]
[:path {:d "M21.75,21.75a1.5,1.5,0,0,1-1.5,1.5H3.75a1.5,1.5,0,0,1-1.5-1.5V2.25A1.5,1.5,0,0,1,3.75.75H14.379a1.5,1.5,0,0,1,1.06.439l5.872,5.872a1.5,1.5,0,0,1,.439,1.06Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:path {:d "M21.75,8.25h-6a1.5,1.5,0,0,1-1.5-1.5v-6", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:path {:d "M6.2,11.594a2.224,2.224,0,0,0,1.858.875c1.139,0,2.063-.693,2.063-1.547S9.2,9.376,8.062,9.376,6,8.683,6,7.828s.924-1.547,2.062-1.547a2.221,2.221,0,0,1,1.858.875", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "12.469", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.062", :y2 "13.5", :x2 "8.062"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "5.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.062", :y2 "6.281", :x2 "8.062"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "15", :stroke-linecap "round", :stroke-width "1.5px", :x1 "12", :y2 "15", :x2 "18"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "19.5", :stroke-linecap "round", :stroke-width "1.5px", :x1 "6.75", :y2 "19.5", :x2 "18"}]])
(def receipt-register-1
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:g
[:path {:d "M17.63,18h3a1.5,1.5,0,0,1,1.5,1.5v2.25a1.5,1.5,0,0,1-1.5,1.5H3.38a1.5,1.5,0,0,1-1.5-1.5V18H7.13", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "17.03", :stroke-linecap "round", :stroke-width "1.5px", :x1 "11.45", :y2 "14.35", :x2 "8.71"}]
[:circle {:cx "12.48", :cy "18.11", :r "1.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:path {:d "M1.88,4.5H8.63L9.81,3.31A1.5,1.5,0,0,0,8.75.75H3.38a1.5,1.5,0,0,0-1.5,1.5V18", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "6.1", :stroke-linecap "round", :stroke-width "1.5px", :x1 "13.22", :y2 "6.59", :x2 "14.2"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "8.54", :stroke-linecap "round", :stroke-width "1.5px", :x1 "17.13", :y2 "9.25", :x2 "17.97"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "12.09", :stroke-linecap "round", :stroke-width "1.5px", :x1 "19.9", :y2 "13.03", :x2 "20.46"}]
[:path {:d "M8.63,4.5V7.74A12.22,12.22,0,0,1,18.92,18", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:circle {:cx "7.13", :cy "12.75", :r "2.25", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]]])
(def payments
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "cash-payment-sign-2"]
[:path {:d "M8.212,15.637l-3.712.53.53-3.712,9.546-9.546a2.25,2.25,0,1,1,3.182,3.182Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:path {:d "M5.25,8.25h-3a1.5,1.5,0,0,0-1.5,1.5v10.5a1.5,1.5,0,0,0,1.5,1.5h19.5a1.5,1.5,0,0,0,1.5-1.5V9.75a1.5,1.5,0,0,0-1.5-1.5h-1.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "18", :stroke-linecap "round", :stroke-width "1.5px", :x1 "20.25", :y2 "18", :x2 "12.75"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "14.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "20.25", :y2 "14.25", :x2 "15.75"}]])
(def bank
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:g
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "23.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "0.75", :y2 "23.25", :x2 "23.25"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "19.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "0.75", :y2 "19.25", :x2 "23.25"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "2", :y2 "16.25", :x2 "2"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "5.5", :y2 "16.25", :x2 "5.5"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "10.25", :y2 "16.25", :x2 "10.25"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "13.75", :y2 "16.25", :x2 "13.75"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "18.5", :y2 "16.25", :x2 "18.5"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "10.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "22", :y2 "16.25", :x2 "22"}]
[:path {:d "M23.25,7.25H.75L11.19,1a1.49,1.49,0,0,1,1.62,0Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]]])
(def receipt
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:g
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "21.75", :stroke-linecap "round", :stroke-width "1.5px", :x1 "17.25", :y2 "21.75", :x2 "11.25"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "13.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.75", :y2 "13.25", :x2 "15.25"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "9.75", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.75", :y2 "9.75", :x2 "15.25"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "6.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "8.75", :y2 "6.25", :x2 "15.25"}]
[:path {:d "M20.25.75h-1.5v5.5h4.5V3.75A3,3,0,0,0,20.25.75Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:path {:d "M13.5,17.25H3.75a3,3,0,0,0-3,3v3h10.5V19.5a2.25,2.25,0,0,1,4.5,0v.75a1.5,1.5,0,0,0,1.5,1.5h0a1.5,1.5,0,0,0,1.5-1.5V.75H8.25a3,3,0,0,0-3,3v13.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]]])
(defn spinner [{:keys [class]}]
[:svg {:aria-hidden "true", :role "status", :class (str "animate-spin " class) :viewbox "0 0 100 101", :fill "none", :xmlns "http://www.w3.org/2000/svg"}
[:path {:d "M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z", :fill "#E5E7EB"}]
[:path {:d "M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z", :fill "currentColor"}]])
(defn spinner-primary [{:keys [class]}]
[:svg {:aria-hidden "true", :role "status", :class (str "animate-spin " class) :viewbox "0 0 100 101", :fill "none", :xmlns "http://www.w3.org/2000/svg"}
[:path {:d "M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z", :fill "#79b52e"}]
[:path {:d "M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z", :fill "currentColor"}]])
(def search
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "search"]
[:circle {:r "9.063", :stroke "currentColor", :transform "translate(-3.056 4.62) rotate(-23.025)", :fill "none", :stroke-linejoin "round", :stroke-linecap "round", :stroke-width "1.5px", :cx "9.813", :cy "9.812"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "16.22", :stroke-linecap "round", :stroke-width "1.5px", :x1 "16.221", :y2 "23.25", :x2 "23.25"}]])
(def moon
[:svg {:id "theme-toggle-dark-icon", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:d "M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"}]])
(def sun
[:svg {:id "theme-toggle-light-icon", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:d "M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z", :fill-rule "evenodd", :clip-rule "evenodd"}]])
(def home
[:svg { :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:d "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"}]])
(def breadcrumb-component
[:svg { :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 refresh
[:svg {:xmlns "http://www.w3.org/2000/svg", :aria-hidden "true", :fill "none", :viewbox "0 0 24 24", :stroke-width "1.5", :stroke "currentColor"}
[:path {:stroke-linecap "round", :stroke-linejoin "round", :d "M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"}]])
(def upload
[:svg { :xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 24 24", :stroke-width "2", :stroke "currentColor", :aria-hidden "true"}
[:path {:stroke-linecap "round", :stroke-linejoin "round", :d "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"}]])
(def vendors
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "messages-people-user-dollar"]
[:circle {:cx "4.5", :cy "6.75", :r "3", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:path {:d "M8.25,17.25V15a3.75,3.75,0,0,0-7.5,0v2.25h1.5l.75,6H6l.75-6Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:path {:d "M17.625,5.25H15a1.341,1.341,0,0,0-.5,2.587l2.654.826a1.341,1.341,0,0,1-.5,2.587H13.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "5.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "15.75", :y2 "3.75", :x2 "15.75"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "12.75", :stroke-linecap "round", :stroke-width "1.5px", :x1 "15.75", :y2 "11.25", :x2 "15.75"}]
[:path {:d "M11.251,15.748h3v4.5l4.5-4.5h3a1.5,1.5,0,0,0,1.5-1.5v-12a1.5,1.5,0,0,0-1.5-1.5h-12a1.5,1.5,0,0,0-1.5,1.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]])
(def report
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "app-window-pie-chart"]
[:rect {:y "2.253", :rx "1.5", :stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "21", :stroke-linecap "round", :stroke-width "1.5px", :x "1.51", :ry "1.5", :height "19.5"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "6.753", :stroke-linecap "round", :stroke-width "1.5px", :x1 "1.51", :y2 "6.753", :x2 "22.51"}]
[:circle {:cx "9.01", :cy "14.253", :r "4.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:polyline {:points "9.01 9.753 9.01 14.253 12.192 17.435", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "11.253", :stroke-linecap "round", :stroke-width "1.5px", :x1 "16.51", :y2 "11.253", :x2 "19.51"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "14.253", :stroke-linecap "round", :stroke-width "1.5px", :x1 "16.51", :y2 "14.253", :x2 "19.51"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "17.253", :stroke-linecap "round", :stroke-width "1.5px", :x1 "16.51", :y2 "17.253", :x2 "19.51"}]])
(def government-building
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:g
[:rect {:y "14.25", :stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "3", :stroke-linecap "round", :stroke-width "1.5px", :x "3.5", :height "6"}]
[:rect {:y "14.25", :stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "3", :stroke-linecap "round", :stroke-width "1.5px", :x "10.5", :height "6"}]
[:rect {:y "14.25", :stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "3", :stroke-linecap "round", :stroke-width "1.5px", :x "17.5", :height "6"}]
[:path {:d "M21.75,13.39a.87.87,0,0,1-.86.86H3.11a.86.86,0,0,1-.25-1.69l8.85-2.72a1,1,0,0,1,.58,0l8.85,2.72A.87.87,0,0,1,21.75,13.39Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:polyline {:points "15.5 8.25 18 8.25 18 11.6", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:polyline {:points "6 11.6 6 8.25 8.5 8.25", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:path {:d "M6,8.25a6,6,0,0,1,12,0", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1.5px"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "0.75", :stroke-linecap "round", :stroke-width "1.5px", :x1 "12", :y2 "2.25", :x2 "12"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "23.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "0.75", :y2 "23.25", :x2 "23.25"}]
[:line {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :y1 "20.25", :stroke-linecap "round", :stroke-width "1.5px", :x1 "2", :y2 "20.25", :x2 "22"}]]])
(def external-link
[:svg
{:xmlns "http://www.w3.org/2000/svg", :viewBox "0 0 24 24"}
[:defs]
[:title "navigation-next"]
[:path
{:d "M23,9.5H12.387a4,4,0,0,0-4,4v2",
:fill "none",
:stroke "currentColor",
:stroke-linecap "round",
:stroke-linejoin "round"}]
[:polyline
{:points "19 13.498 23 9.498 19 5.498",
:fill "none",
:stroke "currentColor",
:stroke-linecap "round",
:stroke-linejoin "round"}]
[:path
{:d
"M14.387,13v5.5a1,1,0,0,1-1,1h-12a1,1,0,0,1-1-1V6.5a1,1,0,0,1,1-1h12a1,1,0,0,1,1,1V7",
:fill "none",
:stroke "currentColor",
:stroke-linecap "round",
:stroke-linejoin "round"}]])
(def pencil
[:svg
{:xmlns "http://www.w3.org/2000/svg", :viewBox "0 0 24 24"}
[:defs]
[:title "pencil"]
[:rect
{:y "1.09",
:stroke "currentColor",
:transform "translate(11.889 -5.238) rotate(45)",
:fill "none",
:stroke-linejoin "round",
:width "6",
:stroke-linecap "round",
:x "9.268",
:height "21.284"}]
[:polygon
{:points "2.621 17.136 0.5 23.5 6.864 21.379 2.621 17.136",
:fill "none",
:stroke "currentColor",
:stroke-linecap "round",
:stroke-linejoin "round"}]
[:path
{:d "M21.914,6.328,17.672,2.086l.707-.707a3,3,0,0,1,4.242,4.242Z",
:fill "none",
: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"}]])
(def dashboard
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "gauge-dashboard"]
[:circle {:cx "12", :cy "14", :r "1.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "13.06", :y1 "12.939", :x2 "18.011", :y2 "7.99", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "2.5", :y1 "14.5", :x2 "4.5", :y2 "14.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "21.5", :y1 "14.5", :x2 "19.5", :y2 "14.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "20.776", :y1 "10.365", :x2 "18.929", :y2 "11.13", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "15.635", :y1 "5.223", :x2 "14.87", :y2 "7.071", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "12", :y1 "4.5", :x2 "12", :y2 "6.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "8.365", :y1 "5.223", :x2 "9.13", :y2 "7.071", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "5.283", :y1 "7.282", :x2 "6.695", :y2 "8.697", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "3.224", :y1 "10.365", :x2 "5.07", :y2 "11.13", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M12,2.5A11.5,11.5,0,0,0,.5,14v3.5a1,1,0,0,0,1,1h21a1,1,0,0,0,1-1V14A11.5,11.5,0,0,0,12,2.5Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]])
(def restaurant
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:g
[:line {:x1 "1", :y1 "14.5", :x2 "23", :y2 "14.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "6", :y1 "17.5", :x2 "11", :y2 "17.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "7.5", :y1 "17.5", :x2 "6", :y2 "23.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "9.5", :y1 "17.5", :x2 "11", :y2 "23.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "6.25", :y1 "22.5", :x2 "10.75", :y2 "22.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "13", :y1 "17.5", :x2 "18", :y2 "17.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "14.5", :y1 "17.5", :x2 "13", :y2 "23.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "16.5", :y1 "17.5", :x2 "18", :y2 "23.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "13.25", :y1 "22.5", :x2 "17.75", :y2 "22.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "2.5", :y1 "23.5", :x2 "2.5", :y2 "7.33", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "21.5", :y1 "23.5", :x2 "21.5", :y2 "7.33", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M16,14.5V12a.5.5,0,0,1,.5-.5h1a.5.5,0,0,1,.5.5v2.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M7,14.5l-.64-1.28a.5.5,0,0,1,.45-.72h4.38a.5.5,0,0,1,.45.72L11,14.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M6.5,4H.5V1.5a1,1,0,0,1,1-1h5Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:rect {:x "6.5", :y "0.5", :width "5.5", :height "3.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:rect {:x "12", :y "0.5", :width "5.5", :height "3.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M23.5,4h-6V.5h5a1,1,0,0,1,1,1Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M23.5,4v.5a3,3,0,0,1-6,0V4", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M6.5,4v.5a3,3,0,0,1-6,0V4", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M17.5,4v.75a2.75,2.75,0,0,1-5.5,0V4", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M12,4v.75a2.75,2.75,0,0,1-5.5,0V4", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]]])
(def user
[:svg {:id "Light", :xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "single-man"]
[:path {:d "M3,22.75a9,9,0,0,1,18,0Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:circle {:cx "12", :cy "6.75", :r "5.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M7.261,3.958A9.124,9.124,0,0,0,13.833,6.75a9.138,9.138,0,0,0,3.617-.744", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]])
(def accounts
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "accounting-abacus"]
[:rect {:y "0.5", :rx "1", :stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "21", :stroke-linecap "round", :x "1.504", :ry "1", :height "23"}]
[:line {:x1 "15.504", :y1 "20.5", :x2 "15.504", :y2 "23.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "8.504", :y1 "17.5", :x2 "8.504", :y2 "23.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M10.5,4.5a1,1,0,0,1-1,1h-2a1,1,0,0,1-1-1h0a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M10.5,6.5a1,1,0,0,1-1,1h-2a1,1,0,0,1-1-1h0a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M17.5,13.5a1,1,0,0,1-1,1h-2a1,1,0,0,1-1-1h0a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M17.5,19.5a1,1,0,0,1-1,1h-2a1,1,0,0,1-1-1h0a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M10.5,16.5a1,1,0,0,1-1,1h-2a1,1,0,0,1-1-1h0a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M17.5,4.5a1,1,0,0,1-1,1h-2a1,1,0,0,1-1-1h0a1,1,0,0,1,1-1h2a1,1,0,0,1,1,1Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "8.504", :y1 "7.5", :x2 "8.504", :y2 "15.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "8.504", :y1 "3.5", :x2 "8.504", :y2 "0.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "15.504", :y1 "14.5", :x2 "15.504", :y2 "18.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "15.504", :y1 "5.5", :x2 "15.504", :y2 "12.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "15.504", :y1 "0.5", :x2 "15.504", :y2 "3.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]])
(def cog
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "cash-toggle"]
[:path {:d "M19.923,7.212a1.874,1.874,0,0,0,1.065,2.571l1.265.45a1.875,1.875,0,0,1,0,3.534l-1.265.45a1.874,1.874,0,0,0-1.065,2.571L20.5,18A1.874,1.874,0,0,1,18,20.5l-1.213-.576a1.874,1.874,0,0,0-2.571,1.065l-.45,1.265a1.875,1.875,0,0,1-3.534,0l-.45-1.265a1.874,1.874,0,0,0-2.571-1.065L6,20.5A1.874,1.874,0,0,1,3.5,18l.576-1.213a1.874,1.874,0,0,0-1.065-2.571l-1.265-.45a1.875,1.875,0,0,1,0-3.534l1.265-.45A1.874,1.874,0,0,0,4.077,7.212L3.5,6A1.874,1.874,0,0,1,6,3.5l1.213.576A1.874,1.874,0,0,0,9.783,3.012l.45-1.265a1.875,1.875,0,0,1,3.534,0l.45,1.265a1.874,1.874,0,0,0,2.571,1.065L18,3.5A1.874,1.874,0,0,1,20.5,6Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M13.5,9H11.467a1.342,1.342,0,0,0-.5,2.587l2.064.826a1.342,1.342,0,0,1-.5,2.587H10.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "12", :y1 "16", :x2 "12", :y2 "15", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "12", :y1 "9", :x2 "12", :y2 "8", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:circle {:cx "12", :cy "12", :r "6.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]])
(def question
[:svg {:xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 24 24"}
[:path {:stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :d "M9.5116 9.51125C9.5116 8.92057 9.68675 8.34316 10.0149 7.85203C10.3431 7.3609 10.8095 6.97811 11.3552 6.75207C11.9009 6.52603 12.5014 6.46689 13.0807 6.58212C13.6601 6.69736 14.1922 6.98179 14.6099 7.39946C15.0276 7.81714 15.312 8.34928 15.4272 8.92861C15.5425 9.50794 15.4833 10.1084 15.2573 10.6541C15.0312 11.1999 14.6485 11.6663 14.1573 11.9944C13.6662 12.3226 13.0888 12.4978 12.4981 12.4978V15.4843"}]
[:path {:stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :d "M23.4486 12C23.4488 11.2025 23.2094 10.4233 22.7616 9.76338C22.3138 9.10345 21.6781 8.5932 20.937 8.29872C21.2535 7.56634 21.3428 6.75575 21.1931 5.97204C21.0435 5.18834 20.6619 4.46766 20.0977 3.90343C19.5336 3.3392 18.813 2.95744 18.0293 2.80766C17.2457 2.65787 16.4351 2.74695 15.7026 3.06336C15.4084 2.32189 14.8983 1.68587 14.2383 1.23773C13.5784 0.789596 12.799 0.550003 12.0013 0.550003C11.2036 0.550003 10.4243 0.789596 9.76434 1.23773C9.1044 1.68587 8.59426 2.32189 8.30005 3.06336C7.56756 2.74664 6.75681 2.65732 5.97294 2.80697C5.18907 2.95662 4.46825 3.33834 3.90396 3.90263C3.33967 4.46692 2.95794 5.18774 2.80829 5.97161C2.65864 6.75548 2.74797 7.56623 3.06469 8.29872C2.32321 8.59293 1.68719 9.10307 1.23906 9.76301C0.790923 10.423 0.551331 11.2023 0.551331 12C0.551331 12.7977 0.790923 13.577 1.23906 14.237C1.68719 14.8969 2.32321 15.4071 3.06469 15.7013C2.74814 16.4337 2.6589 17.2443 2.80854 18.028C2.95818 18.8117 3.3398 19.5324 3.90392 20.0966C4.46804 20.6608 5.18865 21.0426 5.97233 21.1924C6.75601 21.3421 7.56661 21.2531 8.29905 20.9366C8.59327 21.6781 9.1034 22.3141 9.76335 22.7623C10.4233 23.2104 11.2026 23.45 12.0003 23.45C12.7981 23.45 13.5774 23.2104 14.2373 22.7623C14.8973 22.3141 15.4074 21.6781 15.7016 20.9366C16.4341 21.2531 17.2447 21.3421 18.0283 21.1924C18.812 21.0426 19.5326 20.6608 20.0968 20.0966C20.6609 19.5324 21.0425 18.8117 21.1921 18.028C21.3418 17.2443 21.2525 16.4337 20.936 15.7013C21.6773 15.407 22.3132 14.8968 22.7612 14.2368C23.2092 13.5769 23.4487 12.7976 23.4486 12Z"}]
[:path {:stroke "currentColor", :d "M12.4981 17.973C12.3606 17.973 12.2492 17.8616 12.2492 17.7242C12.2492 17.5867 12.3606 17.4753 12.4981 17.4753"}]
[:path {:stroke "currentColor", :d "M12.4981 17.973C12.6356 17.973 12.747 17.8616 12.747 17.7242C12.747 17.5867 12.6356 17.4753 12.4981 17.4753"}]])
(def rabbit
[:svg {:xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 24 24"}
[:path {:stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :d "M9 21.5H6.5"}]
[:path {:stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :d "M6.52169 19.4653H7.51807"}]
[:path {:stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :d "M6.52268 21.4561C6.25869 21.4561 6.00551 21.3512 5.81884 21.1646C5.63217 20.9779 5.5273 20.7247 5.5273 20.4607C5.5273 20.1967 5.63217 19.9435 5.81884 19.7569C6.00551 19.5702 6.25869 19.4653 6.52268 19.4653"}]
[:path {:stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :d "M7.51806 10.5069C9.50883 10.5069 11.3443 11.9999 12.495 15.4838C12.9927 16.9769 14.4857 21.4561 14.4857 21.4561H18.4673C18.7313 21.4561 18.9844 21.5609 19.1711 21.7476C19.3578 21.9343 19.4626 22.1875 19.4626 22.4515C19.4626 22.7154 19.3578 22.9686 19.1711 23.1553C18.9844 23.342 18.7313 23.4468 18.4673 23.4468H12.495C12.495 23.4468 10.979 20.9733 10.0483 19.4653C8.51345 16.9769 5.65172 15.5246 3.66096 14.1559C1.6702 12.7873 0.324441 9.73645 2.04346 7.52073C4.28208 4.63412 8.59606 5.77881 9.97367 7.37142L13.8049 11.2036C13.9426 11.3462 14.1074 11.46 14.2895 11.5382C14.4717 11.6165 14.6676 11.6577 14.8659 11.6594C15.0641 11.6611 15.2607 11.6233 15.4442 11.5483C15.6277 11.4732 15.7944 11.3623 15.9346 11.2221C16.0748 11.082 16.1857 10.9153 16.2607 10.7318C16.3358 10.5483 16.3736 10.3517 16.3719 10.1534C16.3701 9.95516 16.3289 9.75924 16.2507 9.57708C16.1724 9.39492 16.0587 9.23017 15.9161 9.09244L9.94381 3.10323C9.66371 2.823 9.50641 2.44298 9.5065 2.04677C9.5066 1.65057 9.66408 1.27062 9.94431 0.990528C10.2245 0.710433 10.6046 0.553129 11.0008 0.553223C11.397 0.553316 11.7769 0.710798 12.057 0.991026L22.2617 11.1957C22.6601 11.5395 22.974 11.9705 23.1791 12.4552C23.3841 12.9399 23.4748 13.4653 23.4442 13.9907C23.4442 15.4838 22.4488 16.4792 20.458 16.4792H17.9696L15.4751 18.2271"}]
[:path {:stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :d "M1.73291 7.98666C1.46709 7.9302 1.22172 7.80228 1.02324 7.61666C0.824763 7.43105 0.680706 7.19479 0.606596 6.93334C0.532486 6.6719 0.531133 6.39518 0.602684 6.13303C0.674235 5.87087 0.815976 5.63321 1.01263 5.44567C1.20928 5.25812 1.45339 5.1278 1.71864 5.06875C1.9839 5.00971 2.26024 5.02417 2.51788 5.11059C2.77551 5.197 3.00468 5.3521 3.18068 5.55915C3.35668 5.7662 3.47284 6.01735 3.51663 6.28555"}]
[:path {:stroke "currentColor", :d "M20.458 13.742C20.3206 13.742 20.2092 13.6305 20.2092 13.4931C20.2092 13.3557 20.3206 13.2443 20.458 13.2443"}]
[:path {:stroke "currentColor", :d "M20.458 13.742C20.5955 13.742 20.7069 13.6305 20.7069 13.4931C20.7069 13.3557 20.5955 13.2443 20.458 13.2443"}]])
(def arrow-in
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "download-thick-box"]
[:polygon {:points "15.5 14.5 15.5 8.5 8.5 8.5 8.5 14.5 5.5 14.5 12 21 18.469 14.5 15.5 14.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:path {:d "M22.629,4.572A6.22,6.22,0,0,1,23,6.5v16a1,1,0,0,1-1,1H2a1,1,0,0,1-1-1V6.5a6.22,6.22,0,0,1,.371-1.928L2.629,1.428A1.6,1.6,0,0,1,4,.5H20a1.6,1.6,0,0,1,1.371.928Z", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "12", :y1 "6", :x2 "12", :y2 "0.5", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]
[:line {:x1 "1.034", :y1 "6", :x2 "22.966", :y2 "6", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]])
(def thumbs-up
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "like"]
[:path {:d "M19.5,16.065h0a1.5,1.5,0,0,1,0,3h-1a1.5,1.5,0,0,1,0,3H12c-4,0-3-2-11-2v-9H4a7.949,7.949,0,0,0,7.5-8c0-1.581,3-1.781,3,1.219a31.593,31.593,0,0,1-1,5.781h8a1.5,1.5,0,0,1,0,3h-1a1.5,1.5,0,0,1,0,3h-1", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]])
(def thumbs-down
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "dislike"]
[:path {:d "M4.5,8h0a1.5,1.5,0,0,1,0-3h1a1.5,1.5,0,0,1,0-3H12c4,0,3,1.87,11,1.87V13H20a7.811,7.811,0,0,0-7.5,7.856c0,1.582-3,1.813-3-1.187A29.774,29.774,0,0,1,10.5,14h-8a1.5,1.5,0,0,1,0-3h1a1.5,1.5,0,0,1,0-3h1", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]])

View File

@@ -1,16 +1,18 @@
(ns auto-ap.ssr.transaction.insights (ns auto-ap.ssr.transaction.insights
(:require (:require
[auto-ap.client-routes :as client-routes]
[auto-ap.datomic :refer [conn visible-clients]] [auto-ap.datomic :refer [conn visible-clients]]
[auto-ap.shared-views.company.sidebar :refer [company-side-bar]] [auto-ap.rule-matching :refer [spread-cents]]
[auto-ap.ssr-routes :as ssr-routes] [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.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [html-response]] [auto-ap.ssr.utils :refer [html-response]]
[auto-ap.time :as atime] [auto-ap.time :as atime]
[bidi.bidi :as bidi] [bidi.bidi :as bidi]
[clj-time.coerce :as coerce] [clj-time.coerce :as coerce]
[datomic.api :as dc] [datomic.api :as dc]
[hiccup2.core :as hiccup] [iol-ion.tx :refer [random-tempid]]))
[clj-time.core :as time]))
(def pull-expr [:transaction/description-original (def pull-expr [:transaction/description-original
:db/id :db/id
@@ -26,97 +28,138 @@
(defn transaction-recommendations [identity selected-client & {:keys [after]}] (defn transaction-recommendations [identity selected-client & {:keys [after]}]
(let [visible-clients (visible-clients identity)] (let [visible-clients (visible-clients identity)]
(->> (->>
(dc/q '[:find (pull ?t pull-expr) (dc/qseq {:query '[:find (pull ?t pull-expr)
:in $ [?c ...] pull-expr :in $ [?c ...] pull-expr
:where [?t :transaction/recommended-account] :where [?t :transaction/recommended-account]
[?t :transaction/client ?c] [?t :transaction/client ?c]
[?t :transaction/approval-status :transaction-approval-status/unapproved] [?t :transaction/approval-status :transaction-approval-status/unapproved]
(not [?t :transaction/vendor])] (not [?t :transaction/vendor])]
(dc/db conn) :args [(dc/db conn)
(if selected-client (if selected-client
[selected-client] [selected-client]
visible-clients) visible-clients)
pull-expr) pull-expr]})
(map first) (map first)
(sort-by :transaction/date) (sort-by :transaction/date)
(reverse) (reverse)
(drop-while (fn [x] (drop-while (fn [x]
(if after (if after
(not= (Long/parseLong after) (:db/id x)) (not= (Long/parseLong after) (:db/id x))
false))) false)))
(#(if after (#(if after
(drop 1 %) (drop 1 %)
%)) %))
(take 10) (take 10)
(into [])))) (into []))))
(defn transaction-row [r & {:keys [hide-actions? class last?]}] (defn transaction-row [r & {:keys [hide-actions? class last?]}]
[:tr (cond-> {:class class} (com/data-grid-row
last? (assoc :hx-get (bidi/path-for ssr-routes/only-routes (cond-> {:class class}
:transaction-insight-rows last? (assoc :hx-get (bidi/path-for ssr-routes/only-routes
:after (:db/id r)) :transaction-insight-rows
:hx-trigger "intersect once" :after (:db/id r))
:hx-indicator "#insight-table" :hx-trigger "intersect once"
:hx-swap "afterend")) :hx-indicator "#insight-table"
[:td {:style {:width "8em"}}(:client/code (:transaction/client r))] :hx-swap "afterend"))
[:td {:style {:width "10em"}} (:bank-account/code (:transaction/bank-account r))] (com/data-grid-cell {} (:client/code (:transaction/client r)))
[:td {:style {:width "12em"}} (some-> (:transaction/date r) coerce/to-date-time (atime/unparse-local atime/normal-date))] (com/data-grid-cell {} (:bank-account/code (:transaction/bank-account r)))
[:td {:style {:width "30em" :max-width "30em"}} (str (:transaction/description-original r))] (com/data-grid-cell {} (some-> (:transaction/date r) coerce/to-date-time (atime/unparse-local atime/normal-date)))
[:td {:style {:width "10em"}}
(if (> (:transaction/amount r) 0.0 )
[:div.tag.is-success.is-light (str "$" (Math/round (:transaction/amount r)))]
[:div.tag.is-danger.is-light (str "$" (Math/round (:transaction/amount r)))])]
[:td {:style {:width "12em"}}
[:div [:div.tag (:vendor/name (:transaction/recommended-vendor r))]]
[:div [:div.tag (str (:account/numeric-code (:transaction/recommended-account r)) " - " (:account/name (:transaction/recommended-account r)))]]
[:div [:div.tag
{:class (cond
(> (:transaction/account-confidence r) 0.90)
"is-success is-light"
(> (:transaction/account-confidence r) 0.80)
"is-info is-light"
:else (com/data-grid-cell {} (str (:transaction/description-original r)))
"is-warning is-light")} (com/data-grid-cell {}
(str "%" (Math/round (* 100.0 (:transaction/account-confidence r))))]]] (if (> (:transaction/amount r) 0.0)
[:td [:div.tag.is-success.is-light (str "$" (Math/round (:transaction/amount r)))]
(when-not hide-actions? [:div.tag.is-danger.is-light (str "$" (Math/round (:transaction/amount r)))]))
[:div.buttons (com/data-grid-cell {:style {:width "12em"}}
[:button.button {:hx-post (bidi/path-for ssr-routes/only-routes [:div.flex.gap-2.flex-wrap {:style {:width "12em"}}
:transaction-insight-approve (com/pill {:color :primary} (:vendor/name (:transaction/recommended-vendor r)))
:transaction-id (:db/id r)) (com/pill {:color :secondary} (str (:account/numeric-code (:transaction/recommended-account r)) " - " (:account/name (:transaction/recommended-account r))))
:hx-target "closest tr"} (com/pill {:class (cond
[:i.fa.fa-thumbs-up ]] (> (:transaction/account-confidence r) 0.90)
[:button.button "is-success is-light"
[:i.fa.fa-thumbs-down ]] (> (:transaction/account-confidence r) 0.80)
[:a.button {:hx-get (bidi/path-for ssr-routes/only-routes "is-info is-light"
:transaction-insight-explain
:transaction-id (:db/id r)) :else
:hx-target "#modal-holder" "is-warning is-light")} (str "%" (Math/round (* 100.0 (:transaction/account-confidence r)))))])
:hx-swap "beforeend"} (com/data-grid-right-stack-cell {}
[:i.fa.fa-question ]]])]]) (when-not hide-actions?
[:form.flex.gap-2
[:input {:type :hidden :name "id" :value (:db/id r)}]
(com/icon-button {:hx-post (bidi/path-for ssr-routes/only-routes
:transaction-insight-approve
:transaction-id (:db/id r))
:hx-target "closest tr"
:color :primary-light}
svg/thumbs-up)
(com/icon-button {:hx-delete (bidi/path-for ssr-routes/only-routes
:transaction-insight-disapprove
:transaction-id (:db/id r))
:hx-target "closest tr"
:color :danger-light}
svg/thumbs-down)
(com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes
:transaction-insight-explain
:transaction-id (:db/id r))
:hx-target "#modal-holder"
:hx-swap "outerHTML"}
svg/question)]))))
(defn approve [{:keys [identity session] {:keys [transaction-id]} :route-params}] (defn approve [{:keys [identity session] {:keys [transaction-id]} :route-params}]
(html-response (transaction-row (let [approval-details (dc/pull (dc/db conn) [{:transaction/recommended-account [:account/location :db/id]}
(dc/pull (dc/db conn) :transaction/recommended-vendor
pull-expr :transaction/amount
(Long/parseLong transaction-id)) :db/id
:auto-remove? true {:transaction/client [:client/locations]} ]
:hide-actions? true (cond-> transaction-id
:class "live-added"))) string? (Long/parseLong)))
cents-to-distribute (int (Math/round (Math/abs (* (:transaction/amount approval-details) 100))))
valid-locations (or
(some-> approval-details :transaction/recommended-account :account/location vector)
(->> approval-details
:transaction/client
:client/locations))
updated-transaction [:upsert-transaction {:db/id (:db/id approval-details)
:transaction/approval-status :transaction-approval-status/approved
:transaction/vendor (:transaction/recommended-vendor approval-details)
:transaction/accounts (->> valid-locations
(map
(fn [cents location]
{:db/id (random-tempid)
:transaction-account/account (-> approval-details :transaction/recommended-account :db/id)
:transaction-account/amount (* 0.01 cents)
:transaction-account/location location})
(spread-cents cents-to-distribute (count valid-locations))))}]]
@(dc/transact conn [updated-transaction])
(html-response (transaction-row
(dc/pull (dc/db conn)
pull-expr
(Long/parseLong transaction-id))
:auto-remove? true
:hide-actions? true
:class "live-added"))))
(defn disapprove [{:keys [identity session] {:keys [transaction-id]} :route-params}]
(let [transaction-id (cond-> transaction-id string? (Long/parseLong))]
@(dc/transact conn [[:upsert-transaction {:db/id transaction-id :transaction/recommended-account nil :transaction/recommended-vendor nil}]])
(html-response (transaction-row
(dc/pull (dc/db conn) pull-expr transaction-id)
:auto-remove? true
:hide-actions? true
:class "live-removed"))))
(defn explain [{:keys [identity session] {:keys [transaction-id]} :route-params}] (defn explain [{:keys [identity session] {:keys [transaction-id]} :route-params}]
(let [r (dc/pull (dc/db conn) (let [r (dc/pull (dc/db conn)
pull-expr pull-expr
(Long/parseLong transaction-id)) (Long/parseLong transaction-id))
similar (->> (dc/q '[:find ?date ?do ?amt similar (->> (dc/q '[:find ?date ?do ?amt
:in $ ?tr :in $ ?tr
:where :where
[(iol-ion.query/recent-date 180) ?start-date] [(iol-ion.query/recent-date 180) ?start-date]
[?tr :transaction/client ?c] [?tr :transaction/client ?c]
[?tr :transaction/recommended-account ?a ] [?tr :transaction/recommended-account ?a]
[?tr :transaction/recommended-vendor ?v ] [?tr :transaction/recommended-vendor ?v]
[?t2 :transaction/client ?c] [?t2 :transaction/client ?c]
[?t2 :transaction/date ?date] [?t2 :transaction/date ?date]
[(>= ?date ?start-date)] [(>= ?date ?start-date)]
@@ -130,38 +173,31 @@
(take 5) (take 5)
sort sort
reverse)] reverse)]
(html-response [:div.modal.is-active.wide (html-response
[:div.modal-background {"_" (hiccup/raw "on click remove <#modal-holder div/>")}] (com/modal {}
[:div.modal-card (com/modal-card {:style {:width "900px"}}
[:div.modal-card-head [:div.flex [:div.p-2 "Similar Transactions"]]
[:h1.title "Similar transactions"] [:table.w-full
[:div.tags [:thead
[:div.tag.is-large.is-info.is-light (:vendor/name (:transaction/recommended-vendor r))] [:tr
[:div.tag.is-large.is-info.is-light (str (:account/numeric-code (:transaction/recommended-account r)) " - " (:account/name (:transaction/recommended-account r)))]]] [:td "Date"]
[:div.modal-card-body [:td "Description"]
[:table.table [:td "Amount"]]]
[:thead [:tbody
[:tr [:tr
[:td "Date"] [:th.text-left (some-> r :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date))]
[:td "Description"] [:th.text-left (-> r :transaction/description-original)]
[:td "Amount"]]] [:th.text-left (if (> (-> r :transaction/amount) 0.0)
[:tbody [:div.tag.is-success.is-light (str "$" (Math/round (:transaction/amount r)))]
[:tr [:div.tag.is-danger.is-light (str "$" (Math/round (:transaction/amount r)))])]]
[:th (some-> r :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date))] (for [[date description amt] similar]
[:th (-> r :transaction/description-original)] [:tr
[:th (if (> (-> r :transaction/amount) 0.0 ) [:td (some-> date coerce/to-date-time (atime/unparse-local atime/normal-date))]
[:div.tag.is-success.is-light (str "$" (Math/round (:transaction/amount r)))] [:td description]
[:div.tag.is-danger.is-light (str "$" (Math/round (:transaction/amount r)))])]] [:td (if (> amt 0.0)
(for [[date description amt] similar] [:div.tag.is-success.is-light (str "$" (Math/round amt))]
[:tr [:div.tag.is-danger.is-light (str "$" (Math/round amt))])]])]]
[:td (some-> date coerce/to-date-time (atime/unparse-local atime/normal-date))] [:div])))))
[:td description]
[:td (if (> amt 0.0 )
[:div.tag.is-success.is-light (str "$" (Math/round amt))]
[:div.tag.is-danger.is-light (str "$" (Math/round amt))])]])]]
]]
[:button.modal-close.is-large {"_" (hiccup/raw "on click remove <#modal-holder div/>")}]])))
(defn transaction-rows* [{:keys [selected-client identity after]}] (defn transaction-rows* [{:keys [selected-client identity after]}]
(let [recommendations (transaction-recommendations identity selected-client :after after)] (let [recommendations (transaction-recommendations identity selected-client :after after)]
@@ -178,28 +214,23 @@
:after (:after route-params)}))) :after (:after route-params)})))
(defn insight-table* [{:keys [selected-client identity]}] (defn insight-table* [{:keys [selected-client identity]}]
[:div#insight-table {:hx-get (bidi/path-for ssr-routes/only-routes (let [recommendations (transaction-recommendations identity selected-client)]
:transaction-insight-table (com/data-grid-card {:id "insight-table"
:request-method :get) :title "Transaction Insights"
:hx-trigger "clientSelected from:body" :route :transaction-insight-table
:hx-swap "outerHTML swap:100ms"} :paginate? false
:total (count recommendations)
[:table.table :action-buttons nil
[:thead :rows (for [r recommendations
[:tr :let [last? (= r (last recommendations))]]
[:td "Client"] (transaction-row r :last? last?))
[:td "Account"] :headers [(com/data-grid-header {} "Client")
[:td "Date"] (com/data-grid-header {} "Account")
[:td "Description"] (com/data-grid-header {} "Date")
[:td "Amount"] (com/data-grid-header {} "Description")
[:td "Vendor / Account"] (com/data-grid-header {} "Amount")
[:td "action"]]] (com/data-grid-header {:style {:width "4em"}} "Vendor / Account")
[:tbody (com/data-grid-header {})]})))
(transaction-rows* {:selected-client selected-client
:identity identity})]]
[:div.container.htmx-indicator
[:div.column.is-4.is-offset-4.has-text-centered
[:div.loader.is-loading.is-active.big.is-centered]]]])
(defn insight-table [{:keys [session identity]}] (defn insight-table [{:keys [session identity]}]
(html-response (insight-table* {:selected-client (html-response (insight-table* {:selected-client
@@ -208,10 +239,24 @@
(defn page [{:keys [identity matched-route session] :as request}] (defn page [{:keys [identity matched-route session] :as request}]
(base-page (base-page
request request
[:div (com/page {:nav (com/main-aside-nav)
[:h1.title "Transaction Insights"] :active-client (:client (:session request))
(insight-table* {:selected-client :identity (:identity request)
(-> session :client :db/id) :app-params {:hx-get (bidi/path-for ssr-routes/only-routes
:identity identity})] :admin-history)
[:div (company-side-bar matched-route)])) :hx-trigger "clientSelected from:body"
:hx-select "#app-contents"
:hx-swap "outerHTML swap:300ms"}}
(com/breadcrumbs {}
[:a {:href (bidi/path-for client-routes/routes
:transactions)}
"Transactions"]
[:a {:href (bidi/path-for ssr-routes/only-routes
:transaction-insights)}
"Insights"])
(insight-table* {:selected-client
(-> session :client :db/id)
:identity identity}))
"Transaction Insights"))

View File

@@ -1,9 +1,5 @@
(ns auto-ap.ssr.ui (ns auto-ap.ssr.ui
(:require (:require
[auto-ap.client-routes :as client-routes]
[auto-ap.ssr.company-dropdown :as company-dropdown]
[auto-ap.ssr.login-dropdown :as login-dropdown]
[bidi.bidi :as bidi]
[hiccup2.core :as hiccup])) [hiccup2.core :as hiccup]))
(defn html-page [hiccup] (defn html-page [hiccup]
@@ -15,69 +11,32 @@
{} {}
hiccup))}) hiccup))})
(defn base-page [request contents side-bar-contents] (defn base-page [request contents page-name]
(html-page (html-page
[:html.has-navbar-fixed-top [:html.has-navbar-fixed-top
[:head [:head
[:meta {:charset "utf-8"}] [:meta {:charset "utf-8"}]
[:meta {:http-equiv "X-UA-Compatible", :content "IE=edge"}] [:meta {:http-equiv "X-UA-Compatible", :content "IE=edge"}]
[:meta {:name "viewport", :content "width=device-width, initial-scale=1"}] [:meta {:name "viewport", :content "width=device-width, initial-scale=1"}]
[:title "Integreat"] [:title (str "Integreat | " page-name)]
[:link {:rel "stylesheet", :href "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css", :integrity "sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=", :crossorigin "anonymous"}] [:link {:href "/css/font.min.css", :rel "stylesheet"}]
[:link {:href "/css/font.min.css", :rel "stylesheet"}] [:link {:rel "icon" :type "image/png" :href "/favicon.png"}]
[:link {:rel "stylesheet", :href "/css/bulma.min.css"}] [:link {:rel "stylesheet", :href "/css/react-datepicker.min.inc.css"}]
[:link {:rel "stylesheet", :href "/css/bulma-calendar.min.css"}] [:link {:rel "stylesheet", :href "/output.css"}]
[:link {:rel "stylesheet", :href "/css/bulma-badge.min.css"}]
[:link {:rel "stylesheet", :href "/css/react-datepicker.min.inc.css"}]
[:link {:rel "stylesheet", :href "/css/animate.css"}]
[:link {:rel "stylesheet", :href "/finance-font/style.css"}]
[:link {:rel "stylesheet", :href "/css/main.css"}]
[:link {:rel "stylesheet", :href "https://unpkg.com/placeholder-loading/dist/css/placeholder-loading.min.css"}]
#_[:link {:rel "stylesheet", :href "https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.7/dist/css/autoComplete.min.css"}]
[:script {:src "https://unpkg.com/hyperscript.org@0.9.7"}]
[:script {:src "https://unpkg.com/@popperjs/core@2"}]
#_[:script {:src "https://unpkg.com/htmx.org@1.8.4"
:integrity "sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV"
:crossorigin= "anonymous"}]
[:script {:src "https://unpkg.com/htmx.org@1.9.0/dist/htmx.js"
:crossorigin= "anonymous"}]
[: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
[:div {:id "app"}
[:div
[:nav {:class "navbar has-shadow is-fixed-top is-grey"}
[:div {:class "container"} [:script {:src "https://unpkg.com/hyperscript.org@0.9.7/dist/_hyperscript.min.js"}]
[:div {:class "navbar-brand"} [:script {:src "https://unpkg.com/@popperjs/core@2.11.8/dist/umd/popper.min.js"}]
[:a {:class "navbar-item", :href "../"} #_[:script {:src "https://unpkg.com/htmx.org@1.8.4"
[:img {:src "/img/logo.png"}]]] :integrity "sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV"
[:div.navbar-menu {:id "navMenu"} :crossorigin= "anonymous"}]
[:div.navbar-start [:script {:src "https://unpkg.com/htmx.org@1.9.0/dist/htmx.js"
[:a.navbar-item {:href "/"} :crossorigin= "anonymous"}]
"Home" ] [:script {:src "https://unpkg.com/htmx.org/dist/ext/debug.js"}]
[:a.navbar-item {:href (bidi/path-for client-routes/routes :unpaid-invoices)} [:script {:src "/js/htmx-disable.js"}]
"Invoices" ] [:script {:type "text/javascript", :src "https://cdn.yodlee.com/fastlink/v4/initialize.js", :async "async"}]]
[:a.navbar-item {:href (bidi/path-for client-routes/routes :payments)} [:script {:type "text/javascript", :src "https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.7/dist/autoComplete.min.js"}]
"Payments" ] [:script {:src "https://unpkg.com/dropzone@5.9.3/dist/min/dropzone.min.js"}]
[:a.navbar-item {:href (bidi/path-for client-routes/routes :sales-orders)} [:link {:rel "stylesheet" :href "https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" :type "text/css"}]
"POS" ] [:body {:hx-ext "disable-submit"}
[:a.navbar-item {:href (bidi/path-for client-routes/routes :transactions)} contents
"Transactions" ] [:script {:src "/js/flowbite.min.js"}]]]))
[:a.navbar-item {:href (bidi/path-for client-routes/routes :ledger)}
"Ledger" ]]
[:div.navbar-end
(company-dropdown/dropdown request)
(login-dropdown/dropdown request)]]]]
[:div {:class "columns has-shadow", :id "mail-app", :style "margin-bottom: 0px; height: calc(100vh - 46px);"}
[:aside {:class "column aside menu is-2 "}
[:div {:class "main left-nav"}
side-bar-contents]]
[:div {:class "column messages hero ", :id "message-feed", :style "overflow: auto;"}
[:div {:class "inbox-messages"}
contents]]]
[:div]
[:div {:id "dz-hidden"}]]]
[:div#modal-holder]]]))

View File

@@ -2,7 +2,8 @@
(:require (:require
[auto-ap.logging :as alog] [auto-ap.logging :as alog]
[config.core :refer [env]] [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 {}}}] (defn html-response [hiccup & {:keys [status headers] :or {status 200 headers {}}}]
{:status status {:status status
@@ -39,3 +40,24 @@
(ex-message e)] (ex-message e)]
:status 500))))))) :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] [clj-time.coerce :as coerce]
[datomic.api :as dc] [datomic.api :as dc]
[auto-ap.datomic :refer [conn]] [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 ;; switch all of this to use tokens instead of passing around client codes, particularly because the codes
;; need to be tweaked for repeats ;; need to be tweaked for repeats
(defn client-code->login [client-code] (defn client-code->login [client-code]
@@ -61,7 +62,7 @@
(client/post (merge {:headers (assoc base-headers (client/post (merge {:headers (assoc base-headers
"loginName" (:yodlee2-admin-user env) "loginName" (:yodlee2-admin-user env)
"Content-Type" "application/x-www-form-urlencoded") "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} :as :json}
other-config) other-config)
) )
@@ -75,11 +76,13 @@
(log/info "logging in as " client-code) (log/info "logging in as " client-code)
(-> (str (:yodlee2-base-url env) "/auth/token") (-> (str (:yodlee2-base-url env) "/auth/token")
(client/post (merge {:headers (assoc base-headers (client/post (merge {:headers (assoc base-headers
"loginName" (if (<= (count client-code) 3) "loginName" (if (:yodlee2-test-user env)
(str client-code client-code) (:yodlee2-test-user env)
client-code) (if (<= (count client-code) 3)
(str client-code client-code)
client-code))
"Content-Type" "application/x-www-form-urlencoded") "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} :as :json}
other-config) other-config)
) )
@@ -278,10 +281,6 @@
provider-accounts) provider-accounts)
vals))) vals)))
(defn delete-provider-account [client-code id] (defn delete-provider-account [client-code id]
(let [cob-session (login-user (client-code->login client-code))] (let [cob-session (login-user (client-code->login client-code))]

View File

@@ -5,20 +5,30 @@
"admin" {"/history" {"" :admin-history "admin" {"/history" {"" :admin-history
"/" :admin-history "/" :admin-history
#"/search/?" :admin-history-search #"/search/?" :admin-history-search
["/" [#"\d+" :entity-id] #"/?"] :admin-history-search ["/" [#"\d+" :entity-id] #"/?"] :admin-history
["/inspect/" [#"\d+" :entity-id] #"/?"] :admin-history-inspect} ["/inspect/" [#"\d+" :entity-id] #"/?"] :admin-history-inspect}
"/ezcater-xls" :admin-ezcater-xls} "/ezcater-xls" :admin-ezcater-xls}
"transaction" {"/insights" {"" :transaction-insights "transaction" {"/insights" {"" :transaction-insights
"/table" :transaction-insight-table "/table" :transaction-insight-table
["/approve/" [#"\d+" :transaction-id]] {:post :transaction-insight-approve} ["/approve/" [#"\d+" :transaction-id]] {:post :transaction-insight-approve}
["/disapprove/" [#"\d+" :transaction-id]] {:delete :transaction-insight-disapprove}
["/rows/" [#"\d+" :after]] {:get :transaction-insight-rows} ["/rows/" [#"\d+" :after]] {:get :transaction-insight-rows}
["/explain/" [#"\d+" :transaction-id]] {:get :transaction-insight-explain}}} ["/explain/" [#"\d+" :transaction-id]] {:get :transaction-insight-explain}}}
"company" {"/dropdown" :company-dropdown-contents "company" {"" :company
"/active" {:put :active-client} "/dropdown" :company-dropdown-search-results
"/1099" :company-1099 "/active" {:put :active-client}
"/1099/table" {:get :company-1099-vendor-table} "/1099" :company-1099
"/1099/vendor-dialog" {["/" [#"\d+" :vendor-id]] {:get :company-1099-vendor-dialog "/1099/table" {:get :company-1099-vendor-table}
:post :company-1099-vendor-save}}}}) "/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]) (def only-routes ["/" routes])

View File

@@ -1,23 +1,21 @@
(ns auto-ap.views.components.layouts (ns auto-ap.views.components.layouts
(:require (:require
[auto-ap.events :as events] [auto-ap.events :as events]
[auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.routes :as routes] [auto-ap.routes :as routes]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.subs :as subs] [auto-ap.subs :as subs]
[auto-ap.views.components.modal :as modal] [auto-ap.views.components.modal :as modal]
[auto-ap.views.components.search :as search] [auto-ap.views.components.search :as search]
[auto-ap.views.components.vendor-dialog :as vendor-dialog] [auto-ap.views.components.vendor-dialog :as vendor-dialog]
[auto-ap.views.utils [auto-ap.views.utils
:refer [active-when :refer [active-when appearing dispatch-event-with-propagation login-url]]
appearing
dispatch-event-with-propagation
login-url]]
[bidi.bidi :as bidi] [bidi.bidi :as bidi]
[clojure.string :as str] [clojure.string :as str]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[reagent.core :as r] [reagent.core :as r]
[auto-ap.forms.builder :as form-builder] [vimsical.re-frame.cofx.inject :as inject]))
[vimsical.re-frame.cofx.inject :as inject]
[auto-ap.forms :as forms]))
(defn navbar-drop-down [{:keys [ class]} _] (defn navbar-drop-down [{:keys [ class]} _]
(let [!child (r/atom nil)] (let [!child (r/atom nil)]
@@ -47,19 +45,19 @@
(defn login-dropdown [] (defn login-dropdown []
(let [user (re-frame/subscribe [::subs/user])] (let [user (re-frame/subscribe [::subs/user])]
(if @user (if @user
[navbar-drop-down {:header [:span [:span.icon [:i.fa.fa-user] ] [navbar-drop-down {:header [:span [:span.icon [:i.fa.fa-user]]
[:span (:user/name @user)]] [:span (:user/name @user)]]
:id ::account} :id ::account}
[:div [:div
[:a {:class "navbar-item" [:a {:class "navbar-item"
:href (bidi/path-for routes/routes :reports)} "My company"] :href (bidi/path-for auto-ap.ssr-routes/only-routes :company)} "My company"]
[:a.dropdown-item {:on-click (dispatch-event-with-propagation [::vendor-dialog/started {}])} "New Vendor"] [:a.dropdown-item {:on-click (dispatch-event-with-propagation [::vendor-dialog/started {}])} "New Vendor"]
[:a.dropdown-item {:on-click (dispatch-event-with-propagation [::vendor-dialog/edit {}])} "Edit Vendor"] [:a.dropdown-item {:on-click (dispatch-event-with-propagation [::vendor-dialog/edit {}])} "Edit Vendor"]
(when (= "admin" (:user/role @user)) (when (= "admin" (:user/role @user))
[:a {:class "navbar-item" :href (bidi/path-for routes/routes :admin)} "Administration"]) [:a {:class "navbar-item" :href (bidi/path-for routes/routes :admin)} "Administration"])
[:hr {:class "navbar-divider"}] [:hr {:class "navbar-divider"}]
[:a.navbar-item {:on-click (fn [e] (.preventDefault e) (re-frame/dispatch [::events/logout]))} "Logout"]]] [:a.navbar-item {:on-click (fn [e] (.preventDefault e) (re-frame/dispatch [::events/logout]))} "Logout"]]]
[:a.navbar-item {:href login-url} "Login"]))) [:a.navbar-item {:href (login-url)} "Login"])))
(re-frame/reg-sub (re-frame/reg-sub
::client-search ::client-search

View File

@@ -1,7 +1,8 @@
(ns auto-ap.views.pages.login (ns auto-ap.views.pages.login
(:require (:require
[auto-ap.views.utils :refer [login-url]] [auto-ap.views.utils :refer [login-url]]
[re-frame.core :as re-frame])) [re-frame.core :as re-frame]
[cemerick.url :as url]))
(re-frame/reg-sub (re-frame/reg-sub
::logout-reason ::logout-reason
@@ -20,6 +21,6 @@
[:img {:src "/img/logo-big.png"}] [:img {:src "/img/logo-big.png"}]
[:div [:div
[:a.button.is-large.is-primary {:href login-url} "Login with Google"]]] [:a.button.is-large.is-primary {:href (doto (login-url (get (:query (url/url (.-location js/window))) "redirect-to")) println)} "Login with Google"]]]
[:p.has-text-gray [:p.has-text-gray
"Copyright Integreat 2018"]]]]]]) "Copyright Integreat 2018"]]]]]])

View File

@@ -17,6 +17,6 @@
[:a {:on-click (fn [] [:a {:on-click (fn []
(re-frame/dispatch-sync [::relogin]) (re-frame/dispatch-sync [::relogin])
true) true)
:href login-url} :href (login-url)}
"here"] "here"]
" to try again."]]) " to try again."]])

View File

@@ -21,47 +21,47 @@
user @(re-frame/subscribe [::subs/user]) user @(re-frame/subscribe [::subs/user])
client @(re-frame/subscribe [::subs/client])] client @(re-frame/subscribe [::subs/client])]
[:div [:div
[:div [:p.menu-label "Type"] [:div [:p.menu-label "Type"]
[:ul.menu-list [:ul.menu-list
[:li.menu-item [:li.menu-item
[:a.item {:href (bidi/path-for routes/routes :transactions) [:a.item {:href (bidi/path-for routes/routes :transactions)
:class [(active-when ap = :transactions)]} :class [(active-when ap = :transactions)]}
[:span {:class "icon" :style {:font-size "25px"}}] [:span {:class "icon" :style {:font-size "25px"}}]
[:span {:class "name"} "All"]]] [:span {:class "name"} "All"]]]
[:li.menu-item [:li.menu-item
[:a.item {:href (bidi/path-for routes/routes :unapproved-transactions) [:a.item {:href (bidi/path-for routes/routes :unapproved-transactions)
:class [(active-when ap = :unapproved-transactions)]} :class [(active-when ap = :unapproved-transactions)]}
[:span {:class "icon icon-task-list-text-1" :style {:font-size "25px"}}] [:span {:class "icon icon-task-list-text-1" :style {:font-size "25px"}}]
[:span {:class "name"} "Unapproved"]]] [:span {:class "name"} "Unapproved"]]]
[:li.menu-item [:li.menu-item
[:a.item {:href (bidi/path-for routes/routes :requires-feedback-transactions) [:a.item {:href (bidi/path-for routes/routes :requires-feedback-transactions)
:class [(active-when ap = :requires-feedback-transactions)]} :class [(active-when ap = :requires-feedback-transactions)]}
[:span {:class "icon icon-task-list-question" :style {:font-size "25px"}}] [:span {:class "icon icon-task-list-question" :style {:font-size "25px"}}]
[:span {:class "name"} "Client Review"]]] [:span {:class "name"} "Client Review"]]]
[:li.menu-item [:li.menu-item
[:a.item {:href (bidi/path-for routes/routes :approved-transactions) [:a.item {:href (bidi/path-for routes/routes :approved-transactions)
:class [(active-when ap = :approved-transactions)]} :class [(active-when ap = :approved-transactions)]}
[:span {:class "icon icon-task-list-check-1" :style {:font-size "25px"}}] [:span {:class "icon icon-task-list-check-1" :style {:font-size "25px"}}]
[:span {:class "name"} "Approved"]]] [:span {:class "name"} "Approved"]]]
[:li.menu-item [:li.menu-item
[:a.item {:href (bidi/path-for routes/routes :excluded-transactions) [:a.item {:href (bidi/path-for routes/routes :excluded-transactions)
:class [(active-when ap = :excluded-transactions)]} :class [(active-when ap = :excluded-transactions)]}
[:span {:class "icon icon-task-list-disable" :style {:font-size "25px"}}] [:span {:class "icon icon-task-list-disable" :style {:font-size "25px"}}]
[:span {:class "name"} "Excluded"]]] [:span {:class "name"} "Excluded"]]]
(when @(re-frame/subscribe [::subs/is-admin?])
[:li.menu-item [:li.menu-item
[:a.item {:href (bidi/path-for ssr-routes/only-routes :transaction-insights) [:a.item {:href (bidi/path-for ssr-routes/only-routes :transaction-insights)
:class [(active-when ap = :transaction-insights)]} :class [(active-when ap = :transaction-insights)]}
[:span {:class "icon icon-task-list-disable" :style {:font-size "25px"}}] [:span {:class "icon icon-task-list-disable" :style {:font-size "25px"}}]
[:span {:class "name"} "Insights"]]] [:span {:class "name"} "Insights"]]])]]
]]
(when client (when client
[:<> [:<>
[:p.menu-label "Bank Account"] [:p.menu-label "Bank Account"]
@@ -71,7 +71,6 @@
:value @(re-frame/subscribe [::data-page/filter data-page :bank-account]) :value @(re-frame/subscribe [::data-page/filter data-page :bank-account])
:bank-accounts @(re-frame/subscribe [::subs/bank-accounts])}]]]) :bank-accounts @(re-frame/subscribe [::subs/bank-accounts])}]]])
[:p.menu-label "Financial Account"] [:p.menu-label "Financial Account"]
[:div [:div
[search-backed-typeahead {:search-query (fn [i] [search-backed-typeahead {:search-query (fn [i]
@@ -79,7 +78,7 @@
{:query i {:query i
:client-id (:id client)} :client-id (:id client)}
[:name :id]]) [:name :id]])
:entity->text (fn [x ] (str (:numeric-code x) " - " (:name x))) :entity->text (fn [x] (str (:numeric-code x) " - " (:name x)))
:type "typeahead-v3" :type "typeahead-v3"
:on-change #(re-frame/dispatch [::data-page/filter-changed data-page :account (some-> % (select-keys [:name :id :numeric-code]))]) :on-change #(re-frame/dispatch [::data-page/filter-changed data-page :account (some-> % (select-keys [:name :id :numeric-code]))])
:value @(re-frame/subscribe [::data-page/filter data-page :account])}]] :value @(re-frame/subscribe [::data-page/filter data-page :account])}]]
@@ -94,13 +93,11 @@
:on-change #(re-frame/dispatch [::data-page/filter-changed data-page :vendor %]) :on-change #(re-frame/dispatch [::data-page/filter-changed data-page :vendor %])
:value @(re-frame/subscribe [::data-page/filter data-page :vendor])}]] :value @(re-frame/subscribe [::data-page/filter data-page :vendor])}]]
[:p.menu-label "Amount"] [:p.menu-label "Amount"]
[:div [:div
[number-filter [number-filter
{:on-change-event [::data-page/filter-changed data-page :amount-range] {:on-change-event [::data-page/filter-changed data-page :amount-range]
:value @(re-frame/subscribe [::data-page/filter data-page :amount-range])}]] :value @(re-frame/subscribe [::data-page/filter data-page :amount-range])}]]
[:p.menu-label "Date Range"] [:p.menu-label "Date Range"]
[:div [:div
@@ -108,20 +105,19 @@
{:on-change-event [::data-page/filter-changed data-page :date-range] {:on-change-event [::data-page/filter-changed data-page :date-range]
:value @(re-frame/subscribe [::data-page/filter data-page :date-range])}]] :value @(re-frame/subscribe [::data-page/filter data-page :date-range])}]]
[:p.menu-label "Location"] [:p.menu-label "Location"]
[:div.field [:div.field
[:div.control [:input.input {:placeholder "SC" [:div.control [:input.input {:placeholder "SC"
:style {:width "3em"} :style {:width "3em"}
:value @(re-frame/subscribe [::data-page/filter data-page :location]) :value @(re-frame/subscribe [::data-page/filter data-page :location])
:on-change (dispatch-value-change [::data-page/filter-changed data-page :location ])} ]]] :on-change (dispatch-value-change [::data-page/filter-changed data-page :location])}]]]
[:p.menu-label "Description"] [:p.menu-label "Description"]
[:div [:div
[:div.field [:div.field
[:div.control [:input.input {:placeholder "CHECK 123 ABC" [:div.control [:input.input {:placeholder "CHECK 123 ABC"
:value @(re-frame/subscribe [::data-page/filter data-page :description]) :value @(re-frame/subscribe [::data-page/filter data-page :description])
:on-change (dispatch-value-change [::data-page/filter-changed data-page :description])} ]]]] :on-change (dispatch-value-change [::data-page/filter-changed data-page :description])}]]]]
(when-let [exact-match-id @(re-frame/subscribe [::data-page/filter data-page :exact-match-id])] (when-let [exact-match-id @(re-frame/subscribe [::data-page/filter data-page :exact-match-id])]
[:div [:div
[:p.menu-label "Specific Transaction"] [:p.menu-label "Specific Transaction"]
@@ -129,7 +125,6 @@
[:button.delete.is-small {:on-click [:button.delete.is-small {:on-click
(dispatch-event [::data-page/filter-changed data-page :exact-match-id nil])}]]]) (dispatch-event [::data-page/filter-changed data-page :exact-match-id nil])}]]])
(when (= "admin" (:user/role user)) (when (= "admin" (:user/role user))
[:<> [:<>

View File

@@ -57,10 +57,16 @@
(when (apply f (into [active-page] rest)) " is-active")) (when (apply f (into [active-page] rest)) " is-active"))
(def login-url (defn login-url
(let [client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com" ([] (login-url nil))
redirect-uri (js/encodeURI (str (.-origin (.-location js/window)) "/api/oauth"))] ([next]
(str "https://accounts.google.com/o/oauth2/auth?access_type=online&client_id=" client-id "&redirect_uri=" redirect-uri "&response_type=code&max_auth_age=0&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile")))
(let [client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com"
redirect-uri (js/encodeURI (str (.-origin (.-location js/window)) "/api/oauth"
))]
(str "https://accounts.google.com/o/oauth2/auth?access_type=online&client_id=" client-id "&redirect_uri=" redirect-uri "&response_type=code&max_auth_age=0&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile"
(when next
(str "&state=" (js/encodeURIComponent next)))))))
(defn dispatch-value-change [event] (defn dispatch-value-change [event]
(fn [e] (fn [e]
@@ -251,7 +257,7 @@
(defn query-params [] (defn query-params []
(reduce-kv (reduce-kv
(fn [result k v] (fn [result k v]
(assoc result (keyword k) (edn/read-string v))) (assoc result (keyword k) (try (edn/read-string v) (catch js/Error e v))))
{} {}
(:query (cemerick.url/url (.-location js/window))))) (:query (cemerick.url/url (.-location js/window)))))

92
tailwind.config.js Normal file
View File

@@ -0,0 +1,92 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
content: ["./src/**/*.{cljs,clj,cljc}",
"./node_modules/flowbite/**/*.js"],
theme: {
extend: {
"fontFamily": {
"sans": ["Calibri", "ui-sans-serif", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"]
},
"colors": {
"green": {
"50": "#f2f8ea",
"100": "#e4f0d5",
"200": "#c9e1ab",
"300": "#afd382",
"400": "#94c458",
"500": "#79b52e",
"600": "#619125",
"700": "#496d1c",
"800": "#304812",
"900": "#182409"
},
"primary": {
"50": "#f2f8ea",
"100": "#e4f0d5",
"200": "#c9e1ab",
"300": "#afd382",
"400": "#94c458",
"500": "#79b52e",
"600": "#619125",
"700": "#496d1c",
"800": "#304812",
"900": "#182409"
},
"blue": {
"50": "#e6f5fd",
"100": "#ccebfb",
"200": "#99d7f7",
"300": "#66c4f2",
"400": "#33b0ee",
"500": "#009cea",
"600": "#007dbb",
"700": "#005e8c",
"800": "#003e5e",
"900": "#001f2f"
},
"secondary": {
"50": "#e9f5e8",
"100": "#d2ebd2",
"200": "#a6d7a4",
"300": "#79c377",
"400": "#4daf49",
"500": "#209b1c",
"600": "#1a7c16",
"700": "#135d11",
"800": "#0d3e0b",
"900": "#061f06"
},
"red": {
"50": "#ffe6e6",
"100": "#ffcdcd",
"200": "#ff9a9a",
"300": "#ff6868",
"400": "#ff3535",
"500": "#ff0303",
"600": "#cc0202",
"700": "#990202",
"800": "#660101",
"900": "#330101"
},
"orange": {
"50": "#fef2e8",
"100": "#fde6d1",
"200": "#fbcca2",
"300": "#f8b374",
"400": "#f69945",
"500": "#f48017",
"600": "#c36612",
"700": "#924d0e",
"800": "#623309",
"900": "#311a05"
}
}
}
} ,
plugins: [
require('flowbite/plugin')
]
}

View File

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

View File

@@ -8,159 +8,159 @@
(use-fixtures :each wrap-setup) (use-fixtures :each wrap-setup)
(deftest test-account-search #_(deftest test-account-search
(testing "It should find matching account names"
@(dc/transact conn [{:account/name "Food Research"
:db/ident :client-specific-account
:account/numeric-code 51100
:account/search-terms "Food Research"
:account/applicability :account-applicability/global
:account/default-allowance :allowance/allowed}])
(sut/rebuild-search-index)
(is (> (count (sut/search {:id (admin-token)}
{:query "Food Research"}
nil
))
0)))
(testing "It should find exact matches by numbers"
(is (= (count (sut/search {:id (admin-token)}
{:query "51100"}
nil
))
1)))
(testing "It should filter out accounts that are not allowed for clients"
@(dc/transact conn [{:account/name "CLIENT SPECIFIC"
:db/ident :client-specific-account
:account/numeric-code 99999
:account/search-terms "CLIENTSPECIFIC"
:account/applicability :account-applicability/customized
:account/default-allowance :allowance/allowed}])
(sut/rebuild-search-index)
(is (= [] (sut/search {:id (admin-token)}
{:query "CLIENTSPECIFIC"}
nil
)))
(testing "It should show up for the client specific version" (with-redefs [auto-ap.solr/impl (auto-ap.solr/->InMemSolrClient (atom {}))]
(let [client-id (-> @(dc/transact conn [{:client/name "CLIENT" (testing "It should find matching account names"
:db/id "client"} @(dc/transact conn [{:account/name "Food Research"
{:db/ident :client-specific-account :db/ident :client-specific-account
:account/client-overrides [{:account-client-override/client "client" :account/numeric-code 51100
:account-client-override/name "HI" :account/search-terms "Food Research"
:account-client-override/search-terms "HELLOWORLD"}]}]) :account/applicability :account-applicability/global
:tempids :account/default-allowance :allowance/allowed}])
(get "client"))]
(sut/rebuild-search-index)
(is (= 1 (count (sut/search {:id (admin-token)}
{:query "HELLOWORLD"
:client_id client-id}
nil))))))
(testing "It should hide accounts that arent applicable"
@(dc/transact conn [{:account/name "DENIED"
:db/ident :denied-account
:account/numeric-code 99998
:account/search-terms "DENIED"
:account/applicability :account-applicability/global
:account/default-allowance :allowance/denied
:account/vendor-allowance :allowance/denied
:account/invoice-allowance :allowance/denied}])
(is (= 0 (count (sut/search {:id (admin-token)}
{:query "DENIED"}
nil))))
(is (= 0 (count (sut/search {:id (admin-token)}
{:query "DENIED"
:allowance :invoice}
nil))))
(is (= 0 (count (sut/search {:id (admin-token)}
{:query "DENIED"
:allowance :vendor}
nil)))))
(testing "It should warn when using a warn account"
@(dc/transact conn [{:account/name "WARNING"
:db/ident :warn-account
:account/numeric-code 99997
:account/search-terms "WARNING"
:account/applicability :account-applicability/global
:account/default-allowance :allowance/warn
:account/vendor-allowance :allowance/warn
:account/invoice-allowance :allowance/warn}])
(sut/rebuild-search-index) (sut/rebuild-search-index)
(is (some? (:warning (first (sut/search {:id (admin-token)} (clojure.pprint/pprint auto-ap.solr/impl)
{:query "WARNING" (is (> (count (sut/search {:id (admin-token)}
:allowance :global} {:query "Food Research"}
nil))))) nil))
(is (some? (:warning (first (sut/search {:id (admin-token)} 0)))
{:query "WARNING" (testing "It should find exact matches by numbers"
:allowance :invoice} (is (= (count (sut/search {:id (admin-token)}
nil))))) {:query "51100"}
(is (some? (:warning (first (sut/search {:id (admin-token)} nil))
{:query "WARNING" 1)))
:allowance :vendor} (testing "It should filter out accounts that are not allowed for clients"
nil)))))) @(dc/transact conn [{:account/name "CLIENT SPECIFIC"
(testing "It should only include admin accounts for admins" :db/ident :client-specific-account
@(dc/transact conn [{:account/name "ADMINONLY" :account/numeric-code 99999
:db/ident :warn-account :account/search-terms "CLIENTSPECIFIC"
:account/numeric-code 99997 :account/applicability :account-applicability/customized
:account/search-terms "ADMINONLY" :account/default-allowance :allowance/allowed}])
:account/applicability :account-applicability/global
:account/default-allowance :allowance/admin-only
:account/vendor-allowance :allowance/admin-only
:account/invoice-allowance :allowance/admin-only}])
(sut/rebuild-search-index) (sut/rebuild-search-index)
(is (= 1 (count (sut/search {:id (admin-token)} (is (= [] (sut/search {:id (admin-token)}
{:query "ADMINONLY"} {:query "CLIENTSPECIFIC"}
nil)))) nil)))
(is (= 0 (count (sut/search {:id (user-token)}
{:query "ADMINONLY"}
nil)))))
(testing "It should allow searching for vendor accounts for invoices" (testing "It should show up for the client specific version"
(let [vendor-id (-> @(dc/transact conn [{:account/name "VENDORONLY" (let [client-id (-> @(dc/transact conn [{:client/name "CLIENT"
:db/id "vendor-only" :db/id "client"}
:db/ident :vendor-only {:db/ident :client-specific-account
:account/numeric-code 99996 :account/client-overrides [{:account-client-override/client "client"
:account/search-terms "VENDORONLY" :account-client-override/name "HI"
:account/applicability :account-applicability/global :account-client-override/search-terms "HELLOWORLD"}]}])
:account/default-allowance :allowance/allowed :tempids
:account/vendor-allowance :allowance/allowed (get "client"))]
:account/invoice-allowance :allowance/denied} (sut/rebuild-search-index)
{:vendor/name "Allowed" (is (= 1 (count (sut/search {:id (admin-token)}
:vendor/default-account "vendor-only" {:query "HELLOWORLD"
:db/id "vendor"}]) :client_id client-id}
:tempids nil))))))
(get "vendor"))]
(sut/rebuild-search-index) (testing "It should hide accounts that arent applicable"
@(dc/transact conn [{:account/name "DENIED"
:db/ident :denied-account
:account/numeric-code 99998
:account/search-terms "DENIED"
:account/applicability :account-applicability/global
:account/default-allowance :allowance/denied
:account/vendor-allowance :allowance/denied
:account/invoice-allowance :allowance/denied}])
(is (= 0 (count (sut/search {:id (admin-token)} (is (= 0 (count (sut/search {:id (admin-token)}
{:query "VENDORONLY" {:query "DENIED"}
nil))))
(is (= 0 (count (sut/search {:id (admin-token)}
{:query "DENIED"
:allowance :invoice} :allowance :invoice}
nil)))) nil))))
(is (= 0 (count (sut/search {:id (admin-token)}
{:query "DENIED"
:allowance :vendor}
nil)))))
(testing "It should warn when using a warn account"
@(dc/transact conn [{:account/name "WARNING"
:db/ident :warn-account
:account/numeric-code 99997
:account/search-terms "WARNING"
:account/applicability :account-applicability/global
:account/default-allowance :allowance/warn
:account/vendor-allowance :allowance/warn
:account/invoice-allowance :allowance/warn}])
(sut/rebuild-search-index)
(is (some? (:warning (first (sut/search {:id (admin-token)}
{:query "WARNING"
:allowance :global}
nil)))))
(is (some? (:warning (first (sut/search {:id (admin-token)}
{:query "WARNING"
:allowance :invoice}
nil)))))
(is (some? (:warning (first (sut/search {:id (admin-token)}
{:query "WARNING"
:allowance :vendor}
nil))))))
(testing "It should only include admin accounts for admins"
@(dc/transact conn [{:account/name "ADMINONLY"
:db/ident :warn-account
:account/numeric-code 99997
:account/search-terms "ADMINONLY"
:account/applicability :account-applicability/global
:account/default-allowance :allowance/admin-only
:account/vendor-allowance :allowance/admin-only
:account/invoice-allowance :allowance/admin-only}])
(sut/rebuild-search-index)
(is (= 1 (count (sut/search {:id (admin-token)} (is (= 1 (count (sut/search {:id (admin-token)}
{:query "VENDORONLY" {:query "ADMINONLY"}
:allowance :invoice nil))))
:vendor_id vendor-id} (is (= 0 (count (sut/search {:id (user-token)}
nil)))))))) {:query "ADMINONLY"}
nil)))))
(deftest get-graphql (testing "It should allow searching for vendor accounts for invoices"
(testing "should retrieve a single account" (let [vendor-id (-> @(dc/transact conn [{:account/name "VENDORONLY"
@(dc/transact conn [{:account/numeric-code 1 :db/id "vendor-only"
:account/default-allowance :allowance/allowed :db/ident :vendor-only
:account/type :account-type/asset :account/numeric-code 99996
:account/location "A" :account/search-terms "VENDORONLY"
:account/name "Test"}]) :account/applicability :account-applicability/global
:account/default-allowance :allowance/allowed
:account/vendor-allowance :allowance/allowed
:account/invoice-allowance :allowance/denied}
{:vendor/name "Allowed"
:vendor/default-account "vendor-only"
:db/id "vendor"}])
:tempids
(get "vendor"))]
(sut/rebuild-search-index)
(is (= 0 (count (sut/search {:id (admin-token)}
{:query "VENDORONLY"
:allowance :invoice}
nil))))
(is (= {:name "Test", (is (= 1 (count (sut/search {:id (admin-token)}
:invoice_allowance nil, {:query "VENDORONLY"
:numeric_code 1, :allowance :invoice
:vendor_allowance nil, :vendor_id vendor-id}
:location "A", nil)))))))
:applicability nil}
(dissoc (first (:accounts (sut/get-graphql {:id (admin-token)} {} nil))) (deftest get-graphql
:id (testing "should retrieve a single account"
:type @(dc/transact conn [{:account/numeric-code 1
:default_allowance))))) :account/default-allowance :allowance/allowed
:account/type :account-type/asset
:account/location "A"
:account/name "Test"}])
(is (= {:name "Test",
:invoice_allowance nil,
:numeric_code 1,
:vendor_allowance nil,
:location "A",
:applicability nil}
(dissoc (first (:accounts (sut/get-graphql {:id (admin-token)} {} nil)))
:id
:type
:default_allowance)))))))
(deftest upsert-account (deftest upsert-account
(testing "should create a new account" (testing "should create a new account"

View File

@@ -9,50 +9,51 @@
(use-fixtures :each wrap-setup) (use-fixtures :each wrap-setup)
(deftest stream->sales-orders (deftest stream->sales-orders
(testing "Should import nothing when there are no clients" (with-redefs [auto-ap.solr/impl (auto-ap.solr/->InMemSolrClient (atom {}))]
(with-open [s (io/input-stream (io/resource "sample-ezcater.xlsx"))] (testing "Should import nothing when there are no clients"
(is (= [:missing "Nick The Greek (Santa Cruz)"] (first (sut/stream->sales-orders s))))))
(testing "should import for a single client"
(let [{:strs [test-client]} (setup-test-data [(test-client
:db/id "test-client"
:client/code "NGOP"
:client/locations ["DT"]
:client/name "The client"
:client/matches ["Nick the Greek (Elk Grove)"])])]
(with-open [s (io/input-stream (io/resource "sample-ezcater.xlsx"))] (with-open [s (io/input-stream (io/resource "sample-ezcater.xlsx"))]
(is (seq (sut/stream->sales-orders s)))) (is (= [:missing "Nick The Greek (Santa Cruz)"] (first (sut/stream->sales-orders s))))))
(with-open [s (io/input-stream (io/resource "sample-ezcater.xlsx"))] (testing "should import for a single client"
(is (= #:sales-order (let [{:strs [test-client]} (setup-test-data [(test-client
{:vendor :vendor/ccp-ezcater :db/id "test-client"
:service-charge -95.9 :client/code "NGOP"
:date #inst "2023-04-03T18:30:00" :client/locations ["DT"]
:reference-link "ZA2-320" :client/name "The client"
:charges :client/matches ["Nick the Greek (Elk Grove)"])])]
[#:charge{:type-name "CARD" (with-open [s (io/input-stream (io/resource "sample-ezcater.xlsx"))]
:date #inst "2023-04-03T18:30:00" (is (seq (sut/stream->sales-orders s))))
:client test-client (with-open [s (io/input-stream (io/resource "sample-ezcater.xlsx"))]
:location "DT" (is (= #:sales-order
:external-id {:vendor :vendor/ccp-ezcater
"ezcater/charge/17592186045501-DT-ZA2-320-0" :service-charge -95.9
:processor :ccp-processor/ezcater :date #inst "2023-04-03T18:30:00"
:total 516.12 :reference-link "ZA2-320"
:tip 0.0}] :charges
:client test-client [#:charge{:type-name "CARD"
:tip 0.0 :date #inst "2023-04-03T18:30:00"
:tax 37.12 :client test-client
:external-id "ezcater/order/17592186045501-DT-ZA2-320" :location "DT"
:total 516.12 :external-id
:line-items "ezcater/charge/17592186045501-DT-ZA2-320-0"
[#:order-line-item{:external-id :processor :ccp-processor/ezcater
"ezcater/order/17592186045501-DT-ZA2-320-0" :total 516.12
:item-name "EZCater Catering" :tip 0.0}]
:category "EZCater Catering" :client test-client
:discount 0.0 :tip 0.0
:tax 37.12 :tax 37.12
:total 516.12}] :external-id "ezcater/order/17592186045501-DT-ZA2-320"
:discount 0.0 :total 516.12
:location "DT" :line-items
:returns 0.0} [#:order-line-item{:external-id
(last (first (filter (comp #{:order} first) "ezcater/order/17592186045501-DT-ZA2-320-0"
(sut/stream->sales-orders s)))))))))) :item-name "EZCater Catering"
:category "EZCater Catering"
:discount 0.0
:tax 37.12
:total 516.12}]
:discount 0.0
:location "DT"
:returns 0.0}
(last (first (filter (comp #{:order} first)
(sut/stream->sales-orders s)))))))))))