Merge branch 'master' of codecommit://integreat into tr-form

This commit is contained in:
Bryce
2023-10-27 20:38:28 -07:00
33 changed files with 1305 additions and 4863 deletions

View File

@@ -1,7 +1,6 @@
(ns auto-ap.cursor
(:import (clojure.lang IDeref Atom ILookup Counted IFn AFn Indexed ISeq Seqable)))
; TODO not sure if these methods are needed at all; ICursor is used solely as a marker right now
(defprotocol ICursor
(path [cursor])
(state [cursor]))
@@ -125,6 +124,15 @@
[] nil))
(defn synthetic-cursor [v prefix]
(let [internal-cursor (cursor v)]
(reify ICursor
(path [this]
(into prefix (path internal-cursor)))
(state [this]
(state internal-cursor)))))
(defn transact! [cursor f]
"Changes value beneath cursor by passing it to a single-argument
function f. Old value will be passed as function argument. Function

View File

@@ -246,7 +246,7 @@
:with ?s
:in $
:where
[(ground (iol-ion.query/recent-date)) ?min-d]
[(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 ...]]
@@ -265,7 +265,7 @@
:with ?s ?li
:in $
:where
[(ground (iol-ion.query/recent-date)) ?min-d]
[(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 ...]]
@@ -282,7 +282,7 @@
"[:find ?d4 ?t ?f
:in $
:where
[(ground (iol-ion.query/recent-date)) ?min-d]
[(ground (iol-ion.query/recent-date 120)) ?min-d]
[?c :client/code \"%s\"]
[?s :expected-deposit/client ?c]
[?s :expected-deposit/sales-date ?date]
@@ -297,7 +297,7 @@
:with ?charge
:in $
:where
[(ground (iol-ion.query/recent-date)) ?min-d]
[(ground (iol-ion.query/recent-date 120)) ?min-d]
[?c :client/code \"%s\"]
[?s :sales-order/client ?c]
[?s :sales-order/date ?date]
@@ -317,7 +317,7 @@
:with ?charge
:in $
:where
[(ground (iol-ion.query/recent-date)) ?min-d]
[(ground (iol-ion.query/recent-date 120)) ?min-d]
[?charge :charge/date ?date]
[(>= ?date ?min-d)]
[?charge :charge/client ?c]
@@ -344,7 +344,7 @@
:with ?r
:in $
:where
[(ground (iol-ion.query/recent-date)) ?min-d]
[(ground (iol-ion.query/recent-date 120)) ?min-d]
[?r :sales-refund/client [:client/code \"%s\"]]
[?r :sales-refund/date ?date]
[(>= ?date ?min-d)]
@@ -360,7 +360,7 @@
:in $
:where
[?cds :cash-drawer-shift/date ?date]
[(ground (iol-ion.query/recent-date)) ?min-d]
[(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]

View File

@@ -254,7 +254,7 @@
:total [:trim-commas-and-negate nil]}}
;; Young's Market Co new statement
{:vendor "Youngs Market"
{:vendor "RNDC"
:keywords [#"(YOUNG'S MARKET COMPANY|Young.*Statement)"]
:extract {:date #"([0-9]+/[0-9]+/[0-9]+)"
:customer-identifier #"Customer Name +([\w ]+)"
@@ -266,14 +266,15 @@
:multi-match? #"^[0-9]+.*\$?([0-9,]+\.[0-9]+).*\$?([0-9,]+\.[0-9]+)"}
;; Young's Market Co - INVOICE
{:vendor "Youngs Market"
{:vendor "RNDC"
:keywords [#"P.O.Box 743564"]
:extract {:date #"(?:INVOICE|CREDIT) DATE\n(?:.*?)(\S+)\n"
#_#_:customer-identifier #"(?:INVOICE|CREDIT) DATE\n [0-9]+\s+(.*?)\s{2,}"
:account-number #"Store Number:\s+(\d+)"
:invoice-number #"(?:INVOICE|CREDIT) DATE\n(?:.*?)\s{2,}(\d+?)\s+\S+\n"
:total #"Net Amount(?:.*\n){4}(?:.*?)([\-]?[0-9\.]+)\n"}
:parser {:date [:clj-time "dd-MMM-yy"]
:parser {:date [:clj-time ["MM/dd/yy"
"dd-MMM-yy"]]
:total [:trim-commas-and-negate nil]}}
;; WINE WAREHOUSE

View File

@@ -767,6 +767,14 @@
(dc/db conn)
codes))))
(defn get-square-client-and-location [code]
(let [[client] (get-square-clients code)]
(some->> client
:client/square-locations
(filter :square-location/client-location)
seq
(conj [client]))))
(defn upsert-locations
([]
(apply de/zip

View File

@@ -27,24 +27,27 @@
field-validation-error
form-validation-error
html-response
main-transformer
many-entity
modal-response
ref->enum-schema
ref->select-options
strip
temp-id
wrap-entity
wrap-form-4xx-2
wrap-schema-decode]]
[bidi.bidi :as bidi]
[clojure.string :as str]
[datomic.api :as dc]
[hiccup2.core :as hiccup]
[malli.core :as mc]))
(defn filters [request]
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
"hx-get" (bidi/path-for ssr-routes/only-routes
:admin-account-table)
"hx-target" "#account-table"
"hx-indicator" "#account-table"}
"hx-target" "#entity-table"
"hx-indicator" "#entity-table"}
[:fieldset.space-y-6
(com/field {:label "Name"}
@@ -128,26 +131,22 @@
matching-count]))
(def grid-page
(helper/build {:id "account-table"
(helper/build {:id "entity-table"
:nav (com/admin-aside-nav)
:page-specific-nav filters
:fetch-page fetch-page
:parse-query-params (comp
(query-params/parse-key :code query-params/parse-long)
(helper/default-parse-query-params grid-page))
:action-buttons (fn [request]
:action-buttons (fn [_]
[(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes
:admin-account-new-dialog))
:hx-target "#modal-content"
:hx-swap "innerHTML"
:color :primary}
"New Account")])
:row-buttons (fn [request entity]
:row-buttons (fn [_ entity]
[(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes
:admin-account-edit-dialog
:db/id (:db/id entity)))
:hx-target "#modal-content"
:hx-swap "innerHTML"}
:db/id (:db/id entity)))}
svg/pencil)])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:admin)}
@@ -180,32 +179,29 @@
(def table* (partial helper/table* grid-page))
(defn account-save [{:keys [form-params request-method] :as request}]
(let [entity (cond-> form-params
(= :post request-method) (assoc :db/id "new"))
_ (cond (= :post request-method)
(when-let [extant (seq (dc/q '[:find ?x :in $ ?nc :where [?x :account/numeric-code ?nc]] (dc/db conn) (:account/numeric-code entity)))]
(field-validation-error (format "The code %d is already in use." (:account/numeric-code entity))
[:account/numeric-code]
:form form-params))
)
;; TODO the following would work better if the schema was hydrated automatically with needed values
_ (some->> form-params
:account/client-overrides
(group-by :account-client-override/client)
(filter (fn [[client overrides]]
(> (count overrides) 1)))
(map first)
seq
(#(form-validation-error (format "Client(s) %s have more than one override."
(str/join ", "
(map (fn [client]
(format "'%s'" (pull-attr (dc/db conn)
:client/name
(-> client)))
) %)))
:form form-params))
)
(let [entity (cond-> form-params
(= :post request-method) (assoc :db/id "new"))
_ (cond (= :post request-method)
(when (seq (dc/q '[:find ?x :in $ ?nc :where [?x :account/numeric-code ?nc]] (dc/db conn) (:account/numeric-code entity)))
(field-validation-error (format "The code %d is already in use." (:account/numeric-code entity))
[:account/numeric-code]
:form form-params)))
_ (some->> form-params
:account/client-overrides
(group-by :account-client-override/client)
(filter (fn [[_ overrides]]
(> (count overrides) 1)))
(map first)
seq
(#(form-validation-error (format "Client(s) %s have more than one override."
(str/join ", "
(map (fn [client]
(format "'%s'" (pull-attr (dc/db conn)
:client/name
(-> client)))
) %)))
:form form-params)) ;; TODO shouldnt need to bubble this through. See if we can eliminate the passing of form and last-form.
)
{:keys [tempids]} (audit-transact [[:upsert-entity (cond-> entity
(:account/numeric-code entity) (assoc :account/code (str (:account/numeric-code entity))))]]
(:identity request))
@@ -231,24 +227,16 @@
"account_client_override_id" (:db/id o)})))
(html-response
(row* identity updated-account {:flash? true})
:headers {"hx-trigger" "modalclose"
"hx-retarget" (format "#account-table tr[data-id=\"%d\"]" (:db/id updated-account))})))
: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-account)))))))
;; TODO use cursor
;; TODO index based list not dbid
;; TODO lots of weird edge cases with indexes
;; for example, what happens when you have 0, 1, 2, but then delete 1?
;; what happens when you delete 2 but then add another one?
;; preference is that the field parsing logic does better grouping of what
;; goes with what, building index on the server side
;; not needing to pass index in
;; TODO decide when cursors are used. other cases it's not, some are
(defn client-override* [override]
(com/data-grid-row (-> {:x-ref "p"
:data-key "show"
:x-data (hx/json {:show (boolean (not (:new? override)))})}
:x-data (hx/json {:show (boolean (doto (not (fc/field-value (:new? override)))
println))})}
hx/alpine-mount-then-appear)
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
@@ -256,16 +244,13 @@
(fc/with-field :account-client-override/client
(com/data-grid-cell {}
(com/validated-field {:errors (fc/field-errors)}
(com/typeahead-2 {:name (fc/field-name)
(com/typeahead {:name (fc/field-name)
:placeholder "Search..."
:class "w-96"
:url (bidi/path-for ssr-routes/only-routes
:company-search)
:value (fc/field-value)
:value-fn (some-fn :db/id identity) ;; TODO better hydration
:content-fn (fn [value]
(:client/name (cond->> value
(nat-int? value) (dc/pull (dc/db conn) [:client/name]))))}))))
:content-fn #(pull-attr (dc/db conn) :client/name %)}))))
(fc/with-field :account-client-override/name
(com/data-grid-cell
{}
@@ -276,31 +261,24 @@
(com/data-grid-cell {:class "align-top"}
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
;; TODO each form:
;; elimante typeahead1
;; work with new dialog
;; use cursor
;; ensure all dialogs are opened the same way
;; form level validation
;; form level validation
;; componentize
;; ensure all dependency oriented stuff works the same way
;; make sure that "new row index" stuff works ok
;; TODO figure out when hx-targets are decided
;; ensure that adding a new one results in a new row
(defn dialog* [& {:keys [entity form-params form-errors]}]
(fc/start-form entity form-errors
[:div {:x-data (hx/json {"accountName" (:account/name entity)
"accountCode" (:account/numeric-code entity)})}
(defn dialog* [{:keys [entity form-params form-errors]}]
(fc/start-form form-params form-errors
[:div {:x-data (hx/json {"accountName" (or (:account/name form-params) (:account/numeric-code entity))
"accountCode" (or (:account/numeric-code form-params) (:account/numeric-code entity) )})
:hx-target "this"
:class "w-full h-full"}
(com/modal
{}
[:form#edit-form (merge {:hx-ext "response-targets"
:hx-swap "outerHTML swap:300ms"
:hx-target-400 "#form-errors .error-content"}
form-params)
[:fieldset {:class "hx-disable"}
[:form (-> {:hx-ext "response-targets"
:hx-swap "outerHTML swap:300ms"
:hx-target-400 "#form-errors .error-content"
:class "h-full"}
(assoc (if (:db/id entity)
:hx-put
:hx-post)
(str (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-edit-save))))
[:fieldset {:class "hx-disable h-full"}
(com/modal-card
{}
[:div.flex [:div.p-2 "Account"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600
@@ -317,6 +295,7 @@
(com/validated-field {:label "Numeric code"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:value (fc/field-value)
:x-model "accountCode"
:autofocus true
:class "w-32"}))
@@ -375,89 +354,57 @@
(com/data-grid {:headers [(com/data-grid-header {} "Client")
(com/data-grid-header {} "Account name")
(com/data-grid-header {})]
:id "client-override-table"}
(when (fc/field-value)
(doall
(for [override fc/*current*]
(fc/with-cursor override
(client-override* override)))))
(com/data-grid-row
{:class "new-row"}
(com/data-grid-cell {:colspan 3
:class "bg-gray-100"}
[:div.flex.justify-center
(com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes
:admin-account-client-override-new)
:color :secondary
:hx-include "#edit-form"
:hx-vals (hiccup/raw "js:{index: countRows(\"#client-override-table\")}")
:hx-target "#edit-form .new-row"
:hx-swap "beforebegin"}
"New override")])))))
]
:id "client-override-table"}
(fc/cursor-map
#(client-override* %))
(com/data-grid-new-row {:colspan 3
:index (count (fc/field-value))
:hx-get (bidi/path-for ssr-routes/only-routes
:admin-account-client-override-new)}
"New override"))))]
[:div
[:div [:div#form-errors (when (:errors fc/*form-errors*)
[:span.error-content
(com/errors {:errors (:errors fc/*form-errors*)})])]]
(com/form-errors {:errors (:errors fc/*form-errors*)})
(com/validated-save-button {:errors (seq form-errors)}
"Save account")])]])]))
(defn new-client-override [{ {:keys [index]} :query-params}]
(let [index (or index 0)
account {:account/client-overrides (conj (into [] (repeat index {}))
{:db/id (str (java.util.UUID/randomUUID))
:new? true})}] ;; TODO schema decode is not working
(html-response
(fc/start-form account []
(fc/with-cursor (get-in fc/*current* [:account/client-overrides index])
(client-override* fc/*current*))))))
(html-response
(fc/start-form-with-prefix
[:account/client-overrides (or index 0)]
{:db/id (str (java.util.UUID/randomUUID))
:new? true}
[]
(client-override* fc/*current*))))
(defn account-edit-dialog [request]
(let [account (some-> request :route-params :db/id (#(dc/pull (dc/db conn) default-read %)))]
(html-response (dialog* :entity account
:form-params {:hx-put (str (bidi/path-for ssr-routes/only-routes
:admin-account-edit-save))})
:headers {"hx-trigger" "modalopen"})))
(defn account-new-dialog [_]
(html-response (dialog* :entity {}
:form-errors {}
:form-params {:hx-post (str (bidi/path-for ssr-routes/only-routes
:admin-account-new-save))})
:headers {"hx-trigger" "modalopen"}))
(defn account-save-error [request]
;; TODO hydration
;; TODO consistency of error handling and passing, on all form examples
(let [entity (some-> request :last-form)]
(html-response (dialog* :entity entity
:form-errors (:form-errors request)
:form-params (if (:db/id entity)
{:hx-put (str (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-edit-save))}
{:hx-post (str (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-edit-save))}))
:headers {"hx-retarget" "#edit-form fieldset"
"hx-reselect" "#edit-form fieldset"})))
(def account-schema (mc/schema
(def form-schema (mc/schema
[:map
[:db/id {:optional true} [:maybe entity-id]]
[:account/numeric-code {:optional true} [:maybe :int]]
[:account/name [:string {:min 1}]]
[:account/location [:maybe :string]]
[:account/name [:string {:min 1 :decode/string strip}]]
[:account/location {:optional true} [:maybe [:string {:decode/string strip}]]]
[:account/type (ref->enum-schema "account-type")]
[:account/applicability (ref->enum-schema "account-applicability")]
[:account/applicability (ref->enum-schema "account-applicability")] ;
[:account/invoice-allowance (ref->enum-schema "allowance")]
[:account/vendor-allowance (ref->enum-schema "allowance")]
[:account/client-overrides {:optional true}
[:maybe
(many-entity {}
[:db/id [:or entity-id temp-id]]
[:account-client-override/client [:or entity-id :string]]
[:account-client-override/name [:string {:min 2}]])]]]))
[:account-client-override/client entity-id]
[:account-client-override/name [:string {:min 2 :decode/string strip}]])]]]))
(defn account-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})))
(def key->handler
(apply-middleware-to-all-handlers
@@ -470,12 +417,14 @@
:default 0} [nat-int? {:default 0}]]])
wrap-admin wrap-client-redirect-unauthenticated)
:admin-account-save (-> account-save
(wrap-schema-decode :form-schema account-schema)
(wrap-entity [:form-params :db/id] default-read)
(wrap-schema-decode :form-schema form-schema)
(wrap-nested-form-params)
(wrap-form-4xx-2 account-save-error))
:admin-account-edit-dialog (-> account-edit-dialog
(wrap-form-4xx-2 (wrap-entity account-dialog [:form-params :db/id] default-read)))
:admin-account-edit-dialog (-> account-dialog
(wrap-entity [:route-params :db/id] default-read)
(wrap-schema-decode :route-schema [:map [:db/id entity-id]]))
:admin-account-new-dialog account-new-dialog})
:admin-account-new-dialog account-dialog})
(fn [h]
(-> h
(wrap-admin)

View File

@@ -2,24 +2,29 @@
(:require
[amazonica.aws.ecs :as ecs]
[auto-ap.logging :as alog]
[clojure.string :as str]
[auto-ap.routes.utils
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.grid-page-helper :as helper]
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
[auto-ap.ssr.utils
:refer [apply-middleware-to-all-handlers
entity-id
form-validation-error
html-response
validation-error
wrap-form-4xx
modal-response
wrap-form-4xx-2
wrap-schema-decode]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[clojure.string :as str]
[config.core :refer [env]])
[config.core :refer [env]]
[malli.core :as mc]
[auto-ap.ssr.hx :as hx])
(:import
(com.amazonaws.services.ecs.model AssignPublicIp)))
@@ -57,8 +62,8 @@
false))
(defn ecs-task->job [task]
{:status (condp = (:last-status task)
{:arn (:task-arn task)
:status (condp = (:last-status task)
"RUNNING" :running
"PENDING" :pending
"PROVISIONING" :pending
@@ -78,13 +83,11 @@
(def grid-page
(helper/build {:id "job-table"
:id-fn :arn
:nav (com/admin-aside-nav)
:fetch-page fetch-page
:action-buttons (fn [request]
[(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes
:admin-job-start-dialog))
:hx-target "#modal-holder"
:hx-swap "outerHTML"
[(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-job-start-dialog))
:color :primary}
"Run job")])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
@@ -151,86 +154,119 @@
(str "_" (:dd-env env)))
(dissoc form-params :name))]
{:message (str "task " (str new-job) " started.")})
(validation-error "This job is already running")))
(form-validation-error "This job is already running"
:form form-params)))
(defn subform [{{:strs [name]} :query-params }]
(html-response (cond (= "bulk-journal-import" name)
[:div (com/field {:label "Url"}
[:div.flex.place-items-center.gap-2
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
(com/text-input {:placeholder "ledger-data.csv"
:name "ledger-url"} )])]
(= "register-invoice-import" name)
[:div (com/field {:label "Url"}
(defn subform* [{:keys [name]}]
(into [:div {:class "fade-in-settle transition"}]
(cond (= "bulk-journal-import" name)
[(fc/with-field :ledger-url
(com/validated-field {:label "Url"
:errors (fc/field-errors)}
[:div.flex.place-items-center.gap-2
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
(com/text-input {:placeholder "ledger-data.csv"
:name (fc/field-name)
:value (fc/field-value)} )]))]
(= "register-invoice-import" name)
[
(fc/with-field :invoice-url
(com/validated-field {:label "Url"
:errors (fc/field-errors)}
[:div.flex.place-items-center.gap-2
[:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"]
(com/text-input {:placeholder "invoice-data.csv"
:name "invoice-url"} )])]
(= "load-historical-sales" name)
[:div
(com/field {:label "Client"}
(com/typeahead {:name "client"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes
:company-search)
:id (str "client-search")}))
(com/field {:label "Days to load"}
(com/text-input {:placeholder "60"
:name "days"} ))]
:else [:div]))
:name (fc/field-name)
:value (fc/field-value)} )]))]
(= "load-historical-sales" name)
[
(fc/with-field :client
(com/validated-field {:label "Client"
:errors (fc/field-errors)}
(com/typeahead {:name (fc/field-name)
:value (fc/field-value)
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes
:company-search)})))
(fc/with-field :days
(com/validated-field {:label "Days to load"
:errors (fc/field-errors)}
(com/text-input {:placeholder "60"
:name (fc/field-name)
:value (fc/field-value)} )))]
:else nil))
)
(defn job-start-dialog [_]
(html-response (com/modal
{:modal-class "max-w-4xl"}
[:form#edit-form {:hx-ext "response-targets"
:hx-post (bidi/path-for ssr-routes/only-routes :admin-job-start
)
:hx-swap "outerHTML swap:300ms"
:hx-target-400 "#form-errors .error-content"}
[:fieldset {:class "hx-disable"}
(com/modal-card
{}
[:div.flex [:div.p-2 "New job"] ]
[:div.space-y-6
(defn subform [{{:keys [name]} :query-params }]
(html-response
(fc/start-form {} nil
(subform* {:name name}))))
(com/field {:label "Job"}
(com/select {:name "name"
:class "w-64"
:options [["" ""]
["yodlee2" "Yodlee Import"]
["yodlee2-accounts" "Yodlee Account Import"]
["intuit" "Intuit import"]
["plaid" "Plaid import"]
["bulk-journal-import" "Bulk Journal Import"]
["square2-import-job" "Square2 Import"]
["register-invoice-import" "Register Invoice Import "]
["ezcater-upsert" "Upsert recent ezcater orders"]
["load-historical-sales" "Load Historical Square Sales"]
["export-backup" "Export Backup"]]
:hx-get (bidi/path-for ssr-routes/only-routes
:admin-job-subform)
:hx-target "#sub-form"
:hx-swap "innerHTML"}))
(defn job-start-dialog [{:keys [form-errors form-params] :as request}]
(fc/start-form (or form-params {}) form-errors
(modal-response
(com/modal ;; TODO we need a cleaner way to have forms that wrap the whole. In this cas
{}
[:form {:hx-post (bidi/path-for ssr-routes/only-routes :admin-job-start)
:class "h-full w-full"
"x-on:htmx:response-error" "unexpectedError=true"
"x-on:htmx:before-request" "unexpectedError=false"
:x-data (hx/json {:unexpectedError false})}
[:fieldset {:class "hx-disable h-full w-full"}
(com/modal-card {}
[:div.m-2 "New job"]
[:div.space-y-6
[:div#sub-form]
[:div#form-errors [:span.error-content]]
(com/button {:color :primary :form "edit-form" :type "submit"}
"Run")]
[:div])]])))
(fc/with-field :name
(com/validated-field {:label "Job"
:errors (fc/field-errors)}
(com/select {:name (fc/field-name)
:value (fc/field-value)
:class "w-64"
:options [["" ""]
["yodlee2" "Yodlee Import"]
["yodlee2-accounts" "Yodlee Account Import"]
["intuit" "Intuit import"]
["plaid" "Plaid import"]
["bulk-journal-import" "Bulk Journal Import"]
["square2-import-job" "Square2 Import"]
["register-invoice-import" "Register Invoice Import "]
["ezcater-upsert" "Upsert recent ezcater orders"]
["load-historical-sales" "Load Historical Square Sales"]
["export-backup" "Export Backup"]]
:hx-get (bidi/path-for ssr-routes/only-routes
:admin-job-subform)
:hx-target "#sub-form"
:hx-swap "innerHTML"})))
[:div#sub-form (subform* {:name (fc/with-field :name (fc/field-value))}) ]]
[:div
[:div#5xx-error.bg-red-100.mb-2.p-1 {:x-show "unexpectedError"}
"An unexpected error has occured."]
(com/form-errors {:errors (:errors fc/*form-errors*)})
(com/validated-save-button {:errors form-errors} "Run job")])]]))))
(def form-schema (mc/schema [:map
[:name [:string {:min 1}]]
[:ledger-url {:optional true} [:string {:min 1}]]
[:invoice-url {:optional true} [:string {:min 1}]]
[:client {:optional true} entity-id]
[:days {:optional true} [:int {:min 1 :max 120}]]
]))
(def key->handler
(apply-middleware-to-all-handlers
(->>
{:admin-jobs (helper/page-route grid-page)
:admin-job-table (helper/table-route grid-page)
:admin-job-subform (-> subform (wrap-schema-decode :query-schema [:map [:name :string]]))
:admin-job-start (-> job-start
(wrap-schema-decode :form-schema [:map [:name :string]])
(wrap-nested-form-params)
(wrap-form-4xx))
:admin-job-start-dialog job-start-dialog})
{:admin-jobs (helper/page-route grid-page)
:admin-job-table (helper/table-route grid-page)
:admin-job-subform (-> subform (wrap-schema-decode :query-schema [:map [:name {:optional true} [:maybe :string]]]))
:admin-job-start (-> job-start
(wrap-schema-decode :form-schema form-schema)
(wrap-nested-form-params)
(wrap-form-4xx-2 job-start-dialog))
:admin-job-start-dialog job-start-dialog})
(fn [h]
(-> h
(wrap-admin)

View File

@@ -18,9 +18,9 @@
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.company :refer [bank-account-typeahead*]]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.grid-page-helper :as helper]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils
@@ -29,49 +29,42 @@
field-validation-error
form-validation-error
html-response
main-transformer
many-entity
modal-response
money
path->name2
percentage
ref->enum-schema
ref->radio-options
regex
temp-id
wrap-entity
wrap-form-4xx-2
wrap-schema-decode]]
[auto-ap.time :as atime]
[auto-ap.utils :refer [dollars=]]
[bidi.bidi :as bidi]
[cheshire.core :as cheshire]
[clojure.string :as str]
[datomic.api :as dc]
[hiccup2.core :as hiccup]
[iol-ion.query :refer [ident]]
[malli.core :as mc]
[auto-ap.cursor :as cursor]
[auto-ap.ssr.hiccup-helper :as hh]))
;; TODO with dependencies, I really don't like that you have to be ultra specific in what
;; you want to include, and generating the routes and interconnection is weird too.
;; I'm tempted to say to include a full snapshot of the form, and the indicator
;; as to which one to generate.
[malli.core :as mc]))
(defn filters [request]
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
"hx-get" (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-table)
"hx-target" "#transaction-rule-table"
"hx-indicator" "#transaction-rule-table"}
"hx-target" "#entity-table"
"hx-indicator" "#entity-table"}
[:fieldset.space-y-6
(com/field {:label "Vendor"}
(com/typeahead-2 {:name "vendor"
(com/typeahead {:name "vendor"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes
:vendor-search)
:id (str "vendor-search")
:value [(:db/id (:vendor (:parsed-query-params request)))
(:vendor/name (:vendor (:parsed-query-params request)))]}))
:value (:vendor (:parsed-query-params request))
:value-fn :db/id
:content-fn :vendor/name}))
(com/field {:label "Note"}
(com/text-input {:name "note"
:id "note"
@@ -182,7 +175,7 @@
matching-count]))
(def grid-page
(helper/build {:id "transaction-rule-table"
(helper/build {:id "entity-table"
:nav (com/admin-aside-nav)
:page-specific-nav filters
:fetch-page fetch-page
@@ -192,16 +185,12 @@
:action-buttons (fn [request]
[(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-new-dialog))
:hx-target "#modal-content"
:hx-swap "innerHTML"
:color :primary}
"New Transaction Rule")])
:row-buttons (fn [request entity]
[(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-edit-dialog
:db/id (:db/id entity)))
:hx-target "#modal-content"
:hx-swap "innerHTML"}
:db/id (:db/id entity)))}
svg/pencil)])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:admin)}
@@ -270,42 +259,151 @@
(set))
bank-account-id))
(defn validate-transaction-rule [form-params]
(doseq [[{:transaction-rule-account/keys [account location]} i] (map vector (:transaction-rule/accounts form-params) (range))
:let [account-location (pull-attr (dc/db conn) :account/location account)]
:when (and account-location (not= account-location location))]
(field-validation-error (str "must be " account-location)
[:transaction-rule/accounts i :transaction-rule-account/location]
:form form-params))
(let [total (reduce + 0.0 (map :transaction-rule-account/percentage (:transaction-rule/accounts form-params)))]
(when-not (dollars= 1.0 total)
(form-validation-error (format "Expense accounts total (%d%%) must add to 100%%" (int (* 100.0 total)))
:form form-params)))
(when (and (:transaction-rule/bank-account form-params)
(not (bank-account-belongs-to-client? (:transaction-rule/bank-account form-params)
(:transaction-rule/client form-params))))
(field-validation-error "does not belong to client"
[:transaction-rule/bank-account]
:form form-params)))
(defn transaction-rule-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)))
_ (doseq [[{:transaction-rule-account/keys [account location]} i] (map vector (:transaction-rule/accounts entity) (range))
:let [account-location (pull-attr (dc/db conn) :account/location account)]
:when (and account-location (not= account-location location))]
(field-validation-error (str "must be " account-location)
[:transaction-rule/accounts i :transaction-rule-account/location]
:form form-params))
total (reduce +
0.0
(map :transaction-rule-account/percentage
(:transaction-rule/accounts entity)))
_ (when-not (dollars= 1.0 total)
(form-validation-error (format "Expense accounts total (%d%%) must add to 100%%" (int (* 100.0 total)))
:form form-params))
_ (when (and (:transaction-rule/bank-account entity)
(not (bank-account-belongs-to-client? (:transaction-rule/bank-account entity)
(:transaction-rule/client entity))))
(field-validation-error "does not belong to client"
[:transaction-rule/bank-account]
:form form-params))
{:keys [tempids]} (audit-transact [[:upsert-entity entity]]
(:identity request))
updated-account (dc/pull (dc/db conn)
updated-rule (dc/pull (dc/db conn)
default-read
(or (get tempids (:db/id entity)) (:db/id entity)))]
(html-response
(row* identity updated-account {:flash? true})
:headers {"hx-trigger" "modalclose"
"hx-retarget" (format "#transaction-rule-table tr[data-id=\"%d\"]" (:db/id updated-account))})))
(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 transaction-read '[{:transaction/client [:client/name]
:transaction/bank-account [:bank-account/name]}
:transaction/description-original
[:transaction/date :xform clj-time.coerce/from-date]])
(defn transaction-rule-test [{:keys [form-params request-method identity] :as request
{:transaction-rule/keys [description client bank-account amount-lte amount-gte dom-lte dom-gte yodlee-merchant]} :form-params}]
(validate-transaction-rule form-params)
(let [valid-clients (extract-client-ids (:clients request)
client)
query (cond-> {:query {:find ['(pull ?e read)]
:in ['$ 'read]
:where []}
:args [(dc/db conn) transaction-read]}
description
(merge-query {:query {:in ['?descr]
:where ['[(iol-ion.query/->pattern ?descr) ?description-regex]]}
:args [description]})
valid-clients
(merge-query {:query {:in ['[?xx ...]]
:where ['[?e :transaction/client ?xx]]}
:args [(set valid-clients)]})
bank-account
(merge-query {:query {:in ['?bank-account-id]
:where ['[?e :transaction/bank-account ?bank-account-id]]}
:args [bank-account]})
description
(merge-query {:query {:where ['[?e :transaction/description-original ?do]
'[(re-find ?description-regex ?do)]]}})
amount-gte
(merge-query {:query {:in ['?amount-gte]
:where ['[?e :transaction/amount ?ta]
'[(>= ?ta ?amount-gte)]]}
:args [amount-gte]})
amount-lte
(merge-query {:query {:in ['?amount-lte]
:where ['[?e :transaction/amount ?ta]
'[(<= ?ta ?amount-lte)]]}
:args [amount-lte]})
dom-lte
(merge-query {:query {:in ['?dom-lte]
:where ['[?e :transaction/date ?transaction-date]
'[(iol-ion.query/dom ?transaction-date) ?dom]
'[(<= ?dom ?dom-lte)]]}
:args [dom-lte]})
dom-gte
(merge-query {:query {:in ['?dom-gte]
:where ['[?e :transaction/date ?transaction-date]
'[(iol-ion.query/dom ?transaction-date) ?dom]
'[(>= ?dom ?dom-gte)]]}
:args [dom-gte]})
client
(merge-query {:query {:in ['?client-id]
:where ['[?e :transaction/client ?client-id]]}
:args [client]})
true
(merge-query {:query {:where ['[?e :transaction/id]]}}))
results (->>
(query2 query)
(map first))]
(html-response
(com/stacked-modal-card
1
{}
[:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"] [:div.ml-4.relative (com/badge {} (count results))]]
(com/data-grid
{:headers [(com/data-grid-header {} "Client")
(com/data-grid-header {} "Bank")
(com/data-grid-header {} "Date")
(com/data-grid-header {} "Description")]}
(for [r (take 15 results)]
(com/data-grid-row
{}
(com/data-grid-cell {} (-> r :transaction/client :client/name))
(com/data-grid-cell {} (-> r :transaction/bank-account :bank-account/name))
(com/data-grid-cell {} (some-> r :transaction/date (atime/unparse-local atime/normal-date)))
(com/data-grid-cell {} (some-> r :transaction/description-original )))))
[:div.flex.justify-between
(com/button {"@click" "$dispatch('modalpop')"
:class "w-32"}
"Back")
(com/button (cond-> {:color :primary
:hx-vals (hx/json (:raw-form-params request))
:class "w-32"
}
(:db/id form-params) (assoc :hx-put (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-save))
(not (:db/id form-params)) (assoc :hx-post (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-save)))
"Save rule")
])
:headers (-> {}
(assoc "hx-trigger-after-settle" "modalnext")
(assoc "hx-retarget" ".modal-stack")
(assoc "hx-reswap" "beforeend")))))
@@ -328,98 +426,98 @@
(defn- account-typeahead*
[{:keys [name value client-id x-model]}]
[:div.flex.flex-col
(com/typeahead-2 {:name name
:placeholder "Search..."
:url (str (bidi/path-for ssr-routes/only-routes :account-search) "?client-id=" client-id)
:id name
:x-model x-model
:value value
:value-fn (some-fn :db/id identity)
:content-fn (fn [value]
(:account/name (d-accounts/clientize (cond->> value
(nat-int? value) (dc/pull (dc/db conn) d-accounts/default-read))
client-id)))})])
(com/typeahead {:name name
:placeholder "Search..."
:url (str (bidi/path-for ssr-routes/only-routes :account-search) "?client-id=" client-id)
:id name
:x-model x-model
:value value
:content-fn (fn [value]
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
client-id)))})])
(defn- transaction-rule-account-row*
[transaction-rule account]
(com/data-grid-row (-> {:x-data (hx/json {:accountId (or (:db/id (fc/field-value (:transaction-rule-account/account account)))
(fc/field-value (:transaction-rule-account/account account)))
:show (boolean (not (fc/field-value (:new? account))))})
:data-key "show"
:x-ref "p"}
hx/alpine-mount-then-appear)
(let [account-name (fc/field-name (:transaction-rule-account/account account))]
(list
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
:value (fc/field-value)}))
(fc/with-field :transaction-rule-account/account
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)}
[:div {:hx-trigger "changed"
:hx-target "next div"
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" account-name)
:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-account-typeahead))
:x-init "$watch('clientId', cid => $dispatch('changed', $data))"}]
(account-typeahead* {:value (fc/field-value)
:client-id (:db/id (:transaction-rule/client transaction-rule))
:name (fc/field-name)
:x-model "accountId"
}))))
(fc/with-field :transaction-rule-account/location
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)
:x-data (hx/json {:location (fc/field-value)})}
[:div {:hx-trigger "changed"
:hx-target "next *"
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || ''}" (fc/field-name) )
:hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-location-select)
:x-init "$watch('clientId', cid => $dispatch('changed', $data)); $watch('accountId', cid => $dispatch('changed', $data))"}]
(location-select* {:name (fc/field-name)
:account-location (:account/location (cond->> (:transaction-rule-account/account @account)
(nat-int? (:transaction-rule-account/account @account)) (dc/pull (dc/db conn)
'[:account/location])))
:client-locations (:client/locations (:transaction-rule/client transaction-rule))
:hx-model "location"
:value (fc/field-value)}))))
(fc/with-field :transaction-rule-account/percentage
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)}
(com/money-input {:name (fc/field-name)
:class "w-16"
:value (some-> (fc/field-value)
(* 100 )
(long ))}))))))
(com/data-grid-cell {:class "align-top"}
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
[account client-id client-locations]
(com/data-grid-row
(-> {:x-data (hx/json {:accountId (or (:db/id (fc/field-value (:transaction-rule-account/account account)))
(fc/field-value (:transaction-rule-account/account account)))
:location (fc/field-value (:transaction-rule-account/location account))
:show (boolean (not (fc/field-value (:new? account))))})
:data-key "show"
:x-ref "p"}
hx/alpine-mount-then-appear)
(let [account-name (fc/field-name (:transaction-rule-account/account account))]
(list
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
:value (fc/field-value)}))
(fc/with-field :transaction-rule-account/account
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)}
[:div {:hx-trigger "changed"
:hx-target "next div"
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId, value: event.detail.accountId}" account-name)
:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-account-typeahead))
:x-init "$watch('clientId', cid => $dispatch('changed', $data));"}]
(account-typeahead* {:value (fc/field-value)
:client-id client-id
:name (fc/field-name)
:x-model "accountId"}))))
(fc/with-field :transaction-rule-account/location
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)
:x-data (hx/json {:location (fc/field-value)})}
[:div {:hx-trigger "changed"
:hx-target "next *"
:hx-swap "outerHTML"
:hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || '', value: event.detail.location}" (fc/field-name) )
:hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-location-select)
:x-init "$watch('clientId', cid => $dispatch('changed', $data)); $watch('accountId', cid => $dispatch('changed', $data) )"}]
(location-select* {:name (fc/field-name)
:account-location (:account/location (cond->> (:transaction-rule-account/account @account)
(nat-int? (:transaction-rule-account/account @account)) (dc/pull (dc/db conn)
'[:account/location])))
:client-locations client-locations
:x-model "location"
:value (fc/field-value)}))))
(fc/with-field :transaction-rule-account/percentage
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)}
(com/money-input {:name (fc/field-name)
:class "w-16"
:value (some-> (fc/field-value)
(* 100 )
(long ))}))))))
(com/data-grid-cell {:class "align-top"}
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
;; TODO dialog is no longer closeable
(defn dialog* [& {:keys [entity form-params form-errors]}]
(fc/start-form entity form-errors
(defn dialog* [{:keys [entity form-params form-errors]}]
(fc/start-form form-params form-errors
(com/modal
{:modal-class "max-w-2xl"}
{:modal-class "max-w-2xl"
:hx-target "this"}
[:form#edit-form (merge {:hx-ext "response-targets"
:hx-swap "outerHTML swap:300ms"
:hx-target "#modal-holder" ;; TODO sort
:hx-target-400 "#form-errors .error-content"
:x-trap "true"
:class "group/form"}
form-params)
(com/modal-card
{}
[:div.flex [:div.p-2 "Transaction Rule"]]
[:fieldset {:class "hx-disable"
:hx-disinherit "hx-target" ;; TODO why disinherit
:x-data (hx/json {:clientId (or (:db/id (:transaction-rule/client entity))
(:transaction-rule/client entity))})}
(com/stacked-modal-card
0
{}
[:div.flex [:div.p-2 "Transaction Rule"]]
[:form#my-form {:hx-ext "response-targets"
:hx-target-400 "#form-errors .error-content"
:x-trap "true"
(if (:db/id entity)
:hx-put
:hx-post) (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-edit-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)))})}
[:div.space-y-1
(when-let [id (:db/id entity)]
@@ -458,17 +556,14 @@
:x-show "clientFilter"}
(hx/alpine-appear))
[:div.w-96
(com/typeahead-2 {:name (fc/field-name)
:error? (fc/error?)
:class "w-96"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :company-search)
:x-model "clientId"
:value (fc/field-value)
:value-fn (some-fn :db/id identity)
:content-fn (fn [c] (cond->> c
(nat-int? c) (dc/pull (dc/db conn) '[:client/name])
true :client/name))})]))
(com/typeahead {:name (fc/field-name)
:error? (fc/error?)
:class "w-96"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :company-search)
:x-model "clientId"
:value (fc/field-value)
:content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})]))
(fc/with-field :transaction-rule/bank-account
(com/validated-field
@@ -486,7 +581,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 ((some-fn :db/id identity) (:transaction-rule/client entity))
(bank-account-typeahead* {:client-id (:transaction-rule/client form-params)
:name (fc/field-name)
:value (fc/field-value)})]))
@@ -533,44 +628,28 @@
(com/validated-field {:label "Assign Vendor"
:errors (fc/field-errors)}
[:div.w-96
(com/typeahead-2 {:name (fc/field-name)
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
:id (str "form-vendor-search")
:class "w-96"
:value (fc/field-value)
:value-fn (some-fn :db/id identity)
:content-fn (some-fn :vendor/name #(pull-attr (dc/db conn) :vendor/name %))})]))
(com/typeahead {:name (fc/field-name)
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
:class "w-96"
:value (fc/field-value)
:content-fn #(pull-attr (dc/db conn) :vendor/name %)})]))
(fc/with-field :transaction-rule/accounts
(list
(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"})]
:id "transaction-rule-account-table"}
(when (fc/field-value)
(doall (for [tra fc/*current*]
(fc/with-cursor tra
(transaction-rule-account-row* entity tra)))))
(com/data-grid-row
{:class "new-row"}
(com/data-grid-cell {:colspan 4
:class "bg-gray-100"}
[:div.flex.justify-center
(com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-new-account)
:color :secondary
:hx-include "#edit-form"
:hx-ext "rename-params"
;; TODO
:hx-rename-params-ex (cheshire/generate-string {"transaction-rule/client" "client-id"
"index" "index"})
:hx-vals (hiccup/raw "js:{index: countRows(\"#transaction-rule-account-table\")}")
:hx-target "#edit-form .new-row"
:hx-swap "beforebegin"}
"New account")])))
(com/errors {:errors (fc/field-errors)})))
(com/validated-field
{:errors (fc/field-errors)}
(let [client-locations (some->> form-params :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))
(com/data-grid-new-row {:colspan 4
:hx-get (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-new-account)
:index (count (fc/field-value))
:tr-params (hx/bind-alpine-vals {} {:client-id "clientId"})}
"New account")))))
(fc/with-field :transaction-rule/transaction-approval-status
(com/validated-field {:label "Approval status"
@@ -581,109 +660,79 @@
:size :small
:orientation :horizontal})))
;; TODO componentize
]]
[:div
[:div#form-errors (when (:errors fc/*form-errors*)
[:span.error-content
(com/errors {:errors (:errors fc/*form-errors*)})])]
(com/validated-save-button {:errors form-errors} "Save rule")])])))
]]]
[:div
(com/form-errors {:errors (:errors fc/*form-errors*)})
[:div.flex.justify-end
(com/validated-save-button {:errors form-errors :color :secondary
:hx-post (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-test)
:hx-include "#my-form"}
"Test rule")
(com/validated-save-button {:errors form-errors
:form "my-form"} "Save rule")]]))))
;; TODO Should forms have some kind of helper to render an individual field? saving
;; could generate the entire form and swap if there are errors
;; but also when you tab out it could call the same function, and just
;; pull out the single field to swap
(defn new-account [{{:keys [client-id index]} :query-params}]
(let [index (or index 0) ;; TODO schema decode is not working
transaction-rule {:transaction-rule/client (dc/pull (dc/db conn) '[:client/name :client/locations :db/id]
client-id)
:transaction-rule/accounts (conj (into [] (repeat index {} ))
{:db/id (str (java.util.UUID/randomUUID))
:transaction-rule-account/location "Shared"
:new? true})}]
(html-response
(fc/start-form transaction-rule []
(fc/with-cursor (get-in fc/*current* [:transaction-rule/accounts index])
(transaction-rule-account-row*
;; TODO store a pointer to the "head " cursor for errors instead of nesting them
;; makes it so you don't have to do this
transaction-rule
fc/*current*))))))
(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) client-id)))))
;; TODO check to see if it should be called "Shared" or "shared" for the value
;; TODO hydrate nested types more easily. make it easy to hydrate so you don't do weird sometimes pulls
;; TODO is it possible to make it easy to get a virtual cursor in the case of adding a new row? setting up
;; fake data doesn't feel right - maybe have a "prelude" that's dynamic
(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))
(pull-attr (dc/db conn) :account/location))
:client-locations (some->> client-id
(pull-attr (dc/db conn) :client/locations))})))
(pull-attr (dc/db conn) :client/locations))})))
(defn account-typeahead [{{:keys [name value client-id] :as qp} :query-params}]
(let [account (some->> value (dc/pull (dc/db conn) [:account/name :db/id
{:account/client-overrides [:db/id
:account-client-override/name
{:account-client-override/client [:db/id :client/name]}]}]))
client-id client-id]
(html-response (account-typeahead* {:name name
:value account
:client-id client-id}))))
(html-response (account-typeahead* {:name name
:value value
:client-id client-id
:x-model "accountId"})))
(defn transaction-rule-edit-dialog [request]
(let [entity (or
(some-> request :last-form)
(some-> request :route-params :db/id (#(dc/pull (dc/db conn) default-read %))))]
(html-response (dialog* :entity entity
:form-params {:hx-put (str (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-edit-save))})
:headers {"hx-trigger-after-settle" "modalopen"})))
(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 transaction-rule-error [request]
(let [entity (some-> request :last-form)]
(html-response (dialog* :entity entity
:form-errors (:form-errors request)
:form-params (if (:db/id entity)
{:hx-put (str (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-edit-save))}
{:hx-post (str (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-edit-save))}))
:headers {"hx-retarget" "#edit-form fieldset"
"hx-reselect" "#edit-form fieldset"})))
(defn transaction-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 transaction-rule-new-dialog [_]
(html-response (dialog* :entity {}
:form-errors {}
:form-params {:hx-post (str (bidi/path-for ssr-routes/only-routes
:admin-transaction-rule-edit-save))})
:headers {"hx-trigger-after-settle" "modalopen"}))
(def transaction-rule-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])]]))
(def key->handler
(apply-middleware-to-all-handlers
@@ -712,12 +761,28 @@
[:value {:optional true}
[:maybe entity-id]]]))
:admin-transaction-rule-save (-> transaction-rule-save
(wrap-schema-decode :form-schema transaction-rule-schema)
(wrap-nested-form-params)
(wrap-form-4xx-2 transaction-rule-error))
:admin-transaction-rule-edit-dialog (-> transaction-rule-edit-dialog
(wrap-entity [:form-params :db/id] default-read)
(wrap-schema-decode :form-schema form-schema)
(wrap-nested-form-params)
(wrap-form-4xx-2 (-> transaction-dialog
(wrap-entity [:form-params :db/id] default-read))))
:admin-transaction-rule-test (-> transaction-rule-test
(wrap-entity [:form-params :db/id] default-read)
(wrap-schema-decode :form-schema form-schema)
(wrap-nested-form-params)
(wrap-form-4xx-2 (-> transaction-dialog
(wrap-entity [:form-params :db/id] default-read))))
:admin-transaction-rule-filled-account (-> transaction-dialog
(wrap-entity [:form-params :db/id] default-read)
(wrap-schema-decode :form-schema form-schema)
(wrap-nested-form-params)
(wrap-form-4xx-2 (-> transaction-dialog
(wrap-entity [:form-params :db/id] default-read))))
:admin-transaction-rule-edit-dialog (-> transaction-dialog
(wrap-entity [:route-params :db/id] default-read)
(wrap-schema-decode :route-schema [:map [:db/id entity-id]]))
:admin-transaction-rule-new-dialog transaction-rule-new-dialog})
:admin-transaction-rule-new-dialog transaction-dialog})
(fn [h]
(-> h
(wrap-admin)

View File

@@ -108,7 +108,7 @@
(defn bank-account-typeahead* [{:keys [client-id name value]}]
(if client-id
(com/typeahead-2 {:name name
(com/typeahead {:name name
:class "w-96"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :bank-account-search

View File

@@ -1,17 +1,32 @@
(ns auto-ap.ssr.company.company-1099
(:require
[auto-ap.datomic :refer [apply-pagination-raw conn remove-nils]]
[auto-ap.graphql.utils
:refer [assert-can-see-client extract-client-ids is-admin?]]
[auto-ap.datomic :refer [apply-pagination-raw conn]]
[auto-ap.graphql.utils :refer [assert-can-see-client]]
[auto-ap.routes.utils
:refer [wrap-client-redirect-unauthenticated wrap-secure]]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.grid-page-helper :as helper]
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils :refer [form-data->map html-response path->name]]
[auto-ap.ssr.utils
:refer [apply-middleware-to-all-handlers
entity-id
html-response
main-transformer
modal-response
ref->enum-schema
ref->select-options
strip
wrap-entity
wrap-form-4xx-2
wrap-schema-decode]]
[bidi.bidi :as bidi]
[cemerick.url :as url]
[clojure.string :as str]
[datomic.api :as dc]))
[datomic.api :as dc]
[hiccup.util :refer [url]]
[malli.core :as mc]))
(def vendor-read '[:db/id
:vendor/name
@@ -74,7 +89,7 @@
(def grid-page
(helper/build
{:id "vendor-table"
{:id "entity-table"
:nav (com/company-aside-nav)
:id-fn (comp :db/id second)
:fetch-page fetch-page
@@ -89,14 +104,10 @@
:entity-name "Vendors"
:route :company-1099-vendor-table
:row-buttons (fn [request e]
[(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes
[(com/icon-button {:hx-get (url (bidi/path-for ssr-routes/only-routes
:company-1099-vendor-dialog
:vendor-id (:db/id (second e)))
"?"
(url/map->query {:client-id (:db/id (first e))}))
:hx-ext "debug"
:hx-target "#modal-holder"
:hx-swap "outerHTML"}
{:client-id (:db/id (first e))})}
svg/pencil)])
:headers [{:key "Client"
:name "Client"
@@ -161,117 +172,213 @@
(def table* (partial helper/table* grid-page))
(def row* (partial helper/row* grid-page))
(defn vendor-save [{:keys [form-params identity route-params query-params] :as request}]
(let [client-id (Long/parseLong (get query-params "client-id"))
vendor-id (Long/parseLong (:vendor-id route-params))]
(assert-can-see-client identity client-id)
@(dc/transact conn [(remove-nils
(-> (form-data->map form-params)
(assoc :db/id (Long/parseLong (:vendor-id route-params)))
(update :vendor/legal-entity-1099-type #(some->> % not-empty (keyword "legal-entity-1099-type")))
(update :vendor/legal-entity-tin-type #(some->> % not-empty (keyword "legal-entity-tin-type")))))])
(html-response
(defn vendor-save [{:keys [form-params identity route-params query-params request-method] :as request
{:keys [vendor-id]} :route-params
{:keys [client-id]} :query-params}]
(row* identity [(dc/pull (dc/db conn) [:db/id :client/code] client-id)
(dc/pull (dc/db conn) vendor-read vendor-id)
(sum-for-client-vendor client-id vendor-id)
] {:flash? true})
:headers {"hx-trigger" "closeModal"})))
(defn vendor-dialog [request]
(let [vendor (dc/pull (dc/db conn) '[* {:vendor/legal-entity-1099-type [:db/ident]
:vendor/legal-entity-tin-type [:db/ident]}] (Long/parseLong (:vendor-id (:params request))))] ;; TODO perms
(html-response
(assert-can-see-client identity client-id)
@(dc/transact conn [[:upsert-entity (-> form-params
(assoc :db/id (:vendor-id route-params))
(update :vendor/address (fn [a]
(if (or (:address/street1 a)
(:address/street2 a)
(:address/city a)
(:address/state a)
(:address/zip a)
(:db/id a))
a
nil)) ))]])
(html-response
(row* identity [(dc/pull (dc/db conn) [:db/id :client/code] client-id)
(dc/pull (dc/db conn) vendor-read vendor-id)
(sum-for-client-vendor client-id vendor-id)
] {:flash? true})
:headers {"hx-trigger" "modalclose"
"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" vendor-id)}))
(def default-vendor-read '[* {[:vendor/legal-entity-1099-type :xform iol-ion.query/ident] [:db/ident]
[:vendor/legal-entity-tin-type :xform iol-ion.query/ident] [:db/ident]}])
(def form-schema (mc/schema [:map
[:vendor/address {:default {}}
[:maybe
[:map
[:db/id {:optional true} [:maybe entity-id]]
[:address/street1 {:optional true} [:maybe [:string {:decode/string strip}]]]
[:address/street2 {:optional true} [:maybe [:string {:decode/string strip}]]]
[:address/city {:optional true} [:maybe [:string {:decode/string strip}]]]
[:address/state {:optional true} [:maybe [:string {:decode/string strip}]]]
[:address/zip {:optional true} [:maybe [:re { :error/message "invalid zip"
:decode/string strip} #"^(\d{5}|)$"]]]]]]
[:vendor/legal-entity-name {:optional true} [:maybe [:string {:decode/string strip}]]]
[:vendor/legal-entity-first-name {:optional true} [:maybe [:string {:decode/string strip}]]]
[:vendor/legal-entity-middle-name {:optional true} [:maybe [:string {:decode/string strip}]]]
[:vendor/legal-entity-last-name {:optional true} [:maybe [:string {:decode/string strip}]]]
[:vendor/legal-entity-tin {:optional true} [:maybe [:string {:decode/string strip}]]]
[:vendor/legal-entity-tin-type {:optional true} [:maybe (ref->enum-schema "legal-entity-tin-type")]]
[:vendor/legal-entity-1099-type {:optional true} [:maybe (ref->enum-schema "legal-entity-1099-type")]]]))
(defn vendor-dialog [{{:keys [client-id]} :query-params {:keys [vendor-id]} :route-params :keys [entity form-params form-errors]}]
(fc/start-form (or (when (seq form-params)
form-params)
(when entity
(mc/decode form-schema entity main-transformer))
{})
form-errors
(modal-response
(com/modal
{}
[:form {:hx-post (str (bidi/path-for ssr-routes/only-routes
:company-1099-vendor-save
:request-method :post
:vendor-id (Long/parseLong (:vendor-id (:params request))))
"?"
(url/map->query {:client-id (:client-id (:params request))}))
:hx-target (format "#vendor-table tr[data-id=\"%d\"]" (:db/id vendor))
:hx-swap "outerHTML swap:300ms"}
[:fieldset {:class "hx-disable"}
[:form {:hx-post (url (bidi/path-for ssr-routes/only-routes
:company-1099-vendor-save
:request-method :post
:vendor-id vendor-id)
{:client-id client-id})
:class "w-full h-full max-w-2xl"
:hx-swap "outerHTML swap:300ms"}
[:fieldset {:class "hx-disable w-full h-full"}
(com/modal-card
{}
[:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name vendor)]]
[:div.space-y-6
[:div.grid.grid-cols-6.gap-4
[:h4.text-xl.border-b.col-span-6 "Address"]
[:div.col-span-6
(com/field {:label "Street 1"}
(com/text-input {:name (path->name [:vendor/address :address/street1])
:value (-> vendor :vendor/address :address/street1)
:placeholder "1700 Pennsylvania Ave"
:autofocus true}))]
[:div.col-span-6
(com/field {:label "Street 2"}
(com/text-input {:name (path->name [:vendor/address :address/street2])
:value (-> vendor :vendor/address :address/street2)
:placeholder "Suite 200"}))]
[:div.col-span-3
(com/field {:label "City"}
(com/text-input {:name (path->name [:vendor/address :address/city])
:value (-> vendor :vendor/address :address/city)
:placeholder "Cupertino"}))]
[:div.col-span-1
(com/field {:label "State"}
(com/text-input {:name (path->name [:vendor/address :address/state])
:value (-> vendor :vendor/address :address/state)
:placeholder "CA"}))]
[:div.col-span-2
(com/field {:label "Zip"}
(com/text-input {:name (path->name [:vendor/address :address/zip])
:value (-> vendor :vendor/address :address/zip)
:placeholder "98102"}))]
[:h4.text-xl.border-b.col-span-6 "Legal Entity"]
[:div.col-span-6
(com/field {:label "Legal Entity Name"}
(com/text-input {:name (path->name [:vendor/legal-entity-name])
:value (-> vendor :vendor/legal-entity-name)
:placeholder "Good Restaurant LLC"}))]
[:div.col-span-6.text-center " - OR -"]
[:div.col-span-2
(com/field {:label "First Name"}
(com/text-input {:name (path->name [:vendor/legal-entity-first-name])
:value (-> vendor :vendor/legal-entity-first-name)
:placeholder "John"}))]
[:div.col-span-2
(com/field {:label "Middle Name"}
(com/text-input {:name (path->name [:vendor/legal-entity-middle-name])
:value (-> vendor :vendor/legal-entity-middle-name)
:placeholder "C."}))]
[:div.col-span-2
(com/field {:label "Last Name"}
(com/text-input {:name (path->name [:vendor/legal-entity-last-name])
:value (-> vendor :vendor/legal-entity-last-name)
:placeholder "Riley"}))]
[:div.col-span-2
(com/field {:label "TIN"}
(com/text-input {:name (path->name [:vendor/legal-entity-tin])
:value (-> vendor :vendor/legal-entity-tin)
:placeholder "John"}))]
[:div.col-span-2
(com/field {:label "TIN Type"}
(com/select {:name (path->name [:vendor/legal-entity-tin-type])
:allow-blank? true
:value (some-> vendor :vendor/legal-entity-tin-type :db/ident name)
:options [["ein" "EIN"]
["ssn" "SSN"]]}))]
[:div.col-span-2
(com/field {:label "1099 Type"}
(com/select {:name (path->name [:vendor/legal-entity-1099-type])
:allow-blank? true
:value (some-> vendor :vendor/legal-entity-1099-type :db/ident name)
:options [["none" "None"]
["misc" "Misc"]
["landlord" "Landlord"]]}))]
[:div.col-span-6
(com/button {:color :primary}
"Save")]]]
[:div])]]))))
[:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name entity)]]
[:div.grid.grid-cols-6.gap-x-4.gap-y-2
(fc/with-field-default :vendor/address {}
(println "ADDRESS" fc/*current*)
(list [:h4.text-xl.border-b.col-span-6 "Address"]
[:div.col-span-6
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
:value (fc/field-value)}))
(fc/with-field :address/street1
(com/validated-field {:label "Street 1"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:class "w-full"
:value (fc/field-value)
:placeholder "1700 Pennsylvania Ave"
:autofocus true})))]
[:div.col-span-6
(fc/with-field :address/street2
(com/validated-field {:label "Street 2"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:class "w-full"
:value (fc/field-value)
:placeholder "Suite 200"})))]
[:div.col-span-3
(fc/with-field :address/city
(com/validated-field {:label "City"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:class "w-full"
:value (fc/field-value)
:placeholder "Cupertino"})))]
[:div.col-span-1
(fc/with-field :address/state
(com/validated-field {:label "State"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:class "w-full"
:value (fc/field-value)
:placeholder "CA"})))]
[:div.col-span-2
(fc/with-field :address/zip
(com/validated-field {:label "Zip"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:class "w-full"
:value (fc/field-value)
:placeholder "98102"})))]))
[:h4.text-xl.border-b.col-span-6 "Legal Entity"]
[:div.col-span-6
(fc/with-field :vendor/legal-entity-name
(com/validated-field {:label "Legal Entity Name"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:class "w-full"
:value (fc/field-value)
:placeholder "Good Restaurant LLC"})))]
[:div.col-span-6.text-center " - OR -"]
[:div.col-span-2
(fc/with-field :vendor/legal-entity-first-name
(com/validated-field {:label "First Name"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:value (fc/field-value)
:placeholder "John"})))]
[:div.col-span-2
(fc/with-field :vendor/legal-entity-middle-name
(com/validated-field {:label "Middle Name"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:value (fc/field-value)
:placeholder "C."})))]
[:div.col-span-2
(fc/with-field :vendor/legal-entity-last-name
(com/validated-field {:label "Last Name"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:value (fc/field-value)
:placeholder "Riley"})))]
[:div.col-span-2
(fc/with-field :vendor/legal-entity-tin
(com/validated-field {:label "TIN"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:value (fc/field-value)
:placeholder "John"})))]
[:div.col-span-2
(fc/with-field :vendor/legal-entity-tin-type
(com/validated-field {:label "TIN Type"
:errors (fc/field-errors)}
(com/select {:name (fc/field-name)
:allow-blank? true
:value (some-> (fc/field-value) name)
:options [["ein" "EIN"]
["ssn" "SSN"]]})))]
[:div.col-span-2
(fc/with-field :vendor/legal-entity-1099-type
(com/validated-field {:label "1099 Type"
:errors (fc/field-errors)}
(com/select {:name (fc/field-name)
:allow-blank? true
:value (some-> (fc/field-value) name)
:options (ref->select-options "legal-entity-1099-type")})))]]
[:div
(com/form-errors {:errors (:errors fc/*form-errors*)})
(com/validated-save-button {:errors form-errors} "Save vendor")])]]))))
(def vendor-table (helper/table-route grid-page))
(def page (helper/page-route grid-page))
(def key->handler
(apply-middleware-to-all-handlers
(->>
{
:company-1099 page
:company-1099-vendor-table vendor-table
:company-1099-vendor-dialog (-> vendor-dialog
(wrap-entity [:route-params :vendor-id] default-vendor-read)
(wrap-schema-decode :route-schema [:map [:vendor-id entity-id]]
:query-schema [:map [:client-id entity-id]]))
:company-1099-vendor-save (-> vendor-save
(wrap-entity [:form-params :db/id] default-vendor-read)
(wrap-schema-decode :form-schema form-schema
:route-schema [:map [:vendor-id entity-id]]
:query-schema [:map [:client-id entity-id]])
(wrap-nested-form-params)
(wrap-form-4xx-2 (-> vendor-dialog
(wrap-entity [:form-params :db/id] default-vendor-read)
(wrap-entity [:route-params :vendor-id] default-vendor-read)
(wrap-schema-decode :route-schema [:map [:vendor-id entity-id]]
:query-schema [:map [:client-id entity-id]]))))})
(fn [h]
(-> h
(wrap-secure)
(wrap-client-redirect-unauthenticated)))))

View File

@@ -208,7 +208,6 @@ fastlink.open({fastLinkURL: '%s',
(def page (helper/page-route grid-page))
(def table (helper/table-route grid-page))
;; TODO delete-after-settle
(defn refresh-provider-account [{:keys [form-params identity]}]
(let [provider-account (dc/pull (dc/db conn) default-read (some-> (get form-params "id") not-empty Long/parseLong))]
(yodlee/refresh-provider-account (:client/code (:yodlee-provider-account/client provider-account))

View File

@@ -24,6 +24,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 text-input inputs/text-input-)
(def money-input inputs/money-input-)
@@ -32,11 +33,11 @@
(def hidden inputs/hidden-)
(def select inputs/select-)
(def typeahead inputs/typeahead-)
(def typeahead-2 inputs/typeahead-2-)
(def field-errors inputs/field-errors-)
(def field inputs/field-)
(def validated-field inputs/validated-field-)
(def errors inputs/errors-)
(def form-errors inputs/form-errors-)
(def left-aside aside/left-aside-)
(def company-aside-nav aside/company-aside-nav-)
@@ -59,6 +60,7 @@
(def data-grid-row data-grid/row-)
(def data-grid-cell data-grid/cell-)
(def data-grid-right-stack-cell data-grid/right-stack-cell-)
(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 ")]

View File

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

View File

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

View File

@@ -3,8 +3,10 @@
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components.card :refer [content-card-]]
[auto-ap.ssr.components.paginator :refer [paginator-]]
[auto-ap.ssr.components.buttons :refer [a-button-]]
[bidi.bidi :as bidi]
[hiccup2.core :as hiccup]))
[hiccup2.core :as hiccup]
[auto-ap.ssr.hx :as hx]))
(defn header- [params & rest]
(into [:th.px-4.py-3 {:scope "col" :class (:class params)
@@ -103,3 +105,23 @@
[:div {:class "htmx-indicator absolute -translate-x-1/2 -translate-y-1/2 top-2/4 left-1/2 overflow-hidden w-full h-full"}
[:div {:class "flex items-center justify-center w-full h-full border border-gray-200 rounded-lg bg-gray-50 dark:bg-gray-800 dark:border-gray-700 bg-opacity-50" }
[:div {:class "px-3 py-1 text-xs font-medium leading-none text-center text-blue-800 bg-blue-200 rounded-full animate-pulse dark:bg-blue-900 dark:text-blue-200"} "loading..."]]])])
(defn new-row- [{:keys [index colspan tr-params] :as params} & content]
(row-
(merge {:class "new-row"
:x-data (hx/json {:newRowIndex index})
}
tr-params)
(cell- {:colspan colspan
:class "bg-gray-100"}
[:div.flex.justify-center
(a-button- (merge
(dissoc params :index :colspan)
{
"@click" "$dispatch('newRow', {index: newRowIndex++})"
:color :secondary
:hx-trigger "newRow"
:hx-vals (hiccup/raw "js:{index: event.detail.index}")
:hx-target "closest .new-row"
:hx-swap "beforebegin"})
content)])))

View File

@@ -1,21 +1,43 @@
(ns auto-ap.ssr.components.dialog
(:require [hiccup2.core :as hiccup]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.hiccup-helper :as hh]))
(:require
[auto-ap.ssr.hiccup-helper :as hh]
[auto-ap.ssr.hx :as hx]))
(defn modal- [params & children]
[:div {:class (-> (:class params)
(or "max-w-4xl w-1/4 overflow-visible")
(hh/add-class "h-min"))
"@click.outside" "open=false"
} children])
[:div (-> params
(assoc "@click.outside" "open=false"
:x-data (hx/json {:index 0 :hidingIndex -1})
:x-ref "modalStack"
"@modalnext"
"$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 }, 150)"
"@modalprevious"
"$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; }, 150)"
"@modalpop"
"$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 --, 150)
setTimeout(() => { $refs.modalStack.removeChild($refs.modalStack.children[index+1]); hidingIndex=-1; }, 300)
"
)
(update :class (fnil hh/add-class "") "w-full h-full modal-stack"))
children])
(defn modal-card- [params header content footer]
[:div#modal-card (update params
:class (fn [c] (-> c
(or "w-full")
)))
[:div {:class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content w-full flex flex-col max-h-[80vh]"}
[:div (update params
:class (fn [c] (-> c
(or "")
(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.bg-green-300.w-full.h-64
@@ -23,4 +45,19 @@
content]
(when footer [:div {:class "p-4 shrink-0"} footer])]])
;; fade-in-settle slide-up-settle duration-300 transition-all
(defn stacked-modal-card- [index params header content footer]
[:div (merge params
{:class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col"
:x-data (hx/json {:i index})
:x-show "index == i && hidingIndex != i"
"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]
[:div {:class "px-6 space-y-6 overflow-y-scroll w-full shrink"}
content]
(when footer [:div {:class "p-4 shrink-0"} footer])])

View File

@@ -32,66 +32,33 @@
(:allow-blank? params) (conj [:option {:value "" :selected (not (:value params))} ""]))]
children))
(defn typeahead- [params]
[:select (-> params
(dissoc :url)
(dissoc :value)
(dissoc :value-fn)
(dissoc :content-fn))
(for [value (if (:multiple params)
(:value params)
[(:value params)])
:when ((:value-fn params first) value)]
[:option {:value ((:value-fn params first) value) :selected true} ((:content-fn params second) value)])
[:script {:lang "javascript"}
(hiccup/raw (format "
(function () {
var element = document.getElementById('%s');
var c = new Choices(element, {removeItems: true, removeItemButton:true, searchFloor: 3, searchPlaceholderValue: '%s'});
let baseUrl = '%s';
element.addEventListener('search', function (e) {
let fullUrl = baseUrl + (baseUrl.includes(\"?\") ? \"&\" : \"?\") + \"q=\" + e.detail.value;
let data = fetch(fullUrl)
.then(res => res.json())
.then(data => {
c.setChoices(data, 'value', 'label', true)
});
});
element.addEventListener('choice', function (e) {
c.clearChoices();
})
})();
"
(:id params)
(:placeholder params)
(:url params)
))]])
(defn typeahead-2- [params]
[:div {:x-data (hx/json {:open false
:baseUrl (if (str/includes? (:url params) "?")
(str (:url params) "&q=")
(str (:url params) "?q="))
:value {:value ((:value-fn params first) (:value params)) :label ((:content-fn params second) (:value params))}
:value {:value ((:value-fn params identity) (:value params)) :label ((:content-fn params identity) (:value params))}
:search ""
:active -1
:elements (if ((:value-fn params first) (:value params))
[{:value ((:value-fn params first) (:value params)) :label ((:content-fn params second) (:value params))}]
[])})
:elements (if ((:value-fn params identity) (:value params))
[{:value ((:value-fn params identity) (:value params)) :label ((:content-fn params identity) (:value params))}]
[])
:popper nil})
:x-modelable "value.value"
:x-model (:x-model params)
:class "relative"}
:x-init "popper = Popper.createPopper($refs.input, $refs.dropdown, {placement: 'bottom-start', strategy: 'fixed', modifiers: {name: 'offset', options: {offset: [0, 10]}}})"
}
[:a {:class (-> (hh/add-class (or (:class params) "") default-input-classes)
(hh/add-class "cursor-pointer"))
"@click.prevent" "open = !open;"
"@keydown.enter.prevent.stop" "open = !open;"
"@keydown.down.prevent.stop" "open = true;"
"@keydown.backspace" "value = {value: '', label: '' }"
"@click.prevent" "open = !open; popper.update()"
"@keydown.enter.prevent.stop" "open = !open; popper.update()"
"@keydown.down.prevent.stop" "open = true; popper.update()"
"@keydown.backspace" "value = {value: '', label: '' }"
:tabindex 0
:x-init (:x-init params)}
:x-init (:x-init params)
:x-ref "input"
}
[:input (-> params
(dissoc :class)
(dissoc :value-fn)
@@ -109,25 +76,31 @@ c.clearChoices();
[:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"}
svg/drop-down]]]
[:ul.dropdown-contents {:class "absolute bg-gray-50 dark:bg-gray-600 rounded-lg shadow-lg py-1 w-max z-10 mt-1"
[:ul.dropdown-contents {:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 ring-1"
"x-ref" "dropdown"
"@keydown.escape" "open = false; value = {value: '', label: '' }"
"x-transition:enter" "ease-[cubic-bezier(.3,2.3,.6,1)] duration-200"
"x-transition:enter-start" "!opacity-0 !mt-0"
"x-transition:enter-end" "!opacity-1 !mt-1"
"x-transition:enter-start" "!opacity-0"
"x-transition:enter-end" "!opacity-1"
"x-transition:leave" "ease-out duration-200"
"x-transition:leave-start" "!opacity-1 !mt-1"
"x-transition:leave-end" "!opacity-0 !mt-0"
"x-transition:leave-start" "!opacity-1"
"x-transition:leave-end" "!opacity-0"
"x-show " "open"
"x-trap" "open"
"@click.outside" "open=false; console.log('is this ihid')"}
[:input {:type "text" :class (hh/add-class (or (:class params) "") default-input-classes)
"@click.outside" "open=false;"}
[:input {:type "text"
:class (-> (:class params)
(or "")
(hh/add-class default-input-classes)
(hh/replace-wildcard ["rounded" "border"] "border-bottom bg-gray-100 rounded-t-lg w-full"))
"x-model" "search"
"placeholder" (:placeholder params)
"@keydown.down.prevent" "active ++; active = active >= elements.length - 1 ? elements.length - 1 : active"
"@keydown.up.prevent" "active --; active = active < 0 ? 0 : active"
"@keydown.enter.prevent" "open = false; value = elements.length > 0 ? $data.elements[active] : {'value': '', label: ''}; console.log('are we here')"
"@keydown.enter.prevent" "open = false; value = elements.length > 0 ? $data.elements[active] : {'value': '', label: ''};"
"x-init" "$watch('search', s => { if($el.value.length > 2) {fetch(baseUrl + s).then(data=>data.json()).then(data => {elements = data; active=-1}) }})"}]
[:div.dropdown-options
[:div.dropdown-options {:class "rounded-b-lg overflow-hidden"}
[:template {:x-for "(element, index) in elements"}
[:li [:a {:class "px-4 py-2 flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-300 [&.active]:dark:bg-primary-700 text-gray-800 dark:text-gray-100"
:href "#"
@@ -138,7 +111,8 @@ c.clearChoices();
"x-html" "element.label"}]]]
[:template {:x-if "elements.length == 0"}
[:li {:class "px-4 py-2 flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-500 text-gray-800 dark:text-gray-100 text-xs "}
"No results found"]]]]])
"No results found"]]]
]])
(defn use-size [size]
@@ -152,6 +126,7 @@ c.clearChoices();
[:input
(-> params
(dissoc :error?)
(assoc :type "text")
(update
:class (fnil hh/add-class "") default-input-classes)
(update :class #(str % (use-size size))))])
@@ -210,6 +185,11 @@ c.clearChoices();
(when (sequential? errors)
(str/join ", " (filter string? errors)))])
(defn form-errors- [{:keys [errors]}]
[:div#form-errors (when errors
[:span.error-content
(errors- {:errors errors})])])
(defn validated-field- [params & rest]
(field- (cond-> params
true (dissoc :errors)

View File

@@ -13,7 +13,8 @@
[:div {:class "px-3 py-3 lg:px-5 lg:pl-3"}
[:div {:class "flex items-center justify-between"}
[:div {:class "flex items-center justify-start"}
[:button {:aria-controls "left-nav", :id "left-nav-toggle" :type "button", :class "inline-flex items-center p-2 mt-2 ml-2 mr-2 text-sm text-gray-500 rounded-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"}
[:button {:aria-controls "left-nav", :id "left-nav-toggle" :type "button", :class "inline-flex items-center p-2 mt-2 ml-2 mr-2 text-sm text-gray-500 rounded-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
"@click" "leftNavShow = !leftNavShow"}
[:span {:class "sr-only"} "Open sidebar"]
[:svg {:class "w-6 h-6", :aria-hidden "true", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:clip-rule "evenodd", :fill-rule "evenodd", :d "M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z"}]]]
@@ -24,10 +25,7 @@
(when (is-admin? identity)
[:button.mt-1.lg:w-96.relative.hidden.lg:block {:class "bg-gray-50 hover:bg-gray-200 dark:hover:bg-gray-700 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 w-full pl-10 py-4 pr-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 gap-4 "
:hx-get (bidi/path-for ssr-routes/only-routes
:search)
:hx-target "#modal-holder"
:hx-swap "outerHTML"}
:hx-get (bidi/path-for ssr-routes/only-routes :search)}
[:div {:class "absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-gray-500"}
[:div.w-4.h-4 svg/search]
[:span.ml-2 "Search"]]])

View File

@@ -3,20 +3,26 @@
[auto-ap.ssr.components.aside :refer [left-aside-]]
[auto-ap.ssr.components.navbar :refer [navbar-]]
[hiccup2.core :as hiccup]
[auto-ap.ssr.svg :as svg]))
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.hx :as hx]))
(defn page- [{:keys [nav page-specific client client-selection identity app-params] :or {app-params {}}} & children]
[:div#app {"_" (hiccup/raw "
on notification put event.detail.value into #notification-details then add .htmx-added to #notification-holder then remove .hidden from #notification-holder then wait 30ms then remove .htmx-added from #notification-holder
on htmx:responseError put event.detail.xhr.response into #error-details then add .htmx-added to #error-holder then remove .hidden from #error-holder then wait 30ms then remove .htmx-added from #error-holder"
)}
)
:x-data (hx/json {:leftNavShow true})}
(navbar- {:client-selection client-selection
:client client
:identity identity})
[:div#app-contents.flex.pt-16.overflow-hidden (assoc app-params :hx-disinherit "*")
(left-aside- {:nav nav
:client client
:identity identity})
[:div#app-contents.flex.pt-16.overflow-hidden (assoc app-params
:hx-disinherit "*"
:x-init "leftNavShow = true")
(left-aside- {:nav nav
:page-specific page-specific})
[:div#main-content {:class "relative w-full h-full lg:pl-64 overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content "
[:div#main-content {:class "relative w-full h-full overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content lg:pl-64"
":class" "leftNavShow ? 'lg:pl-64' : ''"
:x-effect "leftNavShow ? $el.classList.add('lg:pl-64') : $el.classList.remove('lg:pl-64')"
}
[:div#notification-holder.hidden
[:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg
@@ -49,4 +55,5 @@
(into
[:div.p-4]
children)]]
])

View File

@@ -46,10 +46,6 @@
:bank-account-typeahead (wrap-client-redirect-unauthenticated (wrap-secure company/bank-account-typeahead))
:company (wrap-client-redirect-unauthenticated (wrap-secure company/page))
:company-1099 (wrap-client-redirect-unauthenticated (wrap-secure company-1099/page))
:company-1099-vendor-table (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-table))
:company-1099-vendor-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-dialog))
:company-1099-vendor-save (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-save))
:company-plaid (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/page))
:company-plaid-table (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/table))
:company-plaid-link (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/link))
@@ -76,6 +72,7 @@
:transaction-insight-explain (wrap-client-redirect-unauthenticated (wrap-admin insights/explain))
:admin-ezcater-xls (wrap-client-redirect-unauthenticated (wrap-admin ezcater-xls/page))
:search (wrap-client-redirect-unauthenticated (wrap-secure search/dialog-contents))}
(into company-1099/key->handler)
(into pos-sales/key->handler)
(into pos-expected-deposits/key->handler)
(into pos-tenders/key->handler)

View File

@@ -2,17 +2,26 @@
(:require [auto-ap.ssr.utils :refer [path->name2]]
[auto-ap.cursor :as cursor]))
(def ^:dynamic *prefix* [])
(def ^:dynamic *form-data*)
(def ^:dynamic *form-errors*)
(def ^:dynamic *prev-cursor* nil)
(def ^:dynamic *current* nil)
(defmacro start-form [form-data errors & rest]
`(binding [*form-data* ~form-data
*form-errors* (or ~errors {})]
(binding [*current* (cursor/cursor *form-data*)]
(binding [*current* (if (cursor/cursor? *form-data*)
*form-data*
(cursor/cursor *form-data*))]
~@rest)))
(defmacro start-form-with-prefix [prefix form-data errors & rest]
`(binding [*prefix* ~prefix]
(start-form ~form-data ~errors ~@rest)))
(defmacro with-cursor [cursor & rest]
`(binding [*current* ~cursor]
~@rest))
@@ -21,10 +30,15 @@
`(with-cursor (get *current* ~field )
~@rest))
(defmacro with-field-default [field default & rest]
`(with-cursor (get *current* ~field ~default)
~@rest))
(defn field-name
([] (field-name *current*))
([cursor]
(apply path->name2 (cursor/path cursor))))
(apply path->name2 (into (or *prefix* []) (cursor/path cursor)))))
(defn field-value
([] (field-value *current*))
@@ -45,3 +59,13 @@
(every? string? errors)))))
(defn cursor-map
([f] (cursor-map *current* f))
([cursor f]
(when (field-value)
(doall
(for [n cursor]
(with-cursor n
(f n)))))))

View File

@@ -32,9 +32,21 @@
"x-transition:leave-end" "opacity-0"))
(defn alpine-mount-then-appear [{:keys [data-key] :as params}]
(merge (-> {"x-data" (json {data-key false})
"x-init" (format "$nextTick(() => %s=true)" (name data-key))
"x-show" (name data-key)}
(merge (-> {:x-data (json {data-key false})
:x-init (format "$nextTick(() => %s=true)" (name data-key))
:x-show (name data-key)}
alpine-appear
alpine-disappear)
(dissoc params :data-key)))
(defn bind-alpine-vals [m field->alpine-field]
(assoc m "x-bind:hx-vals"
(format "JSON.stringify({%s})"
(str/join ", "
(map
(fn [[field alpine-field]]
(format "\"%s\": $data.%s" field alpine-field))
field->alpine-field)))))

View File

@@ -72,7 +72,9 @@
(nested-params-request request {}))
([request options]
(let [parse (:key-parser options parse-nested-keys)]
(update-in request [:form-params] nest-params parse))))
(-> request
(assoc :raw-form-params (:form-params request))
(update-in [:form-params] nest-params parse)))))
(defn wrap-nested-form-params
"Middleware to converts a flat map of parameters into a nested map.

View File

@@ -3,9 +3,6 @@
[auto-ap.time :as atime]
[auto-ap.ssr.svg :as svg]))
;; TODO make date-input take clj date
;; TODO make total fields take decimals
(defn date-range-field* [request]
[:div#date-range {}
(com/field {:label "Date Range"}

View File

@@ -8,25 +8,15 @@
merge-query
pull-many
query2]]
[auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.routes.utils
:refer [wrap-client-redirect-unauthenticated wrap-secure]]
[auto-ap.ssr.pos.common :refer [date-range-field* processor-field* total-field*]]
[auto-ap.query-params :as query-params]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.grid-page-helper :as helper]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.pos.common :refer [date-range-field* total-field*]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[clj-time.coerce :as c]
[datomic.api :as dc]
[clojure.set :as set]
[auto-ap.query-params :as query-params]
[malli.core :as m]))
;; TODO refunds
;; always should be fast
;; make params parsing composable
[datomic.api :as dc]))
(defn filters [request]
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"

View File

@@ -2,7 +2,7 @@
(:require
[auto-ap.graphql.utils :refer [can-see-client?]]
[auto-ap.solr :as solr]
[auto-ap.ssr.utils :refer [html-response]]
[auto-ap.ssr.utils :refer [html-response modal-response]]
[auto-ap.time :as atime]
[clojure.string :as str]
[com.brunobonacci.mulog :as mu]
@@ -130,11 +130,11 @@
:form (:form-params request))
(if-let [q (get (:form-params request) "q")]
(html-response (search-results* q (:identity request)))
(html-response
(modal-response
(com/modal {}
(com/modal-card {}
(com/modal-card {:class "w-full h-full"}
[:div.p-2 "Search"]
[:div#search.overflow-auto.space-y-6.p-2.h-96
[:div#search.overflow-auto.space-y-6.p-2.w-full
(com/text-input {:id "search-input"
:type "search"

View File

@@ -1,21 +1,21 @@
(ns auto-ap.ssr.transaction.insights
(:require
[auto-ap.client-routes :as client-routes]
[auto-ap.datomic :refer [conn pull-attr visible-clients]]
[auto-ap.datomic :refer [conn visible-clients]]
[auto-ap.rule-matching :refer [spread-cents]]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [html-response]]
[auto-ap.ssr.utils :refer [html-response modal-response]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[cemerick.url :as url]
[clj-http.client :as http]
[clj-time.coerce :as coerce]
[datomic.api :as dc]
[iol-ion.tx :refer [random-tempid]]
[hiccup2.core :as hiccup]))
[hiccup2.core :as hiccup]
[iol-ion.tx :refer [random-tempid]]))
(def pull-expr [:transaction/description-original
:db/id
@@ -55,7 +55,7 @@
[(>= ?d ?starting)]]
:args [(dc/db conn)
(iol-ion.query/recent-date 120)
(iol-ion.query/recent-date 300)
(map :db/id clients)
pull-expr]})
@@ -234,43 +234,49 @@
:hide-actions? true
"_" (hiccup/raw "init transition opacity to 0 over 500ms then remove me")))))
(defn explain [{:keys [identity session] {:keys [transaction-id]} :route-params}]
(let [r (dc/pull (dc/db conn)
(let [r (dc/pull (dc/db conn)
pull-expr
(Long/parseLong transaction-id))
similar (pinecone-similarity-list transaction-id)]
(html-response
(com/modal {}
(com/modal-card {:style {:width "900px"}}
[:div.flex [:div.p-2 "Similar Transactions"]]
[:table.w-full
[:thead
[:tr
[:td "Date"]
[:td "Description"]
[:td "Amount"]
[:td "Vendor"]
[:td "Account"]
[:td "Score"]]]
[:tbody
[:tr
[:th.text-left (some-> r :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date))]
[:th.text-left (-> r :transaction/description-original)]
[:th.text-left (if (> (-> r :transaction/amount) 0.0)
[:div.tag.is-success.is-light (str "$" (Math/round (:transaction/amount r)))]
[:div.tag.is-danger.is-light (str "$" (Math/round (:transaction/amount r)))])]
[:th]
[:th]
[:th.text-left]]
(take 10
(for [{:keys [amount date description vendor-name numeric-code score]} similar]
[:tr
[:td (subs date 0 10)]
[:td description]
[:td (some->> amount double (format "$%.2f"))]
[:td vendor-name]
[:td numeric-code]
[:td (format "%.1f%%" (* 100 (double score)))]]))]]
[:div])))))
similar (pinecone-similarity-list transaction-id)]
(modal-response
(com/modal {}
(com/modal-card {:style {:width "900px"}}
[:div.flex [:div.p-2 "Similar Transactions"]]
(com/data-grid {:headers [(com/data-grid-header {:name "Date"
:key "date"})
(com/data-grid-header {:name "Description"
:key "description"})
(com/data-grid-header {:name "Amount"
:key "amount"})
(com/data-grid-header {:name "Vendor"
:key "vendor"})
(com/data-grid-header {:name "Account"
:key "account"})
(com/data-grid-header {:name "Score"
:key "score"})]}
(com/data-grid-row {:class "bg-primary-200"}
(com/data-grid-cell {:class "text-left font-bold"} (some-> r :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date)))
(com/data-grid-cell {:class "text-left font-bold"} (-> r :transaction/description-original) )
(com/data-grid-cell {:class "font-bold"} (if (> (-> r :transaction/amount) 0.0)
[:div.tag.is-success.is-light (str "$" (Math/round (:transaction/amount r)))]
[:div.tag.is-danger.is-light (str "$" (Math/round (:transaction/amount r)))]))
(com/data-grid-cell {})
(com/data-grid-cell {})
(com/data-grid-cell {}))
(com/data-grid-row {}
(take 10
(for [{:keys [amount date description vendor-name numeric-code score]} similar]
(com/data-grid-row
{}
(com/data-grid-cell {:class "text-left"} (subs date 0 10))
(com/data-grid-cell {:class "text-left"} description )
(com/data-grid-cell {} (some->> amount double (format "$%.2f")))
(com/data-grid-cell {} vendor-name)
(com/data-grid-cell {} numeric-code)
(com/data-grid-cell {} (format "%.1f%%" (* 100 (double score)))))))))
[:div])))))
(defn transaction-rows* [{:keys [clients identity after]}]
(let [recommendations (transaction-recommendations identity clients :after after)]

View File

@@ -1,7 +1,9 @@
(ns auto-ap.ssr.ui
(:require
[auto-ap.ssr.hx :as hx]
[config.core :refer [env]]
[hiccup2.core :as hiccup]
[auto-ap.ssr.hx :as hx]))
[auto-ap.ssr.components :as com]))
(defn html-page [hiccup]
{:status 200
@@ -12,6 +14,9 @@
{}
hiccup))})
(defn base-page [request contents page-name]
(html-page
[:html.has-navbar-fixed-top
@@ -27,8 +32,14 @@
[:script {:src "https://unpkg.com/hyperscript.org@0.9.7/dist/_hyperscript.min.js"}]
[:script {:src "https://unpkg.com/@popperjs/core@2.11.8/dist/umd/popper.min.js"}]
[:script {:src "https://cdn.plaid.com/link/v2/stable/link-initialize.js"}]
[:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.min.js"
:crossorigin= "anonymous"}]
(if (= "dev" (:dd-env env))
[:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.js"
:crossorigin= "anonymous"}]
[:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.min.js"
:crossorigin= "anonymous"}])
[:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/ext/class-tools.js" :crossorigin= "anonymous"}]
[:script {:src "https://unpkg.com/htmx.org/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"}]]
@@ -56,9 +67,12 @@ input[type=number] {
-moz-appearance:textfield; /* Firefox */
} "]
[:body {:hx-ext "disable-submit"}
[:body {:hx-ext "disable-submit, class-tools"}
contents
[:script {:src "/js/flowbite.min.js"}]
[:div#modal-holder
{:tabindex "-1", :class "fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen"
"x-show" "open"
@@ -67,7 +81,7 @@ input[type=number] {
"@modalopen.document" "open=true"
"@modalclose.document" "open=false"}
[:div {:class "bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40"
[:div {:class "bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40 md:p-12"
"x-show" "open"
":aria-hidden" "!open"
"x-transition:enter" "duration-300"
@@ -77,11 +91,8 @@ input[type=number] {
"x-transition:leave-start" "!bg-opacity-50"
"x-transition:leave-end" "!bg-opacity-0"}
;; TODO to get this right i think what needs to happen is to just set this up as having a single
;; div that is forced to the maximum allowed size. inside that will be a div that just centers
;; the elements, allowing it to grow as necessar. Then make the modal on the inside of this
;; div just use flexbox to make the inside part be the part that scrolls
[:div#modal-content {:class (str "inset-0 max-h-[80vh] sm:m-12 flex justify-center items-center shrink h-full")
[: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"
@@ -90,4 +101,11 @@ input[type=number] {
"x-transition:enter-end" "!bg-opacity-100 !translate-y-0"
"x-transition:leave" "duration-300"
"x-transition:leave-start" "!opacity-100 !translate-y-0"
"x-transition:leave-end" "!opacity-0 !translate-y-32"}]]]]]))
"x-transition:leave-end" "!opacity-0 !translate-y-32"}
[:div.flex.items-center.justify-center.max-w-6xl {:class "min-w-[700px] max-h-full "}
[:div#modal-content.flex.flex-col.self-stretch {:class "min-w-[700px] md:p-12"} ;;.overflow-scroll
]
]]]]]]))

View File

@@ -6,6 +6,7 @@
apply-sort-3
conn
merge-query
pull-attr
pull-many
query2]]
[auto-ap.query-params :as query-params]
@@ -23,9 +24,12 @@
:refer [apply-middleware-to-all-handlers
entity-id
html-response
main-transformer
many-entity
modal-response
ref->enum-schema
ref->select-options
wrap-entity
wrap-form-4xx-2
wrap-schema-decode]]
[auto-ap.time :as atime]
@@ -34,9 +38,7 @@
[clojure.string :as str]
[config.core :refer [env]]
[datomic.api :as dc]
[hiccup2.core :as hiccup]
[malli.core :as mc]
[clj-time.format :as f]))
[malli.core :as mc]))
(defn filters [request]
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
@@ -78,13 +80,14 @@
{:value "none"
:content "None"}]}))
(com/field {:label "Client"}
(com/typeahead-2 {:name "client"
(com/typeahead {:name "client"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes
:company-search)
:id (str "client-search")
:value [(:db/id (:client (:parsed-query-params request)))
(:client/name (:client (:parsed-query-params request)))]}))]])
:value (:client (:parsed-query-params request))
:value-fn :db/id
:content-fn :client/name}))]])
(def default-read '[:db/id
:user/name
@@ -212,9 +215,7 @@
:hx-vals (format "{\"db/id\": \"%s\"}" (:db/id entity))} "Impersonate")
(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes
:user-edit-dialog
:db/id (:db/id entity)))
:hx-target "#modal-content"
:hx-swap "innerHTML"}
:db/id (:db/id entity)))}
svg/pencil)])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:admin)}
@@ -257,14 +258,15 @@
(def table* (partial helper/table* grid-page))
(defn impersonate [request]
(let [user (some-> request :params :db/id (#(dc/pull (dc/db conn) default-read %))) ]
(if (:entity request)
{:status 200
:headers {"hx-redirect" (str "/?jwt=" (jwt/sign (auth/user->jwt user "FAKE_TOKEN")
(:jwt-secret env)
{:alg :hs512}))
}
:session {:identity (dissoc (auth/user->jwt user "FAKE_TOKEN")
:exp)}}))
:headers {"hx-redirect" (str "/?jwt=" (jwt/sign (auth/user->jwt (:entity request) "FAKE_TOKEN")
(:jwt-secret env)
{:alg :hs512}))}
:session {:identity (dissoc (auth/user->jwt (:entity request) "FAKE_TOKEN")
:exp)}}
{:status 404}))
(defn client-row* [client]
(com/data-grid-row (-> {:x-ref "p"
:data-key "show"
@@ -272,36 +274,33 @@
hx/alpine-mount-then-appear)
(com/data-grid-cell {}
(com/validated-field {:errors (fc/field-errors (:db/id fc/*current*))}
(com/typeahead-2 {:name (fc/field-name (:db/id fc/*current*))
(com/typeahead {:name (fc/field-name (:db/id fc/*current*))
:class "w-full"
:url (bidi/path-for ssr-routes/only-routes
:company-search)
:value (fc/field-value)
:value-fn :db/id
:value-fn :db/id ;; TODO better hydration
:content-fn (fn [value]
(:client/name (dc/pull (dc/db conn) [:client/name]
(or (:db/id value)
value))))
:content-fn #(pull-attr (dc/db conn) :client/name (:db/id %))
:size :small})))
(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]}]
(defn dialog* [{:keys [form-params form-errors entity]}]
(fc/start-form
entity form-errors
form-params form-errors
(com/modal
{}
[:form#edit-form (merge {:hx-ext "response-targets"
:hx-put (str (bidi/path-for ssr-routes/only-routes
:user-edit-save
:request-method :put))
:hx-swap "outerHTML swap:300ms"
:hx-target-400 "#form-errors .error-content"
:class "w-full"}
form-params)
[:fieldset {:class "hx-disable"}
{:hx-target "this"}
[:form {:hx-ext "response-targets"
:hx-put (str (bidi/path-for ssr-routes/only-routes
:user-edit-save
:request-method :put))
:hx-swap "outerHTML swap:300ms"
:hx-target-400 "#form-errors .error-content"
:class "w-full h-full"}
[:fieldset {:class "hx-disable h-full"}
(com/modal-card
{}
[:div.flex [:div.p-2 "User"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:user/name entity)]]
@@ -318,36 +317,21 @@
:value (some->> (fc/field-value) name)
:options (ref->select-options "user-role")})))
(fc/with-field :user/clients
(com/validated-field {:label "Clients"}
(com/validated-field {:label "Clients"}
(com/data-grid {:headers [(com/data-grid-header {} "Client")
(com/data-grid-header {} )]
:id "client-table"}
(doall (for [client fc/*current*]
(fc/with-cursor client
(client-row* client))))
(com/data-grid-row
{:class "new-row"}
(com/data-grid-cell {:colspan 2
:class "bg-gray-100"}
[:div.flex.justify-center
(com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes
:user-client-new)
:color :secondary
:hx-include "#edit-form"
:hx-vals (hiccup/raw "js:{index: countRows(\"#client-table\")}")
:hx-target "#edit-form .new-row"
:hx-swap "beforebegin"}
"New override")])))
))
[:div#form-errors [:span.error-content]]]
(fc/cursor-map #(client-row* %))
(com/data-grid-new-row {:colspan 2
:index (count (fc/field-value))
:hx-get (bidi/path-for ssr-routes/only-routes
:user-client-new)}
"Assign new client"))))]
[:div
[:div [:div#form-errors (when (:errors fc/*form-errors*)
[:span.error-content
(com/errors {:errors (:errors fc/*form-errors*)})])]]
(com/form-errors {:errors (:errors fc/*form-errors*)})
(com/validated-save-button {:errors (seq form-errors)}
"Save user")])]])))
;; TODO rename edit-form or make it generic
(defn user-edit-save [{:keys [form-params identity] :as request}]
(let [_ @(dc/transact conn [[:upsert-entity form-params]])
user (some-> form-params :db/id (#(dc/pull (dc/db conn) default-read %)))]
@@ -357,62 +341,54 @@
:headers {"hx-trigger" "modalclose"
"hx-retarget" (format "#user-table tr[data-id=\"%d\"]" (:db/id user))})))
(defn user-save-error [request]
;; TODO hydration
;; TODO consistency of error handling and passing, on all form examples
(let [entity (some-> request :last-form)]
(html-response (dialog* {:entity entity
:form-errors (:form-errors request)})
:headers {"hx-retarget" "#edit-form fieldset"
"hx-reselect" "#edit-form fieldset"})))
(defn user-edit-dialog [request]
(let [user (some-> request
:route-params
:db/id
(#(dc/pull (dc/db conn) default-read %)))]
(html-response
(dialog* {:entity user
:form-errors {}})
:headers {"hx-trigger" "modalopen"})))
(def form-schema
(mc/schema
[:map
[:db/id entity-id]
[:user/clients {:optional true}
[:maybe
(many-entity {} [:db/id entity-id])]]
[:user/role (ref->enum-schema "user-role")]]))
(defn user-dialog [{:keys [form-params entity form-errors]}]
(modal-response
(dialog* {:form-params (or (when (seq form-params)
form-params)
(when entity
(mc/decode form-schema entity main-transformer))
{})
:entity entity
:form-errors form-errors})))
(defn new-client [{ {:keys [index]} :query-params}]
(let [index (or index 0)
account {:user/clients (conj (into [] (repeat index {}))
{:db/id nil
:new? true})}] ;; TODO schema decode is not working
(html-response
(fc/start-form account []
(fc/with-cursor (get-in fc/*current* [:user/clients index])
(client-row* fc/*current*))))))
(html-response
(fc/start-form-with-prefix [:user/clients (or index 0)] {:db/id nil
:new? true} []
(client-row* fc/*current*))))
(def key->handler
(apply-middleware-to-all-handlers
{:users (helper/page-route grid-page)
:user-table (helper/table-route grid-page)
:user-edit-save (-> user-edit-save
(wrap-schema-decode :form-schema (mc/schema
[:map
[:db/id entity-id]
[:user/clients {:optional true}
[:maybe
(many-entity {} [:db/id entity-id])]]
[:user/role (ref->enum-schema "user-role")]]))
(wrap-entity [:form-params :db/id] default-read)
(wrap-schema-decode :form-schema form-schema)
(wrap-nested-form-params)
(wrap-form-4xx-2 user-save-error))
:user-client-new (-> new-client
(wrap-form-4xx-2 (wrap-entity user-dialog [:form-params :db/id] default-read)))
:user-client-new (-> new-client
(wrap-schema-decode :query-schema [:map
[:index {:optional true
:default 0} [nat-int? {:default 0}]]]))
:user-edit-dialog (-> user-edit-dialog
:user-edit-dialog (-> user-dialog
(wrap-entity [:route-params :db/id] default-read)
(wrap-schema-decode
:route-schema (mc/schema [:map [:db/id entity-id]])))
:user-impersonate (-> impersonate
(wrap-entity [:params :db/id] default-read)
(wrap-schema-decode
:params-schema (mc/schema [:map [:db/id entity-id]])))}
(fn [h]
(-> h
(wrap-admin)
(wrap-client-redirect-unauthenticated)))))

View File

@@ -1,15 +1,15 @@
(ns auto-ap.ssr.utils
(:require
[auto-ap.datomic :refer [all-schema]]
[auto-ap.datomic :refer [all-schema conn]]
[auto-ap.logging :as alog]
[clojure.string :as str]
[config.core :refer [env]]
[datomic.api :as dc]
[hiccup2.core :as hiccup]
[malli.core :as mc]
[malli.error :as me]
[malli.transform :as mt2]
[slingshot.slingshot :refer [throw+ try+]]
[manifold.time :as mt]))
[slingshot.slingshot :refer [throw+ try+]]))
(defn html-response [hiccup & {:keys [status headers oob] :or {status 200 headers {} oob []}}]
{:status status
@@ -27,6 +27,25 @@
o))
oob)))})
(defn modal-response [hiccup & {:as opts}]
(apply html-response
(into
[hiccup]
(mapcat identity
(-> opts
(assoc-in [:headers "hx-trigger"] "modalopen")
(assoc-in [:headers "hx-retarget"] "#modal-content")
(assoc-in [:headers "hx-reswap"] "innerHTML"))))))
(defn next-step-modal-response [hiccup & {:as opts}]
(apply html-response
(into
[hiccup]
(mapcat identity
(-> opts
(assoc-in [:headers "hx-retarget"] "#modal-content")
(assoc-in [:headers "hx-reswap"] "innerHTML"))))))
(defn wrap-error-response [handler]
(fn [request]
(try
@@ -92,17 +111,25 @@
(defn parse-empty-as-nil []
(mt2/transformer
{:decoders
{:double empty->nil
{:string empty->nil
:double empty->nil
:int empty->nil
:long empty->nil
'nat-int? empty->nil}}))
(def entity-id (mc/schema [nat-int? {:error/message "required"} ]))
(def entity-id (mc/schema [nat-int? {:error/message "required"
:decode/arbitrary (fn [e]
(if (and (map? e) (:db/id e))
(:db/id e)
e))} ]))
(def temp-id (mc/schema :string))
(def temp-id (mc/schema [:string {:min 1}]))
(def money (mc/schema [:double]))
(def percentage (mc/schema [:double {:decode/arbitrary (fn [x] (some-> x (* 0.01)))
:max 1.0
(def percentage (mc/schema [:double {:decode/string {:enter (fn [x]
(if (and (string? x) (re-find #"^\d+(\.\d+)?$" x))
(-> x (Double/parseDouble) (* 0.01))
x))}
:max 1.0
:error/message "1-100"}]))
(def regex (mc/schema [:fn {:error/message "not a regex"}
@@ -116,15 +143,11 @@
(def map->db-id-decoder
{:enter (fn [x]
(into []
(for [[k v] (sort-by (comp #(Long/parseLong %) name first) x)]
v
#_(assoc v :db/id (cond (and (string? k) (re-find #"^\d+$" k))
(Long/parseLong k)
(keyword? k)
(name k)
:else
k)))))})
(if (sequential? x)
x
(into []
(for [[k v] (sort-by (comp #(Long/parseLong %) name first) x)]
v))))})
(defn many-entity [params & keys]
(mc/schema
@@ -147,10 +170,7 @@
(subs (str k) 1))
;; TODO need to remove or at least remove usages as the form is not included
(defn validation-error [m & {:as data}]
(throw+ (ex-info m (merge data {:type :validation}))))
;; TODO make this bubble the form data automatically
(defn field-validation-error [m path & {:as data}]
(throw+ (ex-info m (merge data {:type :field-validation
:form-errors (assoc-in {} path [m])}))))
@@ -168,6 +188,16 @@
(mt2/transformer {:name :arbitrary})
mt2/default-value-transformer))
(defn strip [s]
(cond (and (string? s) (str/blank? s))
nil
(string? s)
(str/trim s)
:else
s))
(defn wrap-schema-decode [handler & {:keys [form-schema query-schema route-schema params-schema]}]
(fn [{:keys [form-params query-params params] :as request}]
(let [request (try
@@ -201,7 +231,6 @@
main-transformer)))
(catch Exception e
(alog/warn ::validation-error :error e)
(throw (ex-info (->> (-> e
(ex-data )
:data
@@ -219,7 +248,11 @@
(handler request))))
(defn ref->enum-schema [n]
(into [:enum {:decode/string #(keyword n %)}]
(into [:enum {:decode/string #(if (keyword? %)
%
(when (not-empty %)
(keyword n %))
)}]
(for [{:db/keys [ident]} (all-schema)
:when (= n (namespace ident))]
ident)))
@@ -241,42 +274,6 @@
{:value (name ident) :content (str/replace (str/capitalize (name ident)) "-" " ")})))
#_(defn namespaceize-decoder [n]
{:exit (fn [m]
(when m
(reduce
(fn [m [k v]]
(if (= k "id")
(assoc m :db/id v)
(assoc m (keyword n (name k)) v)))
m
m)))})
(defn wrap-form-4xx [handler]
(fn [request]
(try+
(handler request)
(catch [:type :validation] e
(alog/warn ::form-4xx :error e)
(html-response [:span.error-content.text-red-500 (:message &throw-context)]
:status 400)))))
(defn assoc-errors-into-meta [entity errors]
(reduce
(fn add-error [entity {:keys [path message] :as se}]
(if (= (count path) 1)
(with-meta entity (assoc (meta entity) (last path) {:errors message}))
(update-in entity (butlast path)
(fn [terminal]
(with-meta terminal (assoc (meta terminal) (last path) {:errors message}))))))
entity
errors))
(defn wrap-form-4xx-2 [handler form-handler]
(fn [request]
(try+
@@ -292,18 +289,18 @@
(:errors (:explain (:error e))))]
(alog/warn ::form-4xx :errors errors)
(form-handler (assoc request
:last-form (:decoded e)
:form-params (:decoded e)
:field-validation-errors errors
:form-errors humanized)))
#_(html-response [:span.error-content.text-red-500 (:message &throw-context)]
:status 400))
(catch [:type :field-validation] e
(form-handler (assoc request
:last-form (:form e)
:form-params (:form e)
:form-errors (:form-errors e))))
(catch [:type :form-validation] e
(form-handler (assoc request
:last-form (:form e)
:form-params (:form e)
:form-validation-errors (:form-validation-errors e)
:form-errors {:errors (:form-validation-errors e)}))))))
@@ -319,10 +316,24 @@
(defn path->name2 [k & rest]
(let [k->n (fn [k]
(if (keyword? k)
(str (namespace k) "/" (name k))
(str (when (namespace k)
(str (namespace k) "/"))
(name k))
k))]
(str (k->n k)
(str/join ""
(map (fn [k]
(str "[" (k->n k) "]"))
rest)))))
(defn wrap-entity [handler path read]
(fn wrap-entity-request [request]
(let [entity (some->>
(get-in request path)
(#(if (string? %) (Long/parseLong %) %))
(dc/pull (dc/db conn) read ))]
(handler (if entity
(assoc request
:entity entity)
request)))))

View File

@@ -37,9 +37,11 @@
:put :admin-transaction-rule-save
:post :admin-transaction-rule-save}
"/table" :admin-transaction-rule-table
"/account/filled" :admin-transaction-rule-filled-account
"/account/new" :admin-transaction-rule-new-account
"/account/location-select" :admin-transaction-rule-location-select
"/account/typeahead" :admin-transaction-rule-account-typeahead
"/test" :admin-transaction-rule-test
"/new" {:get :admin-transaction-rule-new-dialog}
[[#"\d+" :db/id] "/edit"] :admin-transaction-rule-edit-dialog
}}