Builds client SSR approach, sunsets old cljs.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -43,3 +43,4 @@ data/solr/data/invoices/index/
|
||||
data/solr/data/plaid_merchants/data/
|
||||
data/solr/data/logs
|
||||
data/solr/logs
|
||||
.vscode/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{:db {:server "localhost"}
|
||||
:port "3449"
|
||||
:scheme "http"
|
||||
:base-url "http://localhost:3449"
|
||||
:solr-uri "http://localhost:8983"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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]))
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
10
project.clj
10
project.clj
@@ -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
8
resources/powerqueries/expected_deposits.txt
Normal file
8
resources/powerqueries/expected_deposits.txt
Normal 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"
|
||||
8
resources/powerqueries/refunds.txt
Normal file
8
resources/powerqueries/refunds.txt
Normal 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"
|
||||
8
resources/powerqueries/sales_category.txt
Normal file
8
resources/powerqueries/sales_category.txt
Normal 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"
|
||||
8
resources/powerqueries/sales_summary.txt
Normal file
8
resources/powerqueries/sales_summary.txt
Normal 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"
|
||||
8
resources/powerqueries/tenders.txt
Normal file
8
resources/powerqueries/tenders.txt
Normal 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"
|
||||
@@ -120,12 +120,54 @@ htmx.defineExtension('trigger-filter', {
|
||||
|
||||
|
||||
initDatepicker = function(elem) {
|
||||
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
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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]]))
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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]]))
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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]]))
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
1721
src/clj/auto_ap/ssr/admin/clients.clj
Normal file
1721
src/clj/auto_ap/ssr/admin/clients.clj
Normal file
File diff suppressed because it is too large
Load Diff
0
src/clj/auto_ap/ssr/admin/sales_powerqueries.clj
Normal file
0
src/clj/auto_ap/ssr/admin/sales_powerqueries.clj
Normal 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")))))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -413,37 +379,6 @@
|
||||
(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
|
||||
|
||||
|
||||
(defn- location-select*
|
||||
[{:keys [name account-location client-locations value]}]
|
||||
(com/select {:options (into [["" ""]]
|
||||
@@ -535,31 +470,176 @@
|
||||
(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
32
src/clj/auto_ap/ssr/common_handlers.clj
Normal file
32
src/clj/auto_ap/ssr/common_handlers.clj
Normal 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}]]])))
|
||||
@@ -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))
|
||||
|
||||
@@ -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-)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}]
|
||||
|
||||
@@ -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])
|
||||
|
||||
349
src/clj/auto_ap/ssr/components/multi_modal.clj
Normal file
349
src/clj/auto_ap/ssr/components/multi_modal.clj
Normal 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)))))
|
||||
@@ -26,3 +26,22 @@
|
||||
#_[: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"}]]])
|
||||
|
||||
(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"}]]])
|
||||
|
||||
|
||||
@@ -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)))
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
`(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))
|
||||
@@ -31,9 +35,17 @@
|
||||
~@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
|
||||
|
||||
@@ -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"]))}]))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"}]])
|
||||
@@ -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
|
||||
"
|
||||
@@ -80,8 +80,11 @@ input[type=number] {
|
||||
{: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
|
||||
|
||||
]
|
||||
]]]]]]))
|
||||
]]]]]]]))
|
||||
|
||||
@@ -75,8 +75,7 @@
|
||||
[:vector {:decode/json {:enter (fn [x]
|
||||
(if (sequential? x)
|
||||
x
|
||||
[x])
|
||||
)}}
|
||||
[x]))}}
|
||||
x])
|
||||
|
||||
(defn empty->nil [v]
|
||||
@@ -87,7 +86,11 @@
|
||||
(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
|
||||
@@ -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
|
||||
@@ -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]
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
(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]
|
||||
[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)))
|
||||
|
||||
@@ -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]}
|
||||
@@ -359,6 +352,31 @@
|
||||
(mu/start-publisher! {:type :dev})
|
||||
(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})))
|
||||
|
||||
@@ -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)
|
||||
@@ -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})
|
||||
|
||||
@@ -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 ".")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
19
src/cljc/auto_ap/routes/admin/clients.cljc
Normal file
19
src/cljc/auto_ap/routes/admin/clients.cljc
Normal 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})
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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]]))
|
||||
@@ -122,7 +137,7 @@
|
||||
: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]]
|
||||
default-read]]
|
||||
{:graphql
|
||||
{:token user
|
||||
:owns-state {:single ::vendor-form}
|
||||
@@ -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}
|
||||
@@ -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)])}}
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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]}])
|
||||
|
||||
@@ -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])}))
|
||||
@@ -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])} ]]]])
|
||||
|
||||
|
||||
@@ -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]]))
|
||||
@@ -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]}])
|
||||
@@ -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]]])
|
||||
@@ -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]})))
|
||||
@@ -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])} ]]]]])
|
||||
|
||||
|
||||
@@ -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]]))
|
||||
@@ -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 &'])
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
Reference in New Issue
Block a user