Merge branch 'master' of codecommit://integreat into tr-form
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 ")]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")))
|
||||
|
||||
@@ -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)])))
|
||||
|
||||
@@ -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])])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]]])
|
||||
|
||||
@@ -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)]]
|
||||
|
||||
])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)))))))
|
||||
|
||||
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
]
|
||||
]]]]]]))
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
|
||||
@@ -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)))))
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user