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/logs
data/solr/logs
.vscode/**

View File

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

View File

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

View File

@@ -1,5 +1,8 @@
(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]))
@@ -13,7 +16,7 @@
{}
xs))
(defn -pull-many [db read ids ]
(defn -pull-many [db read ids]
(->> (dc/q '[:find (pull ?e r)
:in $ [?e ...] r]
db
@@ -21,6 +24,18 @@
read)
(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]
(when-not (or (:db/id entity)
@@ -96,7 +111,6 @@
(into [[:upsert-entity (assoc v :db/id id)]])))
:else
(conj ops [:db/add e a v])
))
(conj ops [:db/add e a v])))
[]))]
ops))

View File

@@ -42,6 +42,7 @@
[nrepl "0.8.3" :exclusions [org.clojure/tools.logging]]
[cheshire "5.9.0"]
[hawk "0.2.11"]
[clj-time "0.15.2"]
[ring/ring-json "0.5.0" :exclusions [cheshire]]
[com.cemerick/url "0.1.1"]
@@ -107,19 +108,22 @@
[lein-cljsbuild "1.1.5"]
[lein-ancient "0.6.15"]]
: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" ]
:resource-paths ["resources"]
:aliases {"build" ["do" ["uberjar"]]
"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"]}
:profiles {
:dev
{:resource-paths ["resources" "target"]
{
:resource-paths ["resources" "target"]
:dependencies [#_[binaryage/devteols "1.0.2"]
[postgresql/postgresql "9.3-1102.jdbc41"]
[org.clojure/tools.namespace "1.4.5"]
[org.clojure/java.jdbc "0.7.11"]
#_[com.datomic/dev-local "1.0.243"]
[etaoin "0.4.1"]
@@ -177,7 +181,7 @@
: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"
: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) {
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) {
var table = document.querySelector(id);
var rows = table.querySelectorAll("tbody tr");
console.log("ROWS", 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]
"Replaces value supplied by cursor with value 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
(:require
[amazonica.aws.s3 :as s3]
[auto-ap.datomic :refer [audit-transact conn]]
(:require [auto-ap.datomic :refer [conn]]
[auto-ap.datomic.clients :as d-clients]
[auto-ap.graphql.utils
:refer [->graphql
assert-admin
attach-tracing-resolvers
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]
:refer [->graphql <-graphql assert-admin attach-tracing-resolvers
can-see-client? is-admin? result->page]]
[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))))
[datomic.api :as dc]))
(defn refresh-all-current-balance []
(mu/with-context {:source "current-balance-refresh"}
@@ -238,201 +68,11 @@
bank-accounts))))))]
(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
@@ -535,82 +175,15 @@
:resolve :get-client-page}})
(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
{:edit_location_match {:fields {:location {:type 'String}
:match {:type 'String}
:id {:type :id}}}
:client_filters
{ :client_filters
{:fields {:code {:type 'String}
:name_like {:type 'String}
:start {:type 'Int}
:per_page {:type 'Int}
: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}}}})
:sort {:type '(list :sort_item)}}} })
(def enums
{:bank_account_type {:values [{:enum-value :check}
@@ -620,9 +193,7 @@
(def resolvers
{:get-client get-client
:get-admin-client get-admin-client
:get-client-page get-client-page
:mutation/edit-client edit-client
:mutation/setup-sales-queries setup-sales-queries})
:get-client-page get-client-page })
(defn attach [schema]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,14 +36,20 @@
(sort-by :created-at)
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-definition
:container-definitions
(mapcat :environment)
(filter (comp #{"INTEGREAT_JOB"} :name))
seq))
(defn task-definition->job-name [task-definition]
(->> (:container-definitions task-definition)
(mapcat :environment)

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,8 @@
(ns auto-ap.ssr.admin.transaction-rules
(:require
[auto-ap.datomic
:refer [add-sorter-fields
apply-pagination
apply-sort-3
audit-transact
conn
merge-query
pull-attr
pull-many
query2
remove-nils]]
(:require [auto-ap.datomic
:refer [add-sorter-fields apply-pagination apply-sort-3
audit-transact conn merge-query pull-attr pull-many
query2 remove-nils]]
[auto-ap.datomic.accounts :as d-accounts]
[auto-ap.datomic.transactions :as d-transactions]
[auto-ap.graphql.utils :refer [extract-client-ids]]
@@ -21,30 +13,21 @@
[auto-ap.rule-matching :as rm]
[auto-ap.solr :as solr]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
[auto-ap.ssr.company :refer [bank-account-typeahead*]]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.components.multi-modal :as mm]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.grid-page-helper :as helper]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils
:refer [apply-middleware-to-all-handlers
entity-id
field-validation-error
form-validation-error
html-response
main-transformer
many-entity
modal-response
money
percentage
ref->enum-schema
ref->radio-options
regex
temp-id
wrap-entity
wrap-form-4xx-2
:refer [apply-middleware-to-all-handlers entity-id
field-validation-error form-validation-error
html-response main-transformer many-entity modal-response
money percentage ref->enum-schema ref->radio-options
regex temp-id wrap-entity wrap-form-4xx-2
wrap-schema-enforce]]
[auto-ap.time :as atime]
[auto-ap.utils :refer [dollars=]]
@@ -54,7 +37,7 @@
[clojure.string :as str]
[datomic.api :as dc]
[malli.core :as mc]
[auto-ap.logging :as alog]))
[malli.util :as mut]))
(defn filters [request]
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
@@ -238,8 +221,7 @@
{:key "note"
:name "Note"
:sort-key "note"
:render :transaction-rule/note}
]}))
:render :transaction-rule/note}]}))
(def row* (partial helper/row* grid-page))
(def table* (partial helper/table* grid-page))
@@ -289,23 +271,7 @@
[:transaction-rule/bank-account]
:form form-params)))
(defn save [{:keys [form-params request-method identity] :as request}]
(validate-transaction-rule form-params)
(let [entity (cond-> form-params
(= :post request-method) (assoc :db/id "new")
true (assoc :transaction-rule/note (entity->note form-params)))
{:keys [tempids]} (audit-transact [[:upsert-entity entity]]
(:identity request))
updated-rule (dc/pull (dc/db conn)
default-read
(or (get tempids (:db/id entity)) (:db/id entity)))]
(html-response
(row* identity updated-rule {:flash? true})
:headers (cond-> {"hx-trigger" "modalclose"}
(= :post request-method) (assoc "hx-retarget" "#entity-table tbody"
"hx-reswap" "afterbegin")
(= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-rule))
"hx-reswap" "outerHTML")))))
@@ -411,41 +377,10 @@
(com/data-grid-cell {} (-> r :transaction/client :client/name))
(com/data-grid-cell {} (-> r :transaction/bank-account :bank-account/name))
(com/data-grid-cell {} (some-> r :transaction/date (atime/unparse-local atime/normal-date)))
(com/data-grid-cell {} (some-> r :transaction/description-original )))))]))
(defn test [{:keys [form-params request-method identity] :as request
entity :form-params}]
(validate-transaction-rule form-params)
(html-response
(com/stacked-modal-card
1
{}
[:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"] ]
(transaction-rule-test-table* {:entity entity
:clients (:clients request)})
[:div.flex.justify-between
(com/button {"@click" "$dispatch('modalpop')"
:class "w-32"}
"Back")
(com/button (cond-> {:color :primary
:hx-include "#my-form"
:class "w-32"
}
(:db/id form-params) (assoc :hx-put (bidi/path-for ssr-routes/only-routes ::route/save))
(not (:db/id form-params)) (assoc :hx-post (bidi/path-for ssr-routes/only-routes ::route/save)))
"Save rule")])
:headers (-> {}
(assoc "hx-trigger-after-settle" "modalnext")
(assoc "hx-retarget" ".modal-stack")
(assoc "hx-reswap" "beforeend"))))
;; TODO only uncoded
(com/data-grid-cell {} (some-> r :transaction/description-original)))))]))
(defn- location-select*
[{:keys [ name account-location client-locations value]}]
[{:keys [name account-location client-locations value]}]
(com/select {:options (into [["" ""]]
(cond account-location
[[account-location account-location]]
@@ -512,7 +447,7 @@
[:div {:hx-trigger "changed"
:hx-target "next *"
:hx-swap "outerHTML"
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || '', value: event.detail.location || ''}" (fc/field-name) )
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || '', value: event.detail.location || ''}" (fc/field-name))
:hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select)
:x-init "$watch('clientId', cid => $dispatch('changed', $data)); $watch('accountId', cid => $dispatch('changed', $data) )"}]
(location-select* {:name (fc/field-name)
@@ -530,36 +465,181 @@
(com/money-input {:name (fc/field-name)
:class "w-16"
:value (some-> (fc/field-value)
(* 100 )
(long ))}))))))
(* 100)
(long))}))))))
(com/data-grid-cell {:class "align-top"}
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
(defn dialog* [{:keys [entity form-params form-errors]}]
(fc/start-form form-params form-errors
(com/modal
{:modal-class "max-w-2xl"
:hx-target "this"
}
(com/stacked-modal-card
0
(defn all-ids-not-locked [all-ids]
(->> all-ids
(dc/q '[:find ?t
:in $ [?t ...]
:where
[?t :transaction/client ?c]
[(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
[?t :transaction/date ?d]
[(>= ?d ?lu)]]
(dc/db conn))
(map first)))
(defn execute [{:keys [form-params clients entity identity]}]
(let [all-results (->> (transactions-matching-rule {:entity entity
:clients clients
:only-uncoded? true})
(map :db/id)
(into #{}))
ids (if (not-empty (:all form-params))
all-results
(set/intersection (into #{} (:transaction-id form-params))
all-results))
ids (all-ids-not-locked ids)
transactions (transduce
(comp
(map d-transactions/get-by-id)
(map #(update % :transaction/date coerce/to-date)))
conj
[]
ids)
entity (update entity :transaction-rule/description #(some-> % iol-ion.query/->pattern))
;; TODO
#_#_x (doseq [transaction transactions]
(when (not (rm/rule-applies? transaction entity))
(throw (ex-info "Transaction rule does not apply" {:validation-error "Transaction rule does not apply"
:transaction-rule entity
:transaction transaction})))
(when (:transaction/payment transaction)
(throw (ex-info "Transaction already associated with a payment" {:validation-error "Transaction already associated with a payment"}))))]
(audit-transact (mapv (fn [t]
[:upsert-transaction
(remove-nils (rm/apply-rule {:db/id (:db/id t)
:transaction/amount (:transaction/amount t)}
entity
(or (-> t :transaction/bank-account :bank-account/locations)
(-> t :transaction/client :client/locations))))])
transactions)
identity)
(doseq [n transactions]
(solr/touch-with-ledger (:db/id n)))
(html-response [:div]
:headers {"hx-trigger" (hx/json {:modalclose ""
:notification (format "Successfully coded %d of %d transactions!"
(count ids)
(count all-results))})})))
(defn location-select [{{:keys [name account-id client-id value] :as qp} :query-params}]
(html-response (location-select* {:name name
:value value
:account-location (some->> account-id
(pull-attr (dc/db conn) :account/location))
:client-locations (some->> client-id
(pull-attr (dc/db conn) :client/locations))})))
(defn account-typeahead [{{:keys [name value client-id] :as qp} :query-params}]
(html-response (account-typeahead* {:name name
:value value
:client-id client-id
:x-model "accountId"})))
(def form-schema (mc/schema
[:map
[:db/id {:optional true} [:maybe entity-id]]
[:transaction-rule/client {:optional true} [:maybe entity-id]]
[:transaction-rule/description [:and regex
[:string {:min 3}]]]
[:transaction-rule/bank-account [:maybe entity-id]]
[:transaction-rule/amount-gte {:optional true} [:maybe money]]
[:transaction-rule/amount-lte {:optional true} [:maybe money]]
[:transaction-rule/dom-gte {:optional true} [:maybe :int]]
[:transaction-rule/dom-lte {:optional true} [:maybe :int]]
[:transaction-rule/vendor {:optional true} [:maybe entity-id]]
[:transaction-rule/transaction-approval-status (ref->enum-schema "transaction-approval-status")]
[:transaction-rule/accounts
(many-entity {:min 1}
[:db/id [:or entity-id temp-id]]
[:transaction-rule-account/account entity-id]
[:transaction-rule-account/location [:string {:min 1 :error/message "required"}]]
[:transaction-rule-account/percentage percentage])]]))
(defn check-badges [{query-params :query-params}]
(html-response
[:div (if (not-empty (:all query-params))
(com/pill {:color :secondary}
[:span "All " [:span {:x-text "resultCount" :x-data "{}"}] " transactions"])
(com/pill {:color :primary}
(str (count (:transaction-id query-params)) " transactions")))]))
(defn execute-dialog [{:keys [entity clients]}]
(modal-response
(com/modal {}
(com/modal-card-advanced
{}
[:div.flex [:div.p-2 "Transaction Rule"]]
(com/modal-header {} [:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"]])
(com/modal-body {} [:form#my-form
{:hx-post (bidi/path-for ssr-routes/only-routes ::route/execute
:db/id (:db/id entity))
:hx-indicator "#code"}
[:div
{:hx-get (bidi/path-for ssr-routes/only-routes ::route/check-badges)
:hx-trigger "change"
:hx-target "#transaction-test-results .gutter"
:hx-include "this"}
(transaction-rule-test-table* {:entity entity
:clients clients
:checkboxes? true
:only-uncoded? true})]])
(com/modal-footer {} [:div.flex.justify-end (com/validated-save-button {:form "my-form" :id "code"} "Code transactions")])))
:headers (-> {}
(assoc "hx-trigger-after-settle" "modalnext")
(assoc "hx-retarget" ".modal-stack")
(assoc "hx-reswap" "beforeend"))))
(defn delete [{:keys [entity] :as request}]
@(dc/transact conn [[:db/retractEntity (:db/id entity)]])
(html-response (row* (:identity request) entity {:delete-after-settle? true :class "live-removed"})
:headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id entity))}))
(defrecord EditModal [linear-wizard]
mm/ModalWizardStep
(step-name [_]
"Edit")
(step-key [_]
:edit)
(edit-path [_ _] [])
(step-schema [_]
(mm/form-schema linear-wizard))
(render-step [this request]
(mm/default-render-step
linear-wizard this
:head "Transaction rule"
:body (mm/default-step-body {}
[:form#my-form {:hx-ext "response-targets"
:hx-target-400 "#form-errors .error-content"
:hx-indicator "#submit"
:x-trap "true"
(if (:db/id entity)
(if (:db/id (fc/field-value))
:hx-put
:hx-post) (str (bidi/path-for ssr-routes/only-routes ::route/save))}
[:fieldset {:class "hx-disable"
:x-data (hx/json {:clientId (or (:db/id (:transaction-rule/client form-params))
(:transaction-rule/client form-params)
(:db/id (:transaction-rule/client entity)))})}
:x-data (hx/json {:clientId (or (:db/id (:transaction-rule/client (fc/field-value)))
(:transaction-rule/client (fc/field-value)))})}
[:div.space-y-1
(when-let [id (:db/id entity)]
(when-let [id (:db/id (fc/field-value))]
(com/hidden {:name "db/id"
:value id}))
(fc/with-field :transaction-rule/description
@@ -620,7 +700,7 @@
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" (fc/field-name))
:x-init "$watch('clientId', cid => $dispatch('changed', $data))"}]
(bank-account-typeahead* {:client-id (:transaction-rule/client form-params)
(bank-account-typeahead* {:client-id (:transaction-rule/client (fc/field-value))
:name (fc/field-name)
:value (fc/field-value)})]))
@@ -677,12 +757,12 @@
(fc/with-field :transaction-rule/accounts
(com/validated-field
{:errors (fc/field-errors)}
(let [client-locations (some->> form-params :transaction-rule/client (pull-attr (dc/db conn) :client/locations))]
(let [client-locations (some->> (fc/field-value) :transaction-rule/client (pull-attr (dc/db conn) :client/locations))]
(com/data-grid {:headers [(com/data-grid-header {} "Account")
(com/data-grid-header {:class "w-32"} "Location")
(com/data-grid-header {:class "w-16"} "%")
(com/data-grid-header {:class "w-16"})]}
(fc/cursor-map #(transaction-rule-account-row* % (:transaction-rule/client form-params) client-locations))
(fc/cursor-map #(transaction-rule-account-row* % (:transaction-rule/client (fc/field-value)) client-locations))
(com/data-grid-new-row {:colspan 4
:hx-get (bidi/path-for ssr-routes/only-routes
::route/new-account)
@@ -697,184 +777,90 @@
:value (fc/field-value)
:name (fc/field-name)
:size :small
:orientation :horizontal})))]]]
[:div
(com/form-errors {:errors (:errors fc/*form-errors*)})
[:div.flex.justify-end
:orientation :horizontal})))]]])
:footer
(mm/default-step-footer linear-wizard this :validation-route ::route/navigate)
:validation-route ::route/navigate)))
(com/validated-save-button {:errors form-errors :color :secondary
:hx-post (bidi/path-for ssr-routes/only-routes ::route/test)
:hx-include "#my-form"}
(defrecord TestModal [linear-wizard]
mm/ModalWizardStep
(step-name [_]
"Test")
"Test rule")
(com/validated-save-button {:errors form-errors
:id "submit"
:form "my-form"} "Save rule")]]))))
(step-key [_]
:test)
(edit-path [_ _] [])
(defn new-account [{{:keys [client-id index]} :query-params}]
(step-schema [_]
(mut/select-keys (mm/form-schema linear-wizard) #{}))
(render-step [this request]
(mm/default-render-step
linear-wizard this
:head [:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"]]
:body [:div.space-y-1 {:class "w-[850px] h-[600px]"}
(transaction-rule-test-table* {:entity (:snapshot (:multi-form-state request))
:clients (:clients request)})]
:footer
(mm/default-step-footer linear-wizard this :validation-route ::route/navigate)
:validation-route ::route/navigate)))
(defrecord TransactionRuleWizard [transaction-rule current-step entity]
mm/LinearModalWizard
(hydrate-from-request
[this request]
this
#_(assoc this :entity (:entity request)))
(navigate [this step-key]
(assoc this :current-step step-key))
(get-current-step [this]
(if current-step
(mm/get-step this current-step)
(mm/get-step this :edit)))
(render-wizard [this {:keys [multi-form-state] :as request}]
(mm/default-render-wizard
this request
:form-params
(-> mm/default-form-props
(assoc (if (get-in multi-form-state [:snapshot :db/id])
:hx-put
:hx-post)
(str (bidi/path-for ssr-routes/only-routes ::route/save))))))
(steps [_]
[:edit
:test])
(get-step [this step-key]
(let [step-key-result (mc/parse mm/step-key-schema step-key)
[step-key-type step-key] step-key-result]
(if (= :step step-key-type)
(get {:edit (->EditModal this)
:test (->TestModal this)}
step-key)
nil)))
(form-schema [_] form-schema)
(submit [_ {:keys [multi-form-state request-method identity] :as request}]
(let [transaction-rule (:snapshot multi-form-state)
_ (validate-transaction-rule transaction-rule)
entity (cond-> transaction-rule
(= :post request-method) (assoc :db/id "new")
true (assoc :transaction-rule/note (entity->note transaction-rule)))
{:keys [tempids]} (audit-transact [[:upsert-entity entity]]
(:identity request))
updated-rule (dc/pull (dc/db conn)
default-read
(or (get tempids (:db/id entity)) (:db/id entity)))]
(html-response
(fc/start-form-with-prefix
[:transaction-rule/accounts (or index 0)]
{:db/id (str (java.util.UUID/randomUUID))
:transaction-rule-account/location "Shared"
:new? true}
[]
(transaction-rule-account-row*
fc/*current*
client-id
(some->> client-id (pull-attr (dc/db conn) :client/locations))))))
(defn all-ids-not-locked [all-ids]
(->> all-ids
(dc/q '[:find ?t
:in $ [?t ...]
:where
[?t :transaction/client ?c]
[(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
[?t :transaction/date ?d]
[(>= ?d ?lu)]]
(dc/db conn))
(map first)))
(defn execute [{:keys [form-params clients entity identity]}]
(let [all-results (->> (transactions-matching-rule {:entity entity
:clients clients
:only-uncoded? true})
(map :db/id)
(into #{}))
ids (if (not-empty (:all form-params))
all-results
(set/intersection (into #{} (:transaction-id form-params))
all-results))
ids (all-ids-not-locked ids)
transactions (transduce
(comp
(map d-transactions/get-by-id)
(map #(update % :transaction/date coerce/to-date)))
conj
[]
ids)
entity (update entity :transaction-rule/description #(some-> % iol-ion.query/->pattern))
;; TODO
#_#_x (doseq [transaction transactions]
(when (not (rm/rule-applies? transaction entity))
(throw (ex-info "Transaction rule does not apply" {:validation-error "Transaction rule does not apply"
:transaction-rule entity
:transaction transaction})))
(when (:transaction/payment transaction)
(throw (ex-info "Transaction already associated with a payment" {:validation-error "Transaction already associated with a payment"}))))
]
(audit-transact (mapv (fn [t]
[:upsert-transaction
(remove-nils (rm/apply-rule {:db/id (:db/id t)
:transaction/amount (:transaction/amount t)}
entity
(or (-> t :transaction/bank-account :bank-account/locations)
(-> t :transaction/client :client/locations))))])
transactions)
identity)
(doseq [n transactions]
(solr/touch-with-ledger (:db/id n)))
(html-response [:div]
:headers {"hx-trigger" (hx/json {:modalclose ""
:notification (format "Successfully coded %d of %d transactions!"
(count ids)
(count all-results))})})))
(defn location-select [{{:keys [name account-id client-id value] :as qp} :query-params}]
(html-response (location-select* {:name name
:value value
:account-location (some->> account-id
(pull-attr (dc/db conn) :account/location))
:client-locations (some->> client-id
(pull-attr (dc/db conn) :client/locations))})))
(defn account-typeahead [{{:keys [name value client-id] :as qp} :query-params}]
(html-response (account-typeahead* {:name name
:value value
:client-id client-id
:x-model "accountId"})))
(def form-schema (mc/schema
[:map
[:db/id {:optional true} [:maybe entity-id]]
[:transaction-rule/client {:optional true} [:maybe entity-id]]
[:transaction-rule/description [:and regex
[:string {:min 3}]]]
[:transaction-rule/bank-account [:maybe entity-id]]
[:transaction-rule/amount-gte {:optional true} [:maybe money]]
[:transaction-rule/amount-lte {:optional true} [:maybe money]]
[:transaction-rule/dom-gte {:optional true} [:maybe :int]]
[:transaction-rule/dom-lte {:optional true} [:maybe :int]]
[:transaction-rule/vendor {:optional true} [:maybe entity-id]]
[:transaction-rule/transaction-approval-status (ref->enum-schema "transaction-approval-status")]
[:transaction-rule/accounts
(many-entity {:min 1}
[:db/id [:or entity-id temp-id]]
[:transaction-rule-account/account entity-id]
[:transaction-rule-account/location [:string {:min 1 :error/message "required"}]]
[:transaction-rule-account/percentage percentage])]]))
(defn edit-dialog [{:keys [entity form-params form-errors]}]
(modal-response (dialog* {:entity entity
:form-params (or (when (seq form-params)
form-params)
(when entity
(mc/decode form-schema entity main-transformer))
{})
:form-errors form-errors})))
(defn check-badges [{query-params :query-params}]
(html-response
[:div (if (not-empty (:all query-params))
(com/pill {:color :secondary}
[:span "All " [:span {:x-text "resultCount" :x-data "{}"}] " transactions"])
(com/pill {:color :primary}
(str (count (:transaction-id query-params)) " transactions")))]))
(defn execute-dialog [{:keys [entity clients]}]
(modal-response
(com/modal{}
(com/stacked-modal-card
0
{}
[:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"]]
[:form#my-form
{:hx-post (bidi/path-for ssr-routes/only-routes ::route/execute
:db/id (:db/id entity))
:hx-indicator "#code"}
[:div
{:hx-get (bidi/path-for ssr-routes/only-routes ::route/check-badges)
:hx-trigger "change"
:hx-target "#transaction-test-results .gutter"
:hx-include "this"}
(transaction-rule-test-table* {:entity entity
:clients clients
:checkboxes? true
:only-uncoded? true})]]
[:div.flex.justify-end (com/validated-save-button {:form "my-form" :id "code"} "Code transactions")]))
:headers (-> {}
(assoc "hx-trigger-after-settle" "modalnext")
(assoc "hx-retarget" ".modal-stack")
(assoc "hx-reswap" "beforeend"))))
(defn delete [{:keys [entity] :as request}]
@(dc/transact conn [[:db/retractEntity (:db/id entity)]])
(html-response (row* (:identity request) entity {:delete-after-settle? true :class "live-removed"})
:headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id entity))}))
(row* identity updated-rule {:flash? true})
:headers (cond-> {"hx-trigger" "modalclose"}
(= :post request-method) (assoc "hx-retarget" "#entity-table tbody"
"hx-reswap" "afterbegin")
(= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-rule))
"hx-reswap" "outerHTML"))))))
(def rule-wizard (->TransactionRuleWizard nil nil nil))
(def key->handler
(apply-middleware-to-all-handlers
@@ -884,13 +870,20 @@
::route/delete (-> delete
(wrap-entity [:route-params :db/id] default-read)
(wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
::route/new-account (-> new-account
::route/new-account
(->
(add-new-entity-handler [:step-params :transaction-rule/accounts]
(fn render [cursor request]
(transaction-rule-account-row*
cursor
(:client-id (:query-params request))
(some->> (:client-id (:query-params request)) (pull-attr (dc/db conn) :client/locations))))
(fn build-new-row [base _]
(assoc base :transaction-rule-account/location "Shared")))
(wrap-schema-enforce :query-schema [:map
[:client-id {:optional true}
[:maybe entity-id]]
[:index {:optional true
:default 0} [nat-int? {:default 0}]]])
wrap-admin wrap-client-redirect-unauthenticated)
[:maybe entity-id]]]))
::route/location-select (-> location-select
(wrap-schema-enforce :query-schema [:map
[:name :string]
@@ -905,12 +898,10 @@
[:maybe entity-id]]
[:value {:optional true}
[:maybe entity-id]]]))
::route/save (-> save
(wrap-entity [:form-params :db/id] default-read)
(wrap-schema-enforce :form-schema form-schema)
(wrap-nested-form-params)
(wrap-form-4xx-2 (-> edit-dialog
(wrap-entity [:form-params :db/id] default-read))))
::route/save (-> mm/submit-handler
(mm/wrap-wizard rule-wizard)
(mm/wrap-decode-multi-form-state)
(wrap-entity [:form-params :db/id] default-read))
::route/execute (-> execute
(wrap-entity [:route-params :db/id] default-read)
@@ -927,13 +918,6 @@
#_(wrap-form-4xx-2 (-> edit-dialog ;; TODO for example not having a single one checked
(wrap-entity [:form-params :db/id] default-read))))
::route/test (-> test
(wrap-entity [:form-params :db/id] default-read)
(wrap-schema-enforce :form-schema form-schema)
(wrap-nested-form-params)
(wrap-form-4xx-2 (-> edit-dialog
(wrap-entity [:form-params :db/id] default-read))))
::route/check-badges (-> check-badges
(wrap-schema-enforce :query-schema [:map
[:transaction-id {:optional true}
@@ -947,10 +931,24 @@
(wrap-entity [:route-params :db/id] default-read)
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
::route/edit-dialog (-> edit-dialog
::route/navigate (-> mm/next-handler
(mm/wrap-wizard rule-wizard)
(mm/wrap-decode-multi-form-state))
::route/edit-dialog (-> mm/open-wizard-handler
(mm/wrap-wizard rule-wizard)
(mm/wrap-init-multi-form-state (fn [request]
(mm/->MultiStepFormState (:entity request)
[]
(:entity request))))
(wrap-entity [:route-params :db/id] default-read)
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
::route/new-dialog edit-dialog})
::route/new-dialog (-> mm/open-wizard-handler
(mm/wrap-wizard rule-wizard)
(mm/wrap-init-multi-form-state (fn [_]
(mm/->MultiStepFormState {}
[]
{}))))})
(fn [h]
(-> h
(wrap-admin)

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,32 +1,107 @@
(ns auto-ap.ssr.company
(:require
(:require [amazonica.aws.s3 :as s3]
[auto-ap.datomic :refer [conn pull-attr]]
[auto-ap.datomic.clients :refer [full-read]]
[auto-ap.permissions :as permissions]
[auto-ap.solr :as solr]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [html-response]]
[bidi.bidi :as bidi]
[cemerick.url :as url]
[clojure.java.io :as io]
[clojure.string :as str]
[config.core :refer [env]]
[datomic.api :as dc]
[ring.middleware.json :refer [wrap-json-response]]))
[ring.middleware.json :refer [wrap-json-response]])
(:import [java.util UUID]
(org.apache.commons.codec.binary Base64)))
(defn please-select-client-screen* []
[:div.grid.grid-cols-3
(com/content-card {}
[:div.col-span-1.p-4 {:class "p-4 sm:p-6"}
[:h3 {:class "mb-4 text-xl font-semibold dark:text-white"}
"Please select a company"]
])])
"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
(please-select-client-screen*)
(let [client (dc/pull (dc/db conn) full-read (:db/id client))]
[:div
[:div.grid.grid-cols-3.gap-4
(com/content-card {}
[:div.col-span-1.p-4 {:class "p-4 sm:p-6"}
@@ -38,8 +113,7 @@
[:p (-> address :address/street2)]
[:p (-> address :address/city) " "
(-> address :address/state) ", "
(-> address :address/zip)]])]
)
(-> address :address/zip)]])])
(com/content-card {}
[:div.col-span-1.p-4 {:class "p-4 sm:p-6"}
[:h3 {:class "mb-4 text-xl font-semibold dark:text-white"}
@@ -48,7 +122,10 @@
:query {"client" (:client/code client)}))}
(com/button {:color :primary}
"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}]
(base-page
@@ -57,8 +134,7 @@
:client-selection (:client-selection (:session request))
:client (:client request)
:identity (:identity request)
:app-params {
:hx-get (bidi/path-for ssr-routes/only-routes
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
:company)
:hx-trigger "clientSelected from:body"
:hx-select "#app-contents"
@@ -67,7 +143,7 @@
[:a {:href (bidi/path-for ssr-routes/only-routes
:company)}
"My Company"])
(main-content* {:client (:client request)}))
(main-content* request))
"My Company"))
(defn search [{:keys [clients query-params]}]
@@ -80,8 +156,7 @@
"limit" 300}))))
valid-clients (for [n name-like-ids
: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)}))
(def search (wrap-json-response search))

View File

@@ -11,8 +11,7 @@
[auto-ap.ssr.components.tags :as tags]
[auto-ap.ssr.components.paginator :as paginator]
[auto-ap.ssr.components.radio :as radio]))
;; potemkin can be used here
(def breadcrumbs breadcrumbs/breadcrumbs-)
(def button buttons/button-)
(def validated-save-button buttons/validated-save-button-)
@@ -24,8 +23,7 @@
(def button-group-button buttons/group-button-)
(def modal dialog/modal-)
(def modal-card dialog/modal-card-)
(def stacked-modal-card dialog/stacked-modal-card-)
(def stacked-modal-card-2 dialog/stacked-modal-card-2-)
(def modal-card-advanced dialog/modal-card-advanced-)
(def modal-header dialog/modal-header-)
(def modal-header-attachment dialog/modal-header-attachment-)
(def modal-body dialog/modal-body-)
@@ -70,12 +68,9 @@
(def data-grid-new-row data-grid/new-row-)
(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))
(def paginator paginator/paginator-)
(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.ssr.hiccup-helper :as hh]
[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.vendors :as v-routes]))
[auto-ap.routes.admin.vendors :as v-routes]
[auto-ap.graphql.clients :as clients]))
(defn menu-button- [params & children]
[:div
@@ -199,7 +201,7 @@
[:li
(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"}
"Clients")]
[:li

View File

@@ -1,13 +1,17 @@
(ns auto-ap.ssr.components.card
(: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]
(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))
(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"}
(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"}]

View File

@@ -3,36 +3,19 @@
[auto-ap.ssr.hiccup-helper :as hh]
[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
(assoc "@click.outside" "open=false"
:x-data (hx/json {:index 0 :hidingIndex -1 :unexpectedError false :transitioning false})
"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)
"
)
(assoc "@click.outside" "open=false")
(dissoc :handle-unexpected-error?)
(update :class (fnil hh/add-class "") "w-full h-full modal-stack"))
children])
@@ -40,11 +23,10 @@
[:div (update params
:class (fn [c] (-> c
(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 "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
"hello"]
content]
@@ -55,28 +37,6 @@
[:div {:class "shrink-0"}
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]
[:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"}
children])
@@ -92,21 +52,14 @@
(defn modal-footer- [params & children]
[: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.px-2.py-0.5 "An unexpected error has occured. Integreat staff have been notified."]]
[:div {:class "shrink-0"}]
children])
(defn stacked-modal-card-2- [index params & children]
(defn modal-card-advanced- [params & children]
[: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",
})
{: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 "")) })
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

@@ -4,7 +4,7 @@
(defn 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"} ]
[: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
(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"}
@@ -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"}
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"} ]]])
[: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.transaction-rules :as admin-rules]
[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.company :as company]
[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-link (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/link))
: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-table (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/table))
:company-yodlee-fastlink-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/fastlink-dialog))
@@ -89,5 +91,6 @@
(into admin/key->handler)
(into admin-jobs/key->handler)
(into admin-vendors/key->handler)
(into admin-clients/key->handler)
(into admin-rules/key->handler)))

View File

@@ -22,18 +22,30 @@
`(binding [*prefix* ~prefix]
(start-form ~form-data ~errors ~@rest)))
(defmacro with-prefix [prefix & rest]
`(binding [*prefix* (into (or *prefix* []) ~prefix)]
~@rest))
(defmacro with-cursor [cursor & rest]
`(binding [*current* ~cursor]
~@rest))
(defmacro with-field [field & rest]
`(with-cursor (get *current* ~field )
`(with-cursor (get *current* ~field)
~@rest))
(defmacro with-field-default [field default & rest]
`(with-cursor (get *current* ~field ~default)
~@rest))
`(let [new-cursor# (get *current* ~field ~default)
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
([] (field-name *current*))
@@ -62,7 +74,7 @@
(defn cursor-map
([f] (cursor-map *current* f))
([cursor f]
(when (field-value)
(when (seq (field-value))
(doall
(for [n cursor]
(with-cursor n

View File

@@ -41,8 +41,7 @@
remove-class
this
(filter (fn [c]
(str/starts-with? c wildcard)
) @class-set)))
(str/starts-with? c wildcard)) @class-set)))
this)
(replace-wildcard [this wildcard add]
(remove-wildcard this wildcard)
@@ -60,8 +59,7 @@
(add-class [this add]
(add-class (string->class-list this) add))
(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 (string->class-list this) remove add))
(remove-wildcard [this wildcard]
@@ -70,29 +68,11 @@
(replace-wildcard (string->class-list this) wildcard add))
(add-tw [this tw]
(replace-tw (string->class-list this)
tw)
))
tw)))
(str (hiccup/html [:div {:class (-> "hello bryce hello-1 hello-2"
(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,7 +84,8 @@
matching-count]))
(def grid-page
(helper/build
{}
#_(helper/build
{:id "cash-drawer-shift-table"
:nav (com/main-aside-nav)
:page-specific-nav filters
@@ -127,8 +128,7 @@
{:key "opened-cash"
:name "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))

View File

@@ -93,11 +93,11 @@
(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"}]])
(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"}]])
(def refresh
@@ -106,7 +106,7 @@
(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"}]])
(def vendors
@@ -204,6 +204,16 @@
:stroke-linecap "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
[: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"}]])
@@ -442,3 +452,39 @@
[:defs]
[:title "dislike"]
[:path {:d "M4.5,8h0a1.5,1.5,0,0,1,0-3h1a1.5,1.5,0,0,1,0-3H12c4,0,3,1.87,11,1.87V13H20a7.811,7.811,0,0,0-7.5,7.856c0,1.582-3,1.813-3-1.187A29.774,29.774,0,0,1,10.5,14h-8a1.5,1.5,0,0,1,0-3h1a1.5,1.5,0,0,1,0-3h1", :fill "none", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]])
(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))})
(defn base-page [request contents page-name]
(html-page
[:html.has-navbar-fixed-top
@@ -42,6 +40,7 @@
[: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 "/js/htmx-disable.js"}]
[:script {:type "text/javascript", :src "https://cdn.yodlee.com/fastlink/v4/initialize.js", :async "async"}]]
@@ -57,6 +56,7 @@
[: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@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
"
@@ -77,11 +77,14 @@ input[type=number] {
[: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"
":aria-hidden" "!open"
"x-data" (hx/json {"open" false})
"@modalopen.document" "open=true"
"x-data" (hx/json {"open" false
"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"}
[:div {:class "bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40 md:p-12"
@@ -94,8 +97,7 @@ input[type=number] {
"x-transition:leave-start" "!bg-opacity-50"
"x-transition:leave-end" "!bg-opacity-0"}
[:div {
:class "flex h-full w-full justify-stretch md:justify-center items-stretch md:items-center "
[:div {:class "flex h-full w-full justify-stretch md:justify-center items-stretch md:items-center "
"x-trap.inert.noscroll" "open"
"x-trap.inert" "open"
"x-show" "open"
@@ -109,6 +111,4 @@ input[type=number] {
[: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
]
]]]]]]))
]]]]]]]))

View File

@@ -75,8 +75,7 @@
[:vector {:decode/json {:enter (fn [x]
(if (sequential? x)
x
[x])
)}}
[x]))}}
x])
(defn empty->nil [v]
@@ -84,10 +83,14 @@
nil
v))
(defn parse-empty-as-nil []
(defn parse-empty-as-nil []
(mt2/transformer
{:decoders
{:string empty->nil
{:map (fn [m]
(if (not (seq (filter identity (vals m))))
nil
m))
:string empty->nil
:double empty->nil
:int empty->nil
:long empty->nil
@@ -97,7 +100,7 @@
:decode/arbitrary (fn [e]
(if (and (map? e) (:db/id e))
(:db/id e)
e))} ]))
e))}]))
(def temp-id (mc/schema [:string {:min 1}]))
(def money (mc/schema [:double]))
@@ -173,6 +176,21 @@
: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)}}))))
(defn schema-enforce-request [{:keys [form-params query-params params] :as request} & {:keys [form-schema query-schema route-schema params-schema]}]
(let [request (try
@@ -208,7 +226,7 @@
(catch Exception e
(alog/warn ::validation-error :error e)
(throw (ex-info (->> (-> e
(ex-data )
(ex-data)
:data
:explain
(me/humanize {:errors (assoc me/default-errors
@@ -275,8 +293,7 @@
(into [:enum {:decode/string #(if (keyword? %)
%
(when (not-empty %)
(keyword n %))
)}]
(keyword n %)))}]
(for [{:db/keys [ident]} (all-schema)
:when (= n (namespace ident))]
ident)))
@@ -303,7 +320,6 @@
(try+
(handler request)
(catch [:type :schema-validation] e
(let [humanized (-> e :error :explain (me/humanize {:errors (assoc me/default-errors
::mc/missing-key {:error/message {:en "required"}})}))
errors (map
@@ -334,8 +350,7 @@
(reduce
(fn [key-handler [k v]]
(assoc key-handler k (f v)))
key->handler)
))
key->handler)))
(defn path->name2 [k & rest]
(let [k->n (fn [k]
@@ -356,7 +371,7 @@
(let [entity (some->>
(get-in request path)
(#(if (string? %) (Long/parseLong %) %))
(dc/pull (dc/db conn) read ))]
(dc/pull (dc/db conn) read))]
(handler (if entity
(assoc request
:entity entity)

View File

@@ -1,10 +1,13 @@
(ns user
(ns user
(:require
[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.ledger :as l ]
[auto-ap.ledger :as l]
[clj-http.core :as http]
[clj-http.client :as client]
[figwheel.main.api]
[hawk.core]
[auto-ap.server]
[auto-ap.time :as atime]
[auto-ap.utils :refer [by]]
@@ -29,6 +32,7 @@
(:import
(org.apache.commons.io.input BOMInputStream)))
(defn println-event [item]
(printf "%s: %s - %s:%s by %s\n"
(str (c/to-date-time (:mulog/timestamp item)))
@@ -36,8 +40,7 @@
(if (:mulog/duration item)
(str " " (int (/ (:mulog/duration item) 1000000)) "ms")
"")
(:user-name item)
)
(:user-name item))
(println (reduce
(fn [acc [k v]]
(assoc acc k v))
@@ -70,8 +73,7 @@
(publish [_ buffer]
;; items are pairs [offset <item>]
(doseq [item (transform (map second (rb/items buffer)))]
(println-event item)
)
(println-event item))
(flush)
(rb/clear buffer)))
@@ -99,15 +101,15 @@
also-merge-txes (fn [also-merge old-account-id]
(if old-account-id
(let [[sunset-account]
(first (dc/q {:find ['?a ]
:in ['$ '?ac ]
(first (dc/q {:find ['?a]
:in ['$ '?ac]
:where ['[?a :account/numeric-code ?ac]]}
(dc/db conn) also-merge))]
(into (mapv
(fn [[entity id _]]
[:db/add entity id old-account-id])
(dc/q {:find ['?e '?id '?a ]
:in ['$ '?ac ]
(dc/q {:find ['?e '?id '?a]
:in ['$ '?ac]
:where ['[?a :account/numeric-code ?ac]
'[?e ?at ?a]
'[?at :db/ident ?id]]}
@@ -157,8 +159,7 @@
(if also-merge
(into tx
(also-merge-txes also-merge old-account-id))
tx)
))))
tx)))))
conj
@@ -189,8 +190,7 @@
:in ['$]
:where ['[?e :account/numeric-code ?z]
'[(<= ?z 9999)]]}
(dc/db conn))))
)
(dc/db conn)))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
@@ -214,7 +214,6 @@
:in ['$ '?z]
:where [['?e :client/code '?z]]}
(dc/db conn) customer)))
_ (println client-id)
code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code
{:account/applicability [:db/ident]}
:db/id])]
@@ -237,8 +236,7 @@
(filter (fn [duplicates]
(apply not= (map rest duplicates))))
#_(map (fn [[[_ account]]]
account))
))]
account))))]
(throw (Exception. (str "These accounts are duplicated:" (str bad-rows)))))
rows (vec (set (map rest rows)))
@@ -256,8 +254,7 @@
(:db/ident (:account/applicability existing)))
(and (not-empty override-name)
(not-empty account-name)
(not= override-name account-name)
)))
(not= override-name account-name))))
[{:db/id (:db/id existing)
:account/client-overrides [{:account-client-override/client client-id
:account-client-override/name (or (not-empty override-name)
@@ -299,19 +296,15 @@
'[?e :transaction/approval-status :transaction-approval-status/approved]
'(not [?ta :transaction-account/location])
'[?e :transaction/client ?c]
'[?c :client/code ?client-code]
]}
'[?c :client/code ?client-code]]}
(dc/db conn) client-code)
(mapcat
(fn [[{:transaction/keys [accounts]}]]
(mapv
(fn [a]
{:db/id (:db/id a)
:transaction-account/location location}
)
accounts)
)
)
:transaction-account/location location})
accounts)))
vec))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
@@ -323,7 +316,7 @@
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn entity-history [i]
(vec (sort-by first (dc/q
{:find ['?tx '?z '?v ]
{:find ['?tx '?z '?v]
:in ['?i '$]
:where ['[?i ?a ?v ?tx ?ad]
'[?a :db/ident ?z]
@@ -333,7 +326,7 @@
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn entity-history-with-revert [i]
(vec (sort-by first (dc/q
{:find ['?tx '?z '?v '?ad ]
{:find ['?tx '?z '?v '?ad]
:in ['?i '$]
:where ['[?i ?a ?v ?tx ?ad]
'[?a :db/ident ?z]]}
@@ -357,7 +350,32 @@
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn start-db []
(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 []
(mount.core/start (mount.core/only #{#'auto-ap.graphql.vendors/indexer #'auto-ap.graphql.accounts/indexer})))
@@ -365,17 +383,17 @@
(defn restart-db []
#_(require 'datomic.dev-local)
#_(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))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn spit-csv [columns data ]
(defn spit-csv [columns data]
(csv/write-csv *out*
(into [(map name columns)]
(for [r data]
((apply juxt columns) r )))))
((apply juxt columns) r)))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
@@ -396,8 +414,7 @@
(filter #(->> words
(every? (fn [w] (str/includes? (second %) w)))))
(map first)
(map #(str/replace % #"queries/" ""))
)
(map #(str/replace % #"queries/" "")))
(async/to-chan! (:object-summaries obj))
true
(fn [e]
@@ -423,13 +440,11 @@
(reduce + 0.0
(->> values
(map (fn [[_ _ _ _ amount]]
(- (Double/parseDouble amount))))))
]))
(- (Double/parseDouble amount))))))]))
(into {}))]
(->>
(for [[i invoice-expense-account-id target-account target-date amount _ location] (drop 1 data)
:let [
invoice-id (i->invoice-id i)
:let [invoice-id (i->invoice-id i)
invoice (dc/pull db '[FILL_IN] invoice-id)
current-total (:invoice/total invoice)
@@ -441,7 +456,7 @@
(:db/id (first (:invoice/expense-accounts invoice)))
(random-tempid))
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))
target-account-id (Long/parseLong (str/trim target-account))
@@ -462,13 +477,11 @@
:in $ ?i
:where [?ip :invoice-payment/invoice ?i]
[?ip :invoice-payment/amount ?a]
[?ip :invoice-payment/payment ?p]
]
[?ip :invoice-payment/payment ?p]]
db invoice-id))]
: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
:invoice/total target-total})
@@ -495,7 +508,7 @@
{:db/id invoice-expense-account-id
: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
:invoice-expense-account/account target-account-id})])
(mapcat identity)
@@ -517,7 +530,7 @@
(->> (dc/q '[:find ?i
:in $
:where [_ :db/ident ?i]]
(dc/db conn) )
(dc/db conn))
(mapcat identity)
(map str)
(sort)
@@ -549,7 +562,7 @@
(t/minus (t/days (rand-int 60)))
(atime/unparse atime/normal-date))
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]]]
a)
:separator \tab))))
@@ -580,8 +593,7 @@
:in $
:where [?i :invoice/invoice-number]
(not [?i :invoice/status :invoice-status/voided])]
:args [
(dc/db conn)]})
:args [(dc/db conn)]})
(map first)
(partition-all 1000))]
(print ".")

View File

@@ -31,3 +31,87 @@
[?c :client/code ?cd]
[?c :client/locations ?l]]
(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
"payments/" :payments
"admin/" {"clients/" {"" :admin-clients
[:id] {"" :admin-specific-client
"/bank-accounts/" {[:bank-account] :admin-specific-bank-account}}}
"vendors" :admin-vendors}
"admin/" { "vendors" :admin-vendors}
"invoices/" {"" :invoices
"import" :import-invoices
"unpaid" :unpaid-invoices
@@ -22,8 +19,6 @@
"requires-feedback" :requires-feedback-transactions
"excluded" :excluded-transactions}
"reports/" {"" :reports}
"plaid" :plaid
"yodlee2" :yodlee2
"ledger/" {"" :ledger
"profit-and-loss" :profit-and-loss
"cash-flows" :cash-flows

View File

@@ -1,11 +1,25 @@
(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]}]
(let [role (or (:user/role user) (:role user) user)]
(println "ROLE IS" role)
(let [role (or (:user/role user) (:role user) user)
client-id (get-client-id client)]
(cond (#{:user-role/admin "admin"} role)
true
(and client-id (not (get (into #{} (map :db/id (:clients user))) client-id)))
false
(#{:user-role/power-user "power-user"} role)
(cond
(#{:invoice-page :payment-page :my-company-page :transaction-page :ledger-page} subject)
@@ -49,6 +63,9 @@
(= [:vendor :edit] [subject activity])
true
(= [:signature :edit] [subject activity])
true
:else false)
: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,6 +10,7 @@
"/account/typeahead" ::account-typeahead
"/test" ::test
"/new" {:get ::new-dialog}
"/navigate" ::navigate
["/" [#"\d+" :db/id] "/edit"] ::edit-dialog
["/" [#"\d+" :db/id] "/delete"] ::delete
["/" [#"\d+" :db/id] "/run"] {:get ::execute-dialog

View File

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

View File

@@ -30,16 +30,6 @@
[:i {:class icon-class}]])
[: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]
[:div
(menu-item {:label "Reports"

View File

@@ -3,6 +3,7 @@
[auto-ap.routes.admin.excel-invoices :as ei-routes]
[auto-ap.routes.admin.import-batch :as ib-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]))
(def routes {"impersonate" :impersonate
@@ -15,6 +16,7 @@
"/update" {:patch :invoice-glimpse-update-textract-invoice}}}}}
"account" {"/search" {:get :account-search}}
"admin" {"" :auto-ap.routes.admin/page
"/client" ac-routes/routes
"/history" {"" :admin-history
"/" :admin-history
#"/search/?" :admin-history-search
@@ -61,8 +63,10 @@
"/table" {:get :pos-cash-drawer-shift-table}}}
"vendor" {"/search" :vendor-search}
;; TODO Include IDS in routes for company-specific things, as opposed to headers
"company" {"" :company
"/dropdown" :company-dropdown-search-results
"/signature" {"/put" :company-update-signature}
"/search" :company-search
"/bank-account/typeahead" :bank-account-typeahead
["/" [#"\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]}]
(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])
change-event (when-not on-change
(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.vendor
:refer [search-backed-typeahead]]
[auto-ap.views.pages.admin.vendors.common :as common]
[auto-ap.views.utils
:refer [dispatch-event str->int with-is-admin? with-user]]
[malli.core :as m]
@@ -23,6 +22,22 @@
;; Remaining cleanup todos:
;; 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
[:client schema/reference]
[:terms :int]]))
@@ -72,8 +87,8 @@
(re-frame/reg-event-fx
::save-complete
[(forms/triggers-stop ::vendor-form)]
(fn [_ [_ _ ]]
{:dispatch [::modal/modal-closed ]}))
(fn [_ [_ _]]
{:dispatch [::modal/modal-closed]}))
(re-frame/reg-event-fx
::save
@@ -122,8 +137,8 @@
:legal-entity-tin legal-entity-tin
: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)))}
common/default-read]]
{ :graphql
default-read]]
{:graphql
{:token user
:owns-state {:single ::vendor-form}
:query-obj {:venia/operation
@@ -196,7 +211,7 @@
[form-builder/section {:title "Terms"}
[form-builder/field-v2 {:field :terms}
"Terms"
[number-input ]]
[number-input]]
(when is-admin?
[form-builder/field-v2 {:field [:terms-overrides]}
"Overrides"
@@ -204,8 +219,7 @@
[typeahead-v3 {:entities clients
:entity->text :name
:style {:width "13em"}
:type "typeahead-v3"
}]]
:type "typeahead-v3"}]]
[form-builder/raw-field-v2 {:field :terms}
[number-input]]]
:schema [:sequential terms-override-schema]
@@ -249,8 +263,7 @@
{:query i
:allowance :vendor}
[:name :id :warning]])
:style {:width "19em"}}]
]
:style {:width "19em"}}]]
(when (:warning (:default-account vendor))
[:div.notification.is-warning.is-light
(:warning (:default-account vendor))])
@@ -261,8 +274,7 @@
[[form-builder/raw-field-v2 {:field :client}
[typeahead-v3 {:entities clients
:entity->text :name
:style {:width "19em"}
}]]
:style {:width "19em"}}]]
[form-builder/raw-field-v2 {:field :account}
[search-backed-typeahead {:search-query (fn [i]
[:search_account
@@ -319,8 +331,7 @@
[form-builder/raw-field-v2 {:field :legal-entity-tin}
[:input.input {:type "text"
:placeholder "SSN or EIN"
:size "12"
}]]
:size "12"}]]
[:div.control
[form-builder/raw-field-v2 {:field :legal-entity-tin-type}
@@ -336,7 +347,7 @@
:allow-nil? true}]]])
[form-builder/hidden-submit-button]]))
(defn vendor-dialog [ ]
(defn vendor-dialog []
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::vendor-form])]
[:div
[form-content {:data data}]]))
@@ -349,7 +360,7 @@
{:graphql {:token user
:query-obj {:venia/queries [[:vendor-by-id
{:id (:id (:vendor data))}
common/default-read]]}
default-read]]}
:owns-state {:single ::select-vendor-form}
:on-success (fn [r]
[::started (:vendor-by-id r)])}}

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.login :refer [login-page]]
[auto-ap.views.pages.payments :refer [payments-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]]))
[auto-ap.views.pages.home :refer [home-page]]))
(defmulti page (fn [active-page] active-page))
(defmethod page :unpaid-invoices [_]
@@ -93,14 +91,6 @@
(when (p/can? @(re-frame/subscribe [::subs/user]) {:subject :ledger-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 [_]
(when (p/can? @(re-frame/subscribe [::subs/user]) {:subject :admin-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} */
const plugin = require('tailwindcss/plugin');
module.exports = {
darkMode: "class",
content: ["./src/**/*.{cljs,clj,cljc}",
@@ -103,6 +105,12 @@ module.exports = {
}
} ,
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
:default_allowance)))))))
(deftest upsert-account
#_(deftest upsert-account
(testing "should create a new account"
(let [result (sut/upsert-account {:id (admin-token)} {:account {:client_overrides []
: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)))))