Builds client SSR approach, sunsets old cljs.

This commit is contained in:
2024-01-09 21:40:43 -08:00
parent d824cdfff4
commit 8063a8fcbd
74 changed files with 4603 additions and 4047 deletions

1
.gitignore vendored
View File

@@ -43,3 +43,4 @@ data/solr/data/invoices/index/
data/solr/data/plaid_merchants/data/ data/solr/data/plaid_merchants/data/
data/solr/data/logs data/solr/data/logs
data/solr/logs data/solr/logs
.vscode/**

View File

@@ -1,4 +1,5 @@
{:db {:server "localhost"} {:db {:server "localhost"}
:port "3449"
:scheme "http" :scheme "http"
:base-url "http://localhost:3449" :base-url "http://localhost:3449"
:solr-uri "http://localhost:8983" :solr-uri "http://localhost:8983"

View File

@@ -1,5 +1,3 @@
{:watch-dirs ["src/cljs", "src/cljc"] {:watch-dirs ["src/cljs", "src/cljc"]
:css-dirs ["resources/public/css"] :css-dirs ["resources/public/css"]
:ring-server-options {:port 3449}
:ring-handler auto-ap.handler/app
:open-url false} :open-url false}

View File

@@ -1,5 +1,8 @@
(ns iol-ion.tx.upsert-entity (ns iol-ion.tx.upsert-entity
(:require [datomic.api :as dc]) (:require [datomic.api :as dc]
;; [clj-time.core :as time]
;; [clj-time.coerce :as coerce]
)
(:import [java.util UUID])) (:import [java.util UUID]))
@@ -9,11 +12,11 @@
(defn -by (defn -by
[f fv xs] [f fv xs]
(reduce (reduce
#(assoc %1 (f %2) (fv %2)) #(assoc %1 (f %2) (fv %2))
{} {}
xs)) xs))
(defn -pull-many [db read ids ] (defn -pull-many [db read ids]
(->> (dc/q '[:find (pull ?e r) (->> (dc/q '[:find (pull ?e r)
:in $ [?e ...] r] :in $ [?e ...] r]
db db
@@ -21,82 +24,93 @@
read) read)
(map first))) (map first)))
;; TODO add DATOMIC_EXT_CLASSPATH ala https://docs.datomic.com/pro/reference/database-functions.html#transaction-functions
;; (defn transform-common [v]
;; (cond
;; (nil? v)
;; v
;; (satisfies? clj-time.core/DateTimeProtocol v)
;; (clj-time.coerce/to-date v)
;; :else
;; v))
(defn upsert-entity [db entity] (defn upsert-entity [db entity]
(when-not (or (:db/id entity) (when-not (or (:db/id entity)
(:db/ident entity)) (:db/ident entity))
(datomic.api/cancel {:cognitect.anomalies/category :cognitect.anomalies/incorrect (datomic.api/cancel {:cognitect.anomalies/category :cognitect.anomalies/incorrect
:cognitect.anomalies/message :cognitect.anomalies/message
(str "Cannot upsert without :db/id or :db/ident, " entity)})) (str "Cannot upsert without :db/id or :db/ident, " entity)}))
(let [e (or (:db/id entity) (:db/ident entity)) (let [e (or (:db/id entity) (:db/ident entity))
is-new? (string? e) is-new? (string? e)
extant-entity (when-not is-new? extant-entity (when-not is-new?
(dc/pull db (keys entity) (or (:db/id entity) (:db/ident entity)))) (dc/pull db (keys entity) (or (:db/id entity) (:db/ident entity))))
ident->value-type (-by :db/ident (comp :db/ident ident->value-type (-by :db/ident (comp :db/ident
:db/valueType) :db/valueType)
(-pull-many (-pull-many
db db
[{:db/valueType [:db/ident]} :db/ident] [{:db/valueType [:db/ident]} :db/ident]
(keys entity))) (keys entity)))
ident->cardinality (-by :db/ident (comp :db/ident ident->cardinality (-by :db/ident (comp :db/ident
:db/cardinality) :db/cardinality)
(-pull-many (-pull-many
db db
[{:db/cardinality [:db/ident]} :db/ident] [{:db/cardinality [:db/ident]} :db/ident]
(keys entity))) (keys entity)))
ops (->> entity ops (->> entity
(reduce (reduce
(fn [ops [a v]] (fn [ops [a v]]
(cond (cond
(= :db/id a) (= :db/id a)
ops ops
(= :db/ident a) (= :db/ident a)
ops ops
(or (= v (a extant-entity)) (or (= v (a extant-entity))
(= v (:db/ident (a extant-entity) :nope)) (= v (:db/ident (a extant-entity) :nope))
(= v (:db/id (a extant-entity)) :nope)) (= v (:db/id (a extant-entity)) :nope))
ops ops
(and (nil? v)
(not (nil? (a extant-entity))))
(if (= :db.cardinality/many (ident->cardinality a))
(into ops (map (fn [v]
[:db/retract e a (cond-> v
(:db/id v) :db/id)])
(a extant-entity)))
(conj ops [:db/retract e a (cond-> (a extant-entity) (and (nil? v)
(:db/id (a extant-entity)) :db/id)])) (not (nil? (a extant-entity))))
(if (= :db.cardinality/many (ident->cardinality a))
(into ops (map (fn [v]
[:db/retract e a (cond-> v
(:db/id v) :db/id)])
(a extant-entity)))
(nil? v) (conj ops [:db/retract e a (cond-> (a extant-entity)
ops (:db/id (a extant-entity)) :db/id)]))
(nil? v)
ops
;; reset relationships if it's refs, and not a lookup (i.e., seq of maps, or empty seq) ;; reset relationships if it's refs, and not a lookup (i.e., seq of maps, or empty seq)
(and (sequential? v) (= :db.type/tuple (ident->value-type a)) (not (= :db.cardinality/many (ident->cardinality a))))
(conj ops [:db/add e a v])
(and (sequential? v) (= :db.type/tuple (ident->value-type a)) (not (= :db.cardinality/many (ident->cardinality a)))) (and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v))
(conj ops [:db/add e a v]) (into ops [[:reset-rels e a v]])
(and (sequential? v) (= :db.type/ref (ident->value-type a)) (every? map? v)) (= :db.cardinality/many (ident->cardinality a))
(into ops [[:reset-rels e a v]]) (into ops [[:reset-scalars e a v]])
(= :db.cardinality/many (ident->cardinality a)) (and (sequential? v) (not= :db.type/ref (ident->value-type a)))
(into ops [[:reset-scalars e a v]]) (into ops [[:reset-scalars e a v]])
(and (sequential? v) (not= :db.type/ref (ident->value-type a))) (and (map? v)
(into ops [[:reset-scalars e a v]]) (= :db.type/ref (ident->value-type a)))
(let [id (or (:db/id v) (-random-tempid))]
(-> ops
(conj [:db/add e a id])
(into [[:upsert-entity (assoc v :db/id id)]])))
(and (map? v) :else
(= :db.type/ref (ident->value-type a))) (conj ops [:db/add e a v])))
(let [id (or (:db/id v) (-random-tempid))] []))]
(-> ops
(conj [:db/add e a id])
(into [[:upsert-entity (assoc v :db/id id)]])))
:else
(conj ops [:db/add e a v])
))
[]))]
ops)) ops))

View File

@@ -23,7 +23,7 @@
#_org.eclipse.jetty/jetty-http #_org.eclipse.jetty/jetty-http
#_org.eclipse.jetty/jetty-util #_org.eclipse.jetty/jetty-util
#_org.eclipse.jetty/jetty-server]] #_org.eclipse.jetty/jetty-server]]
[ring/ring-jetty-adapter "1.9.6" :exclusions [ring/ring-jetty-adapter "1.9.6" :exclusions
[org.eclipse.jetty/jetty-server]] [org.eclipse.jetty/jetty-server]]
[yogthos/config "1.1.7"] [yogthos/config "1.1.7"]
@@ -39,9 +39,10 @@
[buddy/buddy-auth "2.2.0" [buddy/buddy-auth "2.2.0"
:exclusions [com.fasterxml.jackson.dataformat/jackson-dataformat-cbor :exclusions [com.fasterxml.jackson.dataformat/jackson-dataformat-cbor
com.fasterxml.jackson.core/jackson-core]] com.fasterxml.jackson.core/jackson-core]]
[nrepl "0.8.3" :exclusions [org.clojure/tools.logging]] [nrepl "0.8.3" :exclusions [org.clojure/tools.logging]]
[cheshire "5.9.0"] [cheshire "5.9.0"]
[hawk "0.2.11"]
[clj-time "0.15.2"] [clj-time "0.15.2"]
[ring/ring-json "0.5.0" :exclusions [cheshire]] [ring/ring-json "0.5.0" :exclusions [cheshire]]
[com.cemerick/url "0.1.1"] [com.cemerick/url "0.1.1"]
@@ -91,7 +92,7 @@
[org.clojure/core.async]] [org.clojure/core.async]]
[hiccup "2.0.0-alpha2"] [hiccup "2.0.0-alpha2"]
;; needed for java 11 ;; needed for java 11
[javax.xml.bind/jaxb-api "2.4.0-b180830.0359"] [javax.xml.bind/jaxb-api "2.4.0-b180830.0359"]
[io.forward/clojure-mail "1.0.8"] [io.forward/clojure-mail "1.0.8"]
@@ -107,19 +108,22 @@
[lein-cljsbuild "1.1.5"] [lein-cljsbuild "1.1.5"]
[lein-ancient "0.6.15"]] [lein-ancient "0.6.15"]]
:clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"]
:ring {:handler auto-ap.handler/app} #_#_:ring {:handler auto-ap.handler/app}
:source-paths ["src/clj" "src/cljc" "src/cljs" "iol_ion/src" ] :source-paths ["src/clj" "src/cljc" "src/cljs" "iol_ion/src" ]
:resource-paths ["resources"] :resource-paths ["resources"]
:aliases {"build" ["do" ["uberjar"]] :aliases {"build" ["do" ["uberjar"]]
"fig:dev" ["run" "-m" "figwheel.main" "-b" "dev" "-r"] "fig:dev" ["run" "-m" "figwheel.main" "-b" "dev" "-r"]
"build-dev" ["trampoline" "run" "-m" "figwheel.main" "-b" "dev" "-r"]
"fig:min" ["run" "-m" "figwheel.main" "-O" "whitespace" "-bo" "min"]} "fig:min" ["run" "-m" "figwheel.main" "-O" "whitespace" "-bo" "min"]}
:profiles { :profiles {
:dev :dev
{:resource-paths ["resources" "target"] {
:resource-paths ["resources" "target"]
:dependencies [#_[binaryage/devteols "1.0.2"] :dependencies [#_[binaryage/devteols "1.0.2"]
[postgresql/postgresql "9.3-1102.jdbc41"] [postgresql/postgresql "9.3-1102.jdbc41"]
[org.clojure/tools.namespace "1.4.5"]
[org.clojure/java.jdbc "0.7.11"] [org.clojure/java.jdbc "0.7.11"]
#_[com.datomic/dev-local "1.0.243"] #_[com.datomic/dev-local "1.0.243"]
[etaoin "0.4.1"] [etaoin "0.4.1"]
@@ -177,7 +181,7 @@
:main auto-ap.server :main auto-ap.server
:aot [auto-ap.server auto-ap.time clj-time.core clj-time.coerce clj-time.format clojure.tools.logging.impl] #_#_:aot [auto-ap.server auto-ap.time clj-time.core clj-time.coerce clj-time.format clojure.tools.logging.impl]
:uberjar-name "auto-ap.jar" :uberjar-name "auto-ap.jar"
:test-paths ["test/clj"] :test-paths ["test/clj"]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
let
Source = Json.Document(Web.Contents("https://app.integreatconsult.com/api/queries/%s/results/json", [Headers=[#"Accept-Encoding"="gzip"]])),
rawtable = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
mytable = Table.FromRecords(Table.TransformRows(rawtable, (x) as record => Record.FromList(x[Column1], {"Date", "Total", "Fee"}))),
#"Changed Type" = Table.TransformColumnTypes(mytable,{{"Total", Currency.Type}, {"Fee", Currency.Type}, {"Date", type date}}),
#"Sorted Rows" = Table.Sort(#"Changed Type",{{"Date", Order.Ascending}})
in
#"Sorted Rows"

View File

@@ -0,0 +1,8 @@
let
Source = Json.Document(Web.Contents("https://app.integreatconsult.com/api/queries/%s/results/json", [Headers=[#"Accept-Encoding"="gzip"]])),
rawtable = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
mytable = Table.FromRecords(Table.TransformRows(rawtable, (x) as record => Record.FromList(x[Column1], {"Date", "Type", "Total", "Fee"}))),
#"Changed Type" = Table.TransformColumnTypes(mytable,{{"Total", Currency.Type}, {"Fee", Currency.Type}, {"Date", type date}}),
#"Sorted Rows" = Table.Sort(#"Changed Type",{{"Date", Order.Ascending}})
in
#"Sorted Rows"

View File

@@ -0,0 +1,8 @@
let
Source = Json.Document(Web.Contents("https://app.integreatconsult.com/api/queries/%s/results/json", [Headers=[#"Accept-Encoding"="gzip"]])),
rawtable = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
mytable = Table.FromRecords(Table.TransformRows(rawtable, (x) as record => Record.FromList(x[Column1], {"Date", "Category", "Name", "Total", "Tax", "Discount"}))),
#"Changed Type" = Table.TransformColumnTypes(mytable,{{"Total", Currency.Type}, {"Tax", Currency.Type}, {"Discount", Currency.Type}, {"Date", type date}}),
#"Sorted Rows" = Table.Sort(#"Changed Type",{{"Date", Order.Ascending}, {"Category", Order.Ascending}})
in
#"Sorted Rows"

View File

@@ -0,0 +1,8 @@
let
Source = Json.Document(Web.Contents("https://app.integreatconsult.com/api/queries/%s/results/json", [Headers=[#"Accept-Encoding"="gzip"]])),
rawtable = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
mytable = Table.FromRecords(Table.TransformRows(rawtable, (x) as record => Record.FromList(x[Column1], {"Date", "Total", "Tax", "Tip", "Service Charge", "Discount", "Returns"}))),
#"Changed Type" = Table.TransformColumnTypes(mytable,{{"Total", Currency.Type}, {"Tax", Currency.Type}, {"Discount", Currency.Type}, {"Service Charge", Currency.Type}, {"Returns", Currency.Type}, {"Tip", Currency.Type}, {"Date", type date}}),
#"Sorted Rows" = Table.Sort(#"Changed Type",{{"Date", Order.Ascending}})
in
#"Sorted Rows"

View File

@@ -0,0 +1,8 @@
let
Source = Json.Document(Web.Contents("https://app.integreatconsult.com/api/queries/%s/results/json", [Headers=[#"Accept-Encoding"="gzip"]])),
rawtable = Table.FromList(Source, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
mytable = Table.FromRecords(Table.TransformRows(rawtable, (x) as record => Record.FromList(x[Column1], {"Date", "Type", "Processor", "Total", "Tip"}))),
#"Changed Type" = Table.TransformColumnTypes(mytable,{{"Total", Currency.Type}, {"Tip", Currency.Type}, {"Date", type date}}),
#"Sorted Rows" = Table.Sort(#"Changed Type",{{"Date", Order.Ascending}})
in
#"Sorted Rows"

View File

@@ -120,12 +120,54 @@ htmx.defineExtension('trigger-filter', {
initDatepicker = function(elem) { initDatepicker = function(elem) {
elem.dp = new Datepicker(elem, {format: "mm/dd/yyyy", autohide: true}); const modalParent = elem.closest('#modal-content');
if (modalParent) {
elem.dp = new Datepicker(elem, {format: "mm/dd/yyyy", autohide: true, container: ".modal-stack"});
} else {
elem.dp = new Datepicker(elem, {format: "mm/dd/yyyy", autohide: true});
}
} }
countRows = function(id) { countRows = function(id) {
var table = document.querySelector(id); var table = document.querySelector(id);
var rows = table.querySelectorAll("tbody tr"); var rows = table.querySelectorAll("tbody tr");
console.log("ROWS", rows.length);
return rows.length; return rows.length;
} }
htmx.onLoad(function(content) {
var sortables = content.querySelectorAll(".sortable");
for (var i = 0; i < sortables.length; i++) {
var sortable = sortables[i];
var sortableInstance = new Sortable(sortable, {
animation: 150,
ghostClass: 'bg-blue-100',
// Make the `.htmx-indicator` unsortable
filter: ".htmx-indicator",
onMove: function (evt) {
return evt.related.className.indexOf('htmx-indicator') === -1;
},
// Disable sorting on the `end` event
onEnd: function (evt) {
this.option("disabled", true);
}
});
// Re-enable sorting on the `htmx:afterSwap` event
sortable.addEventListener("htmx:afterSwap", function() {
sortableInstance.option("disabled", false);
});
}
})
async function copyToClipboard(text) {
try {
// Write the text to the clipboard
await navigator.clipboard.writeText(text);
console.log('Text copied to clipboard:', text);
} catch (err) {
console.error('Failed to copy text to clipboard:', err);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -143,3 +143,10 @@
(defn update! [cursor v] (defn update! [cursor v]
"Replaces value supplied by cursor with value v." "Replaces value supplied by cursor with value v."
(-transact! cursor (constantly v))) (-transact! cursor (constantly v)))
(defn ensure-path! [cursor p default]
(let [next-to-last (get-in cursor (butlast p))
next-to-last-v @next-to-last]
(when (not (get next-to-last-v (last p)))
(transact! next-to-last #(assoc % (last p) default))))
cursor)

View File

@@ -1,182 +1,12 @@
(ns auto-ap.graphql.clients (ns auto-ap.graphql.clients
(:require (:require [auto-ap.datomic :refer [conn]]
[amazonica.aws.s3 :as s3] [auto-ap.datomic.clients :as d-clients]
[auto-ap.datomic :refer [audit-transact conn]] [auto-ap.graphql.utils
[auto-ap.datomic.clients :as d-clients] :refer [->graphql <-graphql assert-admin attach-tracing-resolvers
[auto-ap.graphql.utils can-see-client? is-admin? result->page]]
:refer [->graphql [clojure.set :as set]
assert-admin [com.brunobonacci.mulog :as mu]
attach-tracing-resolvers [datomic.api :as dc]))
can-see-client?
<-graphql
result->page
is-admin?]]
[auto-ap.routes.queries :as q]
[auto-ap.square.core3 :as square]
[auto-ap.utils :refer [heartbeat]]
[clj-time.coerce :as coerce]
[clojure.java.io :as io]
[clojure.set :as set]
[clojure.string :as str]
[com.brunobonacci.mulog :as mu]
[datomic.api :as dc]
[iol-ion.tx :refer [random-tempid]]
[mount.core :as mount]
[yang.scheduler :as scheduler]
[auto-ap.solr :as solr])
(:import
(java.util UUID)
(org.apache.commons.codec.binary Base64)))
(defn assert-client-code-is-unique [code]
(when (seq (dc/q {:find '[?id]
:in ['$ '?code]
:where ['[?id :client/code ?code]]}
(dc/db conn) code))
(throw (ex-info "Client is not unique" {:validation-error (str "Client code '" code "' is not unique.")}))))
(defn upload-signature-data [signature-data]
(let [prefix "data:image/jpeg;base64,"]
(when signature-data
(when-not (str/starts-with? signature-data prefix)
(throw (ex-info "Invalid signature image" {:validation-error (str "Invalid signature image.")})))
(let [signature-id (str (UUID/randomUUID))
raw-bytes (Base64/decodeBase64 (subs signature-data (count prefix)))]
(s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env)
:key (str signature-id ".jpg")
:input-stream (io/make-input-stream raw-bytes {})
:metadata {:content-type "image/jpeg"
:content-length (count raw-bytes)}
:canned-acl "public-read")
(str "https://integreat-signature-images.s3.amazonaws.com/" signature-id ".jpg")))))
(defn assert-no-shared-transaction-sources [client-code txes]
(let [new-db (:db-after (dc/with (dc/db conn)
txes))]
(when (seq (->> (dc/q '[:find ?src (count ?ba)
:in $ ?c
:where [?c :client/bank-accounts ?ba]
(or
[?ba :bank-account/intuit-bank-account ?src]
[?ba :bank-account/plaid-account ?src]
[?ba :bank-account/yodlee-account-id ?src])]
new-db [:client/code client-code])
(filter (fn [[_ cnt]]
(> cnt 1)))))
(throw (ex-info "Cannot reuse yodlee/plaid/intuit account" {:validation-error (str "Cannot reuse yodlee/plaid/intuit account")})))))
(defn edit-client [context {:keys [edit_client]} _]
(assert-admin (:id context))
(when-not (:id edit_client)
(assert-client-code-is-unique (:code edit_client)))
(let [client (when (:id edit_client) (d-clients/get-by-id (:id edit_client)))
id (or (:db/id client) "new-client")
signature-file (upload-signature-data (:signature_data edit_client))
client-code (if (str/blank? (:client/code client))
(:code edit_client)
(:client/code client))
updated-entity (cond-> {:db/id id
:client/code client-code
:client/name (:name edit_client)
:client/matches (:matches edit_client)
:client/email (:email edit_client)
:client/locked-until (some-> (:locked_until edit_client) (coerce/to-date))
:client/locations (filter identity (:locations edit_client))
:client/week-a-debits (:week_a_debits edit_client)
:client/week-a-credits (:week_a_credits edit_client)
:client/week-b-debits (:week_b_debits edit_client)
:client/square-auth-token (:square_auth_token edit_client)
:client/square-locations (map
(fn [sl]
{:db/id (or (:id sl) (random-tempid))
:square-location/client-location (:client_location sl)})
(:square_locations edit_client))
:client/emails (map (fn [e]
{:db/id (or (:id e)
(random-tempid))
:email-contact/email (:email e)
:email-contact/description (:description e)})
(:emails edit_client))
:client/feature-flags (:feature_flags edit_client)
:client/ezcater-locations (map
(fn [el]
{:db/id (or (:id el) (random-tempid))
:ezcater-location/location (:location el)
:ezcater-location/caterer (:caterer el)})
(:ezcater_locations edit_client))
:client/week-b-credits (:week_b_credits edit_client)
:client/location-matches (->> (:location_matches edit_client)
(filter (fn [lm] (and (:location lm) (:match lm))))
(map (fn [lm] {:db/id (or (:id lm) (random-tempid))
:location-match/location (:location lm)
:location-match/matches [(:match lm)]})))
:client/address (when (seq (filter identity (vals (:address edit_client))))
{:db/id (or (:id (:address edit_client)) (random-tempid))
:address/street1 (:street1 (:address edit_client))
:address/street2 (:street2 (:address edit_client))
:address/city (:city (:address edit_client))
:address/state (:state (:address edit_client))
:address/zip (:zip (:address edit_client))})
:client/bank-accounts (map (fn [ba]
{:db/id (or (:id ba) (random-tempid))
:bank-account/code (:code ba)
:bank-account/bank-name (:bank_name ba)
:bank-account/bank-code (:bank_code ba)
:bank-account/start-date (-> (:start_date ba) (coerce/to-date))
:bank-account/routing (:routing ba)
:bank-account/include-in-reports (:include_in_reports ba)
:bank-account/name (:name ba)
:bank-account/visible (:visible ba)
:bank-account/number (:number ba)
:bank-account/check-number (:check_number ba)
:bank-account/numeric-code (:numeric_code ba)
:bank-account/sort-order (:sort_order ba)
:bank-account/locations (:locations ba)
:bank-account/use-date-instead-of-post-date? (boolean (:use_date_instead_of_post_date ba))
:bank-account/yodlee-account-id (:yodlee_account_id ba)
:bank-account/type (keyword "bank-account-type" (name (:type ba)))
:bank-account/yodlee-account (when (:yodlee_account ba)
[:yodlee-account/id (:yodlee_account ba)])
:bank-account/plaid-account (:plaid_account ba)
:bank-account/intuit-bank-account (:intuit_bank_account ba)})
(:bank_accounts edit_client))}
signature-file (assoc :client/signature-file signature-file))
_ (mu/log ::upserting :up updated-entity)
_ (assert-no-shared-transaction-sources client-code [[:upsert-entity updated-entity]])
result (audit-transact [[:upsert-entity updated-entity]] (:id context))]
(when (:square_auth_token edit_client)
@(square/upsert-locations (-> result :tempids (get id) (or id) d-clients/get-by-id)))
(let [updated-client (-> result :tempids (get id) (or id) d-clients/get-by-id)]
(when (:client/name updated-client)
(solr/index-documents-raw solr/impl "clients"
[{"id" (:db/id updated-client)
"name" (conj (or (:client/matches updated-client) [])
(:client/name updated-client))
"code" (:client/code updated-client)
"exact" (map str/upper-case (conj (or (:client/matches updated-client) [])
(:client/name updated-client)))}]))
(-> updated-client
(update :client/bank-accounts
(fn [bas]
(map #(set/rename-keys % {:bank-account/use-date-instead-of-post-date? :use-date-instead-of-post-date}) bas)))
(update :client/location-matches
(fn [lms]
(mapcat (fn [lm]
(map (fn [m]
{:location-match/match m
:location-match/location (:location-match/location lm)})
(:location-match/matches lm)))
lms)))
->graphql))))
(defn refresh-all-current-balance [] (defn refresh-all-current-balance []
(mu/with-context {:source "current-balance-refresh"} (mu/with-context {:source "current-balance-refresh"}
@@ -238,201 +68,11 @@
bank-accounts))))))] bank-accounts))))))]
(result->page clients clients-count :clients (:filters args)))) (result->page clients clients-count :clients (:filters args))))
(def sales-summary-query
"[:find ?d4 (sum ?total) (sum ?tax) (sum ?tip) (sum ?service-charge) (sum ?discount) (sum ?returns)
:with ?s
:in $
:where
[(ground (iol-ion.query/recent-date 120)) ?min-d]
[(ground #inst \"2040-01-01\") ?max-d]
[?c :client/code \"%s\"]
[(iol-ion.query/sales-orders-in-range $ ?c ?min-d ?max-d) [?s ...]]
[?s :sales-order/date ?d]
[?s :sales-order/total ?total]
[?s :sales-order/tax ?tax]
[?s :sales-order/tip ?tip]
[?s :sales-order/service-charge ?service-charge]
[?s :sales-order/returns ?returns]
[?s :sales-order/discount ?discount]
[(iol-ion.query/excel-date ?d) ?d4]
]")
(def sales-category-query
"[:find ?d4 ?n ?n2 (sum ?total) (sum ?tax) (sum ?discount)
:with ?s ?li
:in $
:where
[(ground (iol-ion.query/recent-date 120)) ?min-d]
[(ground #inst \"2040-01-01\") ?max-d]
[?c :client/code \"%s\"]
[(iol-ion.query/sales-orders-in-range $ ?c ?min-d ?max-d) [?s ...]]
[?s :sales-order/date ?d]
[?s :sales-order/line-items ?li]
[?li :order-line-item/category ?n]
[(get-else $ ?li :order-line-item/item-name \"\") ?n2]
[?li :order-line-item/total ?total]
[?li :order-line-item/tax ?tax]
[?li :order-line-item/discount ?discount]
[(iol-ion.query/excel-date ?d) ?d4]]")
(def expected-deposits-query
"[:find ?d4 ?t ?f
:in $
:where
[(ground (iol-ion.query/recent-date 120)) ?min-d]
[?c :client/code \"%s\"]
[?s :expected-deposit/client ?c]
[?s :expected-deposit/sales-date ?date]
[(>= ?date ?min-d)]
[?s :expected-deposit/total ?t]
[?s :expected-deposit/fee ?f]
[(iol-ion.query/excel-date ?date) ?d4]
]")
(def tenders-query
"[:find ?d4 ?type ?p2 (sum ?total) (sum ?tip)
:with ?charge
:in $
:where
[(ground (iol-ion.query/recent-date 120)) ?min-d]
[?c :client/code \"%s\"]
[?s :sales-order/client ?c]
[?s :sales-order/date ?date]
[(>= ?date ?min-d)]
[?s :sales-order/charges ?charge]
[?charge :charge/type-name ?type]
[?charge :charge/total ?total]
[?charge :charge/tip ?tip]
[(get-else $ ?charge :charge/processor :na) ?ccp]
[(get-else $ ?ccp :db/ident :na) ?p]
[(name ?p) ?p2]
[(iol-ion.query/excel-date ?date) ?d4]
]")
(def tenders2-query
"[:find ?d4 ?type ?p2 (sum ?total) (sum ?tip)
:with ?charge
:in $
:where
[(ground (iol-ion.query/recent-date 120)) ?min-d]
[?charge :charge/date ?date]
[(>= ?date ?min-d)]
[?charge :charge/client ?c]
[?c :client/code \"%s\"]
[?charge :charge/type-name ?type]
[?charge :charge/total ?total]
[?charge :charge/tip ?tip]
(or
(and [_ :expected-deposit/charges ?charge ]
[(ground :settlement) ?ccp]
[(ground :settlement) ?p])
(and
(not [_ :expected-deposit/charges ?charge])
[(get-else $ ?charge :charge/processor :na) ?ccp]
[(get-else $ ?ccp :db/ident :na) ?p]
))
[(name ?p) ?p2]
[(iol-ion.query/excel-date ?date) ?d4]]
" )
(def refunds-query
"[:find ?d4 ?t (sum ?total) (sum ?fee)
:with ?r
:in $
:where
[(ground (iol-ion.query/recent-date 120)) ?min-d]
[?r :sales-refund/client [:client/code \"%s\"]]
[?r :sales-refund/date ?date]
[(>= ?date ?min-d)]
[?r :sales-refund/total ?total]
[?r :sales-refund/fee ?fee]
[?r :sales-refund/type ?t]
[(iol-ion.query/excel-date ?date) ?d4]
]")
(def cash-drawer-shift-query
"[:find ?d4 (sum ?paid-in) (sum ?paid-out) (sum ?expected-cash) (sum ?opened-cash)
:with ?cds
:in $
:where
[?cds :cash-drawer-shift/date ?date]
[(ground (iol-ion.query/recent-date 120)) ?min-d]
[(>= ?date ?min-d)]
[?cds :cash-drawer-shift/client [:client/code \"%s\"]]
[?cds :cash-drawer-shift/paid-in ?paid-in]
[?cds :cash-drawer-shift/paid-out ?paid-out]
[?cds :cash-drawer-shift/expected-cash ?expected-cash]
[?cds :cash-drawer-shift/opened-cash ?opened-cash]
[(iol-ion.query/excel-date ?date) ?d4]]")
(defn setup-sales-queries-impl [client-id]
(let [{client-code :client/code feature-flags :client/feature-flags} (dc/pull (dc/db conn) '[:client/code :client/feature-flags] client-id)
is-new-square? ((set feature-flags) "new-square")]
(q/put-query (str (UUID/randomUUID))
(format sales-summary-query client-code)
(str "sales query for " client-code)
(str client-code "-sales-summary")
[:client/code client-code]
)
(q/put-query (str (UUID/randomUUID))
(format sales-category-query client-code)
(str "sales category query for " client-code)
(str client-code "-sales-category")
[:client/code client-code]
)
(q/put-query (str (UUID/randomUUID))
(format expected-deposits-query client-code)
(str "expected deposit query for " client-code)
(str client-code "-expected-deposit")
[:client/code client-code]
)
(q/put-query (str (UUID/randomUUID))
(format (if is-new-square? tenders2-query tenders-query) client-code)
(str "tender query for " client-code)
(str client-code "-tender")
[:client/code client-code]
)
(q/put-query (str (UUID/randomUUID))
(format refunds-query client-code)
(str "refunds query for " client-code)
(str client-code "-refund")
[:client/code client-code])
(q/put-query (str (UUID/randomUUID))
(format cash-drawer-shift-query client-code)
(str "cash drawer shift query for " client-code)
(str client-code "-cash-drawer-shift")
[:client/code client-code])
(let [sales-summary-id (:saved-query/guid (dc/pull (dc/db conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-sales-summary")]))
sales-category-id (:saved-query/guid (dc/pull (dc/db conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-sales-category")]))
expected-deposit-id (:saved-query/guid (dc/pull (dc/db conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-expected-deposit")]))
tender-id (:saved-query/guid (dc/pull (dc/db conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-tender")]))
refund-id (:saved-query/guid (dc/pull (dc/db conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-refund")]))
cash-drawer-shift-id (:saved-query/guid (dc/pull (dc/db conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-cash-drawer-shift")]))]
{:message (str/join "\n"
[
(str "For " client-code ":")
(str "Sales: " "https://app.integreatconsult.com/api/queries/" sales-summary-id "/results/json")
(str "Sales Category: " "https://app.integreatconsult.com/api/queries/" sales-category-id "/results/json")
(str "Expected Deposits: " "https://app.integreatconsult.com/api/queries/" expected-deposit-id "/results/json")
(str "Tenders: " "https://app.integreatconsult.com/api/queries/" tender-id "/results/json")
(str "Refund: " "https://app.integreatconsult.com/api/queries/" refund-id "/results/json")
(str "Cash Drawer Shift: " "https://app.integreatconsult.com/api/queries/" cash-drawer-shift-id "/results/json")])})))
(defn setup-sales-queries [context args _]
(assert-admin (:id context))
(setup-sales-queries-impl (:client_id args)))
(defn reset-all-queries []
(doseq [[c] (dc/q '[:find ?c :where [?c :client/code]] (dc/db conn))]
(setup-sales-queries-impl c)))
(def objects (def objects
@@ -535,82 +175,15 @@
:resolve :get-client-page}}) :resolve :get-client-page}})
(def mutations (def mutations
{:edit_client {:type :client {})
:args {:edit_client {:type :edit_client}}
:resolve :mutation/edit-client}
:setup_sales_queries {:type :message
:args {:client_id {:type :id}}
:resolve :mutation/setup-sales-queries}})
(def input-objects (def input-objects
{:edit_location_match {:fields {:location {:type 'String} { :client_filters
:match {:type 'String}
:id {:type :id}}}
:client_filters
{:fields {:code {:type 'String} {:fields {:code {:type 'String}
:name_like {:type 'String} :name_like {:type 'String}
:start {:type 'Int} :start {:type 'Int}
:per_page {:type 'Int} :per_page {:type 'Int}
:sort {:type '(list :sort_item)}}} :sort {:type '(list :sort_item)}}} })
:edit_square_location {:fields {:client_location {:type 'String}
:id {:type :id}}}
:edit_ezcater_location {:fields {:location {:type 'String}
:caterer {:type :id}
:id {:type :id}}}
:edit_forecasted_transaction {:fields {:identifier {:type 'String}
:id {:type :id}
:day_of_month {:type 'Int}
:amount {:type :money}}}
:edit_email_contact {:fields {:id {:type :id}
:email {:type 'String}
:description {:type 'String}}}
:edit_client {:fields {:id {:type :id}
:name {:type 'String}
:locked_until {:type :iso_date}
:code {:type 'String}
:square_auth_token {:type 'String}
:feature_flags {:type '(list String)}
:signature_data {:type 'String}
:email {:type 'String}
:emails {:type '(list :edit_email_contact)}
:week_a_credits {:type :money}
:week_a_debits {:type :money}
:week_b_credits {:type :money}
:week_b_debits {:type :money}
:address {:type :add_address}
:locations {:type '(list String)}
:matches {:type '(list String)}
:location_matches {:type '(list :edit_location_match)}
:square_locations {:type '(list :edit_square_location)}
:ezcater_locations {:type '(list :edit_ezcater_location)}
:bank_accounts {:type '(list :edit_bank_account)}
:forecasted_transactions {:type '(list :edit_forecasted_transaction)}}}
:edit_bank_account
{:fields {:id {:type :id}
:code {:type 'String}
:type {:type :bank_account_type}
:start_date {:type :iso_date}
:number {:type 'String}
:check_number {:type 'Int}
:numeric_code {:type 'Int}
:visible {:type 'Boolean}
:include_in_reports {:type 'Boolean}
:sort_order {:type 'Int}
:name {:type 'String}
:bank_code {:type 'String}
:routing {:type 'String}
:bank_name {:type 'String}
:locations {:type '(list String)}
:yodlee_account_id {:type 'Int}
:use_date_instead_of_post_date {:type 'Boolean}
:intuit_bank_account {:type :id}
:plaid_account {:type :id}
:yodlee_account {:type 'Int}}}})
(def enums (def enums
{:bank_account_type {:values [{:enum-value :check} {:bank_account_type {:values [{:enum-value :check}
@@ -620,9 +193,7 @@
(def resolvers (def resolvers
{:get-client get-client {:get-client get-client
:get-admin-client get-admin-client :get-admin-client get-admin-client
:get-client-page get-client-page :get-client-page get-client-page })
:mutation/edit-client edit-client
:mutation/setup-sales-queries setup-sales-queries})
(defn attach [schema] (defn attach [schema]

View File

@@ -217,7 +217,7 @@
(pull-many (dc/db conn) (pull-many (dc/db conn)
d-clients/full-read))] d-clients/full-read))]
(mu/with-context {:clients (map :client/code clients)} (mu/with-context {:clients (take 10 (map :client/code clients))}
(handler (assoc request (handler (assoc request
:clients clients :clients clients
:client (when (= 1 (count clients)) :client (when (= 1 (count clients))
@@ -273,7 +273,7 @@
(handler request))) (handler request)))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(def app (defonce app
(-> route-handler (-> route-handler
(wrap-hx-current-url-params) (wrap-hx-current-url-params)
(wrap-guess-route) (wrap-guess-route)
@@ -293,7 +293,7 @@
(byte-array (byte-array
[42, 52, -31, 105, -126, -33, -118, -69, -82, -59, -15, -69, -38, 103, -102, -1])} )}) [42, 52, -31, 105, -126, -33, -118, -69, -82, -59, -15, -69, -38, 103, -102, -1])} )})
(wrap-reload) #_(wrap-reload)
(wrap-params) (wrap-params)
(mp/wrap-multipart-params) (mp/wrap-multipart-params)
(wrap-edn-params) (wrap-edn-params)

View File

@@ -1,5 +1,5 @@
(ns auto-ap.jobs.bulk-journal-import (ns auto-ap.jobs.bulk-journal-import
(:gen-class) #_(:gen-class)
(:require (:require
[amazonica.aws.s3 :as s3] [amazonica.aws.s3 :as s3]
[auto-ap.graphql.ledger :refer [import-ledger]] [auto-ap.graphql.ledger :refer [import-ledger]]

View File

@@ -1,5 +1,5 @@
(ns auto-ap.jobs.close-auto-invoices (ns auto-ap.jobs.close-auto-invoices
(:gen-class) #_(:gen-class)
(:require (:require
[auto-ap.datomic :refer [conn]] [auto-ap.datomic :refer [conn]]
[auto-ap.jobs.core :refer [execute]] [auto-ap.jobs.core :refer [execute]]

View File

@@ -1,5 +1,5 @@
(ns auto-ap.jobs.current-balance-cache (ns auto-ap.jobs.current-balance-cache
(:gen-class) #_(:gen-class)
(:require (:require
[auto-ap.graphql.clients :as clients] [auto-ap.graphql.clients :as clients]
[auto-ap.jobs.core :refer [execute]])) [auto-ap.jobs.core :refer [execute]]))

View File

@@ -1,5 +1,5 @@
(ns auto-ap.jobs.ezcater-upsert (ns auto-ap.jobs.ezcater-upsert
(:gen-class) #_(:gen-class)
(:require (:require
[auto-ap.jobs.core :refer [execute]] [auto-ap.jobs.core :refer [execute]]
[auto-ap.ezcater.core :as ezcater])) [auto-ap.ezcater.core :as ezcater]))

View File

@@ -1,5 +1,5 @@
(ns auto-ap.jobs.intuit (ns auto-ap.jobs.intuit
(:gen-class) #_(:gen-class)
(:require (:require
[auto-ap.import.intuit :as intuit] [auto-ap.import.intuit :as intuit]
[auto-ap.jobs.core :refer [execute]])) [auto-ap.jobs.core :refer [execute]]))

View File

@@ -1,5 +1,5 @@
(ns auto-ap.jobs.ledger-reconcile (ns auto-ap.jobs.ledger-reconcile
(:gen-class) #_(:gen-class)
(:require (:require
[auto-ap.jobs.core :refer [execute]] [auto-ap.jobs.core :refer [execute]]
[auto-ap.ledger :as ledger])) [auto-ap.ledger :as ledger]))

View File

@@ -1,5 +1,5 @@
(ns auto-ap.jobs.load-historical-sales (ns auto-ap.jobs.load-historical-sales
(:gen-class) #_(:gen-class)
(:require (:require
[auto-ap.datomic :refer [conn]] [auto-ap.datomic :refer [conn]]
[auto-ap.jobs.core :refer [execute]] [auto-ap.jobs.core :refer [execute]]

View File

@@ -1,5 +1,5 @@
(ns auto-ap.jobs.plaid (ns auto-ap.jobs.plaid
(:gen-class) #_(:gen-class)
(:require (:require
[auto-ap.import.plaid :as plaid] [auto-ap.import.plaid :as plaid]
[auto-ap.jobs.core :refer [execute]])) [auto-ap.jobs.core :refer [execute]]))

View File

@@ -1,5 +1,5 @@
(ns auto-ap.jobs.register-invoice-import (ns auto-ap.jobs.register-invoice-import
(:gen-class) #_(:gen-class)
(:require (:require
[amazonica.aws.s3 :as s3] [amazonica.aws.s3 :as s3]
[auto-ap.datomic :refer [audit-transact conn pull-attr]] [auto-ap.datomic :refer [audit-transact conn pull-attr]]

View File

@@ -1,5 +1,5 @@
(ns auto-ap.jobs.square (ns auto-ap.jobs.square
(:gen-class) #_(:gen-class)
(:require (:require
[auto-ap.jobs.core :refer [execute]] [auto-ap.jobs.core :refer [execute]]
[auto-ap.square.core3 :as square3])) [auto-ap.square.core3 :as square3]))

View File

@@ -1,5 +1,5 @@
(ns auto-ap.jobs.vendor-usages (ns auto-ap.jobs.vendor-usages
(:gen-class) #_(:gen-class)
(:require (:require
[auto-ap.datomic :refer [conn]] [auto-ap.datomic :refer [conn]]
[auto-ap.jobs.core :refer [execute]] [auto-ap.jobs.core :refer [execute]]

View File

@@ -1,5 +1,5 @@
(ns auto-ap.jobs.yodlee2 (ns auto-ap.jobs.yodlee2
(:gen-class) #_(:gen-class)
(:require (:require
[auto-ap.import.yodlee2 :as yodlee2] [auto-ap.import.yodlee2 :as yodlee2]
[auto-ap.jobs.core :refer [execute]] [auto-ap.jobs.core :refer [execute]]

View File

@@ -1,5 +1,5 @@
(ns auto-ap.server (ns auto-ap.server
(:gen-class) #_(:gen-class)
(:require (:require
[auto-ap.handler :refer [app]] [auto-ap.handler :refer [app]]
[auto-ap.jobs.restore-from-backup :as job-restore-from-backup] [auto-ap.jobs.restore-from-backup :as job-restore-from-backup]

View File

@@ -36,14 +36,20 @@
(sort-by :created-at) (sort-by :created-at)
reverse)) reverse))
(defn is-background-job? [task]
(defn is-background-job?
"This function checks whether a given task is a background job.
It does this by checking the environment of the task's container definitions for an environment variable
with the name 'INTEGREAT_JOB'. If such a variable exists, the function returns true, otherwise, it returns false.
Parameters: task - a map representing the task to be checked.
Returns: true if the task is a background job, false otherwise."
[task]
(->> task (->> task
:task-definition :task-definition
:container-definitions :container-definitions
(mapcat :environment) (mapcat :environment)
(filter (comp #{"INTEGREAT_JOB"} :name)) (filter (comp #{"INTEGREAT_JOB"} :name))
seq)) seq))
(defn task-definition->job-name [task-definition] (defn task-definition->job-name [task-definition]
(->> (:container-definitions task-definition) (->> (:container-definitions task-definition)
(mapcat :environment) (mapcat :environment)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
(ns auto-ap.ssr.common-handlers
(:require [auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.utils :refer [html-response wrap-schema-enforce]]))
(defn add-new-entity-handler
([path render-fn] (add-new-entity-handler path
render-fn
(fn default-data [base _]
base)))
([path render-fn build-data]
(-> (fn new-entity [{{:keys [index]} :query-params :as request}]
(html-response
(fc/start-form-with-prefix (conj path (or index 0))
(build-data {:db/id (str (java.util.UUID/randomUUID))
:new? true} request)
[]
(render-fn fc/*current* request))))
(wrap-schema-enforce :query-schema [:map
[:index {:optional true
:default 0} [nat-int? {:default 0}]]]))))
(defn add-new-primitive-handler [path default-value render-fn]
(-> (fn new-location-match [{{:keys [index]} :query-params}]
(html-response
(fc/start-form-with-prefix (conj path (or index 0))
default-value
[]
(render-fn fc/*current*))))
(wrap-schema-enforce :query-schema [:map
[:index {:optional true
:default 0} [nat-int? {:default 0}]]])))

View File

@@ -1,87 +1,162 @@
(ns auto-ap.ssr.company (ns auto-ap.ssr.company
(:require (:require [amazonica.aws.s3 :as s3]
[auto-ap.datomic :refer [conn pull-attr]] [auto-ap.datomic :refer [conn pull-attr]]
[auto-ap.datomic.clients :refer [full-read]] [auto-ap.datomic.clients :refer [full-read]]
[auto-ap.solr :as solr] [auto-ap.permissions :as permissions]
[auto-ap.ssr-routes :as ssr-routes] [auto-ap.solr :as solr]
[auto-ap.ssr.components :as com] [auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.svg :as svg] [auto-ap.ssr.components :as com]
[auto-ap.ssr.ui :refer [base-page]] [auto-ap.ssr.hx :as hx]
[auto-ap.ssr.utils :refer [html-response]] [auto-ap.ssr.svg :as svg]
[bidi.bidi :as bidi] [auto-ap.ssr.ui :refer [base-page]]
[cemerick.url :as url] [auto-ap.ssr.utils :refer [html-response]]
[clojure.string :as str] [bidi.bidi :as bidi]
[config.core :refer [env]] [cemerick.url :as url]
[datomic.api :as dc] [clojure.java.io :as io]
[ring.middleware.json :refer [wrap-json-response]])) [clojure.string :as str]
[config.core :refer [env]]
[datomic.api :as dc]
[ring.middleware.json :refer [wrap-json-response]])
(:import [java.util UUID]
(org.apache.commons.codec.binary Base64)))
(defn please-select-client-screen* [] (defn please-select-client-screen* []
[:div.grid.grid-cols-3 [:div.grid.grid-cols-3
(com/content-card {} (com/content-card {}
[:div.col-span-1.p-4 {:class "p-4 sm:p-6"} [:div.col-span-1.p-4 {:class "p-4 sm:p-6"}
[:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"}
"Please select a company"] "Please select a company"]])])
])])
(defn main-content* [{:keys [client]}] (defn signature [request]
(let [signature-file (pull-attr (dc/db conn) :client/signature-file (:db/id (:client request)))]
(com/content-card {:class " w-[748px]"
:hx-target "this"
:hx-swap "outerHTML"}
[:div.col-span-1.p-4 {:class "p-4 sm:p-6 space-y-4 overflow-visible "
:x-data (hx/json {"signature" nil
"editing" false
"existing" (boolean signature-file)})
:hx-put (bidi/path-for ssr-routes/only-routes
:company-update-signature)
:hx-trigger "accepted"
:hx-vals "js:{signatureData: event.detail.signatureData}"}
[:h3 {:class "mb-4 text-xl font-semibold dark:text-white"}
"Signature"]
[:div.htmx-indicator
[:div.bg-gray-100.flex.items-center.text-green-500.justify-center.rounded.rounded-lg.border.border-gray-400 {:style {:width "696px" :height "261px"}}
(svg/spinner {:class "w-4 h-4 text-primary-300"})
[:div.ml-3 "Loading..."]]]
[:div.htmx-indicator-hidden
(when signature-file
[:img.rounded.rounded-lg.border.border-gray-300.bg-gray-50 {:src signature-file
:width 696
:height 261
:x-show "existing && !editing"}])
[:canvas.rounded.rounded-lg.border.border-gray-300
{:style {:width 696
:height 261}
:x-init "signature= new SignaturePad($el); signature.off()"
":class" "editing ? 'bg-white' : 'bg-gray-50' "
:width 696
:height 261
:x-show "existing ? editing: true"}]]
[:div.flex.gap-2.justify-end
(com/button {:color :primary
:x-show "!editing"
"@click" "signature.clear(); signature.on(); editing=true;"}
"New signature")
(com/button {:color :primary
:x-show "editing"
"@click" "signature.clear();"}
"Clear")
(com/button {:color :primary
"@click" "$data.signatureData=signature.toDataURL('image/png'); signature.off(); editing=false; $dispatch('accepted', {signatureData: $data.signatureData}) "
:x-show "editing"}
"Accept")]])))
(defn upload-signature-data [{{:strs [signatureData]} :form-params client :client :as request}]
(let [prefix "data:image/png;base64,"]
(when signatureData
(when-not (str/starts-with? signatureData prefix)
(throw (ex-info "Invalid signature image" {:validation-error (str "Invalid signature image.")})))
(let [signature-id (str (UUID/randomUUID))
raw-bytes (Base64/decodeBase64 (subs signatureData (count prefix)))]
(s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env)
:key (str signature-id ".png")
:input-stream (io/make-input-stream raw-bytes {})
:metadata {:content-type "image/png"
:content-length (count raw-bytes)}
:canned-acl "public-read")
@(dc/transact conn [{:db/id (:db/id client)
:client/signature-file (str "https://integreat-signature-images.s3.amazonaws.com/" signature-id ".png")}])
(html-response
(signature request))))))
(defn main-content* [{:keys [client identity] :as request}]
(if-not client (if-not client
(please-select-client-screen*) (please-select-client-screen*)
(let [client (dc/pull (dc/db conn) full-read (:db/id client))] (let [client (dc/pull (dc/db conn) full-read (:db/id client))]
[:div.grid.grid-cols-3.gap-4 [:div
(com/content-card {} [:div.grid.grid-cols-3.gap-4
[:div.col-span-1.p-4 {:class "p-4 sm:p-6"} (com/content-card {}
[:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} [:div.col-span-1.p-4 {:class "p-4 sm:p-6"}
(:client/name client)] [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"}
(when-let [address (-> client :client/address)] (:client/name client)]
[:div.flex.flex-col.gap-1.text-lg.dark:text-white.text-gray-700 (when-let [address (-> client :client/address)]
[:p (-> address :address/street1)] [:div.flex.flex-col.gap-1.text-lg.dark:text-white.text-gray-700
[:p (-> address :address/street2)] [:p (-> address :address/street1)]
[:p (-> address :address/city) " " [:p (-> address :address/street2)]
(-> address :address/state) ", " [:p (-> address :address/city) " "
(-> address :address/zip)]])] (-> address :address/state) ", "
) (-> address :address/zip)]])])
(com/content-card {} (com/content-card {}
[:div.col-span-1.p-4 {:class "p-4 sm:p-6"} [:div.col-span-1.p-4 {:class "p-4 sm:p-6"}
[:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"}
"Downloads"] "Downloads"]
[:a {:href (str (assoc (url/url (str (:base-url env) "/api/vendors/company/export")) [:a {:href (str (assoc (url/url (str (:base-url env) "/api/vendors/company/export"))
:query {"client" (:client/code client)}))} :query {"client" (:client/code client)}))}
(com/button {:color :primary} (com/button {:color :primary}
"Download vendor list" "Download vendor list"
(com/button-icon {} svg/download))]])]))) (com/button-icon {} svg/download))]])
[:div]]
(when (permissions/can? identity {:client client :subject :signature :activity :edit})
(signature request))])))
(defn page [{:keys [identity matched-route] :as request}] (defn page [{:keys [identity matched-route] :as request}]
(base-page (base-page
request request
(com/page {:nav (com/company-aside-nav) (com/page {:nav (com/company-aside-nav)
:client-selection (:client-selection (:session request)) :client-selection (:client-selection (:session request))
:client (:client request) :client (:client request)
:identity (:identity request) :identity (:identity request)
:app-params { :app-params {:hx-get (bidi/path-for ssr-routes/only-routes
:hx-get (bidi/path-for ssr-routes/only-routes :company)
:company) :hx-trigger "clientSelected from:body"
:hx-trigger "clientSelected from:body" :hx-select "#app-contents"
:hx-select "#app-contents" :hx-swap "outerHTML swap:300ms"}}
:hx-swap "outerHTML swap:300ms"}} (com/breadcrumbs {}
(com/breadcrumbs {} [:a {:href (bidi/path-for ssr-routes/only-routes
[:a {:href (bidi/path-for ssr-routes/only-routes :company)}
:company)} "My Company"])
"My Company"]) (main-content* request))
(main-content* {:client (:client request)})) "My Company"))
"My Company"))
(defn search [{:keys [clients query-params]}] (defn search [{:keys [clients query-params]}]
(let [valid-client-ids (set (map :db/id clients)) (let [valid-client-ids (set (map :db/id clients))
name-like-ids (when (not-empty (get query-params "q")) name-like-ids (when (not-empty (get query-params "q"))
(set (map (comp #(Long/parseLong %) :id) (set (map (comp #(Long/parseLong %) :id)
(solr/query solr/impl "clients" (solr/query solr/impl "clients"
{"query" (format "_text_:(%s*)" (str/upper-case (solr/escape (get query-params "q")))) {"query" (format "_text_:(%s*)" (str/upper-case (solr/escape (get query-params "q"))))
"fields" "id" "fields" "id"
"limit" 300})))) "limit" 300}))))
valid-clients (for [n name-like-ids valid-clients (for [n name-like-ids
:when (valid-client-ids n)] :when (valid-client-ids n)]
{"value" n "label" (pull-attr (dc/db conn) :client/name n)} {"value" n "label" (pull-attr (dc/db conn) :client/name n)})]
)]
{:body (take 10 valid-clients)})) {:body (take 10 valid-clients)}))
(def search (wrap-json-response search)) (def search (wrap-json-response search))
@@ -109,13 +184,13 @@
(defn bank-account-typeahead* [{:keys [client-id name value]}] (defn bank-account-typeahead* [{:keys [client-id name value]}]
(if client-id (if client-id
(com/typeahead {:name name (com/typeahead {:name name
:class "w-96" :class "w-96"
:placeholder "Search..." :placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :bank-account-search :url (bidi/path-for ssr-routes/only-routes :bank-account-search
:db/id client-id) :db/id client-id)
:value value :value value
:value-fn (some-fn :db/id identity) :value-fn (some-fn :db/id identity)
:content-fn (some-fn :bank-account/name #(pull-attr (dc/db conn) :bank-account/name %))}) :content-fn (some-fn :bank-account/name #(pull-attr (dc/db conn) :bank-account/name %))})
[:span.text-xs.text-gray-500 "Please select a client before selecting a bank account." [:span.text-xs.text-gray-500 "Please select a client before selecting a bank account."
[:input {:type "hidden" [:input {:type "hidden"
:name name}]])) :name name}]]))

View File

@@ -11,8 +11,7 @@
[auto-ap.ssr.components.tags :as tags] [auto-ap.ssr.components.tags :as tags]
[auto-ap.ssr.components.paginator :as paginator] [auto-ap.ssr.components.paginator :as paginator]
[auto-ap.ssr.components.radio :as radio])) [auto-ap.ssr.components.radio :as radio]))
;; potemkin can be used here
(def breadcrumbs breadcrumbs/breadcrumbs-) (def breadcrumbs breadcrumbs/breadcrumbs-)
(def button buttons/button-) (def button buttons/button-)
(def validated-save-button buttons/validated-save-button-) (def validated-save-button buttons/validated-save-button-)
@@ -24,8 +23,7 @@
(def button-group-button buttons/group-button-) (def button-group-button buttons/group-button-)
(def modal dialog/modal-) (def modal dialog/modal-)
(def modal-card dialog/modal-card-) (def modal-card dialog/modal-card-)
(def stacked-modal-card dialog/stacked-modal-card-) (def modal-card-advanced dialog/modal-card-advanced-)
(def stacked-modal-card-2 dialog/stacked-modal-card-2-)
(def modal-header dialog/modal-header-) (def modal-header dialog/modal-header-)
(def modal-header-attachment dialog/modal-header-attachment-) (def modal-header-attachment dialog/modal-header-attachment-)
(def modal-body dialog/modal-body-) (def modal-body dialog/modal-body-)
@@ -70,12 +68,9 @@
(def data-grid-new-row data-grid/new-row-) (def data-grid-new-row data-grid/new-row-)
(defn link [params & children] (defn link [params & children]
(into [:a (update params :class str " font-medium text-blue-600 dark:text-blue-500 hover:underline ")] (into [:a (update params :class str " font-medium text-blue-600 dark:text-blue-500 hover:underline cursor-pointer")]
children)) children))
(def paginator paginator/paginator-) (def paginator paginator/paginator-)
(def data-grid-card data-grid/data-grid-card-) (def data-grid-card data-grid/data-grid-card-)

View File

@@ -8,8 +8,10 @@
[auto-ap.routes.admin.transaction-rules :as transaction-rules] [auto-ap.routes.admin.transaction-rules :as transaction-rules]
[auto-ap.ssr.hiccup-helper :as hh] [auto-ap.ssr.hiccup-helper :as hh]
[auto-ap.routes.admin.import-batch :as ib-routes] [auto-ap.routes.admin.import-batch :as ib-routes]
[auto-ap.routes.admin.clients :as ac-routes]
[auto-ap.routes.admin.excel-invoices :as ei-routes] [auto-ap.routes.admin.excel-invoices :as ei-routes]
[auto-ap.routes.admin.vendors :as v-routes])) [auto-ap.routes.admin.vendors :as v-routes]
[auto-ap.graphql.clients :as clients]))
(defn menu-button- [params & children] (defn menu-button- [params & children]
[:div [:div
@@ -199,7 +201,7 @@
[:li [:li
(menu-button- {:icon svg/restaurant (menu-button- {:icon svg/restaurant
:href (bidi/path-for client-routes/routes :admin-clients) :href (bidi/path-for ssr-routes/only-routes ::ac-routes/page)
:target "_new"} :target "_new"}
"Clients")] "Clients")]
[:li [:li

View File

@@ -176,7 +176,7 @@
(defn validated-save-button- [{:keys [errors class] :as params} & children] (defn validated-save-button- [{:keys [errors class] :as params} & children]
(button- (-> {:color (or (:color params) :primary) (button- (-> {:color (or (:color params) :primary)
:type "submit" :class (cond-> (or class "") :type "submit" :class (cond-> (or class "")
true (hh/add-class "w-32") true (hh/add-class "w-32")
(seq errors) (hh/add-class "animate-shake"))} (seq errors) (hh/add-class "animate-shake"))}

View File

@@ -1,13 +1,17 @@
(ns auto-ap.ssr.components.card (ns auto-ap.ssr.components.card
(:require [auto-ap.ssr.hiccup-helper :as hh] (:require [auto-ap.ssr.hiccup-helper :as hh]
[auto-ap.ssr.hx :as hx])) [auto-ap.ssr.hx :as hx]
[clojure.string :as str]))
(defn card- [params & children] (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")] (into [:div (update params :class
#(cond-> (or % "")
(not (str/includes? % "bg-")) (hh/add-class "dark:bg-gray-800 bg-white ")
true (hh/add-class "shadow-md sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 overflow-hidden")))]
children)) children))
(defn content-card- [params & children] (defn content-card- [params & children]
[:section {:class (hh/add-class " py-3 sm:py-5" (:class params))} [:section (merge params {:class (hh/add-class " py-3 sm:py-5" (:class params))})
[:div {:class "max-w-screen-2xl"} [:div {:class "max-w-screen-2xl"}
(into (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"}] [: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"}]

View File

@@ -3,36 +3,19 @@
[auto-ap.ssr.hiccup-helper :as hh] [auto-ap.ssr.hiccup-helper :as hh]
[auto-ap.ssr.hx :as hx])) [auto-ap.ssr.hx :as hx]))
(defn modal- [params & children]
(defn modal-
"This modal function is used to create a modal window with a stack that allows for transitioning between modals.
:params should include the following keys:
- :handle-unexpected-error? (default: true) - A boolean indicating whether to handle unexpected errors.
- :class (optional) - A string representing additional CSS classes to add to the modal.
&children should include the child components to be rendered within the modal."
[{:as params} & children]
[:div (-> params [:div (-> params
(assoc "@click.outside" "open=false" (assoc "@click.outside" "open=false")
:x-data (hx/json {:index 0 :hidingIndex -1 :unexpectedError false :transitioning false}) (dissoc :handle-unexpected-error?)
"x-on:htmx:response-error" "unexpectedError=true"
"x-on:htmx:before-request" "unexpectedError=false"
:x-ref "modalStack"
"@modalnext.window"
" $refs.modalStack.children[index].setAttribute('x-transition:leave-end', '-translate-x-full scale-0 opacity-0' );
$refs.modalStack.children[index + 1].setAttribute('x-transition:enter-start', 'translate-x-full scale-0 opacity-0' );
hidingIndex = index;
setTimeout(() => {index ++; transitioning=true; hidingIndex = -1; }, 150);
setTimeout(() => transitioning=false, 320)"
"@modalprevious.window"
" $refs.modalStack.children[index].setAttribute('x-transition:leave-end', 'translate-x-full scale-0 opacity-0' );
$refs.modalStack.children[index - 1].setAttribute('x-transition:enter-start', '-translate-x-full scale-0 opacity-0' );
hidingIndex = index;
setTimeout(() => { index --; hidingIndex = -1; transitioning=true; }, 150);
setTimeout(() => transitioning=false, 320)"
"@modalpop.window"
" $refs.modalStack.children[index].setAttribute('x-transition:leave-end', 'translate-x-full scale-0 opacity-0' );
$refs.modalStack.children[index - 1].setAttribute('x-transition:enter-start', '-translate-x-full scale-0 opacity-0' );
hidingIndex = index;
setTimeout(() => {index --; transitioning=true;}, 150);
setTimeout(() => { $refs.modalStack.removeChild($refs.modalStack.children[index+1]); hidingIndex=-1; }, 300);
setTimeout(() => transitioning=false, 320)
"
)
(update :class (fnil hh/add-class "") "w-full h-full modal-stack")) (update :class (fnil hh/add-class "") "w-full h-full modal-stack"))
children]) children])
@@ -40,11 +23,10 @@
[:div (update params [:div (update params
:class (fn [c] (-> c :class (fn [c] (-> c
(or "") (or "")
(hh/add-class "w-full p-4 h-full modal-card") (hh/add-class "w-full p-4 h-full modal-card"))))
)))
[:div {:class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content w-full flex flex-col h-full"} [:div {:class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content w-full flex flex-col h-full"}
[:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} header] [:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} header]
[:div {:class "px-6 space-y-6 overflow-y-scroll w-full shrink"} [:div {:class "px-6 py-2 space-y-6 overflow-y-scroll w-full shrink"}
#_[:div.bg-green-300.w-full.h-64 #_[:div.bg-green-300.w-full.h-64
"hello"] "hello"]
content] content]
@@ -55,28 +37,6 @@
[:div {:class "shrink-0"} [:div {:class "shrink-0"}
footer]])]]) footer]])]])
(defn stacked-modal-card- [index params header content footer]
[:div (merge params
{:class (hh/add-class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col h-full" (:class params ""))
:x-data (hx/json {:i index})
:x-show "index == i && hidingIndex != i"
"x-trap" "index == i && hidingIndex == -1 && !transitioning"
"x-transition:enter" "transition duration-150",
"x-transition:enter-end" "translate-x-0 scale-100 opacity-100",
"x-transition:leave" "transition duration-150",
"x-transition:leave-start" "translate-x-0 scale-100 opacity-100",
})
[:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} header] ;; todo componentize these
[:div {:class "px-6 space-y-6 overflow-y-scroll w-full shrink"}
content] ;; TODO componentize
(when footer [:div {:class "p-4"}
[:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex (hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"})
[:span {:class "w-2 h-2 bg-red-500 rounded-full"}]
[:span.px-2.py-0.5 "An unexpected error has occured. Integreat staff have been notified."]]
[:div {:class "shrink-0"}]footer])])
(defn modal-header- [params & children] (defn modal-header- [params & children]
[:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} [:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"}
children]) children])
@@ -92,21 +52,14 @@
(defn modal-footer- [params & children] (defn modal-footer- [params & children]
[:div {:class "p-4"} [:div {:class "p-4"}
[:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex (hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"}) [:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex
(hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"})
[:span {:class "w-2 h-2 bg-red-500 rounded-full"}] [:span {:class "w-2 h-2 bg-red-500 rounded-full"}]
[:span.px-2.py-0.5 "An unexpected error has occured. Integreat staff have been notified."]] [:span.px-2.py-0.5 "An unexpected error has occured. Integreat staff have been notified."]]
[:div {:class "shrink-0"}] [:div {:class "shrink-0"}]
children]) children])
(defn stacked-modal-card-2- [index params & children] (defn modal-card-advanced- [params & children]
[:div (merge params [:div (merge params
{:class (hh/add-class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col h-full" (:class params "")) {:class (hh/add-class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col h-full" (:class params "")) })
:x-data (hx/json {:i index})
:x-show "index == i && hidingIndex != i"
"x-trap" "index == i && hidingIndex == -1 && !transitioning"
"x-transition:enter" "transition duration-150",
"x-transition:enter-end" "translate-x-0 scale-100 opacity-100",
"x-transition:leave" "transition duration-150",
"x-transition:leave-start" "translate-x-0 scale-100 opacity-100",
})
children]) children])

View File

@@ -0,0 +1,349 @@
(ns auto-ap.ssr.components.multi-modal
(:require [auto-ap.ssr.components :as com]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
[auto-ap.ssr.utils
:refer [ html-response
assert-schema
main-transformer
modal-response
wrap-form-4xx-2
wrap-schema-enforce]]
[auto-ap.ssr.components.timeline :as timeline]
[bidi.bidi :as bidi]
[hiccup.util :as hu]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.hx :as hx]
[malli.core :as mc]
[hiccup2.core :as hiccup2]
[hiccup2.core :as hiccup]
[auto-ap.cursor :as cursor]
[malli.core :as m]
[auto-ap.logging :as alog])
(:import [auto_ap.cursor VecCursor]))
(def default-form-props {:hx-ext "response-targets"
:hx-swap "outerHTML"
:hx-target-400 "#form-errors .error-content"
:hx-trigger "submit"
:hx-target "this"
"x-trap" "true"
:class "h-full w-full" })
(defprotocol ModalWizardStep
(step-key [this])
(edit-path [this request])
(render-step [this request])
(step-schema [this])
(step-name [this]))
(defprotocol Initializable
(init-step-params [this request]))
(defprotocol Discardable
(can-discard? [this step-params])
(discard-changes [this request]))
(defn- init-step-params- [step request]
(if (satisfies? Initializable step)
(init-step-params step request)
{}))
(defprotocol LinearModalWizard
(hydrate-from-request [this request])
(get-current-step [this])
(navigate [this step-key])
(form-schema [this])
(steps [this])
(get-step [this step-key])
(render-wizard [this request])
(submit [this request]))
(defrecord MultiStepFormState [snapshot edit-path step-params])
(defn select-state [multi-form-state edit-path default]
(->MultiStepFormState (:snapshot multi-form-state)
edit-path
(or (get-in (:snapshot multi-form-state) edit-path)
default)))
(defn merge-multi-form-state [{:keys [snapshot edit-path step-params] :as multi-form-state}]
(let [cursor (cursor/cursor (or snapshot {}))
;; this hack makes sure that, in the event of a missing vector entry, will make sure to add it first
edit-cursor (cond-> cursor
(seq edit-path) (cursor/ensure-path! edit-path {})
(seq edit-path) (get-in edit-path {}))
_ (cursor/transact! edit-cursor (fn [spot]
(merge spot step-params)))]
(assoc multi-form-state
:snapshot @cursor
:edit-path []
:step-params @cursor)))
(def step-key-schema (mc/schema [:orn {:decode/arbitrary clojure.edn/read-string
:encode/arbitrary pr-str}
[:sub-step [:cat :keyword [:or :int :string]]]
[:step :keyword]]))
(def encode-step-key
(m/-instrument {:schema [:=> [:cat step-key-schema] :any]}
(fn encode-step-key [sk]
(mc/encode step-key-schema sk main-transformer))))
(defn render-timeline [linear-wizard current-step validation-route]
(let [step-names (map #(step-name (get-step linear-wizard %)) (steps linear-wizard))
active-index (.indexOf step-names (step-name current-step))]
(timeline/vertical-timeline
{}
(for [[n i] (map vector (steps linear-wizard) (range))]
(timeline/vertical-timeline-step (cond-> {}
(= i active-index) (assoc :active? true)
(< i active-index) (assoc :visited? true)
(= i (dec (count step-names))) (assoc :last? true))
[:a.cursor-pointer.whitespace-nowrap {:x-data (hx/json {:timelineIndex i})
:hx-put (hu/url (bidi/path-for ssr-routes/only-routes validation-route)
{:from (encode-step-key (step-key current-step))
:to (encode-step-key (step-key (get-step linear-wizard n)))})}
(step-name (get-step linear-wizard n))])))))
(defn back-button [linear-wizard step validation-route]
[:a.cursor-pointer.whitespace-nowrap {:hx-put (hu/url (bidi/path-for ssr-routes/only-routes validation-route)
{:from (encode-step-key (step-key step))
:to (encode-step-key (->> (partition-all 2 1 (steps linear-wizard))
(filter (fn [[from to]]
(= to (step-key step))))
ffirst))})}
"Back"])
(defn default-next-button [linear-wizard step validation-route]
(let [steps (steps linear-wizard)
last? (= (step-key step) (last steps))
next-step (when-not last? (->> steps
(drop-while #(not= (step-key step)
%))
(drop 1)
first
(get-step linear-wizard)))]
(com/validated-save-button (cond-> {:errors (seq fc/*form-errors*)
;;:x-data (hx/json {})
:x-ref "next"
:class "w-48"}
(not last?) (assoc :hx-put (hu/url (bidi/path-for ssr-routes/only-routes validation-route)
{:from (encode-step-key (step-key step))
:to (encode-step-key (step-key next-step))})))
(if next-step
(step-name next-step)
"Save")
(when-not last?
[:div.w-5.h-5 svg/arrow-right]))))
(defn default-step-body [params & children]
[:div.space-y-1 {:class "w-[600px] h-[700px]"}
children])
(defn default-step-footer [linear-wizard step & {:keys [validation-route
discard-button
next-button]}]
[:div.flex.justify-end
[:div.flex.items-baseline.gap-x-4
(com/form-errors {:errors (:errors (:step-params fc/*form-errors*))})
(when (not= (first (steps linear-wizard))
(step-key step))
(when validation-route
(back-button linear-wizard step validation-route)))
(when (and (satisfies? Discardable step) (can-discard? step @fc/*current*))
discard-button)
(cond next-button
next-button
validation-route
(default-next-button linear-wizard step validation-route)
:else
[:div "No action possible."])]])
(defn default-render-step [linear-wizard step & {:keys [head body footer validation-route discard-route]}]
(let [is-last? (= (step-key step) (last (steps linear-wizard)))]
(com/modal-card-advanced
{"@keydown.enter.prevent.stop" "$refs.next.click()"
:class (str (when is-last? "last-modal-step")
" transition duration-300 ease-in-out
")
":class" (hiccup/raw "{
\"htmx-swapping:-translate-x-2/3 htmx-swapping:opacity-0 htmx-swapping:scale-0 htmx-added:translate-x-2/3 htmx-added:opacity-0 htmx-added:scale-0 scale-100 translate-x-0 opacity-100\": $data.transitionType=='forward',
\"htmx-swapping:translate-x-2/3 htmx-swapping:opacity-0 htmx-swapping:scale-0 htmx-added:-translate-x-2/3 htmx-added:opacity-0 htmx-added:scale-0 scale-100 translate-x-0 opacity-100\": $data.transitionType=='backward'
}
")
"x-data" ""}
(com/modal-header {}
head)
#_(com/modal-header-attachment {})
[:div.flex.shrink
[:div.grow-0.pr-6.pt-2.bg-gray-100.self-stretch #_{:style "margin-left:-20px"} (render-timeline linear-wizard step validation-route)]
(com/modal-body {}
body)]
(com/modal-footer {}
footer))))
(defn wrap-ensure-step [handler]
(->
(fn [{:keys [wizard multi-form-state] :as request}]
(assert-schema (step-schema (get-current-step wizard)) (:step-params multi-form-state))
(handler request))
(wrap-form-4xx-2 (fn [{:keys [wizard] :as request}] ;; THIS MAY BE BETTER TO JUST MAKE THE LINEAR WIZARD POPULATE FROM THE REQUEST
(html-response
(render-wizard wizard request)
:headers {"x-transition-type" "none"
"HX-reswap" "outerHTML"})))))
(defn get-transition-type [wizard from-step-key to-step-key]
(let [to-step-index (.indexOf (steps wizard) to-step-key)
from-step-index (.indexOf (steps wizard)
from-step-key)]
(cond (= -1 to-step-index)
nil
(= -1 from-step-index)
nil
(= from-step-index to-step-index)
nil
(> from-step-index to-step-index)
"backward"
:else
"forward")))
(def next-handler
(-> (fn [{:keys [wizard] :as request}]
(let [current-step (get-current-step wizard)
to-step (:to (:query-params request))
wizard (navigate wizard to-step)
new-step (get-current-step wizard)
transition-type (get-transition-type wizard (step-key current-step) to-step)]
(html-response
(render-wizard wizard
(-> request
(assoc :multi-form-state (-> (:multi-form-state request)
(merge-multi-form-state)
(select-state
(edit-path new-step request)
(init-step-params- new-step request))))))
:headers {"HX-reswap" (when transition-type "outerHTML swap:0.15s")
"x-transition-type" (or transition-type "none")})))
(wrap-ensure-step)
(wrap-schema-enforce :query-schema
[:map
[:to step-key-schema]])))
(def discard-handler
(->
(fn [{:keys [wizard multi-form-state] :as request}]
(let [current-step (get-current-step wizard)
to-step (:to (:query-params request))
wizard (navigate wizard to-step)
transition-type (get-transition-type wizard (step-key current-step) to-step)]
(html-response
(render-wizard wizard
(-> request
(assoc :multi-form-state (discard-changes current-step multi-form-state))))
:headers {"HX-reswap" (when transition-type "outerHTML swap:0.15s")
"x-transition-type" (or transition-type "none")})))
(wrap-schema-enforce :query-schema
[:map
[:to step-key-schema]])))
(def submit-handler
(-> (fn [{:keys [wizard multi-form-state] :as request}]
(submit wizard (-> request
(assoc :multi-form-state (merge-multi-form-state multi-form-state)))))
(wrap-ensure-step)))
(defn default-render-wizard [linear-wizard {:keys [multi-form-state form-errors snapshot current-step] :as request} & {:keys [form-params]}]
(let [current-step (get-current-step linear-wizard)
edit-path (edit-path current-step request)]
[:form#wizard-form form-params
(fc/start-form multi-form-state (when form-errors {:step-params form-errors})
(list
(fc/with-field :snapshot
(com/hidden {:name (fc/field-name)
:value (pr-str (fc/field-value))}))
(fc/with-field :edit-path
(com/hidden {:name (fc/field-name)
:value (pr-str (or edit-path []))}))
(com/hidden {:name "current-step"
:value (pr-str (step-key current-step))})
(fc/with-field :step-params
(com/modal
{:id "wizardmodal"}
(render-step current-step request)))))]))
(defn wrap-wizard [handler linear-wizard]
(fn [request]
(let [current-step-key (if-let [current-step (get (:form-params request) "current-step")]
(mc/decode step-key-schema current-step main-transformer)
(first (steps linear-wizard)))
current-step (get-step linear-wizard current-step-key)
multi-form-state (-> (:multi-form-state request)
(update :snapshot (fn [snapshot]
(mc/decode (form-schema linear-wizard)
snapshot
main-transformer)))
(update :step-params (fn [step-params]
(or
(mc/decode (step-schema current-step)
step-params
main-transformer)
{} ;; Todo add a defaultable
))))
request (-> request
(assoc :multi-form-state multi-form-state))
linear-wizard (navigate linear-wizard current-step-key)]
(handler
(assoc request :wizard (hydrate-from-request linear-wizard request))))))
(defn open-wizard-handler [{:keys [wizard current-step] :as request}]
(modal-response
[:div {:x-data (hx/json {"transitionType" "none"
}
)
"@htmx:after-request" "if(event.detail.xhr.getResponseHeader('x-transition-type')) { $data.transitionType = event.detail.xhr.getResponseHeader('x-transition-type');}"
}
(render-wizard wizard request)]))
(defn wrap-init-multi-form-state [handler get-multi-form-state]
(->
(fn init-multi-form [request]
(handler (assoc request :multi-form-state (get-multi-form-state request))))
(wrap-nested-form-params)))
(defn wrap-decode-multi-form-state [handler]
(wrap-init-multi-form-state
handler
(fn parse-multi-form-state [request]
(map->MultiStepFormState (mc/decode [:map
[:snapshot {:optional true
:decode/arbitrary
#(clojure.edn/read-string {:readers clj-time.coerce/data-readers
:eof nil}
%)}
[:maybe :any]]
[:edit-path {:optional true :decode/arbitrary (fn [z]
(clojure.edn/read-string z))} [:maybe [:sequential {:min 0} any?]]]
[:step-params {:optional true}
[:maybe
:any]]]
(:form-params request)
main-transformer)))))

View File

@@ -2,9 +2,9 @@
(:require [auto-ap.ssr.hiccup-helper :as hh])) (:require [auto-ap.ssr.hiccup-helper :as hh]))
(defn timeline-step [{:keys [active? visited? last?]} & children] (defn timeline-step [{:keys [active? visited? last?]} & children]
(if active? (if active?
[:li {:class "flex items-center text-primary-600 font-medium dark:text-primary-500"} [:li {:class "flex items-center text-primary-600 font-medium dark:text-primary-500"}
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border-2 border-primary-600 rounded-full shrink-0 dark:border-primary-500"} ] [:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border-2 border-primary-600 rounded-full shrink-0 dark:border-primary-500"}]
children children
(when-not last? (when-not last?
[:svg {:class "w-3 h-3 ml-2 sm:ml-4", :aria-hidden "true", :xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 12 10"} [:svg {:class "w-3 h-3 ml-2 sm:ml-4", :aria-hidden "true", :xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 12 10"}
@@ -24,5 +24,24 @@
[:ol {:class "flex items-center w-full space-x-2 text-xs text-center text-gray-500 bg-white dark:text-gray-400 sm:text-base dark:bg-gray-800 sm:space-x-4 px-2"} [:ol {:class "flex items-center w-full space-x-2 text-xs text-center text-gray-500 bg-white dark:text-gray-400 sm:text-base dark:bg-gray-800 sm:space-x-4 px-2"}
children children
#_[:li {:class "flex items-center"} #_[:li {:class "flex items-center"}
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border border-gray-500 rounded-full shrink-0 dark:border-gray-400"} ]]]) [:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border border-gray-500 rounded-full shrink-0 dark:border-gray-400"}]]])
(defn vertical-timeline-step [{:keys [active? visited? last?]} & children]
(if active?
[:li {:class "flex items-center text-primary-600 font-medium dark:text-primary-500"}
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border-2 border-primary-600 rounded-full shrink-0 dark:border-primary-500"}]
children ]
[:li {:class (cond-> "flex items-center"
(not visited?) (hh/add-class "text-gray-400"))}
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border border-gray-500 rounded-full shrink-0 dark:border-gray-400"}
(when visited?
[:svg {:class "w-3 h-3 text-primary-600 dark:text-primary-500", :aria-hidden "true", :xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 16 12"}
[:path {:stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "2", :d "M1 5.917 5.724 10.5 15 1.5"}]])]
children ]))
(defn vertical-timeline [params & children]
[:ol {:class "flex flex-col items-start space-y-2 text-xs text-center text-gray-500 bg-gray-100 dark:text-gray-400 sm:text-base dark:bg-gray-800 sm:space-y-4 px-2"}
children
#_[:li {:class "flex items-center"}
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border border-gray-500 rounded-full shrink-0 dark:border-gray-400"}]]])

View File

@@ -12,6 +12,7 @@
[auto-ap.ssr.admin.import-batch :as import-batch] [auto-ap.ssr.admin.import-batch :as import-batch]
[auto-ap.ssr.admin.transaction-rules :as admin-rules] [auto-ap.ssr.admin.transaction-rules :as admin-rules]
[auto-ap.ssr.admin.vendors :as admin-vendors] [auto-ap.ssr.admin.vendors :as admin-vendors]
[auto-ap.ssr.admin.clients :as admin-clients]
[auto-ap.ssr.auth :as auth] [auto-ap.ssr.auth :as auth]
[auto-ap.ssr.company :as company] [auto-ap.ssr.company :as company]
[auto-ap.ssr.company-dropdown :as company-dropdown] [auto-ap.ssr.company-dropdown :as company-dropdown]
@@ -54,6 +55,7 @@
:company-plaid-table (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/table)) :company-plaid-table (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/table))
:company-plaid-link (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/link)) :company-plaid-link (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/link))
:company-plaid-relink (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/relink)) :company-plaid-relink (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/relink))
:company-update-signature (wrap-client-redirect-unauthenticated (wrap-secure company/upload-signature-data))
:company-yodlee (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/page)) :company-yodlee (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/page))
:company-yodlee-table (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/table)) :company-yodlee-table (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/table))
:company-yodlee-fastlink-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/fastlink-dialog)) :company-yodlee-fastlink-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/fastlink-dialog))
@@ -89,5 +91,6 @@
(into admin/key->handler) (into admin/key->handler)
(into admin-jobs/key->handler) (into admin-jobs/key->handler)
(into admin-vendors/key->handler) (into admin-vendors/key->handler)
(into admin-clients/key->handler)
(into admin-rules/key->handler))) (into admin-rules/key->handler)))

View File

@@ -2,7 +2,7 @@
(:require [auto-ap.ssr.utils :refer [path->name2]] (:require [auto-ap.ssr.utils :refer [path->name2]]
[auto-ap.cursor :as cursor])) [auto-ap.cursor :as cursor]))
(def ^:dynamic *prefix* []) (def ^:dynamic *prefix* [])
(def ^:dynamic *form-data*) (def ^:dynamic *form-data*)
(def ^:dynamic *form-errors*) (def ^:dynamic *form-errors*)
(def ^:dynamic *prev-cursor* nil) (def ^:dynamic *prev-cursor* nil)
@@ -22,18 +22,30 @@
`(binding [*prefix* ~prefix] `(binding [*prefix* ~prefix]
(start-form ~form-data ~errors ~@rest))) (start-form ~form-data ~errors ~@rest)))
(defmacro with-prefix [prefix & rest]
`(binding [*prefix* (into (or *prefix* []) ~prefix)]
~@rest))
(defmacro with-cursor [cursor & rest] (defmacro with-cursor [cursor & rest]
`(binding [*current* ~cursor] `(binding [*current* ~cursor]
~@rest)) ~@rest))
(defmacro with-field [field & rest] (defmacro with-field [field & rest]
`(with-cursor (get *current* ~field ) `(with-cursor (get *current* ~field)
~@rest)) ~@rest))
(defmacro with-field-default [field default & rest] (defmacro with-field-default [field default & rest]
`(with-cursor (get *current* ~field ~default) `(let [new-cursor# (get *current* ~field ~default)
~@rest)) new-cursor2# (if (not (deref new-cursor#))
(do
(cursor/transact! *current*
(fn [c#]
(assoc c# ~field ~default)))
(get *current* ~field ~default))
new-cursor#)]
(with-cursor new-cursor2#
~@rest)))
(defn field-name (defn field-name
([] (field-name *current*)) ([] (field-name *current*))
@@ -62,10 +74,10 @@
(defn cursor-map (defn cursor-map
([f] (cursor-map *current* f)) ([f] (cursor-map *current* f))
([cursor f] ([cursor f]
(when (field-value) (when (seq (field-value))
(doall (doall
(for [n cursor] (for [n cursor]
(with-cursor n (with-cursor n
(f n))))))) (f n)))))))

View File

@@ -34,15 +34,14 @@
(remove-wildcard [this wildcard] (remove-wildcard [this wildcard]
(if (sequential? wildcard) (if (sequential? wildcard)
(reduce (reduce
remove-wildcard remove-wildcard
this this
wildcard) wildcard)
(reduce (reduce
remove-class remove-class
this this
(filter (fn [c] (filter (fn [c]
(str/starts-with? c wildcard) (str/starts-with? c wildcard)) @class-set)))
) @class-set)))
this) this)
(replace-wildcard [this wildcard add] (replace-wildcard [this wildcard add]
(remove-wildcard this wildcard) (remove-wildcard this wildcard)
@@ -51,7 +50,7 @@
(replace-tw [this add] (replace-tw [this add]
;; TODO ;; TODO
) )
Object Object
(toString [this] (toString [this]
(str/join " " @class-set))))) (str/join " " @class-set)))))
@@ -60,8 +59,7 @@
(add-class [this add] (add-class [this add]
(add-class (string->class-list this) add)) (add-class (string->class-list this) add))
(remove-class [this remove] (remove-class [this remove]
(remove-class (string->class-list this) remove) (remove-class (string->class-list this) remove))
)
(replace-class [this remove add] (replace-class [this remove add]
(replace-class (string->class-list this) remove add)) (replace-class (string->class-list this) remove add))
(remove-wildcard [this wildcard] (remove-wildcard [this wildcard]
@@ -70,29 +68,11 @@
(replace-wildcard (string->class-list this) wildcard add)) (replace-wildcard (string->class-list this) wildcard add))
(add-tw [this tw] (add-tw [this tw]
(replace-tw (string->class-list this) (replace-tw (string->class-list this)
tw) tw)))
))
(str (hiccup/html [:div {:class (-> "hello bryce hello-1 hello-2" (str (hiccup/html [:div {:class (-> "hello bryce hello-1 hello-2"
(replace-wildcard ["hello-" "b"] ["hi" "there"]))}])) (replace-wildcard ["hello-" "b"] ["hi" "there"]))}]))
(str (hiccup/html [:div {:class (-> "p-1.5 "
(add-class "bg-blue-500")
#_(replace-wildcard ["hello-" "b"] ["hi" "there"]))}]))

View File

@@ -84,51 +84,51 @@
matching-count])) matching-count]))
(def grid-page (def grid-page
(helper/build {}
{:id "cash-drawer-shift-table" #_(helper/build
:nav (com/main-aside-nav) {:id "cash-drawer-shift-table"
:page-specific-nav filters :nav (com/main-aside-nav)
:fetch-page fetch-page :page-specific-nav filters
:oob-render :fetch-page fetch-page
(fn [request] :oob-render
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)]) (fn [request]
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes [(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)])
:company)} :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
"POS"] :company)}
"POS"]
[:a {:href (bidi/path-for ssr-routes/only-routes [:a {:href (bidi/path-for ssr-routes/only-routes
:pos-cash-drawer-shifts)} :pos-cash-drawer-shifts)}
"Cash Drawer Shifts"]] "Cash Drawer Shifts"]]
:title "Cash drawer shifts" :title "Cash drawer shifts"
:entity-name "Cash drawer shift" :entity-name "Cash drawer shift"
:route :pos-cash-drawer-shift-table :route :pos-cash-drawer-shift-table
:headers [{:key "client" :headers [{:key "client"
:name "Client" :name "Client"
:sort-key "client" :sort-key "client"
:hide? (fn [args] :hide? (fn [args]
(= (count (:clients args)) 1)) (= (count (:clients args)) 1))
:render #(-> % :cash-drawer-shift/client :client/code)} :render #(-> % :cash-drawer-shift/client :client/code)}
{:key "date" {:key "date"
:name "Date" :name "Date"
:sort-key "date" :sort-key "date"
:render #(atime/unparse-local (:cash-drawer-shift/date %) atime/standard-time)} :render #(atime/unparse-local (:cash-drawer-shift/date %) atime/standard-time)}
{:key "paid-in" {:key "paid-in"
:name "Paid in" :name "Paid in"
:sort-key "paid-in" :sort-key "paid-in"
:render #(some->> % :cash-drawer-shift/paid-in (format "$%.2f"))} :render #(some->> % :cash-drawer-shift/paid-in (format "$%.2f"))}
{:key "paid-out" {:key "paid-out"
:name "Paid out" :name "Paid out"
:sort-key "paid-out" :sort-key "paid-out"
:render #(some->> % :cash-drawer-shift/paid-out (format "$%.2f"))} :render #(some->> % :cash-drawer-shift/paid-out (format "$%.2f"))}
{:key "expected-cash" {:key "expected-cash"
:name "Expected cash" :name "Expected cash"
:sort-key "expected-cash" :sort-key "expected-cash"
:render #(some->> % :cash-drawer-shift/expected-cash (format "$%.2f"))} :render #(some->> % :cash-drawer-shift/expected-cash (format "$%.2f"))}
{:key "opened-cash" {:key "opened-cash"
:name "Opened cash" :name "Opened cash"
:sort-key "opened-cash" :sort-key "opened-cash"
:render #(some->> % :cash-drawer-shift/opened-cash (format "$%.2f"))} :render #(some->> % :cash-drawer-shift/opened-cash (format "$%.2f"))}]}))
]}))
(def row* (partial helper/row* grid-page)) (def row* (partial helper/row* grid-page))

View File

@@ -1,6 +1,6 @@
(ns auto-ap.ssr.svg) (ns auto-ap.ssr.svg)
(def pie (def pie
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs] [:defs]
[:title "analytics-pie-2"] [:title "analytics-pie-2"]
@@ -11,28 +11,28 @@
(def accounting-invoice-mail (def accounting-invoice-mail
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs] [:defs]
[:title "accounting-document"] [: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,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 "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"}] [: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 "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 "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 "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"}]]) [: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 (def receipt-register-1
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:g [: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"}] [: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"}] [: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"}] [: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"}] [: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 "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 "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"}] [: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"}] [: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"}]]]) [: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 (def payments
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
@@ -45,38 +45,38 @@
(def bank (def bank
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:g [: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 "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 "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 "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 "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 "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 "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 "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"}] [: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"}]]]) [: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 (def receipt
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:g [: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 "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 "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 "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"}] [: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 "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"}]]]) [: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]}] (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"} [: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 "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"}]]) [: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]}] (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"} [: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 "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"}]]) [: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 (def search
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs] [:defs]
[:title "search"] [:title "search"]
@@ -93,11 +93,11 @@
(def home (def home
[:svg { :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} [: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"}]]) [: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 (def breadcrumb-component
[:svg { :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} [: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"}]]) [: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 (def refresh
@@ -106,7 +106,7 @@
(def upload (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"} [: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"}]]) [: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 (def vendors
@@ -122,29 +122,29 @@
(def report (def report
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs] [:defs]
[:title "app-window-pie-chart"] [: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"}] [: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"}] [: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"}] [: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"}] [: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 "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 "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"}]]) [: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 (def government-building
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:g [: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 "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 "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"}] [: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"}] [: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 "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"}] [: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"}] [: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 "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 "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"}]]]) [: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 (def external-link
[:svg [:svg
@@ -168,7 +168,7 @@
"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", "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", :fill "none",
:stroke "currentColor", :stroke "currentColor",
:stroke-linecap "round", :stroke-linecap "round",
:stroke-linejoin "round"}]]) :stroke-linejoin "round"}]])
(def play (def play
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "-0.5 -0.5 24 24"} [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "-0.5 -0.5 24 24"}
@@ -201,9 +201,19 @@
{:d "M21.914,6.328,17.672,2.086l.707-.707a3,3,0,0,1,4.242,4.242Z", {:d "M21.914,6.328,17.672,2.086l.707-.707a3,3,0,0,1,4.242,4.242Z",
:fill "none", :fill "none",
:stroke "currentColor", :stroke "currentColor",
:stroke-linecap "round", :stroke-linecap "round",
:stroke-linejoin "round"}]]) :stroke-linejoin "round"}]])
(def dollar-tag
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "tag-dollar"]
[:path {:d "M17.5 5a1.5 1.5 0 1 0 3 0 1.5 1.5 0 1 0 -3 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m14.257 12.571 -1.985 -1.985a1.107 1.107 0 0 0 -1.686 0c-0.925 0.924 1.2 4.46 0.272 5.384a1.171 1.171 0 0 1 -1.687 0l-1.985 -1.985", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m12.843 11.157 1.061 -1.061", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m7.54 16.46 1.061 -1.061", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "M0.854 12.646a1.207 1.207 0 0 0 0 1.708l8.792 8.792a1.207 1.207 0 0 0 1.708 0l11.439 -11.439A2.414 2.414 0 0 0 23.5 10V1.5a1 1 0 0 0 -1 -1H14a2.414 2.414 0 0 0 -1.707 0.707Z", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]])
(def drop-down (def drop-down
[:svg {:aria-hidden "true", :fill "none", :stroke "currentColor", :viewbox "0 0 24 24", :xmlns "http://www.w3.org/2000/svg"} [:svg {: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"}]]) [:path {:stroke-linecap "round", :stroke-linejoin "round", :stroke-width "2", :d "M19 9l-7 7-7-7"}]])
@@ -224,63 +234,63 @@
{:d "M23.5,18.5v4a1,1,0,0,1-1,1H1.5a1,1,0,0,1-1-1v-4", {:d "M23.5,18.5v4a1,1,0,0,1-1,1H1.5a1,1,0,0,1-1-1v-4",
:fill "none", :fill "none",
:stroke "currentColor", :stroke "currentColor",
:stroke-linecap "round", :stroke-linecap "round",
:stroke-linejoin "round"}]]) :stroke-linejoin "round"}]])
(def trash (def trash
[:svg [:svg
{:xmlns "http://www.w3.org/2000/svg", :viewBox "0 0 24 24"} {:xmlns "http://www.w3.org/2000/svg", :viewBox "0 0 24 24"}
[:defs] [:defs]
[:title "bin-1"] [:title "bin-1"]
[:path [:path
{:d {: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", "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", :fill "none",
:stroke "currentColor", :stroke "currentColor",
:stroke-linecap "round", :stroke-linecap "round",
:stroke-linejoin "round"}] :stroke-linejoin "round"}]
[:line [:line
{:x1 "0.5", {:x1 "0.5",
:y1 "4.5", :y1 "4.5",
:x2 "23.5", :x2 "23.5",
:y2 "4.5", :y2 "4.5",
:fill "none", :fill "none",
:stroke "currentColor", :stroke "currentColor",
:stroke-linecap "round", :stroke-linecap "round",
:stroke-linejoin "round"}] :stroke-linejoin "round"}]
[:path [:path
{:d "M7.5,4.5v-3a1,1,0,0,1,1-1h7a1,1,0,0,1,1,1v3", {:d "M7.5,4.5v-3a1,1,0,0,1,1-1h7a1,1,0,0,1,1,1v3",
:fill "none", :fill "none",
:stroke "currentColor", :stroke "currentColor",
:stroke-linecap "round", :stroke-linecap "round",
:stroke-linejoin "round"}] :stroke-linejoin "round"}]
[:line [:line
{:x1 "12", {:x1 "12",
:y1 "9", :y1 "9",
:x2 "12", :x2 "12",
:y2 "19.5", :y2 "19.5",
:fill "none", :fill "none",
:stroke "currentColor", :stroke "currentColor",
:stroke-linecap "round", :stroke-linecap "round",
:stroke-linejoin "round"}] :stroke-linejoin "round"}]
[:line [:line
{:x1 "16.5", {:x1 "16.5",
:y1 "9", :y1 "9",
:x2 "16", :x2 "16",
:y2 "19.5", :y2 "19.5",
:fill "none", :fill "none",
:stroke "currentColor", :stroke "currentColor",
:stroke-linecap "round", :stroke-linecap "round",
:stroke-linejoin "round"}] :stroke-linejoin "round"}]
[:line [:line
{:x1 "7.5", {:x1 "7.5",
:y1 "9", :y1 "9",
:x2 "8", :x2 "8",
:y2 "19.5", :y2 "19.5",
:fill "none", :fill "none",
:stroke "currentColor", :stroke "currentColor",
:stroke-linecap "round", :stroke-linecap "round",
:stroke-linejoin "round"}]]) :stroke-linejoin "round"}]])
(def alert (def alert
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"} [:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
@@ -442,3 +452,39 @@
[:defs] [:defs]
[:title "dislike"] [: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"}]]) [: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"}]])
(def dollar
[: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 "M21 7H3a0.5 0.5 0 0 0 -0.5 0.5v9a0.5 0.5 0 0 0 0.5 0.5h18a0.5 0.5 0 0 0 0.5 -0.5v-9A0.5 0.5 0 0 0 21 7Z", :stroke-width "1"}]
[:path {:stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :d "M22.5 5h-21a1 1 0 0 0 -1 1v12a1 1 0 0 0 1 1h21a1 1 0 0 0 1 -1V6a1 1 0 0 0 -1 -1Z", :stroke-width "1"}]
[:path {:stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :d "M12 15a3 3 0 1 0 0 -6 3 3 0 0 0 0 6Z", :stroke-width "1"}]
[:path {:stroke "currentcolor", :d "M4.996 9.75a0.25 0.25 0 0 1 0 -0.5", :stroke-width "1"}]
[:path {:stroke "currentcolor", :d "M4.996 9.75a0.25 0.25 0 0 0 0 -0.5", :stroke-width "1"}]
[:g
[:path {:stroke "currentcolor", :d "M18.996 14.75a0.25 0.25 0 1 1 0 -0.5", :stroke-width "1"}]
[:path {:stroke "currentcolor", :d "M18.996 14.75a0.25 0.25 0 1 0 0 -0.5", :stroke-width "1"}]]])
(def credit-card
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "credit-card-1"]
[:path {:d "M2.504 4h19s2 0 2 2v12s0 2 -2 2h-19s-2 0 -2 -2V6s0 -2 2 -2", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m0.504 8 23 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m20.504 12 -3 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m11.504 12 -8 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m6.504 15 -3 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]])
(def check
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "check-payment-sign"]
[:path {:d "m2.5 20.5 7 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m14 19.616 0.751 -0.751a1 1 0 0 1 1.677 0.465l0.072 0.286 0.818 -0.545a1 1 0 0 1 1.262 0.125l0.42 0.42h2", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m2.504 17.616 5 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m14.075 5.505 2.122 2.122", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m18.813 2.465 0.425 0.424a1.2 1.2 0 0 1 0 1.697l-8.698 8.697 0 0 -2.121 -2.121 0 0 8.697 -8.697a1.2 1.2 0 0 1 1.697 0Z", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m8.418 11.162 -1.414 3.536 3.536 -1.414", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m19.025 2.677 1.293 -1.293", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "M6.5 9.616H1a0.5 0.5 0 0 0 -0.5 0.5v12a0.5 0.5 0 0 0 0.5 0.5h22a0.5 0.5 0 0 0 0.5 -0.5v-12a0.5 0.5 0 0 0 -0.5 -0.5h-5", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "M17.004 12.616h3s0.5 0 0.5 0.5v2s0 0.5 -0.5 0.5h-3s-0.5 0 -0.5 -0.5v-2s0 -0.5 0.5 -0.5", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m16.757 2.823 -0.8 -0.8a1 1 0 0 0 -1.414 0L12.5 4.07", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]])

View File

@@ -15,8 +15,6 @@
hiccup))}) hiccup))})
(defn base-page [request contents page-name] (defn base-page [request contents page-name]
(html-page (html-page
[:html.has-navbar-fixed-top [:html.has-navbar-fixed-top
@@ -38,10 +36,11 @@
[:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.js" [:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.js"
:crossorigin= "anonymous"}] :crossorigin= "anonymous"}]
[:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.min.js" [:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.min.js"
:crossorigin= "anonymous"}]) :crossorigin= "anonymous"}])
[:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/ext/class-tools.js" :crossorigin= "anonymous"}] [:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/ext/class-tools.js" :crossorigin= "anonymous"}]
[:script {:src "https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"}]
[:script {:src "https://unpkg.com/htmx.org/dist/ext/debug.js"}] [:script {:src "https://unpkg.com/htmx.org/dist/ext/debug.js"}]
[:script {:src "/js/htmx-disable.js"}] [:script {:src "/js/htmx-disable.js"}]
[:script {:type "text/javascript", :src "https://cdn.yodlee.com/fastlink/v4/initialize.js", :async "async"}]] [:script {:type "text/javascript", :src "https://cdn.yodlee.com/fastlink/v4/initialize.js", :async "async"}]]
@@ -57,7 +56,8 @@
[:link {:rel "stylesheet" :href "https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" :type "text/css"}] [:link {:rel "stylesheet" :href "https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" :type "text/css"}]
[:script {:defer true :src "https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"}] [:script {:defer true :src "https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"}]
[:script {:defer true :src "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"}] [:script {:defer true :src "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"}]
[:script {:src "https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js"}]
[:style [:style
" "
input::-webkit-outer-spin-button, input::-webkit-outer-spin-button,
@@ -74,14 +74,17 @@ input[type=number] {
[:body {:hx-ext "disable-submit, class-tools"} [:body {:hx-ext "disable-submit, class-tools"}
contents contents
[:script {:src "/js/flowbite.min.js"}] [:script {:src "/js/flowbite.min.js"}]
[:div#modal-holder [:div#modal-holder
{ :class "fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen" {:class "fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen"
"x-show" "open" "x-show" "open"
":aria-hidden" "!open" ":aria-hidden" "!open"
"x-data" (hx/json {"open" false}) "x-data" (hx/json {"open" false
"@modalopen.document" "open=true" "unexpectedError" false})
"x-on:htmx:response-error" "unexpectedError=true;"
"x-on:htmx:before-request" "unexpectedError=false"
"@modalopen.document" "open=true; unexpectedError=null"
"@modalclose.document" "open=false"} "@modalclose.document" "open=false"}
[:div {:class "bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40 md:p-12" [:div {:class "bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40 md:p-12"
@@ -94,21 +97,18 @@ input[type=number] {
"x-transition:leave-start" "!bg-opacity-50" "x-transition:leave-start" "!bg-opacity-50"
"x-transition:leave-end" "!bg-opacity-0"} "x-transition:leave-end" "!bg-opacity-0"}
[:div { [:div {:class "flex h-full w-full justify-stretch md:justify-center items-stretch md:items-center "
:class "flex h-full w-full justify-stretch md:justify-center items-stretch md:items-center " "x-trap.inert.noscroll" "open"
"x-trap.inert.noscroll" "open" "x-trap.inert" "open"
"x-trap.inert" "open" "x-show" "open"
"x-show" "open" "x-transition:enter" "ease-out duration-300"
"x-transition:enter" "ease-out duration-300" "x-transition:enter-start" "!bg-opacity-0 !translate-y-32"
"x-transition:enter-start" "!bg-opacity-0 !translate-y-32" "x-transition:enter-end" "!bg-opacity-100 !translate-y-0"
"x-transition:enter-end" "!bg-opacity-100 !translate-y-0" "x-transition:leave" "duration-300"
"x-transition:leave" "duration-300" "x-transition:leave-start" "!opacity-100 !translate-y-0"
"x-transition:leave-start" "!opacity-100 !translate-y-0" "x-transition:leave-end" "!opacity-0 !translate-y-32"}
"x-transition:leave-end" "!opacity-0 !translate-y-32"}
[:div.flex.items-center.justify-center.max-w-6xl {:class "min-w-[700px] max-h-full "} [:div.flex.items-center.justify-center.max-w-6xl {:class "min-w-[700px] max-h-full "}
[:div#modal-content.flex.flex-col.self-stretch {:class "min-w-[700px] md:p-12"} ;;.overflow-scroll [:div#modal-content.flex.flex-col.self-stretch {:class "min-w-[700px] md:p-12"} ;;.overflow-scroll
]]]]]]]))
]
]]]]]]))

View File

@@ -16,51 +16,51 @@
:headers (into {"Content-Type" "text/html"} :headers (into {"Content-Type" "text/html"}
headers) headers)
:body (str :body (str
(hiccup/html (hiccup/html
{} {}
hiccup) hiccup)
"\n" "\n"
(str/join "\n" (str/join "\n"
(map (fn [o] (map (fn [o]
(hiccup/html (hiccup/html
{} {}
o)) o))
oob)))}) oob)))})
(defn modal-response [hiccup & {:as opts}] (defn modal-response [hiccup & {:as opts}]
(apply html-response (apply html-response
(into (into
[hiccup] [hiccup]
(mapcat identity (mapcat identity
(-> opts (-> opts
(assoc-in [:headers "hx-trigger"] "modalopen") (assoc-in [:headers "hx-trigger"] "modalopen")
(assoc-in [:headers "hx-retarget"] "#modal-content") (assoc-in [:headers "hx-retarget"] "#modal-content")
(assoc-in [:headers "hx-reswap"] "innerHTML")))))) (assoc-in [:headers "hx-reswap"] "innerHTML"))))))
(defn next-step-modal-response [hiccup & {:as opts}] (defn next-step-modal-response [hiccup & {:as opts}]
(apply html-response (apply html-response
(into (into
[hiccup] [hiccup]
(mapcat identity (mapcat identity
(-> opts (-> opts
(assoc-in [:headers "hx-retarget"] "#modal-content") (assoc-in [:headers "hx-retarget"] "#modal-content")
(assoc-in [:headers "hx-reswap"] "innerHTML")))))) (assoc-in [:headers "hx-reswap"] "innerHTML"))))))
(defn form-data->map [form-data] (defn form-data->map [form-data]
(reduce-kv (reduce-kv
(fn [acc k v] (fn [acc k v]
(cond (and (string? v) (cond (and (string? v)
(empty? v)) (empty? v))
acc acc
:else :else
(assoc-in acc (->> (str/split k #"_") (assoc-in acc (->> (str/split k #"_")
(mapv #(apply keyword (str/split % #"/")))) (mapv #(apply keyword (str/split % #"/"))))
v))) v)))
{} {}
form-data)) form-data))
(defn path->name [k] (defn path->name [k]
(cond (keyword? k) (cond (keyword? k)
@@ -75,29 +75,32 @@
[:vector {:decode/json {:enter (fn [x] [:vector {:decode/json {:enter (fn [x]
(if (sequential? x) (if (sequential? x)
x x
[x]) [x]))}}
)}}
x]) x])
(defn empty->nil [v] (defn empty->nil [v]
(if (and (string? v) (clojure.string/blank? v)) (if (and (string? v) (clojure.string/blank? v))
nil nil
v)) v))
(defn parse-empty-as-nil [] (defn parse-empty-as-nil []
(mt2/transformer (mt2/transformer
{:decoders {:decoders
{:string empty->nil {:map (fn [m]
:double empty->nil (if (not (seq (filter identity (vals m))))
:int empty->nil nil
:long empty->nil m))
'nat-int? empty->nil}})) :string empty->nil
:double empty->nil
:int empty->nil
:long empty->nil
'nat-int? empty->nil}}))
(def entity-id (mc/schema [nat-int? {:error/message "required" (def entity-id (mc/schema [nat-int? {:error/message "required"
:decode/arbitrary (fn [e] :decode/arbitrary (fn [e]
(if (and (map? e) (:db/id e)) (if (and (map? e) (:db/id e))
(:db/id e) (:db/id e)
e))} ])) e))}]))
(def temp-id (mc/schema [:string {:min 1}])) (def temp-id (mc/schema [:string {:min 1}]))
(def money (mc/schema [:double])) (def money (mc/schema [:double]))
@@ -110,12 +113,12 @@
(def regex (mc/schema [:fn {:error/message "not a regex"} (def regex (mc/schema [:fn {:error/message "not a regex"}
(fn check-regx [x] (fn check-regx [x]
(try (try
(and (string? x) (and (string? x)
(. java.util.regex.Pattern (compile x java.util.regex.Pattern/CASE_INSENSITIVE))) (. java.util.regex.Pattern (compile x java.util.regex.Pattern/CASE_INSENSITIVE)))
true true
(catch Exception _ (catch Exception _
false)))])) false)))]))
(def map->db-id-decoder (def map->db-id-decoder
{:enter (fn [x] {:enter (fn [x]
@@ -127,12 +130,12 @@
(defn many-entity [params & keys] (defn many-entity [params & keys]
(mc/schema (mc/schema
[:vector (merge params {:decode/json map->db-id-decoder [:vector (merge params {:decode/json map->db-id-decoder
:decode/arbitrary (fn [x] :decode/arbitrary (fn [x]
(if (sequential? x) (if (sequential? x)
x x
[x]))}) [x]))})
(into [:map] keys)])) (into [:map] keys)]))
(defn str->keyword [s] (defn str->keyword [s]
(if (string? s) (if (string? s)
@@ -156,23 +159,38 @@
:form-validation-errors [m]})))) :form-validation-errors [m]}))))
(def main-transformer (def main-transformer
(mt2/transformer (mt2/transformer
parse-empty-as-nil parse-empty-as-nil
(mt2/key-transformer {:encode keyword->str :decode str->keyword}) (mt2/key-transformer {:encode keyword->str :decode str->keyword})
mt2/string-transformer mt2/string-transformer
mt2/json-transformer mt2/json-transformer
(mt2/transformer {:name :arbitrary}) (mt2/transformer {:name :arbitrary})
mt2/default-value-transformer)) mt2/default-value-transformer))
(defn strip [s] (defn strip [s]
(cond (and (string? s) (str/blank? s)) (cond (and (string? s) (str/blank? s))
nil nil
(string? s) (string? s)
(str/trim s) (str/trim s)
:else
s))
(defn assert-schema [schema entity]
(when-not (mc/validate schema entity)
(throw (ex-info #_(->> (-> (mc/explain schema entity)
(me/humanize {:errors (assoc me/default-errors
::mc/missing-key {:error/message {:en "required"}})}))
(map (fn [[k v]]
(str (if (keyword? k)
(name k)
k) ": " (str/join ", " v))))
(str/join ", "))
"validation failed"
{:type :schema-validation
:decoded entity
:error {:explain (mc/explain schema entity)}}))))
:else
s))
(defn schema-enforce-request [{:keys [form-params query-params params] :as request} & {:keys [form-schema query-schema route-schema params-schema]}] (defn schema-enforce-request [{:keys [form-params query-params params] :as request} & {:keys [form-schema query-schema route-schema params-schema]}]
(let [request (try (let [request (try
@@ -180,35 +198,35 @@
(and (:params request) params-schema) (and (:params request) params-schema)
(assoc :params (assoc :params
(mc/coerce (mc/coerce
params-schema params-schema
(:params request) (:params request)
main-transformer)) main-transformer))
(and (:route-params request) route-schema) (and (:route-params request) route-schema)
(assoc :route-params (assoc :route-params
(mc/coerce (mc/coerce
route-schema route-schema
(:route-params request) (:route-params request)
main-transformer)) main-transformer))
(and form-schema form-params) (and form-schema form-params)
(assoc :form-params (assoc :form-params
(mc/coerce (mc/coerce
form-schema form-schema
form-params form-params
main-transformer)) main-transformer))
(and query-schema query-params) (and query-schema query-params)
(assoc :query-params (assoc :query-params
(mc/coerce (mc/coerce
query-schema query-schema
query-params query-params
main-transformer))) main-transformer)))
(catch Exception e (catch Exception e
(alog/warn ::validation-error :error e) (alog/warn ::validation-error :error e)
(throw (ex-info (->> (-> e (throw (ex-info (->> (-> e
(ex-data ) (ex-data)
:data :data
:explain :explain
(me/humanize {:errors (assoc me/default-errors (me/humanize {:errors (assoc me/default-errors
@@ -237,30 +255,30 @@
(and (:params request) params-schema) (and (:params request) params-schema)
(assoc :params (assoc :params
(mc/decode (mc/decode
params-schema params-schema
(:params request) (:params request)
main-transformer)) main-transformer))
(and (:route-params request) route-schema) (and (:route-params request) route-schema)
(assoc :route-params (assoc :route-params
(mc/decode (mc/decode
route-schema route-schema
(:route-params request) (:route-params request)
main-transformer)) main-transformer))
(and form-schema form-params) (and form-schema form-params)
(assoc :form-params (assoc :form-params
(mc/decode (mc/decode
form-schema form-schema
form-params form-params
main-transformer)) main-transformer))
(and query-schema query-params) (and query-schema query-params)
(assoc :query-params (assoc :query-params
(mc/decode (mc/decode
query-schema query-schema
query-params query-params
main-transformer)))] main-transformer)))]
request)) request))
(defn wrap-schema-decode [handler & {:keys [form-schema query-schema route-schema params-schema]}] (defn wrap-schema-decode [handler & {:keys [form-schema query-schema route-schema params-schema]}]
@@ -275,8 +293,7 @@
(into [:enum {:decode/string #(if (keyword? %) (into [:enum {:decode/string #(if (keyword? %)
% %
(when (not-empty %) (when (not-empty %)
(keyword n %)) (keyword n %)))}]
)}]
(for [{:db/keys [ident]} (all-schema) (for [{:db/keys [ident]} (all-schema)
:when (= n (namespace ident))] :when (= n (namespace ident))]
ident))) ident)))
@@ -301,41 +318,39 @@
(defn wrap-form-4xx-2 [handler form-handler] (defn wrap-form-4xx-2 [handler form-handler]
(fn [request] (fn [request]
(try+ (try+
(handler request) (handler request)
(catch [:type :schema-validation] e (catch [:type :schema-validation] e
(let [humanized (-> e :error :explain (me/humanize {:errors (assoc me/default-errors
(let [humanized (-> e :error :explain (me/humanize {:errors (assoc me/default-errors ::mc/missing-key {:error/message {:en "required"}})}))
::mc/missing-key {:error/message {:en "required"}})})) errors (map
errors (map (fn [e]
(fn [e] {:path (:in e)
{:path (:in e) :message (get-in humanized (:in e))})
:message (get-in humanized (:in e))}) (:errors (:explain (:error e))))]
(:errors (:explain (:error e))))] (alog/warn ::form-4xx :errors errors)
(alog/warn ::form-4xx :errors errors) (form-handler (assoc request
(form-handler (assoc request :form-params (:decoded e)
:form-params (:decoded e) :field-validation-errors errors
:field-validation-errors errors :form-errors humanized)))
:form-errors humanized))) #_(html-response [:span.error-content.text-red-500 (:message &throw-context)]
#_(html-response [:span.error-content.text-red-500 (:message &throw-context)] :status 400))
:status 400)) (catch [:type :field-validation] e
(catch [:type :field-validation] e (form-handler (assoc request
(form-handler (assoc request :form-params (:form e)
:form-params (:form e) :form-errors (:form-errors e))))
:form-errors (:form-errors e)))) (catch [:type :form-validation] e
(catch [:type :form-validation] e (form-handler (assoc request
(form-handler (assoc request :form-params (:form e)
:form-params (:form e) :form-validation-errors (:form-validation-errors e)
:form-validation-errors (:form-validation-errors e) :form-errors {:errors (:form-validation-errors e)}))))))
:form-errors {:errors (:form-validation-errors e)}))))))
(defn apply-middleware-to-all-handlers [key->handler f] (defn apply-middleware-to-all-handlers [key->handler f]
(->> key->handler (->> key->handler
(reduce (reduce
(fn [key-handler [k v]] (fn [key-handler [k v]]
(assoc key-handler k (f v))) (assoc key-handler k (f v)))
key->handler) key->handler)))
))
(defn path->name2 [k & rest] (defn path->name2 [k & rest]
(let [k->n (fn [k] (let [k->n (fn [k]
@@ -354,10 +369,10 @@
(defn wrap-entity [handler path read] (defn wrap-entity [handler path read]
(fn wrap-entity-request [request] (fn wrap-entity-request [request]
(let [entity (some->> (let [entity (some->>
(get-in request path) (get-in request path)
(#(if (string? %) (Long/parseLong %) %)) (#(if (string? %) (Long/parseLong %) %))
(dc/pull (dc/db conn) read ))] (dc/pull (dc/db conn) read))]
(handler (if entity (handler (if entity
(assoc request (assoc request
:entity entity) :entity entity)
request))))) request)))))

View File

@@ -1,10 +1,13 @@
(ns user (ns user
(:require (:require
[amazonica.aws.s3 :as s3] [amazonica.aws.s3 :as s3]
[clojure.tools.namespace.repl :refer [set-refresh-dirs refresh]]
[auto-ap.datomic :refer [conn pull-attr random-tempid]] [auto-ap.datomic :refer [conn pull-attr random-tempid]]
[auto-ap.ledger :as l ] [auto-ap.ledger :as l]
[clj-http.core :as http] [clj-http.core :as http]
[clj-http.client :as client] [clj-http.client :as client]
[figwheel.main.api]
[hawk.core]
[auto-ap.server] [auto-ap.server]
[auto-ap.time :as atime] [auto-ap.time :as atime]
[auto-ap.utils :refer [by]] [auto-ap.utils :refer [by]]
@@ -29,6 +32,7 @@
(:import (:import
(org.apache.commons.io.input BOMInputStream))) (org.apache.commons.io.input BOMInputStream)))
(defn println-event [item] (defn println-event [item]
(printf "%s: %s - %s:%s by %s\n" (printf "%s: %s - %s:%s by %s\n"
(str (c/to-date-time (:mulog/timestamp item))) (str (c/to-date-time (:mulog/timestamp item)))
@@ -36,23 +40,22 @@
(if (:mulog/duration item) (if (:mulog/duration item)
(str " " (int (/ (:mulog/duration item) 1000000)) "ms") (str " " (int (/ (:mulog/duration item) 1000000)) "ms")
"") "")
(:user-name item) (:user-name item))
)
(println (reduce (println (reduce
(fn [acc [k v]]
(assoc acc k v))
{}
(dissoc
item
:user)))
#_(puget/cprint (reduce
(fn [acc [k v]] (fn [acc [k v]]
(assoc acc k v)) (assoc acc k v))
{} {}
(dissoc (dissoc
item item
:user)) :user)))
{:seq-limit 10}) #_(puget/cprint (reduce
(fn [acc [k v]]
(assoc acc k v))
{}
(dissoc
item
:user))
{:seq-limit 10})
(println)) (println))
@@ -70,8 +73,7 @@
(publish [_ buffer] (publish [_ buffer]
;; items are pairs [offset <item>] ;; items are pairs [offset <item>]
(doseq [item (transform (map second (rb/items buffer)))] (doseq [item (transform (map second (rb/items buffer)))]
(println-event item) (println-event item))
)
(flush) (flush)
(rb/clear buffer))) (rb/clear buffer)))
@@ -91,23 +93,23 @@
(defn load-accounts [conn] (defn load-accounts [conn]
(let [[header & rows] (-> "master-account-list.csv" (io/resource) io/input-stream (BOMInputStream.) (io/reader) csv/read-csv) (let [[header & rows] (-> "master-account-list.csv" (io/resource) io/input-stream (BOMInputStream.) (io/reader) csv/read-csv)
code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code
:db/id])] :db/id])]
:in ['$] :in ['$]
:where ['[?e :account/name]]} :where ['[?e :account/name]]}
(dc/db conn)))) (dc/db conn))))
also-merge-txes (fn [also-merge old-account-id] also-merge-txes (fn [also-merge old-account-id]
(if old-account-id (if old-account-id
(let [[sunset-account] (let [[sunset-account]
(first (dc/q {:find ['?a ] (first (dc/q {:find ['?a]
:in ['$ '?ac ] :in ['$ '?ac]
:where ['[?a :account/numeric-code ?ac]]} :where ['[?a :account/numeric-code ?ac]]}
(dc/db conn) also-merge))] (dc/db conn) also-merge))]
(into (mapv (into (mapv
(fn [[entity id _]] (fn [[entity id _]]
[:db/add entity id old-account-id]) [:db/add entity id old-account-id])
(dc/q {:find ['?e '?id '?a ] (dc/q {:find ['?e '?id '?a]
:in ['$ '?ac ] :in ['$ '?ac]
:where ['[?a :account/numeric-code ?ac] :where ['[?a :account/numeric-code ?ac]
'[?e ?at ?a] '[?e ?at ?a]
'[?at :db/ident ?id]]} '[?at :db/ident ?id]]}
@@ -122,7 +124,7 @@
(into {} (map vector header r)))) (into {} (map vector header r))))
(map (fn parse-map [r] (map (fn parse-map [r]
{:old-account-id (:db/id (code->existing-account {:old-account-id (:db/id (code->existing-account
(or (or
(if (= (get r "IOL Account #") (if (= (get r "IOL Account #")
"NEW") "NEW")
nil nil
@@ -157,8 +159,7 @@
(if also-merge (if also-merge
(into tx (into tx
(also-merge-txes also-merge old-account-id)) (also-merge-txes also-merge old-account-id))
tx) tx)))))
))))
conj conj
@@ -168,7 +169,7 @@
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn find-bad-accounts [] (defn find-bad-accounts []
(set (map second (dc/q {:find ['(pull ?x [*]) '?z] (set (map second (dc/q {:find ['(pull ?x [*]) '?z]
:in ['$] :in ['$]
:where ['[?e :account/numeric-code ?z] :where ['[?e :account/numeric-code ?z]
'[(<= ?z 9999)] '[(<= ?z 9999)]
@@ -177,32 +178,31 @@
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn delete-4-digit-accounts [] (defn delete-4-digit-accounts []
@(dc/transact conn @(dc/transact conn
(transduce (transduce
(comp (comp
(map first) (map first)
(map (fn [old-account-id] (map (fn [old-account-id]
[:db/retractEntity old-account-id]))) [:db/retractEntity old-account-id])))
conj conj
[] []
(dc/q {:find ['?e] (dc/q {:find ['?e]
:in ['$] :in ['$]
:where ['[?e :account/numeric-code ?z] :where ['[?e :account/numeric-code ?z]
'[(<= ?z 9999)]]} '[(<= ?z 9999)]]}
(dc/db conn)))) (dc/db conn)))))
)
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn find-conflicting-accounts [] (defn find-conflicting-accounts []
(filter (filter
(fn [[_ v]] (fn [[_ v]]
(> (count v) 1)) (> (count v) 1))
(reduce (reduce
(fn [acc [e z]] (fn [acc [e z]]
(update acc z conj e)) (update acc z conj e))
{} {}
(dc/q {:find ['?e '?z] (dc/q {:find ['?e '?z]
:in ['$] :in ['$]
:where ['[?e :account/numeric-code ?z]]} :where ['[?e :account/numeric-code ?z]]}
(dc/db conn))))) (dc/db conn)))))
@@ -214,31 +214,29 @@
:in ['$ '?z] :in ['$ '?z]
:where [['?e :client/code '?z]]} :where [['?e :client/code '?z]]}
(dc/db conn) customer))) (dc/db conn) customer)))
_ (println client-id)
code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code
{:account/applicability [:db/ident]} {:account/applicability [:db/ident]}
:db/id])] :db/id])]
:in ['$] :in ['$]
:where ['[?e :account/name]]} :where ['[?e :account/name]]}
(dc/db conn)))) (dc/db conn))))
existing-account-overrides (dc/q {:find ['?e] existing-account-overrides (dc/q {:find ['?e]
:in ['$ '?client-id] :in ['$ '?client-id]
:where [['?e :account-client-override/client '?client-id]]} :where [['?e :account-client-override/client '?client-id]]}
(dc/db conn) client-id) (dc/db conn) client-id)
_ (when-let [bad-rows (seq (->> rows _ (when-let [bad-rows (seq (->> rows
(group-by (fn [[_ account]] (group-by (fn [[_ account]]
account)) account))
vals vals
(filter #(> (count %) 1)) (filter #(> (count %) 1))
(filter (fn [duplicates] (filter (fn [duplicates]
(apply not= (map rest duplicates)))) (apply not= (map rest duplicates))))
#_(map (fn [[[_ account]]] #_(map (fn [[[_ account]]]
account)) account))))]
))]
(throw (Exception. (str "These accounts are duplicated:" (str bad-rows))))) (throw (Exception. (str "These accounts are duplicated:" (str bad-rows)))))
rows (vec (set (map rest rows))) rows (vec (set (map rest rows)))
@@ -256,8 +254,7 @@
(:db/ident (:account/applicability existing))) (:db/ident (:account/applicability existing)))
(and (not-empty override-name) (and (not-empty override-name)
(not-empty account-name) (not-empty account-name)
(not= override-name account-name) (not= override-name account-name))))
)))
[{:db/id (:db/id existing) [{:db/id (:db/id existing)
:account/client-overrides [{:account-client-override/client client-id :account/client-overrides [{:account-client-override/client client-id
:account-client-override/name (or (not-empty override-name) :account-client-override/name (or (not-empty override-name)
@@ -284,34 +281,30 @@
[:db/retractEntity x]) [:db/retractEntity x])
existing-account-overrides) existing-account-overrides)
rows)] rows)]
txes txes
#_@(d/transact conn txes))) #_@(d/transact conn txes)))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn fix-transactions-without-locations [client-code location] (defn fix-transactions-without-locations [client-code location]
(->> (->>
(dc/q {:find ['(pull ?e [*])] (dc/q {:find ['(pull ?e [*])]
:in ['$ '?client-code] :in ['$ '?client-code]
:where ['[?e :transaction/accounts ?ta] :where ['[?e :transaction/accounts ?ta]
'[?e :transaction/matched-rule] '[?e :transaction/matched-rule]
'[?e :transaction/approval-status :transaction-approval-status/approved] '[?e :transaction/approval-status :transaction-approval-status/approved]
'(not [?ta :transaction-account/location]) '(not [?ta :transaction-account/location])
'[?e :transaction/client ?c] '[?e :transaction/client ?c]
'[?c :client/code ?client-code] '[?c :client/code ?client-code]]}
]} (dc/db conn) client-code)
(dc/db conn) client-code)
(mapcat (mapcat
(fn [[{:transaction/keys [accounts]}]] (fn [[{:transaction/keys [accounts]}]]
(mapv (mapv
(fn [a] (fn [a]
{:db/id (:db/id a) {:db/id (:db/id a)
:transaction-account/location location} :transaction-account/location location})
) accounts)))
accounts)
)
)
vec)) vec))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
@@ -323,21 +316,21 @@
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn entity-history [i] (defn entity-history [i]
(vec (sort-by first (dc/q (vec (sort-by first (dc/q
{:find ['?tx '?z '?v ] {:find ['?tx '?z '?v]
:in ['?i '$] :in ['?i '$]
:where ['[?i ?a ?v ?tx ?ad] :where ['[?i ?a ?v ?tx ?ad]
'[?a :db/ident ?z] '[?a :db/ident ?z]
'[(= ?ad true)]]} '[(= ?ad true)]]}
i (dc/history (dc/db conn)))))) i (dc/history (dc/db conn))))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn entity-history-with-revert [i] (defn entity-history-with-revert [i]
(vec (sort-by first (dc/q (vec (sort-by first (dc/q
{:find ['?tx '?z '?v '?ad ] {:find ['?tx '?z '?v '?ad]
:in ['?i '$] :in ['?i '$]
:where ['[?i ?a ?v ?tx ?ad] :where ['[?i ?a ?v ?tx ?ad]
'[?a :db/ident ?z]]} '[?a :db/ident ?z]]}
i (dc/history (dc/db conn)))))) i (dc/history (dc/db conn))))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn tx-detail [i] (defn tx-detail [i]
@@ -357,47 +350,71 @@
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn start-db [] (defn start-db []
(mu/start-publisher! {:type :dev}) (mu/start-publisher! {:type :dev})
(mount.core/start (mount.core/only #{#'auto-ap.datomic/conn }))) (mount.core/start (mount.core/only #{#'auto-ap.datomic/conn})))
(defn- auto-reset-handler [ctx event]
(require 'figwheel.main.api)
(binding [*ns* *ns*]
(clojure.tools.namespace.repl/refresh)
ctx))
(defn auto-reset
"Automatically reset the system when a Clojure or edn file is changed in
`src` or `resources`."
[]
(println "starting auto reset")
(hawk.core/watch! [{:paths ["src/"]
:handler auto-reset-handler}]))
(defn start-http []
(mount.core/start (mount.core/only #{#'auto-ap.server/port #'auto-ap.server/jetty})))
(defn start-dev []
(set-refresh-dirs "src")
(start-db)
(start-http)
(auto-reset))
#_(defn start-search [] #_(defn start-search []
(mount.core/start (mount.core/only #{#'auto-ap.graphql.vendors/indexer #'auto-ap.graphql.accounts/indexer}))) (mount.core/start (mount.core/only #{#'auto-ap.graphql.vendors/indexer #'auto-ap.graphql.accounts/indexer})))
(defn restart-db [] (defn restart-db []
#_(require 'datomic.dev-local) #_(require 'datomic.dev-local)
#_(datomic.dev-local/release-db {:system "dev" :db-name "prod-migration"}) #_(datomic.dev-local/release-db {:system "dev" :db-name "prod-migration"})
(mount.core/stop (mount.core/only #{#'auto-ap.datomic/conn })) (mount.core/stop (mount.core/only #{#'auto-ap.datomic/conn}))
(start-db)) (start-db))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn spit-csv [columns data ] (defn spit-csv [columns data]
(csv/write-csv *out* (csv/write-csv *out*
(into [(map name columns)] (into [(map name columns)]
(for [r data] (for [r data]
((apply juxt columns) r ))))) ((apply juxt columns) r)))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn find-queries [words] (defn find-queries [words]
(let [obj (s3/list-objects-v2 :bucket-name (:data-bucket env) (let [obj (s3/list-objects-v2 :bucket-name (:data-bucket env)
:prefix (str "queries/")) :prefix (str "queries/"))
concurrent 30 concurrent 30
output-chan (async/chan)] output-chan (async/chan)]
(async/pipeline-blocking concurrent (async/pipeline-blocking concurrent
output-chan output-chan
(comp (comp
(map #(do (map #(do
[(:key %) [(:key %)
(str (slurp (:object-content (s3/get-object (str (slurp (:object-content (s3/get-object
:bucket-name (:data-bucket env) :bucket-name (:data-bucket env)
:key (:key %)))))])) :key (:key %)))))]))
(filter #(->> words (filter #(->> words
(every? (fn [w] (str/includes? (second %) w))))) (every? (fn [w] (str/includes? (second %) w)))))
(map first) (map first)
(map #(str/replace % #"queries/" "")) (map #(str/replace % #"queries/" "")))
)
(async/to-chan! (:object-summaries obj)) (async/to-chan! (:object-summaries obj))
true true
(fn [e] (fn [e]
@@ -408,13 +425,13 @@
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn upsert-invoice-amounts [tsv] (defn upsert-invoice-amounts [tsv]
(let [data (with-open [reader (io/reader (char-array tsv))] (let [data (with-open [reader (io/reader (char-array tsv))]
(doall (csv/read-csv reader :separator \tab))) (doall (csv/read-csv reader :separator \tab)))
db (dc/db conn) db (dc/db conn)
i->invoice-id (fn [i] i->invoice-id (fn [i]
(try (Long/parseLong i) (try (Long/parseLong i)
(catch Exception e (catch Exception e
(:db/id (dc/pull db '[:db/id] (:db/id (dc/pull db '[:db/id]
[:invoice/original-id (Long/parseLong (first (str/split i #"-")))]))))) [:invoice/original-id (Long/parseLong (first (str/split i #"-")))])))))
invoice-totals (->> data invoice-totals (->> data
(drop 1) (drop 1)
(group-by first) (group-by first)
@@ -423,13 +440,11 @@
(reduce + 0.0 (reduce + 0.0
(->> values (->> values
(map (fn [[_ _ _ _ amount]] (map (fn [[_ _ _ _ amount]]
(- (Double/parseDouble amount)))))) (- (Double/parseDouble amount))))))]))
]))
(into {}))] (into {}))]
(->> (->>
(for [[i invoice-expense-account-id target-account target-date amount _ location] (drop 1 data) (for [[i invoice-expense-account-id target-account target-date amount _ location] (drop 1 data)
:let [ :let [invoice-id (i->invoice-id i)
invoice-id (i->invoice-id i)
invoice (dc/pull db '[FILL_IN] invoice-id) invoice (dc/pull db '[FILL_IN] invoice-id)
current-total (:invoice/total invoice) current-total (:invoice/total invoice)
@@ -441,34 +456,32 @@
(:db/id (first (:invoice/expense-accounts invoice))) (:db/id (first (:invoice/expense-accounts invoice)))
(random-tempid)) (random-tempid))
invoice-expense-account (when-not new-account? invoice-expense-account (when-not new-account?
(dc/pull db '[FILL_IN]invoice-expense-account-id)) (dc/pull db '[FILL_IN] invoice-expense-account-id))
current-account-id (:db/id (:invoice-expense-account/account invoice-expense-account)) current-account-id (:db/id (:invoice-expense-account/account invoice-expense-account))
target-account-id (Long/parseLong (str/trim target-account)) target-account-id (Long/parseLong (str/trim target-account))
target-date (clj-time.coerce/to-date (atime/parse target-date atime/normal-date)) target-date (clj-time.coerce/to-date (atime/parse target-date atime/normal-date))
current-date (:invoice/date invoice) current-date (:invoice/date invoice)
current-expense-account-amount (:invoice-expense-account/amount invoice-expense-account 0.0)
current-expense-account-amount (:invoice-expense-account/amount invoice-expense-account 0.0)
target-expense-account-amount (- (Double/parseDouble amount)) target-expense-account-amount (- (Double/parseDouble amount))
current-expense-account-location (:invoice-expense-account/location invoice-expense-account) current-expense-account-location (:invoice-expense-account/location invoice-expense-account)
target-expense-account-location location target-expense-account-location location
[[_ _ invoice-payment]] (vec (dc/q [[_ _ invoice-payment]] (vec (dc/q
'[:find ?p ?a ?ip '[:find ?p ?a ?ip
:in $ ?i :in $ ?i
:where [?ip :invoice-payment/invoice ?i] :where [?ip :invoice-payment/invoice ?i]
[?ip :invoice-payment/amount ?a] [?ip :invoice-payment/amount ?a]
[?ip :invoice-payment/payment ?p] [?ip :invoice-payment/payment ?p]]
] db invoice-id))]
db invoice-id))]
:when current-total] :when current-total]
[ [(when (not (auto-ap.utils/dollars= current-total target-total))
(when (not (auto-ap.utils/dollars= current-total target-total))
{:db/id invoice-id {:db/id invoice-id
:invoice/total target-total}) :invoice/total target-total})
@@ -486,7 +499,7 @@
[:db/retractEntity invoice-payment]) [:db/retractEntity invoice-payment])
(when (or new-account? (when (or new-account?
(not (auto-ap.utils/dollars= current-expense-account-amount target-expense-account-amount))) (not (auto-ap.utils/dollars= current-expense-account-amount target-expense-account-amount)))
{:db/id invoice-expense-account-id {:db/id invoice-expense-account-id
:invoice-expense-account/amount target-expense-account-amount}) :invoice-expense-account/amount target-expense-account-amount})
@@ -495,7 +508,7 @@
{:db/id invoice-expense-account-id {:db/id invoice-expense-account-id
:invoice-expense-account/location target-expense-account-location}) :invoice-expense-account/location target-expense-account-location})
(when (not= current-account-id target-account-id ) (when (not= current-account-id target-account-id)
{:db/id invoice-expense-account-id {:db/id invoice-expense-account-id
:invoice-expense-account/account target-account-id})]) :invoice-expense-account/account target-account-id})])
(mapcat identity) (mapcat identity)
@@ -506,18 +519,18 @@
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn get-schema [prefix] (defn get-schema [prefix]
(->> (dc/q '[:find ?i (->> (dc/q '[:find ?i
:in $ ?p :in $ ?p
:where [_ :db/ident ?i] :where [_ :db/ident ?i]
[(namespace ?i) ?p]] (dc/db auto-ap.datomic/conn) prefix) [(namespace ?i) ?p]] (dc/db auto-ap.datomic/conn) prefix)
(mapcat identity) (mapcat identity)
vec)) vec))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn get-idents [] (defn get-idents []
(->> (dc/q '[:find ?i (->> (dc/q '[:find ?i
:in $ :in $
:where [_ :db/ident ?i]] :where [_ :db/ident ?i]]
(dc/db conn) ) (dc/db conn))
(mapcat identity) (mapcat identity)
(map str) (map str)
(sort) (sort)
@@ -532,45 +545,45 @@
(defn sample-ledger-import (defn sample-ledger-import
([client-code] ([client-code]
(sample-ledger-import client-code 10)) (sample-ledger-import client-code 10))
([client-code n] ([client-code n]
(let [client-location (ffirst (d/q '[:find ?l :in $ ?c :where [?c :client/locations ?l]] (dc/db conn) [:client/code client-code]))] (let [client-location (ffirst (d/q '[:find ?l :in $ ?c :where [?c :client/locations ?l]] (dc/db conn) [:client/code client-code]))]
(clojure.data.csv/write-csv (clojure.data.csv/write-csv
*out* *out*
(for [n (range n) (for [n (range n)
:let [v (rand-nth (map first (d/q '[:find ?vn :where [_ :vendor/name ?vn]] (dc/db conn)))) :let [v (rand-nth (map first (d/q '[:find ?vn :where [_ :vendor/name ?vn]] (dc/db conn))))
[{a-1 :account/numeric-code a-1-location :account/location} [{a-1 :account/numeric-code a-1-location :account/location}
{a-2 :account/numeric-code a-2-location :account/location}] (->> (d/q '[:find (pull ?a [:account/numeric-code :account/location]) :where [?a :account/numeric-code]] {a-2 :account/numeric-code a-2-location :account/location}] (->> (d/q '[:find (pull ?a [:account/numeric-code :account/location]) :where [?a :account/numeric-code]]
(dc/db conn)) (dc/db conn))
(map first) (map first)
(shuffle) (shuffle)
(take 2)) (take 2))
amount (rand-int 2000) amount (rand-int 2000)
d (-> (t/now) d (-> (t/now)
(t/minus (t/days (rand-int 60))) (t/minus (t/days (rand-int 60)))
(atime/unparse atime/normal-date)) (atime/unparse atime/normal-date))
id (rand-int 100000)] id (rand-int 100000)]
a [[(str id) client-code "synthetic" v d a-1 (or a-1-location client-location) 0 amount ] a [[(str id) client-code "synthetic" v d a-1 (or a-1-location client-location) 0 amount]
[(str id) client-code "synthetic" v d a-2 (or a-2-location client-location) amount 0]]] [(str id) client-code "synthetic" v d a-2 (or a-2-location client-location) amount 0]]]
a) a)
:separator \tab)))) :separator \tab))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn sample-manual-yodlee (defn sample-manual-yodlee
([client-code] ([client-code]
(sample-ledger-import client-code 10)) (sample-ledger-import client-code 10))
([client-code n] ([client-code n]
(let [bank-accounts (map first (d/q '[:find ?bac :in $ ?c :where [?c :client/bank-accounts ?b] [?b :bank-account/code ?bac]] (dc/db conn) [:client/code client-code]))] (let [bank-accounts (map first (d/q '[:find ?bac :in $ ?c :where [?c :client/bank-accounts ?b] [?b :bank-account/code ?bac]] (dc/db conn) [:client/code client-code]))]
(clojure.data.csv/write-csv (clojure.data.csv/write-csv
*out* *out*
(for [n (range n) (for [n (range n)
:let [amount (rand-int 2000) :let [amount (rand-int 2000)
d (-> (t/now) d (-> (t/now)
(t/minus (t/days (rand-int 60))) (t/minus (t/days (rand-int 60)))
(atime/unparse atime/normal-date)) (atime/unparse atime/normal-date))
id (rand-int 100000)]] id (rand-int 100000)]]
["posted" d (str "Random Description - " id) "Travel" nil nil (- amount) nil nil nil nil nil (rand-nth bank-accounts) client-code]) ["posted" d (str "Random Description - " id) "Travel" nil nil (- amount) nil nil nil nil nil (rand-nth bank-accounts) client-code])
:separator \tab)))) :separator \tab))))
@@ -580,8 +593,7 @@
:in $ :in $
:where [?i :invoice/invoice-number] :where [?i :invoice/invoice-number]
(not [?i :invoice/status :invoice-status/voided])] (not [?i :invoice/status :invoice-status/voided])]
:args [ :args [(dc/db conn)]})
(dc/db conn)]})
(map first) (map first)
(partition-all 1000))] (partition-all 1000))]
(print ".") (print ".")
@@ -627,4 +639,4 @@
(print ".") (print ".")
@(dc/transact auto-ap.datomic/conn n))) @(dc/transact auto-ap.datomic/conn n)))

View File

@@ -31,3 +31,87 @@
[?c :client/code ?cd] [?c :client/code ?cd]
[?c :client/locations ?l]] [?c :client/locations ?l]]
(dc/db conn)) (dc/db conn))
(init-repl)
(seq (dc/q
'[:find ?a ?n ?n2
:where [?a :account/name ?n]
[?a :account/numeric-code ?n2]
(not [?a :account/code])]
(dc/db conn)))
(dc/q
'[:find (pull ?a [* {:account/applicability [:db/ident] :account/default-allowance [*]}])
:where [?a :account/numeric-code 34090]]
(dc/db conn))
(dc/q
'[:find ?a
:where [?a :account/numeric-code ?nc]
(not [?a :account/default-allowance])]
(dc/since (dc/db conn) #inst "2023-02-01"))
@(dc/transact conn
(->>
(dc/q
'[:find ?a
:where [?a :account/numeric-code ?nc]
(not [?a :account/default-allowance])]
(dc/since (dc/db conn) #inst "2023-02-01"))
(map (fn [[a]]
{:db/id a
:account/default-allowance :allowance/allowed})))
)
(dc/q '[:find (pull ?l [*])
:in $ ?a
:where [?a :invoice/client]
[?l :journal-entry/original-entity ?a]]
(dc/db conn)
17592316421929)
(dc/pull (dc/db conn) '[*] 17592316421929)
(entity-history 17592316421929)
(dc/q '[:find (pull ?l [*])
:in $ ?a
:where [?a :invoice/client]
[?l :journal-entry/original-entity ?a]]
(dc/db conn)
17592316421929)
;; Find journal entries that have been divorced from the original entity
@(dc/transact auto-ap.datomic/conn
(->>
(dc/q '[:find ?l
:in $ $$ $$$
:where [$$ ?l :journal-entry/amount]
(not [$ ?l :journal-entry/external-id])
[$ ?l :journal-entry/source "invoice"]
(not [$ ?l :journal-entry/original-entity])
[$ ?l :journal-entry/client ?c]
[$ ?c :client/code ?cd]
[$$$ ?l :journal-entry/original-entity _ ?tx false]]
(dc/db conn)
(dc/since (dc/db conn) #inst "2024-02-04")
(dc/history (dc/db conn)))
(map (fn [[jl]]
[:db/retractEntity jl]))
seq))
(entity-history 13194269907490)
(user/tx-detail 13194269907766)
(dc/tx-range (dc/log conn)
13194269907490
13194269907490)

View File

@@ -6,10 +6,7 @@
"needs-activation/" :needs-activation "needs-activation/" :needs-activation
"needs-activation" :needs-activation "needs-activation" :needs-activation
"payments/" :payments "payments/" :payments
"admin/" {"clients/" {"" :admin-clients "admin/" { "vendors" :admin-vendors}
[:id] {"" :admin-specific-client
"/bank-accounts/" {[:bank-account] :admin-specific-bank-account}}}
"vendors" :admin-vendors}
"invoices/" {"" :invoices "invoices/" {"" :invoices
"import" :import-invoices "import" :import-invoices
"unpaid" :unpaid-invoices "unpaid" :unpaid-invoices
@@ -22,8 +19,6 @@
"requires-feedback" :requires-feedback-transactions "requires-feedback" :requires-feedback-transactions
"excluded" :excluded-transactions} "excluded" :excluded-transactions}
"reports/" {"" :reports} "reports/" {"" :reports}
"plaid" :plaid
"yodlee2" :yodlee2
"ledger/" {"" :ledger "ledger/" {"" :ledger
"profit-and-loss" :profit-and-loss "profit-and-loss" :profit-and-loss
"cash-flows" :cash-flows "cash-flows" :cash-flows

View File

@@ -1,11 +1,25 @@
(ns auto-ap.permissions) (ns auto-ap.permissions)
;; TODO after getting rid of cljs, use malli schemas to decode this
(defn get-client-id [client]
(cond (nat-int? client)
client
(:db/id client)
(:db/id client)
:else
nil))
(defn can? [user {:keys [client subject activity]}] (defn can? [user {:keys [client subject activity]}]
(let [role (or (:user/role user) (:role user) user)] (let [role (or (:user/role user) (:role user) user)
(println "ROLE IS" role) client-id (get-client-id client)]
(cond (#{:user-role/admin "admin"} role) (cond (#{:user-role/admin "admin"} role)
true true
(and client-id (not (get (into #{} (map :db/id (:clients user))) client-id)))
false
(#{:user-role/power-user "power-user"} role) (#{:user-role/power-user "power-user"} role)
(cond (cond
(#{:invoice-page :payment-page :my-company-page :transaction-page :ledger-page} subject) (#{:invoice-page :payment-page :my-company-page :transaction-page :ledger-page} subject)
@@ -49,6 +63,9 @@
(= [:vendor :edit] [subject activity]) (= [:vendor :edit] [subject activity])
true true
(= [:signature :edit] [subject activity])
true
:else false) :else false)
:else :else

View File

@@ -0,0 +1,19 @@
(ns auto-ap.routes.admin.clients)
(def routes {"" {:get ::page
:put ::save
:post ::save}
"/table" ::table
"/navigate" ::navigate
"/bank-accounts/sort" ::sort-bank-accounts
"/discard" ::discard
"/square-locations" ::refresh-square-locations
"/location/new" ::new-location
"/match/new" ::new-match
"/location-match/new" ::new-location-match
"/email-contact/new" ::new-email-contact
"/feature-flag/new" ::new-feature-flag
"/new" {:get ::new-dialog}
["/" [#"\d+" :db/id] "/sales-powerquery"] ::biweekly-sales-powerquery
["/" [#"\d+" :db/id] "/edit"] ::edit-dialog})

View File

@@ -10,9 +10,10 @@
"/account/typeahead" ::account-typeahead "/account/typeahead" ::account-typeahead
"/test" ::test "/test" ::test
"/new" {:get ::new-dialog} "/new" {:get ::new-dialog}
"/navigate" ::navigate
["/" [#"\d+" :db/id] "/edit"] ::edit-dialog ["/" [#"\d+" :db/id] "/edit"] ::edit-dialog
["/" [#"\d+" :db/id] "/delete"] ::delete ["/" [#"\d+" :db/id] "/delete"] ::delete
["/" [#"\d+" :db/id] "/run"] {:get ::execute-dialog ["/" [#"\d+" :db/id] "/run"] {:get ::execute-dialog
:post ::execute} :post ::execute}
"/check-badges" ::check-badges "/check-badges" ::check-badges
}) })

View File

@@ -9,6 +9,7 @@
"/account-override" ::new-account-override "/account-override" ::new-account-override
"/account-typeahead" ::account-typeahead "/account-typeahead" ::account-typeahead
"/validate" ::validate "/validate" ::validate
"/navigat" ::navigate
"/new" {:get ::new} "/new" {:get ::new}
"/merge" {:get ::merge "/merge" {:get ::merge
:put ::merge-submit} :put ::merge-submit}

View File

@@ -30,16 +30,6 @@
[:i {:class icon-class}]]) [:i {:class icon-class}]])
[:span {:class "name"} label]]]) [:span {:class "name"} label]]])
(defn menu-item [{:keys [label route test-route active-route icon-class icon-style]}]
[:p.menu-item
[:a.item {:href (bidi/path-for all-client-visible-routes route)
:class (when (test-route active-route) "is-active")}
(if icon-style
[:span {:class icon-class :style icon-style}]
[:span {:class "icon"}
[:i {:class icon-class}]])
[:span {:class "name"} label]]])
(defn company-side-bar-impl [active-route] (defn company-side-bar-impl [active-route]
[:div [:div
(menu-item {:label "Reports" (menu-item {:label "Reports"

View File

@@ -3,6 +3,7 @@
[auto-ap.routes.admin.excel-invoices :as ei-routes] [auto-ap.routes.admin.excel-invoices :as ei-routes]
[auto-ap.routes.admin.import-batch :as ib-routes] [auto-ap.routes.admin.import-batch :as ib-routes]
[auto-ap.routes.admin.vendors :as v-routes] [auto-ap.routes.admin.vendors :as v-routes]
[auto-ap.routes.admin.clients :as ac-routes]
[auto-ap.routes.admin.transaction-rules :as tr-routes])) [auto-ap.routes.admin.transaction-rules :as tr-routes]))
(def routes {"impersonate" :impersonate (def routes {"impersonate" :impersonate
@@ -15,6 +16,7 @@
"/update" {:patch :invoice-glimpse-update-textract-invoice}}}}} "/update" {:patch :invoice-glimpse-update-textract-invoice}}}}}
"account" {"/search" {:get :account-search}} "account" {"/search" {:get :account-search}}
"admin" {"" :auto-ap.routes.admin/page "admin" {"" :auto-ap.routes.admin/page
"/client" ac-routes/routes
"/history" {"" :admin-history "/history" {"" :admin-history
"/" :admin-history "/" :admin-history
#"/search/?" :admin-history-search #"/search/?" :admin-history-search
@@ -61,8 +63,10 @@
"/table" {:get :pos-cash-drawer-shift-table}}} "/table" {:get :pos-cash-drawer-shift-table}}}
"vendor" {"/search" :vendor-search} "vendor" {"/search" :vendor-search}
;; TODO Include IDS in routes for company-specific things, as opposed to headers
"company" {"" :company "company" {"" :company
"/dropdown" :company-dropdown-search-results "/dropdown" :company-dropdown-search-results
"/signature" {"/put" :company-update-signature}
"/search" :company-search "/search" :company-search
"/bank-account/typeahead" :bank-account-typeahead "/bank-account/typeahead" :bank-account-typeahead
["/" [#"\d+" :db/id] "/bank-account"] {"/search" :bank-account-search} ["/" [#"\d+" :db/id] "/bank-account"] {"/search" :bank-account-search}

View File

@@ -47,7 +47,7 @@
(defn builder [{:keys [value on-change can-submit data-sub error-messages change-event submit-event id fullwidth? schema validation-error-string]}] (defn builder [{:keys [value on-change can-submit data-sub error-messages change-event submit-event id fullwidth? schema validation-error-string]}]
(when (and change-event on-change) (when (and change-event on-change)
(throw "Either the form is to be managed by ::forms, or it should have value and on-change passed in")) (throw (js/Error. "Either the form is to be managed by ::forms, or it should have value and on-change passed in")))
(let [data-sub (or data-sub [::forms/form id]) (let [data-sub (or data-sub [::forms/form id])
change-event (when-not on-change change-event (when-not on-change
(or change-event [::forms/change id])) (or change-event [::forms/change id]))

View File

@@ -14,7 +14,6 @@
[auto-ap.views.components.typeahead :refer [typeahead-v3]] [auto-ap.views.components.typeahead :refer [typeahead-v3]]
[auto-ap.views.components.typeahead.vendor [auto-ap.views.components.typeahead.vendor
:refer [search-backed-typeahead]] :refer [search-backed-typeahead]]
[auto-ap.views.pages.admin.vendors.common :as common]
[auto-ap.views.utils [auto-ap.views.utils
:refer [dispatch-event str->int with-is-admin? with-user]] :refer [dispatch-event str->int with-is-admin? with-user]]
[malli.core :as m] [malli.core :as m]
@@ -23,9 +22,25 @@
;; Remaining cleanup todos: ;; Remaining cleanup todos:
;; test minification ;; test minification
(def default-read [:id :name :hidden :terms [:default-account [:name :id :location]]
[:account-overrides [[:client [:id :name]] :id [:account [:id :numeric-code :name]]]]
[:automatically-paid-when-due [:id :name]]
[:terms-overrides [[:client [:id :name]] :id :terms]]
[:schedule-payment-dom [[:client [:id :name]] :id :dom]]
[:usage [:client-id :count]]
[:primary-contact [:name :phone :email :id]]
[:plaid-merchant [:name :id]]
[:secondary-contact [:id :name :phone :email]]
:print-as :invoice-reminder-schedule :code
:legal-entity-name
:legal-entity-first-name :legal-entity-middle-name :legal-entity-last-name
:legal-entity-tin :legal-entity-tin-type
:legal-entity-1099-type
[:address [:id :street1 :street2 :city :state :zip]]])
(def terms-override-schema (m/schema [:map (def terms-override-schema (m/schema [:map
[:client schema/reference] [:client schema/reference]
[:terms :int]])) [:terms :int]]))
(def automatically-paid-schema (m/schema [:map (def automatically-paid-schema (m/schema [:map
[:client schema/reference]])) [:client schema/reference]]))
@@ -35,8 +50,8 @@
[:dom [:int {:max 30}]]])) [:dom [:int {:max 30}]]]))
(def account-override-schema (m/schema [:map (def account-override-schema (m/schema [:map
[:client schema/reference] [:client schema/reference]
[:account schema/reference]])) [:account schema/reference]]))
(def schema (m/schema [:map [:name schema/not-empty-string] (def schema (m/schema [:map [:name schema/not-empty-string]
[:print-as {:optional true} [:print-as {:optional true}
@@ -72,73 +87,73 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::save-complete ::save-complete
[(forms/triggers-stop ::vendor-form)] [(forms/triggers-stop ::vendor-form)]
(fn [_ [_ _ ]] (fn [_ [_ _]]
{:dispatch [::modal/modal-closed ]})) {:dispatch [::modal/modal-closed]}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::save ::save
[with-user with-is-admin? (forms/triggers-loading ::vendor-form) (forms/in-form ::vendor-form)] [with-user with-is-admin? (forms/triggers-loading ::vendor-form) (forms/in-form ::vendor-form)]
(fn [{:keys [user is-admin?] {{:keys [name hidden print-as terms invoice-reminder-schedule plaid-merchant primary-contact automatically-paid-when-due schedule-payment-dom secondary-contact address default-account terms-overrides account-overrides id legal-entity-name legal-entity-tin legal-entity-tin-type legal-entity-first-name legal-entity-last-name legal-entity-middle-name legal-entity-1099-type] :as data} :data} :db} _] (fn [{:keys [user is-admin?] {{:keys [name hidden print-as terms invoice-reminder-schedule plaid-merchant primary-contact automatically-paid-when-due schedule-payment-dom secondary-contact address default-account terms-overrides account-overrides id legal-entity-name legal-entity-tin legal-entity-tin-type legal-entity-first-name legal-entity-last-name legal-entity-middle-name legal-entity-1099-type] :as data} :data} :db} _]
(if (m/validate schema data) (if (m/validate schema data)
(let [query [:upsert-vendor (let [query [:upsert-vendor
{:vendor (cond-> {:id id {:vendor (cond-> {:id id
:name name :name name
:print-as print-as :print-as print-as
:terms (or terms :terms (or terms
nil) nil)
:default-account-id (:id default-account) :default-account-id (:id default-account)
:address address :address address
:primary-contact primary-contact :primary-contact primary-contact
:secondary-contact secondary-contact :secondary-contact secondary-contact
:invoice-reminder-schedule invoice-reminder-schedule} :invoice-reminder-schedule invoice-reminder-schedule}
is-admin? (assoc :hidden hidden is-admin? (assoc :hidden hidden
:terms-overrides (mapv :terms-overrides (mapv
(fn [{:keys [client terms id]}] (fn [{:keys [client terms id]}]
{:id id {:id id
:client-id (:id client) :client-id (:id client)
:terms (or (str->int terms) 0)}) :terms (or (str->int terms) 0)})
terms-overrides) terms-overrides)
:account-overrides (mapv :account-overrides (mapv
(fn [{:keys [client account id]}] (fn [{:keys [client account id]}]
{:id id {:id id
:client-id (:id client) :client-id (:id client)
:account-id (:id account)}) :account-id (:id account)})
account-overrides) account-overrides)
:schedule-payment-dom (mapv :schedule-payment-dom (mapv
(fn [{:keys [client dom id]}] (fn [{:keys [client dom id]}]
{:id id {:id id
:client-id (:id client) :client-id (:id client)
:dom (or (str->int dom) :dom (or (str->int dom)
0)}) 0)})
schedule-payment-dom) schedule-payment-dom)
:automatically-paid-when-due (mapv :automatically-paid-when-due (mapv
(comp :id :client) (comp :id :client)
automatically-paid-when-due) automatically-paid-when-due)
:plaid-merchant (:id plaid-merchant) :plaid-merchant (:id plaid-merchant)
:legal-entity-name legal-entity-name :legal-entity-name legal-entity-name
:legal-entity-first-name legal-entity-first-name :legal-entity-first-name legal-entity-first-name
:legal-entity-middle-name legal-entity-middle-name :legal-entity-middle-name legal-entity-middle-name
:legal-entity-last-name legal-entity-last-name :legal-entity-last-name legal-entity-last-name
:legal-entity-tin legal-entity-tin :legal-entity-tin legal-entity-tin
:legal-entity-tin-type (some-> legal-entity-tin-type clojure.core/name not-empty keyword) :legal-entity-tin-type (some-> legal-entity-tin-type clojure.core/name not-empty keyword)
:legal-entity-1099-type (some-> legal-entity-1099-type clojure.core/name not-empty keyword)))} :legal-entity-1099-type (some-> legal-entity-1099-type clojure.core/name not-empty keyword)))}
common/default-read]] default-read]]
{ :graphql {:graphql
{:token user {:token user
:owns-state {:single ::vendor-form} :owns-state {:single ::vendor-form}
:query-obj {:venia/operation :query-obj {:venia/operation
{:operation/type :mutation {:operation/type :mutation
:operation/name "UpsertVendor"} :venia/queries [{:query/data query}]} :operation/name "UpsertVendor"} :venia/queries [{:query/data query}]}
:on-success [::save-complete]}}) :on-success [::save-complete]}})
{:dispatch-n [[::forms/attempted-submit ::vendor-form] {:dispatch-n [[::forms/attempted-submit ::vendor-form]
[::status/error ::vendor-form [{:message "Please fix the errors and try again."}]]]}))) [::status/error ::vendor-form [{:message "Please fix the errors and try again."}]]]})))
(defn contact-field [{:keys [name field]}] (defn contact-field [{:keys [name field]}]
[form-builder/with-scope {:scope field} [form-builder/with-scope {:scope field}
[form-builder/vertical-control [form-builder/vertical-control
name name
[left-stack [left-stack
[form-builder/vertical-control {:is-small? true} [form-builder/vertical-control {:is-small? true}
"Name" "Name"
[:div.control.has-icons-left [:div.control.has-icons-left
@@ -196,23 +211,22 @@
[form-builder/section {:title "Terms"} [form-builder/section {:title "Terms"}
[form-builder/field-v2 {:field :terms} [form-builder/field-v2 {:field :terms}
"Terms" "Terms"
[number-input ]] [number-input]]
(when is-admin? (when is-admin?
[form-builder/field-v2 {:field [:terms-overrides]} [form-builder/field-v2 {:field [:terms-overrides]}
"Overrides" "Overrides"
[multi-field-v2 {:template [[form-builder/raw-field-v2 {:field [:client]} [multi-field-v2 {:template [[form-builder/raw-field-v2 {:field [:client]}
[typeahead-v3 {:entities clients [typeahead-v3 {:entities clients
:entity->text :name :entity->text :name
:style {:width "13em"} :style {:width "13em"}
:type "typeahead-v3" :type "typeahead-v3"}]]
}]]
[form-builder/raw-field-v2 {:field :terms} [form-builder/raw-field-v2 {:field :terms}
[number-input]]] [number-input]]]
:schema [:sequential terms-override-schema] :schema [:sequential terms-override-schema]
:key-fn :id :key-fn :id
:next-key (random-uuid) :next-key (random-uuid)
:new-text "New Terms Override"}]])] :new-text "New Terms Override"}]])]
(when is-admin? (when is-admin?
[form-builder/section {:title "Schedule payment when due"} [form-builder/section {:title "Schedule payment when due"}
[form-builder/field-v2 {:field [:automatically-paid-when-due]} [form-builder/field-v2 {:field [:automatically-paid-when-due]}
@@ -228,7 +242,7 @@
(when is-admin? (when is-admin?
[form-builder/section {:title "Schedule payment on day of month"} [form-builder/section {:title "Schedule payment on day of month"}
[form-builder/field-v2 {:field [:schedule-payment-dom]} [form-builder/field-v2 {:field [:schedule-payment-dom]}
"Overrides" "Overrides"
[multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :client} [multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :client}
[typeahead-v3 {:entities clients [typeahead-v3 {:entities clients
@@ -249,8 +263,7 @@
{:query i {:query i
:allowance :vendor} :allowance :vendor}
[:name :id :warning]]) [:name :id :warning]])
:style {:width "19em"}}] :style {:width "19em"}}]]
]
(when (:warning (:default-account vendor)) (when (:warning (:default-account vendor))
[:div.notification.is-warning.is-light [:div.notification.is-warning.is-light
(:warning (:default-account vendor))]) (:warning (:default-account vendor))])
@@ -258,23 +271,22 @@
[form-builder/field-v2 {:field [:account-overrides]} [form-builder/field-v2 {:field [:account-overrides]}
"Overrides" "Overrides"
[multi-field-v2 {:template (fn [entity] [multi-field-v2 {:template (fn [entity]
[[form-builder/raw-field-v2 {:field :client} [[form-builder/raw-field-v2 {:field :client}
[typeahead-v3 {:entities clients [typeahead-v3 {:entities clients
:entity->text :name :entity->text :name
:style {:width "19em"} :style {:width "19em"}}]]
}]] [form-builder/raw-field-v2 {:field :account}
[form-builder/raw-field-v2 {:field :account} [search-backed-typeahead {:search-query (fn [i]
[search-backed-typeahead {:search-query (fn [i] [:search_account
[:search_account {:query i
{:query i :client_id (:id (:client entity))
:client_id (:id (:client entity)) :allowance :vendor}
:allowance :vendor} [:name :id :warning]])
[:name :id :warning]]) :style {:width "15em"}}]]])
:style {:width "15em"}}]]]) :schema [:sequential account-override-schema]
:schema [:sequential account-override-schema] :key-fn :id
:key-fn :id :next-key (random-uuid)
:next-key (random-uuid) :new-text "Add override"}]])]
:new-text "Add override"}]])]
[form-builder/section {:title "Address"} [form-builder/section {:title "Address"}
[:div {:style {:width "30em"}} [:div {:style {:width "30em"}}
@@ -319,8 +331,7 @@
[form-builder/raw-field-v2 {:field :legal-entity-tin} [form-builder/raw-field-v2 {:field :legal-entity-tin}
[:input.input {:type "text" [:input.input {:type "text"
:placeholder "SSN or EIN" :placeholder "SSN or EIN"
:size "12" :size "12"}]]
}]]
[:div.control [:div.control
[form-builder/raw-field-v2 {:field :legal-entity-tin-type} [form-builder/raw-field-v2 {:field :legal-entity-tin-type}
@@ -336,25 +347,25 @@
:allow-nil? true}]]]) :allow-nil? true}]]])
[form-builder/hidden-submit-button]])) [form-builder/hidden-submit-button]]))
(defn vendor-dialog [ ] (defn vendor-dialog []
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::vendor-form])] (let [{:keys [data]} @(re-frame/subscribe [::forms/form ::vendor-form])]
[:div [:div
[form-content {:data data}]])) [form-content {:data data}]]))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::vendor-selected ::vendor-selected
[with-user (forms/in-form ::select-vendor-form)] [with-user (forms/in-form ::select-vendor-form)]
(fn [{{:keys [data]} :db :keys [user]} _] (fn [{{:keys [data]} :db :keys [user]} _]
(if (:vendor data) (if (:vendor data)
{:graphql {:token user {:graphql {:token user
:query-obj {:venia/queries [[:vendor-by-id :query-obj {:venia/queries [[:vendor-by-id
{:id (:id (:vendor data))} {:id (:id (:vendor data))}
common/default-read]]} default-read]]}
:owns-state {:single ::select-vendor-form} :owns-state {:single ::select-vendor-form}
:on-success (fn [r] :on-success (fn [r]
[::started (:vendor-by-id r)])}} [::started (:vendor-by-id r)])}}
{:dispatch-n [[::forms/attempted-submit ::select-vendor-form] {:dispatch-n [[::forms/attempted-submit ::select-vendor-form]
[::status/error ::select-vendor-form [{:message "Please select a vendor."}]]]}))) [::status/error ::select-vendor-form [{:message "Please select a vendor."}]]]})))
(defn select-vendor-form-content [] (defn select-vendor-form-content []
[form-builder/builder {:submit-event [::vendor-selected] [form-builder/builder {:submit-event [::vendor-selected]

View File

@@ -18,9 +18,7 @@
[auto-ap.views.pages.ledger.profit-and-loss-detail :refer [profit-and-loss-detail-page]] [auto-ap.views.pages.ledger.profit-and-loss-detail :refer [profit-and-loss-detail-page]]
[auto-ap.views.pages.login :refer [login-page]] [auto-ap.views.pages.login :refer [login-page]]
[auto-ap.views.pages.payments :refer [payments-page]] [auto-ap.views.pages.payments :refer [payments-page]]
[auto-ap.views.pages.home :refer [home-page]] [auto-ap.views.pages.home :refer [home-page]]))
[auto-ap.views.pages.admin.clients :refer [admin-clients-page]]
[auto-ap.views.pages.admin.vendors :refer [admin-vendors-page]]))
(defmulti page (fn [active-page] active-page)) (defmulti page (fn [active-page] active-page))
(defmethod page :unpaid-invoices [_] (defmethod page :unpaid-invoices [_]
@@ -93,14 +91,6 @@
(when (p/can? @(re-frame/subscribe [::subs/user]) {:subject :ledger-page}) (when (p/can? @(re-frame/subscribe [::subs/user]) {:subject :ledger-page})
(balance-sheet-page))) (balance-sheet-page)))
(defmethod page :admin-clients [_]
(when (p/can? @(re-frame/subscribe [::subs/user]) {:subject :admin-page})
(admin-clients-page)))
(defmethod page :admin-specific-client [_]
(when (p/can? @(re-frame/subscribe [::subs/user]) {:subject :admin-page})
(admin-clients-page)))
(defmethod page :admin-vendors [_] (defmethod page :admin-vendors [_]
(when (p/can? @(re-frame/subscribe [::subs/user]) {:subject :admin-page}) (when (p/can? @(re-frame/subscribe [::subs/user]) {:subject :admin-page})
(admin-vendors-page))) (admin-vendors-page)))

View File

@@ -1,106 +0,0 @@
(ns auto-ap.views.pages.admin.clients
(:require
[auto-ap.routes :as routes]
[auto-ap.status :as status]
[auto-ap.subs :as subs]
[auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]]
[auto-ap.views.pages.data-page :as data-page]
[auto-ap.views.components.grid :as grid]
[auto-ap.views.components.layouts :refer [side-bar-layout]]
[auto-ap.views.pages.admin.clients.form :as form]
[auto-ap.views.pages.admin.clients.side-bar :as side-bar]
[auto-ap.views.pages.admin.clients.table :as table]
[auto-ap.views.pages.page-stack :as page-stack]
[auto-ap.views.utils :refer [with-user]]
[bidi.bidi :as bidi]
[clojure.string :as str]
[clojure.set :as set]
[re-frame.core :as re-frame]
[vimsical.re-frame.fx.track :as track]
[auto-ap.events :as events]
[auto-ap.forms :as forms]
[auto-ap.db :as db]))
(re-frame/reg-event-db
::received-intuit-bank-accounts
(fn [db [_ result]]
(assoc db ::subs/intuit-bank-accounts (:intuit-bank-accounts result))))
(re-frame/reg-event-fx
::mounted
[with-user]
(fn [{:keys [user db]} _]
{::track/register [{:id ::params
:subscription [::data-page/params ::page]
:event-fn (fn [params] [::params-change params])}
{:id ::active-route
:subscription [::subs/active-route]
:event-fn (fn [params] [::params-change params])}]
:db (-> db
(forms/stop-form [::form/form]))
:graphql {:token user
:query-obj {:venia/queries [[:intuit_bank_accounts [:external_id :id :name]]]}
:owns-state {:single [::load-intuit-bank-accounts]}
:on-success [::received-intuit-bank-accounts]} }))
(re-frame/reg-event-fx
::unmounted
(fn [{:keys [db]} _]
{:db (dissoc db ::table/params ::side-bar/filter-params)
::track/dispose [{:id ::params}
{:id ::active-route}]}))
(defn data-params->query-params [params]
{:start (:start params 0)
:per-page (:per-page params)
:sort (:sort params)
:name-like (:name-like params)
:code (:code params)})
(re-frame/reg-event-fx
::params-change
[with-user]
(fn [{:keys [user]} [_ params]]
{:graphql {:token user
:owns-state {:single [::data-page/page ::page]}
:query-obj {:venia/queries [[:client-page
{:filters (data-params->query-params params)}
[[:clients (events/client-detail-query user)]
:total
:start
:end]]]}
:on-success (fn [result]
[::data-page/received ::page (set/rename-keys (:client-page result)
{:clients :data})])}}))
(def admin-clients-content
(with-meta
(fn []
[:div
[page-stack/page-stack
{:active @(re-frame/subscribe [::subs/active-route])
:pages [{:key :admin-clients
:breadcrumb "Clients"
:content [:<>
[:div.is-pulled-right
[:a.button.is-primary.is-outlined {:href (bidi/path-for routes/routes :admin-specific-client :id "new")} "New client"]]
[table/clients-table {:data-page ::page
:id :clients}]]}
{:key :admin-specific-client
:breadcrumb [:span [:a {:href (bidi/path-for routes/routes :admin-clients)}
"Clients"]
" / "
(or (:name (:data @(re-frame/subscribe [::forms/form ::form/form])))
[:i "New client"])]
:content [form/new-client-form]}
]}]])
{:component-did-mount #(re-frame/dispatch [::mounted])
:component-will-unmount #(re-frame/dispatch-sync [::unmounted])}))
(defn admin-clients-page []
[side-bar-layout {:side-bar [admin-side-bar {}
[side-bar/client-side-bar {:data-page ::page}]]
:main [admin-clients-content]}])

View File

@@ -1,761 +0,0 @@
(ns auto-ap.views.pages.admin.clients.form
(:require
[auto-ap.events :as events]
[auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.routes :as routes]
[auto-ap.subs :as subs]
[auto-ap.views.components.address :refer [address2-field]]
[react-signature-canvas]
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
[auto-ap.views.components.level :refer [left-stack] :as level]
[auto-ap.views.components :as com]
[auto-ap.views.components.typeahead.vendor
:refer [search-backed-typeahead]]
[auto-ap.views.utils
:refer [date-picker
with-user
dispatch-event]]
[bidi.bidi :as bidi]
[cljs-time.coerce :as coerce]
[cljs-time.core :as t]
[re-frame.core :as re-frame]
[reagent.core :as r]
[vimsical.re-frame.cofx.inject :as inject]
[auto-ap.schema :as schema]
[malli.core :as m]))
(def signature-canvas (r/adapt-react-class (.-default react-signature-canvas)))
(def location-schema (m/schema [:map
[:location schema/not-empty-string]]))
(def feature-flag-schema (m/schema [:map
[:feature-flag schema/not-empty-string]]))
(def square-location-schema (m/schema [:map
[:square-location schema/reference]
[:client-location schema/not-empty-string]]))
(def ezcater-schema (m/schema [:map
[:caterer schema/reference]
[:client-location schema/not-empty-string]]))
(def name-match-schema (m/schema [:map
[:match schema/not-empty-string]]))
(def location-match-schema (m/schema [:map
[:match schema/not-empty-string]
[:location schema/not-empty-string]]))
(def email-schema [:map
[:email schema/not-empty-string]
[:description schema/not-empty-string]])
(def client-schema [:map
[:name schema/not-empty-string]
[:code schema/code-string]
[:locations [:sequential location-schema]]
[:feature-flags {:optional true} [:maybe [:sequential feature-flag-schema]]]
[:emails {:optional true}
[:maybe [:sequential email-schema]]]
[:matches {:optional true}
[:maybe [:sequential name-match-schema]]]
[:location-matches {:optional true}
[:maybe [:sequential location-match-schema]]]
[:selected-square-locations {:optional true}
[:maybe [:sequential square-location-schema]]]])
(defn upload-replacement-button [{:keys [on-change]} text]
(let [button (atom nil)]
(r/create-class {:display-name "Upload button"
:reagent-render
(fn []
[:<>
[:label.button {:for "upload_replacement_signature"} text]
[:input.button {:type "file" :id "upload_replacement_signature"
:style {:display "none"}
:on-change (fn []
(let [fr (js/FileReader.)]
(.addEventListener fr "load" (fn []
(on-change (.-result fr))))
(.readAsDataURL fr (aget (.-files @button) 0)))
)
:ref (fn [i] (reset! button i))} ]])})))
(defn signature [_]
(let [canvas (atom nil)
edit-mode? (r/atom false)
w (* 1.5 464)
h (* 1.5 174)]
(fn [{:keys [signature-file signature-data on-change]}]
[:div
(if @edit-mode?
[:div
[signature-canvas {"canvasProps" {"width" w
"height" h
"style" #js {"border" "1px solid #CCC"
"border-radius" "10px"}}
"backgroundColor" "#FFF"
:ref (fn [el]
(reset! canvas el))}]
[:div.buttons
[:a.button.is-primary.is-outlined {:on-click (fn []
(on-change (.toDataURL @canvas "image/jpeg"))
(reset! edit-mode? false))}
"Accept"]
[:a.button.is-warning.is-outlined {:on-click (fn []
(.clear @canvas)
(reset! edit-mode? false))}
"Cancel"]]]
(if (or signature-data signature-file)
[:div
[:img {:src (or signature-data signature-file)
:style {:width w
:height h
:border "1px solid #CCC"
:border-radius "10px"}}]
[:div.buttons
[:a.button {:on-click (fn []
(reset! edit-mode? true))}
"Replace Signature"]
[upload-replacement-button {:on-change on-change} "Upload replacement"]]]
[:div
[:div.has-text-centered.is-vcentered {:style {:width w
:height h
:margin-bottom "8px"
:border "1px solid #CCC"
:border-radius "10px"
:background "#EEE"
}}
"No signature"]
[:div.buttons
[:a.button.is-primary.is-outlined {:on-click (fn []
(reset! edit-mode? true))}
"New Signature"]
[upload-replacement-button {:on-change on-change} "Upload signature"]]]))
])))
(re-frame/reg-sub
::new-client-request
:<- [::forms/form ::form]
(fn [{new-client-data :data} _]
(cond->
{:id (:id new-client-data),
:name (:name new-client-data)
:code (:code new-client-data) ;; TODO add validation can't change
:emails (map #(select-keys % [:id :email :description])
(:emails new-client-data))
:square-auth-token (:square-auth-token new-client-data)
:square-locations (map
(fn [x]
{:id (:id (:square-location x))
:client-location (:client-location x)})
(:selected-square-locations new-client-data))
:ezcater-locations (map
(fn [x]
{:id (:id x)
:caterer (:id (:caterer x))
:location (:location x)})
(:ezcater-locations new-client-data))
:locked-until (:locked-until new-client-data)
:locations (mapv :location (:locations new-client-data))
:feature-flags (mapv :feature-flag (:feature-flags new-client-data))
:matches (mapv :match (:matches new-client-data))
:location-matches (:location-matches new-client-data)
:week-a-credits (:week-a-credits new-client-data)
:week-a-debits (:week-a-debits new-client-data)
:week-b-credits (:week-b-credits new-client-data)
:week-b-debits (:week-b-debits new-client-data)
:address {:id (:id (:address new-client-data))
:street1 (:street1 (:address new-client-data))
:street2 (:street2 (:address new-client-data)),
:city (:city (:address new-client-data))
:state (:state (:address new-client-data))
:zip (:zip (:address new-client-data))}
:signature-data (:signature-data new-client-data)
:forecasted-transactions (map (fn [{:keys [id day-of-month identifier amount]}]
{:id id
:day-of-month (js/parseInt day-of-month)
:identifier identifier
:amount amount})
(:forecasted-transactions new-client-data))
:bank-accounts (map-indexed (fn [i {:keys [number name check-number plaid-account intuit-bank-account include-in-reports type id code numeric-code start-date bank-name routing bank-code new? visible locations yodlee-account use-date-instead-of-post-date]}]
{:number number
:name name
:check-number check-number
:numeric-code numeric-code
:include-in-reports include-in-reports
:start-date start-date
:type type
:id id
:sort-order i
:visible visible
:locations (mapv :location locations)
:use-date-instead-of-post-date use-date-instead-of-post-date
:yodlee-account (:id yodlee-account)
:plaid-account (:id plaid-account)
:intuit-bank-account (:id intuit-bank-account)
:code (if new?
(str (:code new-client-data) "-" code)
code)
:bank-name bank-name
:routing routing
:bank-code bank-code})
(:bank-accounts new-client-data))})))
(re-frame/reg-event-fx
::mounted
[with-user (re-frame/inject-cofx ::inject/sub [::subs/route-params])]
(fn [{:keys [user db] ::subs/keys [route-params]} _]
(when-let [id (some-> (:id route-params) (js/parseInt ) (#(if (js/Number.isNaN %) nil %)))]
{:graphql {:token user
:query-obj {:venia/queries [[:admin-client
{:id id}
(events/client-detail-query user)]]}
:on-success (fn [result]
[::received (:admin-client result)])}
:db (-> db
(forms/stop-form ::form))})))
(re-frame/reg-event-db
::received
(fn [db [_ client]]
(-> db
(forms/stop-form ::form)
(forms/start-form ::form (-> client
(assoc :selected-square-locations (->> (:square-locations client)
(filter :client-location )
(mapv (fn [sl]
{:id (:id sl)
:square-location sl
:client-location (:client-location sl)}))))
(update :locations #(mapv (fn [l] {:location l
:id (random-uuid)}) %))
(update :feature-flags #(mapv (fn [l] {:feature-flag l
:id (random-uuid)}) %))
(update :matches #(mapv (fn [l] {:match l
:id (random-uuid)}) %))
(update :bank-accounts
(fn [bas]
(mapv (fn [ba]
(update ba :locations (fn [ls]
(map (fn [l] {:location l
:id (random-uuid)})
ls))))
bas))))))))
(re-frame/reg-event-fx
::save-new-client
[(forms/in-form ::form)]
(fn [_ _]
(let [new-client-req @(re-frame/subscribe [::new-client-request])
user @(re-frame/subscribe [::subs/token])]
{:graphql
{:token user
:owns-state {:single ::form}
:query-obj {:venia/operation {:operation/type :mutation
:operation/name "EditClient"}
:venia/queries [{:query/data [:edit-client
{:edit-client new-client-req}
(events/client-detail-query user)]}]}
:on-success [::save-complete]
:on-error [::forms/save-error ::form]}})))
(re-frame/reg-event-fx
::save-complete
(fn [{:keys [db]} [_ client]]
{:db
(-> db
#_(forms/stop-form ::form)
(assoc-in [:clients (:id (:edit-client client))] (update (:edit-client client) :bank-accounts (fn [bas] (->> bas (sort-by :sort-order) vec)))))
:redirect (bidi/path-for routes/routes :admin-clients)}))
(re-frame/reg-event-db
::add-new-bank-account
[(forms/in-form ::form) (re-frame/path [:data])]
(fn [client [_ type]]
(update client :bank-accounts conj {:type type :active? true :new? true :visible true :sort-order (count (:bank-accounts client))})))
(re-frame/reg-event-db
::bank-account-activated
[(forms/in-form ::form) (re-frame/path [:data :bank-accounts])]
(fn [bank-accounts [_ index]]
(update (vec (sort-by :sort-order bank-accounts)) index assoc :active? true)))
(re-frame/reg-event-db
::bank-account-deactivated
[(forms/in-form ::form) (re-frame/path [:data :bank-accounts])]
(fn [bank-accounts [_ index]]
(update (vec (sort-by :sort-order bank-accounts)) index assoc :active? false)))
(re-frame/reg-event-db
::bank-account-removed
[(forms/in-form ::form) (re-frame/path [:data :bank-accounts])]
(fn [bank-accounts [_ index]]
(vec (concat (take index bank-accounts)
(drop (inc index) bank-accounts)))))
(re-frame/reg-event-db
::sort-swapped
[(forms/in-form ::form) (re-frame/path [:data :bank-accounts])]
(fn [bank-accounts [_ source dest]]
(->> (-> bank-accounts
(assoc-in [source :sort-order] (get-in bank-accounts [dest :sort-order]))
(assoc-in [dest :sort-order] (get-in bank-accounts [source :sort-order]))
)
(sort-by :sort-order)
vec)))
(re-frame/reg-event-db
::toggle-visible
[(forms/in-form ::form) (re-frame/path [:data :bank-accounts])]
(fn [bank-accounts [_ account]]
(-> (->> bank-accounts
(sort-by :sort-order)
vec)
(update-in [account :visible] #(not %)))))
(def first-week-a (coerce/to-date-time #inst "1999-12-27T00:00:00.000-07:00"))
(defn is-week-a? [d]
(= 0 (mod (t/in-weeks (t/interval first-week-a d)) 2)))
(re-frame/reg-sub
::yodlee-accounts
:<- [::subs/clients-by-id]
(fn [clients [_ id]]
(if id
(mapcat :accounts (:yodlee-provider-accounts (get clients id) ))
[])))
(re-frame/reg-sub
::plaid-accounts
:<- [::subs/clients-by-id]
(fn [clients [_ id]]
(if id
(mapcat :accounts (:plaid-items (get clients id) ))
[])))
(defn bank-account-card [new-client {:keys [active? new? type visible code name sort-order]} first? last?]
[:div.card {:style {:margin-bottom "1em"
:width "600px"}}
[:header.card-header.has-background-primary-light
[:div.card-header-title {:style {:text-overflow "ellipsis"}}
[:div.level {:style {:width "100%"}}
[:div.level-left
[:div.level-item
[:span.icon.inline
(cond
(#{:check ":check"} type) [:span.icon-check-payment-sign]
(#{:credit ":credit"} type) [:span.icon-credit-card-1]
:else [:span.icon-accounting-bill])]]
[:div.level-item code ": " name]]
[:div.level-right
[:div.level-item
[:div.buttons
[:a.button {:on-click (dispatch-event [::toggle-visible sort-order])} [:span.icon (if visible
[:span.fa.fa-eye]
[:span.fa.fa-eye-slash]
)]]
(when-not last?
[:a.button {:on-click (dispatch-event [::sort-swapped sort-order (inc sort-order)])} [:span.icon [:span.fa.fa-sort-down]]])
(when-not first?
[:a.button {:on-click (dispatch-event [::sort-swapped sort-order (dec sort-order)])} [:span.icon [:span.fa.fa-sort-up]]])]]]]]
(if active?
[:a.card-header-icon
{:on-click (dispatch-event [::bank-account-deactivated sort-order])}
[:span.icon
[:span.fa.fa-angle-up]]]
[:a.card-header-icon
{:on-click (dispatch-event [::bank-account-activated sort-order])}
[:span.icon
[:span.fa.fa-angle-down]]])]
(when active?
[:div.card-content
[:label.label "General"]
[level/left-stack
[:div.control
[:p.help "Account Code"]
(if new?
[:div.field.has-addons
[:p.control [:a.button.is-static (:code new-client) "-" ]]
[:p.control
[form-builder/raw-field-v2 {:field :code}
[:input.input {:type "text"}]]]]
[:div.field [:p.control code]])]
[form-builder/field-v2 {:field :name}
"Nickname"
[:input.input {:placeholder "BOA Checking #1"
:type "text"}]]
[form-builder/field-v2 {:field :numeric-code}
"Numeric Code"
[com/number-input {:placeholder "20101"
:style {:width "8em"}}]]
[form-builder/field-v2 {:field :start-date}
"Start date"
[date-picker {:output :cljs-date}]]]
(when (#{:check ":check"} type )
[:div
[:label.label "Bank"]
[level/left-stack
[form-builder/field-v2 {:field :bank-name}
"Bank Name"
[:input.input {:placeholder "Bank of America"
:type "text"}]]
[form-builder/field-v2 {:field [:routing]}
"Routing #"
[:input.input {:placeholder "104819123"
:style {:width "9em"}
:type "text"}]]
[form-builder/field-v2 {:field :bank-code}
"Bank code"
[:input.input {:placeholder "12/10123"
:type "text"}]]]
[level/left-stack
[form-builder/field-v2 {:field :number}
"Account #"
[:input.input {:placeholder "123456789"
:type "text"
:style {:width "20em"}}]]
[form-builder/field-v2 {:field :check-number}
"Check Number"
[com/number-input {:style {:width "8em"}
:placeholder "10000"}]]]
[form-builder/field-v2 {:field :yodlee-account}
"Yodlee Account (new)"
[typeahead-v3 {:entities (mapcat :accounts (:yodlee-provider-accounts new-client ))
:entity->text (fn [m] (str (:name m) " - " (:number m)))}]]
[form-builder/raw-field-v2 {:field :use-date-instead-of-post-date}
[com/checkbox {:label " (Yodlee only) Use 'date' instead of 'postDate'"}]]
[form-builder/field-v2 {:field :intuit-bank-account}
"Intuit Bank Account"
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts])
:entity->text (fn [m] (str (:name m)))}]]
[form-builder/field-v2 {:field :plaid-account}
"Plaid Account"
[typeahead-v3 {:entities (mapcat :accounts (:plaid-items new-client ))
:entity->text (fn [m] (str (:name m)))}]]])
(when (#{:credit ":credit"} type )
[:div
[:label.label "Account"]
[form-builder/field-v2 {:field :bank-name}
"Bank Name"
[:input.input {:placeholder "Bank of America"
:type "text"}]]
[form-builder/field-v2 {:field :number}
"Account #"
[:input.input {:placeholder "123456789"
:type "text"
:style {:width "20em"}}]]
[form-builder/field-v2 {:field :yodlee-account}
"Yodlee Account (new)"
[typeahead-v3 {:entities (mapcat :accounts (:yodlee-provider-accounts new-client ))
:entity->text (fn [m] (str (:name m) " - " (:number m)))}]]
[form-builder/raw-field-v2 {:field :use-date-instead-of-post-date}
[com/checkbox {:label "(Yodlee only) Use 'date' instead of 'postDate'"}]
[:input {:type "checkbox"
:field [:use-date-instead-of-post-date]}]]
[form-builder/field-v2 {:field :intuit-bank-account}
"Intuit Bank Account"
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts])
:entity->text (fn [m] (str (:name m)))}]]
[form-builder/field-v2 {:field :plaid-account}
"Plaid Account"
[typeahead-v3 {:entities (mapcat :accounts (:plaid-items new-client ))
:entity->text (fn [m] (str (:name m)))}]]])
[:div.field
[:label.label "Locations"]
[:div.control
[:p.help "If this account is location-specific, add the valid locations"]
[form-builder/raw-field-v2 {:field :locations}
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :location}
[com/select-field {:options (map (fn [l]
[(:location l) (:location l)])
(get-in new-client [:locations]))
:allow-nil? true
:style {:width "7em"}
}]]]
:schema [:sequential location-schema]
:key-fn :id}]]]]
[form-builder/raw-field-v2 {:field :include-in-reports}
[com/checkbox {:label "Include in reports"}]
]
])
(when active?
[:footer.card-footer
[:a.card-footer-item {:href "#" :on-click (dispatch-event [::bank-account-deactivated sort-order])} "Done"]
(when new?
[:a.card-footer-item.is-warning {:href "#" :on-click (dispatch-event [::bank-account-removed sort-order])} "Remove"])])])
(defn general-section []
(let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])]
[form-builder/section {:title "General"}
[form-builder/field-v2 {:field :name}
"Name"
[:input.input {:type "text"
:style {:width "20em"}}]]
[form-builder/field-v2 {:field :code}
"Client code"
[:input.input {:type "code"
:style {:width "5em"}
:disabled (boolean (:id new-client))}]]
[:div.field
[:label.label "Feature Flags"]
[:div.control
[:p.help "These are specific new features that can be enabled or disabled on a per-client basis"]
[form-builder/raw-field-v2 {:field :feature-flags}
[com/multi-field-v2 {:allow-change? true
:template [[form-builder/raw-field-v2 {:field :feature-flag}
[com/select-field {:options [[nil nil]
["new-square" "New Square+Ezcater (no effect)"]
["manually-pay-cintas" "Manually Pay Cintas"]
["include-in-ntg-corp-reports" "Include in NTG Corporate reports"]]
:allow-nil? false
:style {:width "18em"}}]]]
:key-fn :id
:schema [:sequential feature-flag-schema]
:next-key (random-uuid)}]]]]
[form-builder/field-v2 {:field :locations}
"Locations"
[com/multi-field-v2 {:allow-change? false
:template [[form-builder/raw-field-v2 {:field :location}
[:input.input {:max-length 2
:style {:width "4em"}}]]]
:disable-remove? true
:key-fn :id
:schema [:sequential location-schema]
:next-key (random-uuid)}]]
[form-builder/vertical-control
"Signature"
[signature {:signature-file (:signature-file new-client)
:signature-data (:signature-data new-client)
:on-change (fn [uri]
(re-frame/dispatch [::forms/change ::form [:signature-data] uri]))}]]
[form-builder/field-v2 {:field :locked-until}
"Locked Until"
[date-picker {:output :cljs-date
:style {:width "15em"}}]]]))
(defn contacts-section []
[form-builder/section {:title "Contacts"}
[form-builder/field-v2 {:field :emails}
"Emails (address/description)"
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :email}
[:input.input {:type "email"
:placeholder "tom@myspace.com"}]]
[form-builder/raw-field-v2 {:field :description}
[:input.input {:type "text"
:placeholder "Manager"}]]]
:key-fn :id
:schema [:sequential email-schema]
:next-key (random-uuid)}]]
[form-builder/vertical-control
"Address"
[:div {:style {:width "30em"}}
[form-builder/raw-field-v2 {:field :address}
[address2-field]]]]])
;; TODO Name matches, locations, bank account locations are all "single field multis", and require weird mounting and
;; unmounting. A new field could sort that out easily
(defn matching-section []
[form-builder/section {:title "Matching"}
[form-builder/field-v2 {:field :matches}
"Name matches"
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field [:match]}
[:input.input {:placeholder "Harry's burger joint"
:style { :width "15em"}}]]]
:key-fn :id
:next-key (random-uuid)
:schema [:sequential name-match-schema]}]]
[form-builder/field-v2 {:field :location-matches}
"Location Matches"
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :match}
[:input.input {:placeholder "Downtown"
:style { :width "15em"}}]]
[form-builder/raw-field-v2 {:field :location}
[:input.input {:placeholder "DT"
:max-length 2
:style { :width "4em"}}]]]
:schema [:sequential location-match-schema]
:next-key (random-uuid)
:key-fn :id}]]])
(defn bank-accounts-section []
(let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])]
[form-builder/section {:title "Bank Accounts"}
(for [bank-account (sort-by :sort-order (:bank-accounts new-client))]
^{:key (:sort-order bank-account)}
[form-builder/with-scope {:scope [:bank-accounts (:sort-order bank-account)]}
[bank-account-card new-client bank-account (= 0 (:sort-order bank-account)) (= (:sort-order bank-account) (dec (count (:bank-accounts new-client))))]])
[:div.buttons
[:a.button.is-primary.is-outlined {:on-click (dispatch-event [::add-new-bank-account :credit])} "Add Credit Account"]
[:a.button.is-primary.is-outlined {:on-click (dispatch-event [::add-new-bank-account :check])} "Add Checking Account"]
[:a.button.is-primary.is-outlined {:on-click (dispatch-event [::add-new-bank-account :cash])} "Add Cash Account"]]]))
(defn cash-flow-section []
(let [next-week-a (if (is-week-a? (t/now))
"This week"
"Next week")
next-week-b (if (is-week-a? (t/now))
"Next week"
"This week")]
[form-builder/section {:title "Cash Flow"}
[:label.label (str "Week A (" next-week-a ")")]
[left-stack
[form-builder/field-v2 {:field :week-a-credits}
"Regular Credits"
[com/money-input]]
[form-builder/field-v2 {:field :week-a-debits}
"Regular Debits"
[com/money-input]]]
[:label.label (str "Week B (" next-week-b ")")]
[left-stack
[form-builder/field-v2 {:field :week-b-credits}
"Regular Credits"
[com/money-input]]
[form-builder/field-v2 {:field :week-b-debits}
"Regular Debits"
[com/money-input]]]
[form-builder/field-v2 {:field :forecasted-transactions}
"Forecasted transactions"
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :identifier}
[:input.input {:type "text"
:placeholder "Identifier"
:style {:width "10em"}}]]
[form-builder/raw-field-v2 {:field :day-of-month}
[com/number-input {:placeholder "DOM"}]]
[form-builder/raw-field-v2 {:field :amount
:placeholder "AMT"}
[com/money-input]]]
:key-fn :id}]]]))
(defn square-section []
(let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])]
[form-builder/section {:title "Square Integration"}
[form-builder/field-v2 {:field :square-auth-token}
"Square Authentication Token"
[:input.input {:type "text"
:style {:width "40em"}}]]
[form-builder/field-v2 {:field :selected-square-locations}
"Square Locations"
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :square-location}
[typeahead-v3 {:entities (:square-locations new-client)
:entity->text :name
:style {:width "15em"}}]]
[form-builder/raw-field-v2 {:field :client-location}
[com/select-field {:options (map (fn [l]
[(:location l) (:location l)])
(get-in new-client [:locations]))
:allow-nil? true
:style {:width "7em"}
}]]]
:disable-remove? true
:key-fn :id
:schema [:sequential square-location-schema]}]]]))
(defn ezcater-section []
(let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])]
[form-builder/section {:title "EZCater integration"}
[form-builder/field-v2 {:field :ezcater-locations}
"EZCater Locations"
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :caterer}
[search-backed-typeahead {:search-query (fn [i]
[:search_ezcater_caterer
{:query i}
[:name :id]])
:entity->text :name
:style {:width "20em"}}]]
[form-builder/raw-field-v2 {:field [:location]}
[com/select-field {:options (map (fn [l]
[(:location l) (:location l)])
(get-in new-client [:locations]))
:allow-nil? true
:style {:width "7em"}}]]]
:key-fn :id
:schema [:sequential ezcater-schema]
:disable-remove? true}]]]))
(defn form-content []
(let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])]
^{:key (or (:id new-client)
"new")}
[form-builder/builder {:submit-event [::save-new-client ]
:id ::form
:fullwidth? false
:schema client-schema}
[general-section]
[contacts-section]
[matching-section]
[bank-accounts-section]
[cash-flow-section]
[square-section]
[ezcater-section]
[form-builder/error-notification]
[form-builder/submit-button "Save"]]))
(def new-client-form
(with-meta
(fn []
(let [_ @(re-frame/subscribe [::subs/route-params])]
[form-content]))
{:component-did-mount #(re-frame/dispatch [::mounted])}))

View File

@@ -1,23 +0,0 @@
(ns auto-ap.views.pages.admin.clients.side-bar
(:require
[re-frame.core :as re-frame]
[auto-ap.views.utils :refer [dispatch-value-change]]
[auto-ap.views.pages.data-page :as data-page]))
(defn client-side-bar [{:keys [data-page]}]
[:div
[:p.menu-label "Name"]
[:div.field
[:div.control [:input.input {:placeholder "Harry's Food Products"
:value @(re-frame/subscribe [::data-page/filter data-page :name-like])
:on-change (dispatch-value-change [::data-page/filter-changed data-page :name-like])} ]]]
[:p.menu-label "Code"]
[:div.field
[:div.control [:input.input {:placeholder "CBC"
:value @(re-frame/subscribe [::data-page/filter data-page :code])
:on-change (dispatch-value-change [::data-page/filter-changed data-page :code])} ]]]])

View File

@@ -1,123 +0,0 @@
(ns auto-ap.views.pages.admin.clients.table
(:require [auto-ap.subs :as subs]
[clojure.string :as str]
[re-frame.core :as re-frame]
[auto-ap.views.utils :refer [action-cell-width date->str with-user]]
[auto-ap.views.components.grid :as grid]
[auto-ap.views.components.modal :as modal]
[auto-ap.views.components.buttons :as buttons]
[auto-ap.status :as status]
[bidi.bidi :as bidi]
[auto-ap.routes :as routes]
[auto-ap.views.pages.data-page :as data-page]))
(re-frame/reg-sub
::specific-params
(fn [db]
(::params db)))
(re-frame/reg-event-fx
::params-changed
(fn [{:keys [db]} [_ p]]
{:db (assoc db ::params p)}))
(re-frame/reg-event-fx
::sales-queries-setup
(fn [_ [_ results]]
{:dispatch [::modal/modal-requested {:title "Sales Queries"
:body [:div [:pre (:message (:setup-sales-queries results))]]}]}))
(re-frame/reg-event-fx
::setup-sales-queries
[with-user]
(fn [{:keys [user]} [_ client-id]]
{:graphql
{:token user
:owns-state {:multi ::setup-sales-queries
:which client-id}
:query-obj {:venia/operation {:operation/type :mutation
:operation/name "SetupSalesQueries"}
:venia/queries [{:query/data [:setup-sales-queries
{:client-id client-id}
[:message]]}]}
:on-success [::sales-queries-setup]}}
))
(re-frame/reg-sub
::params
:<- [::specific-params]
:<- [::subs/query-params]
(fn [[specific-params query-params]]
(merge (select-keys query-params #{:start :sort}) specific-params )))
(defn integration-status-badge [name status]
(condp = (:state status)
:success
[:div.tag.has-tooltip-right.has-tooltip-arrow {:data-tooltip (str "Last updated:" (date->str (:last-updated status))
"\n"
"Last Attempted:" (date->str (:last-attempt status)))} [:span.icon [:i.has-text-success.fa.fa-check]] [:span name]]
:failed
[:div.tag.is-danger.is-light.has-tooltip-right.has-tooltip-arrow {:data-tooltip (str "Last updated:" (date->str (:last-updated status))
"\n"
"Last Attempted:" (date->str (:last-attempt status))
"\n"
(:message status))
} [:span.icon [:i.has-text-danger.fa.fa-warning]] [:span name]]
:unauthorized
[:div.tag.is-danger.is-light.has-tooltip-right.has-tooltip-arrow {:data-tooltip (str "Last updated:" (date->str (:last-updated status))
"\n"
"Last Attempted:" (date->str (:last-attempt status))
"\n"
"Your user is unauthorized. Detail:\n"
(:message status))
} [:span.icon [:i.has-text-danger.fa.fa-warning]] [:span name]]
nil
))
(defn clients-table [{:keys [data-page status]}]
(let [states @(re-frame/subscribe [::status/multi ::setup-sales-queries])
{:keys [data]} @(re-frame/subscribe [::data-page/page data-page])]
[grid/grid {:on-params-change (fn [p]
(re-frame/dispatch [::params-changed p]))
:data-page data-page
:status status
:params @(re-frame/subscribe [::params])
:column-count 5}
[grid/controls data]
[grid/table {:fullwidth true}
[grid/header
[grid/row {}
[grid/header-cell {} "Name"]
[grid/header-cell {:style {:width "20em"}} "Code"]
[grid/header-cell {} "Locations"]
[grid/header-cell {} "Status"]
[grid/header-cell {} "Email"]
[grid/header-cell {:style {:width (action-cell-width 2)}}]]]
[grid/body
(for [{:keys [id name email square-integration-status locked-until code locations bank-accounts]} (:data data)]
^{:key (str name "-" id)}
[grid/row {:id id}
[grid/cell {} name]
[grid/cell {} code]
[grid/cell {} (str/join ", " locations)]
[grid/cell {:class "expandable"} [:div.tags
[:div.tag (or (some-> locked-until date->str (#(str "Locked " %))) "Not locked")]
[integration-status-badge "Square" square-integration-status]
[:<>
(for [bank-account bank-accounts
:let [code (:code bank-account)
integration-status (:integration-status bank-account)]
:when (:id integration-status)]
^{:key (:id integration-status)}
[integration-status-badge code integration-status])]]]
[grid/cell {} email]
[grid/cell {} [:div.buttons [buttons/fa-icon {:event [::setup-sales-queries id]
:class (status/class-for (get states id))
:icon :fa-dollar}]
[buttons/fa-icon {:href (bidi/path-for routes/routes :admin-specific-client :id id)
:icon :fa-pencil}]]]])]]
[grid/bottom-paginator data]]))

View File

@@ -1,94 +0,0 @@
(ns auto-ap.views.pages.admin.vendors
(:require
[auto-ap.effects.forward :as forward]
[auto-ap.subs :as subs]
[auto-ap.shared-views.admin.side-bar :refer [admin-side-bar]]
[auto-ap.views.components.layouts :refer [side-bar-layout]]
[auto-ap.views.pages.admin.vendors.merge-dialog :as merge-dialog]
[auto-ap.views.pages.admin.vendors.side-bar :as side-bar]
[auto-ap.views.pages.admin.vendors.table :as table]
[auto-ap.views.pages.data-page :as data-page]
[auto-ap.views.utils :refer [dispatch-event with-user]]
[clojure.set :as set]
[re-frame.core :as re-frame]
[vimsical.re-frame.fx.track :as track]
[auto-ap.views.components.vendor-dialog :as vendor-dialog]))
(def default-read [:id :name :hidden :terms [:default-account [:name :id :location]]
[:account-overrides [[:client [:id :name]] :id [:account [:id :numeric-code :name]]]]
[:automatically-paid-when-due [:id :name]]
[:terms-overrides [[:client [:id :name]] :id :terms]]
[:schedule-payment-dom [[:client [:id :name]] :id :dom]]
[:usage [:client-id :count]]
[:primary-contact [:name :phone :email :id]]
[:secondary-contact [:id :name :phone :email]]
[:plaid-merchant [:id :name]]
:print-as :invoice-reminder-schedule :code
:legal-entity-name
:legal-entity-first-name :legal-entity-middle-name :legal-entity-last-name
:legal-entity-tin :legal-entity-tin-type
:legal-entity-1099-type
[:address [:id :street1 :street2 :city :state :zip]]])
(re-frame/reg-event-fx
::params-change
[with-user]
(fn [{:keys [user]} [_ params]]
{:graphql {:token user
:owns-state {:single [::data-page/page ::page]}
:query-obj {:venia/queries [{:query/data [:vendor
{:sort (:sort params)
:start (:start params 0)
:per-page (:per-page params)
:name-like (:name-like params)}
[[:vendors default-read]
:total
:start
:end]]
:query/alias :result}]}
:on-success (fn [result]
[::data-page/received ::page
(set/rename-keys (:result result)
{:vendors :data})])}}))
(re-frame/reg-event-fx
::mounted
(fn [_ _]
{::forward/register [{:id ::merge-complete
:events #{::merge-dialog/complete}
:event-fn (fn [_]
[::params-change {}])}
{:id ::save-complete
:events #{::vendor-dialog/save-complete}
:event-fn (fn [_]
[::params-change {}])}]
::track/register {:id ::params
:subscription [::data-page/params ::page]
:event-fn (fn [params]
[::params-change params])}}))
(re-frame/reg-event-fx
::unmounted
(fn [_ _]
{:dispatch [::data-page/dispose ::page]
::forward/dispose [{:id ::merge-complete} {:id ::save-complete}]
::track/dispose {:id ::params}}))
(defn admin-vendors-content []
[(with-meta
(fn []
[:div.inbox-messages
(when-let [banner (:banner @(re-frame/subscribe [::subs/admin]))]
[:div.notification banner])
[:div
[:h1.title "Vendors"]
[:div.is-pulled-right [:a.button.is-primary.is-outlined {:on-click (dispatch-event [::merge-dialog/show])} "Merge vendors"]]
[table/vendors-table {:id :vendors
:data-page ::page}]]])
{:component-did-mount #(re-frame/dispatch [::mounted])
:component-will-unmount #(re-frame/dispatch-sync [::unmounted])})])
(defn admin-vendors-page []
[side-bar-layout {:side-bar [admin-side-bar {}
[side-bar/vendor-side-bar {:data-page ::page}]]
:main [admin-vendors-content]}])

View File

@@ -1,17 +0,0 @@
(ns auto-ap.views.pages.admin.vendors.common)
(def default-read [:id :name :hidden :terms [:default-account [:name :id :location]]
[:account-overrides [[:client [:id :name]] :id [:account [:id :numeric-code :name]]]]
[:automatically-paid-when-due [:id :name]]
[:terms-overrides [[:client [:id :name]] :id :terms]]
[:schedule-payment-dom [[:client [:id :name]] :id :dom]]
[:usage [:client-id :count]]
[:primary-contact [:name :phone :email :id]]
[:plaid-merchant [:name :id]]
[:secondary-contact [:id :name :phone :email]]
:print-as :invoice-reminder-schedule :code
:legal-entity-name
:legal-entity-first-name :legal-entity-middle-name :legal-entity-last-name
:legal-entity-tin :legal-entity-tin-type
:legal-entity-1099-type
[:address [:id :street1 :street2 :city :state :zip]]])

View File

@@ -1,80 +0,0 @@
(ns auto-ap.views.pages.admin.vendors.merge-dialog
(:require
[auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.schema :as schema]
[auto-ap.status :as status]
[auto-ap.subs :as subs]
[auto-ap.views.components :as com]
[auto-ap.views.components.modal :as modal]
[auto-ap.views.utils :refer [dispatch-event]]
[malli.core :as m]
[re-frame.core :as re-frame]))
(def merge-schema
(m/schema [:map
[:from schema/reference]
[:to schema/reference]]))
(defn form []
[form-builder/builder {:submit-event [::try-save]
:id ::form
:schema merge-schema}
[form-builder/field-v2 {:field :from}
"Form Vendor (will be deleted)"
[com/search-backed-typeahead {:search-query (fn [i]
[:search_vendor
{:query i}
[:name :id]])
:auto-focus true}]]
[form-builder/field-v2 {:field :to}
"To Vendor"
[com/search-backed-typeahead {:search-query (fn [i]
[:search_vendor
{:query i}
[:name :id]])}]]
[form-builder/hidden-submit-button]])
(re-frame/reg-event-fx
::show
(fn [{:keys [db]} _]
{:dispatch [::modal/modal-requested {:title "Merge Vendors"
:body [form]
:confirm {:value "Merge"
:status-from [::status/single ::form]
:class "is-primary"
:on-click (dispatch-event [::try-save])
:close-event [::status/completed ::form]}}]
:db (forms/start-form db ::form {})}))
(re-frame/reg-event-fx
::complete
(fn [{:keys [db]} _]
{:db (forms/stop-form db ::form)
:dispatch [::modal/modal-closed ]}))
(re-frame/reg-event-fx
::save
[(forms/in-form ::form)]
(fn [{{{:keys [from to]} :data} :db} _]
(let [user @(re-frame/subscribe [::subs/token])]
{:graphql
{:token user
:owns-state {:single ::form}
:query-obj {:venia/operation {:operation/type :mutation
:operation/name "MergeVendors"}
:venia/queries [{:query/data [:merge-vendors
{:from (:id from) :to (:id to)} []]}]}
:on-success [::complete]}})))
(re-frame/reg-event-fx
::try-save
[(forms/in-form ::form)]
(fn [{:keys [db]}]
(if (not (m/validate merge-schema (:data db)))
{:dispatch-n [[::status/error ::form [{:message "Please correct any errors and try again"}]]
[::forms/attempted-submit ::form]]}
{:dispatch [::save]})))

View File

@@ -1,16 +0,0 @@
(ns auto-ap.views.pages.admin.vendors.side-bar
(:require
[auto-ap.views.pages.data-page :as data-page]
[auto-ap.views.utils :refer [dispatch-value-change]]
[re-frame.core :as re-frame]))
(defn vendor-side-bar [{:keys [data-page]}]
[:div
[:p.menu-label "Name"]
[:div
[:div.field
[:div.control [:input.input {:placeholder "HOME DEPOT"
:value @(re-frame/subscribe [::data-page/filter data-page :name-like])
:on-change (dispatch-value-change [::data-page/filter-changed data-page :name-like])} ]]]]])

View File

@@ -1,36 +0,0 @@
(ns auto-ap.views.pages.admin.vendors.table
(:require
[auto-ap.views.components.buttons :as buttons]
[auto-ap.views.components.grid :as grid]
[auto-ap.views.components.vendor-dialog :as vendor-dialog]
[auto-ap.views.pages.data-page :as data-page]
[auto-ap.views.utils :refer [action-cell-width]]
[re-frame.core :as re-frame]))
(defn vendors-table [{:keys [data-page]}]
(let [{:keys [data]} @(re-frame/subscribe [::data-page/page data-page])]
[grid/grid {:data-page data-page
:column-count 4}
[grid/controls data]
[grid/table {:fullwidth true}
[grid/header
[grid/row {}
[grid/header-cell {} "Name"]
[grid/header-cell {} "Email"]
[grid/header-cell {} "Default Account"]
[grid/header-cell {:style {:width (action-cell-width 1)}}]]]
[grid/body
(for [v (:data data)]
^{:key (str (:id v))}
[grid/row {:class (:class v) :id (:id v)}
[grid/cell {} (:name v)
(let [total-usage (reduce + 0 (map :count (:usage v)))]
(if (> total-usage 0)
[:div.mx-2.tag.is-info.is-light total-usage " usages, " (count (:usage v)) " clients"]
[:div.mx-2.tag.is-warning.is-light "Unused"]))]
[grid/cell {} (:email (:primary-contact v))]
[grid/cell {} (-> v :default-account :name)]
[grid/cell {}
[buttons/fa-icon {:event [::vendor-dialog/started v]
:icon "fa-pencil"}]]])]]
[grid/bottom-paginator data]]))

View File

@@ -1,4 +1,6 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
const plugin = require('tailwindcss/plugin');
module.exports = { module.exports = {
darkMode: "class", darkMode: "class",
content: ["./src/**/*.{cljs,clj,cljc}", content: ["./src/**/*.{cljs,clj,cljc}",
@@ -103,6 +105,12 @@ module.exports = {
} }
} , } ,
plugins: [ plugins: [
require('flowbite/plugin') require('flowbite/plugin'),
plugin(function ({ addVariant }) {
addVariant('htmx-settling', ['&.htmx-settling', '.htmx-settling &'])
addVariant('htmx-request', ['&.htmx-request', '.htmx-request &'])
addVariant('htmx-swapping', ['&.htmx-swapping', '.htmx-swapping &'])
addVariant('htmx-added', ['&.htmx-added', '.htmx-added &'])
}),
] ]
} }

View File

@@ -162,7 +162,7 @@
:type :type
:default_allowance))))))) :default_allowance)))))))
(deftest upsert-account #_(deftest upsert-account
(testing "should create a new account" (testing "should create a new account"
(let [result (sut/upsert-account {:id (admin-token)} {:account {:client_overrides [] (let [result (sut/upsert-account {:id (admin-token)} {:account {:client_overrides []
:numeric_code 123 :numeric_code 123

View File

@@ -1,91 +0,0 @@
(ns auto-ap.integration.graphql.clients
(:require
[auto-ap.time-reader]
[auto-ap.datomic :refer [conn pull-attr]]
[auto-ap.graphql.clients :as sut]
[auto-ap.integration.util :refer [wrap-setup user-token admin-token]]
[datomic.api :as dc]
[clojure.test :as t :refer [deftest is testing use-fixtures]]))
(use-fixtures :each wrap-setup)
(deftest upsert-client
(testing "Should create a new client"
(let [create-client-request {:code "TEST"
:name "Test Co"
:matches ["Test Company"]
:email "hello@hi.com"
:locked_until #clj-time/date-time "2022-01-01"
:locations ["DT"]
:week_a_debits 1000.0
:week_a_credits 2000.0
:week_b_debits 3000.0
:week_b_credits 9000.0
:location_matches [{:location "DT"
:match "DOWNTOWN"}]
:address {:street1 "hi street"
:street2 "downtown"
:city "seattle"
:state "wa"
:zip "1238"}
:feature_flags ["new-square"]
:bank_accounts [{:code "TEST-1"
:bank_name "Bank of America"
:bank_code "BANKCODE"
:start_date #clj-time/date-time "2022-01-01"
:routing "1235"
:include_in_reports true
:name "Bank of Am Check"
:visible true
:number "1000"
:check_number 1001
:numeric_code 12001
:sort_order 1
:locations ["DT"]
:use_date_instead_of_post_date? false
:type :cash}]
}
create-result (sut/edit-client {:id (admin-token)} {:edit_client create-client-request} nil)]
(is (some? (-> create-result :id)))
(is (some? (-> create-result :bank_accounts first :id)))
(is (= (set (keys create-client-request)) (disj (set (keys create-result))
:square_integration_status :yodlee_provider_accounts :plaid_items :id)))
(testing "Should be able to retrieve created client"
(let [created-client (sut/get-admin-client {:id (admin-token)} {:id (:id create-result)} nil)]
(is (some? (-> created-client :id)))
(is (some? (-> created-client :bank_accounts first :id)))
(is (= (set (keys create-client-request)) (disj (set (keys created-client))
:square_integration_status :yodlee_provider_accounts :plaid_items :id)))))
(testing "Should edit an existing client"
(let [edit-result (sut/edit-client {:id (admin-token)} {:edit_client {:id (:id create-result)
:name "New Company Name"
:code "TEST"}} nil)]
(is (some? (:id edit-result)))
(is (= "New Company Name" (:name edit-result)))))
(testing "Should support removing collections"
(let [edit-result (sut/edit-client {:id (admin-token)} {:edit_client {:id (:id create-result)
:matches []
:location_matches []
:feature_flags []}}
nil)]
(is (some? (:id edit-result)))
(is (seq (:location_matches create-result)))
(is (not (seq (:location_matches edit-result))))
(is (seq (:matches create-result)))
(is (not (seq (:matches edit-result))))
(is (seq (:feature_flags create-result)))
(is (not (seq (:feature_flags edit-result))))
))
))
(testing "Only admins can create clients"
(is (thrown? Exception (sut/edit-client {:id (user-token)} {:edit_client {:code "INVALID"}} nil)))))