This commit is contained in:
2024-04-04 21:19:40 -07:00
91 changed files with 13241 additions and 2406 deletions

View File

@@ -37,11 +37,10 @@
:bank-account/plaid-account [:plaid-account/name :db/id :plaid-account/number :plaid-account/balance]
:bank-account/intuit-bank-account [:intuit-bank-account/name :intuit-bank-account/external-id :db/id]
:bank-account/integration-status [:integration-status/message
:db/id
:integration-status/last-attempt
:integration-status/last-updated
{:integration-status/state [:db/ident]}]}
]}
:db/id
:integration-status/last-attempt
:integration-status/last-updated
{:integration-status/state [:db/ident]}]}]}
{:yodlee-provider-account/_client [*]}
{:plaid-item/_client [*]}
{:client/emails [:db/id :email-contact/email :email-contact/description]}])
@@ -61,7 +60,7 @@
(fn [bas]
(map (fn [i ba]
(-> ba
(update :bank-account/type :db/ident )
(update :bank-account/type :db/ident)
(update-in [:bank-account/integration-status :integration-status/state] :db/ident)
(update-in [:bank-account/integration-status :integration-status/last-attempt] #(some-> % coerce/to-date-time))
(update-in [:bank-account/integration-status :integration-status/last-updated] #(some-> % coerce/to-date-time))
@@ -71,9 +70,9 @@
(defn get-all []
(->> (dc/q '[:find (pull ?e r)
:in $ r
:where [?e :client/name]]
(dc/db conn)
full-read)
:where [?e :client/name]]
(dc/db conn)
full-read)
(map first)
(map cleanse)))
@@ -98,23 +97,23 @@
(map cleanse)))
(defn get-by-id [id]
(->>
(dc/pull (dc/db conn )
full-read
id)
(->>
(dc/pull (dc/db conn)
full-read
id)
(cleanse)))
(defn code->id [code]
(->>
(->>
(dc/q '[:find ?e
:in $ ?code
:where [?e :client/code ?code]]
:in $ ?code
:where [?e :client/code ?code]]
(dc/db conn) code)
(first)
(first)))
(defn best-match [identifier]
(when (and identifier (not-empty identifier))
(when (and identifier (not-empty identifier))
(some-> (solr/query solr/impl "clients"
{"query" (format "_text_:\"%s\"" (str/upper-case (solr/escape identifier)))
"fields" "id"})
@@ -126,7 +125,7 @@
(defn exact-match [identifier]
(when (and identifier (not-empty identifier))
(when (and identifier (not-empty identifier))
(some-> (solr/query solr/impl "clients"
{"query" (format "exact:\"%s\"" (str/upper-case (solr/escape identifier)))
"fields" "id"})
@@ -149,7 +148,6 @@
"code" (:client/code result)
"exact" (map str/upper-case matches)})))
(defn raw-graphql-ids [db args]
(let [name-like-ids (cond (not (str/blank? (:name-like args)))
(set (map (comp #(Long/parseLong %) :id)
@@ -172,24 +170,24 @@
matching-ids)
(set (map :db/id (:clients args))))
query (cond-> {:query {:find []
:in ['$ ]
:in ['$]
:where []}
:args [db]}
valid-ids
(merge-query {:query {:in ['[?e ...]]}
:args [(set valid-ids)]})
(:sort args) (add-sorter-fields {"name" ['[?e :client/name ?sort-name]]}
args)
true
(merge-query {:query {:find ['?sort-default '?e] :where ['[?e :client/name ?sort-default]]}}))]
(->> (query2 query)
(apply-sort-3 (update args :sort conj {:sort-key "default-2" :asc true}))
(apply-pagination args))))
(apply-sort-3 (update args :sort conj {:sort-key "default-2" :asc true}))
(apply-pagination args))))
(defn graphql-results [ids db args]
(let [results (->> (pull-many db full-read
@@ -201,6 +199,6 @@
(defn get-graphql-page [args]
(let [db (dc/db conn)
{ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args)]
[(->> (graphql-results ids-to-retrieve db args))
matching-count]))

View File

@@ -22,7 +22,7 @@
(defn get-all-graphql [context args _]
(assert-admin (:id context))
(let [args (assoc args :id (:id context))
[accounts _ ] (d-accounts/get-graphql (assoc (<-graphql args) :per-page Integer/MAX_VALUE))]
[accounts _] (d-accounts/get-graphql (assoc (<-graphql args) :per-page Integer/MAX_VALUE))]
(map ->graphql accounts)))
(defn default-for-vendor [context args _]
@@ -31,34 +31,32 @@
(->graphql (d-accounts/clientize result (:client_id args)))))
(def search-pattern [:db/id
:account/numeric-code
:account/location
{:account/vendor-allowance [:db/ident]
:account/default-allowance [:db/ident]
:account/invoice-allowance [:db/ident]}])
:account/numeric-code
:account/location
{:account/vendor-allowance [:db/ident]
:account/default-allowance [:db/ident]
:account/invoice-allowance [:db/ident]}])
(defn search- [id query client]
(let [client-part (if (some->> client (can-see-client? id))
(format "((applicability:(global OR optional) AND -client_id:*) OR (account_client_override_id:* AND client_id:%s))" client)
"(applicability:(global OR optional) AND -client_id:*)"
)
"(applicability:(global OR optional) AND -client_id:*)")
query (format "_text_:(%s) AND %s" (cleanse-query query) client-part)]
(mu/log ::searching :search-query query)
(for [{:keys [account_id name] :as g} (solr/query solr/impl "accounts"
{"query" query
"fields" "id, name, client_id, numeric_code, applicability, account_id"})]
{"query" query
"fields" "id, name, client_id, numeric_code, applicability, account_id"})]
{:account_id (first account_id)
:name (first name)})))
:name (first name)})))
(defn search [context {query :query client :client_id allowance :allowance vendor-id :vendor_id} _]
(when client
(assert-can-see-client (:id context) client))
(let [num (some-> (re-find #"([0-9]+)" query)
second
(not-empty )
(not-empty)
Integer/parseInt)
valid-allowances (cond-> #{:allowance/allowed
:allowance/warn}
(is-admin? (:id context)) (conj :allowance/admin-only))
@@ -71,77 +69,76 @@
vendor-account (when vendor-id
(-> (dc/q '[:find ?da
:in $ ?v
:where [?v :vendor/default-account ?da]]
(dc/db conn)
vendor-id)
:in $ ?v
:where [?v :vendor/default-account ?da]]
(dc/db conn)
vendor-id)
ffirst))
xform (comp
(filter (fn [[_ a]]
(or
(valid-allowances (-> a allowance :db/ident))
(= (:db/id a) vendor-account))))
(map (fn [[n a]]
{:name (str (:account/numeric-code a) " - " n)
:id (:db/id a)
:location (:account/location a)
:warning (when (= :allowance/warn (-> a allowance :db/ident))
"This account is not typically used for this purpose.")})))]
(if query
(filter (fn [[_ a]]
(or
(valid-allowances (-> a allowance :db/ident))
(= (:db/id a) vendor-account))))
(map (fn [[n a]]
{:name (str (:account/numeric-code a) " - " n)
:id (:db/id a)
:location (:account/location a)
:warning (when (= :allowance/warn (-> a allowance :db/ident))
"This account is not typically used for this purpose.")})))]
(if query
(if num
(->> (dc/q '[:find ?n (pull ?i pattern)
:in $ ?numeric-code ?allowance pattern
:where [?i :account/numeric-code ?numeric-code]
[?i :account/name ?n]
(or [?i :account/applicability :account-applicability/global]
[?i :account/applicability :account-applicability/optional]
[?i :account/applicability :account-applicability/customized])]
(dc/db conn)
num
allowance
search-pattern)
:in $ ?numeric-code ?allowance pattern
:where [?i :account/numeric-code ?numeric-code]
[?i :account/name ?n]
(or [?i :account/applicability :account-applicability/global]
[?i :account/applicability :account-applicability/optional]
[?i :account/applicability :account-applicability/customized])]
(dc/db conn)
num
allowance
search-pattern)
(sequence xform))
(->> (search- (:id context) query client)
(sequence
(comp (map (fn [i] [(:name i) (dc/pull (dc/db conn) search-pattern (:account_id i))]))
xform))))
(comp (map (fn [i] [(:name i) (dc/pull (dc/db conn) search-pattern (:account_id i))]))
xform))))
[])))
(defn rebuild-search-index []
(solr/index-documents-raw
solr/impl
"accounts"
(for [result (map first (dc/qseq {:query '[:find (pull ?aco [:account-client-override/search-terms :account-client-override/client :db/id {:account/_client-overrides [:account/numeric-code :account/location :db/id {:account/applicability [:db/ident]}]}])
:in $
:where [?aco :account-client-override/client ]
[?aco :account-client-override/search-terms ]
[_ :account/client-overrides ?aco]]
:args [(dc/db conn)]}))
:when (:account/numeric-code (:account/_client-overrides result))]
{"id" (:db/id result)
"account_id" (:db/id (:account/_client-overrides result))
"account_client_override_id" (str (:db/id result))
"name" (:account-client-override/search-terms result)
"client_id" (str (:db/id (:account-client-override/client result)))
"numeric_code" (:account/numeric-code (:account/_client-overrides result))
"location" (:account/location (:account/_client-overrides result))
"applicability" (name (:db/ident (:account/applicability (:account/_client-overrides result))))}))
solr/impl
"accounts"
(for [result (map first (dc/qseq {:query '[:find (pull ?aco [:account-client-override/search-terms :account-client-override/client :db/id {:account/_client-overrides [:account/numeric-code :account/location :db/id {:account/applicability [:db/ident]}]}])
:in $
:where [?aco :account-client-override/client]
[?aco :account-client-override/search-terms]
[_ :account/client-overrides ?aco]]
:args [(dc/db conn)]}))
:when (:account/numeric-code (:account/_client-overrides result))]
{"id" (:db/id result)
"account_id" (:db/id (:account/_client-overrides result))
"account_client_override_id" (str (:db/id result))
"name" (:account-client-override/search-terms result)
"client_id" (str (:db/id (:account-client-override/client result)))
"numeric_code" (:account/numeric-code (:account/_client-overrides result))
"location" (:account/location (:account/_client-overrides result))
"applicability" (name (:db/ident (:account/applicability (:account/_client-overrides result))))}))
(solr/index-documents-raw
solr/impl
"accounts"
(for [result (map first (dc/qseq {:query '[:find (pull ?a [:account/numeric-code
:account/search-terms
{:account/applicability [:db/ident]}
:db/id
:account/location])
:in $
:where [?a :account/search-terms ]]
:args [(dc/db conn)]}))
:when (:account/search-terms result)
]
{"id" (:db/id result)
"account_id" (:db/id result)
"name" (:account/search-terms result)
"numeric_code" (:account/numeric-code result)
"location" (:account/location result)
"applicability" (name (:db/ident (:account/applicability result)))})))
solr/impl
"accounts"
(for [result (map first (dc/qseq {:query '[:find (pull ?a [:account/numeric-code
:account/search-terms
{:account/applicability [:db/ident]}
:db/id
:account/location])
:in $
:where [?a :account/search-terms]]
:args [(dc/db conn)]}))
:when (:account/search-terms result)]
{"id" (:db/id result)
"account_id" (:db/id result)
"name" (:account/search-terms result)
"numeric_code" (:account/numeric-code result)
"location" (:account/location result)
"applicability" (name (:db/ident (:account/applicability result)))})))

View File

@@ -423,7 +423,6 @@
nil)}))
(defn get-payment-page [context args _]
(alog/info ::TEST)
(let [[payments checks-count] (d-checks/get-graphql (-> args
:filters
(<-graphql)

View File

@@ -1,16 +1,17 @@
(ns auto-ap.graphql.utils
(:require [clojure.string :as str]
[auto-ap.datomic :refer [conn]]
[clj-time.coerce :as coerce]
(:require [auto-ap.datomic :refer [conn]]
[auto-ap.logging :as alog]
[auto-ap.time :as atime]
[buddy.auth :refer [throw-unauthorized]]
[clj-time.coerce :as coerce]
[clojure.set :as set]
[clojure.string :as str]
[clojure.walk :as walk]
[com.brunobonacci.mulog :as mu]
[com.walmartlabs.lacinia.util :refer [attach-resolvers]]
[datomic.api :as dc]
[iol-ion.query :refer [entid]]
[clojure.walk :as walk]
[com.walmartlabs.lacinia.util :refer [attach-resolvers]]
[com.brunobonacci.mulog :as mu]
[clojure.set :as set]
[auto-ap.logging :as alog]))
[slingshot.slingshot :refer [throw+]]))
(defn snake->kebab [s]
@@ -192,3 +193,29 @@
(if (seq extra-client-ids)
(set/intersection user-client-ids extra-client-ids)
user-client-ids)))
(defn exception->notification [f]
(try
(f)
(catch Throwable e
(throw (ex-info (.getMessage e)
{:type :notification}
e)))))
(defn exception->4xx [f]
(try
(f)
(catch Throwable e
(throw+ (ex-info (.getMessage e) {:type :form-validation
:form-validation-errors [(.getMessage e)]}))
#_(throw (ex-info (.getMessage e)
{:type :notification}
e)))))
(defn notify-if-locked [client-id date]
(try
(assert-not-locked client-id date)
(catch Exception e
(throw (ex-info (.getMessage e)
{:type :notification}
e)))))

View File

@@ -21,18 +21,18 @@
(defn can-user-edit-vendor? [vendor-id id]
(if (is-admin? id)
true
(empty?
(set/difference (set (->> (dc/q '[:find ?c
:in $ ?v
:where [?vu :vendor-usage/vendor ?v]
[?vu :vendor-usage/client ?c]
[?vu :vendor-usage/count ?d]
[(>= ?d 0)]]
(dc/db conn)
vendor-id)
(map first)))
(set (map :db/id (:user/clients id)))))))
true
(empty?
(set/difference (set (->> (dc/q '[:find ?c
:in $ ?v
:where [?vu :vendor-usage/vendor ?v]
[?vu :vendor-usage/client ?c]
[?vu :vendor-usage/count ?d]
[(>= ?d 0)]]
(dc/db conn)
vendor-id)
(map first)))
(set (map :db/id (:user/clients id)))))))
(defn upsert-vendor [context {{:keys [id name hidden terms code print_as primary_contact plaid_merchant secondary_contact address default_account_id invoice_reminder_schedule schedule_payment_dom terms_overrides account_overrides] :as in} :vendor} _]
(when (and id (not (can-user-edit-vendor? id (:id context))))
@@ -63,11 +63,11 @@
hidden
false)
terms-overrides (mapv
(fn [to]
#:vendor-terms-override {:client (:client_id to)
:terms (:terms to)
:db/id (or (:id to) (random-tempid))})
terms_overrides)
(fn [to]
#:vendor-terms-override {:client (:client_id to)
:terms (:terms to)
:db/id (or (:id to) (random-tempid))})
terms_overrides)
account-overrides (mapv
(fn [ao]
#:vendor-account-override {:client (:client_id ao)
@@ -75,11 +75,11 @@
:db/id (or (:id ao) (random-tempid))})
account_overrides)
schedule-payment-dom (mapv
(fn [ao]
#:vendor-schedule-payment-dom {:client (:client_id ao)
:dom (:dom ao)
:db/id (or (:id ao) (random-tempid))})
schedule_payment_dom)
(fn [ao]
#:vendor-schedule-payment-dom {:client (:client_id ao)
:dom (:dom ao)
:db/id (or (:id ao) (random-tempid))})
schedule_payment_dom)
transaction [:upsert-entity (cond-> #:vendor {:db/id (if id
id
"vendor")
@@ -114,41 +114,40 @@
"secondary")
:name (:name secondary_contact)
:phone (:phone secondary_contact)
:email (:email secondary_contact)})
)
:email (:email secondary_contact)}))
:search-terms [name]}
(is-admin? (:id context)) (assoc
:vendor/legal-entity-name (:legal_entity_name in)
:vendor/legal-entity-first-name (:legal_entity_first_name in)
:vendor/legal-entity-middle-name (:legal_entity_middle_name in)
:vendor/legal-entity-last-name (:legal_entity_last_name in)
:vendor/legal-entity-tin (:legal_entity_tin in)
:vendor/legal-entity-tin-type (enum->keyword (:legal_entity_tin_type in) "legal-entity-tin-type")
:vendor/legal-entity-1099-type (enum->keyword (:legal_entity_1099_type in) "legal-entity-1099-type")
:vendor/plaid-merchant plaid_merchant
:vendor/account-overrides account-overrides
:vendor/terms-overrides terms-overrides
:vendor/schedule-payment-dom schedule-payment-dom
:vendor/automatically-paid-when-due (:automatically_paid_when_due in)))]
:vendor/legal-entity-name (:legal_entity_name in)
:vendor/legal-entity-first-name (:legal_entity_first_name in)
:vendor/legal-entity-middle-name (:legal_entity_middle_name in)
:vendor/legal-entity-last-name (:legal_entity_last_name in)
:vendor/legal-entity-tin (:legal_entity_tin in)
:vendor/legal-entity-tin-type (enum->keyword (:legal_entity_tin_type in) "legal-entity-tin-type")
:vendor/legal-entity-1099-type (enum->keyword (:legal_entity_1099_type in) "legal-entity-1099-type")
:vendor/plaid-merchant plaid_merchant
:vendor/account-overrides account-overrides
:vendor/terms-overrides terms-overrides
:vendor/schedule-payment-dom schedule-payment-dom
:vendor/automatically-paid-when-due (:automatically_paid_when_due in)))]
transaction-result (audit-transact [transaction] (:id context))
new-vendor (d-vendors/get-by-id (or (-> transaction-result :tempids (get "vendor"))
id))]
(auto-ap.solr/index-documents-raw
auto-ap.solr/impl
"vendors"
[{"id" (:db/id new-vendor)
"name" (:vendor/name new-vendor)
"hidden" (boolean (:vendor/hidden new-vendor))}])
auto-ap.solr/impl
"vendors"
[{"id" (:db/id new-vendor)
"name" (:vendor/name new-vendor)
"hidden" (boolean (:vendor/hidden new-vendor))}])
(-> new-vendor
(->graphql))))
(defn merge-vendors [context {:keys [from to]} _]
(let [transaction (->> (dc/q {:find '[?x ?a2]
:in '[$ ?vendor-from ]
:in '[$ ?vendor-from]
:where ['[?x ?a ?vendor-from]
'[?a :db/ident ?a2]]}
(dc/db conn)
@@ -165,13 +164,13 @@
(defn get-graphql [context args _]
(assert-admin (:id context))
(let [args (assoc args :id (:id context))
[vendors vendors-count ] (d-vendors/get-graphql (<-graphql args))]
[vendors vendors-count] (d-vendors/get-graphql (<-graphql args))]
(result->page vendors vendors-count :vendors args)))
(defn get-by-id [context args _]
(->graphql
(d-vendors/get-graphql-by-id (assoc args :id (:id context))
(:id args))))
(d-vendors/get-graphql-by-id (assoc args :id (:id context))
(:id args))))
(defn partial-match-first [query matches]
(if-let [best-match (->> matches
@@ -187,7 +186,7 @@
(defn search [context args _]
(if-let [query (not-empty (cleanse-query (:query args)))]
(let [search-query (str "name:(" query ")")]
(for [{:keys [id name]} (solr/query solr/impl "vendors" {"query" (cond-> search-query
(not (is-admin? (:id context))) (str " hidden:false"))
@@ -210,4 +209,4 @@
:args [(dc/db conn)]})]
{"id" (:db/id result)
"name" (:vendor/name result)
"hidden" (boolean (:vendor/hidden result))}))))
"hidden" (boolean (:vendor/hidden result))}))))

View File

@@ -1,42 +1,44 @@
(ns auto-ap.handler
(:require
[amazonica.core :refer [defcredential]]
[auto-ap.client-routes :as client-routes]
[auto-ap.datomic :refer [conn pull-many]]
[auto-ap.datomic.clients :as d-clients]
[auto-ap.graphql.utils :refer [assert-can-see-client limited-clients]]
[auto-ap.logging :as alog]
[auto-ap.routes.auth :as auth]
[auto-ap.routes.exports :as exports]
[auto-ap.routes.ezcater :as ezcater]
[auto-ap.routes.graphql :as graphql]
[auto-ap.routes.health :as health]
[auto-ap.routes.invoices :as invoices]
[auto-ap.routes.queries :as queries]
[auto-ap.routes.yodlee2 :as yodlee2]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.core :as ssr]
[bidi.bidi :as bidi]
[bidi.ring :refer [->ResourcesMaybe make-handler]]
[buddy.auth.backends.session :refer [session-backend]]
[buddy.auth.backends.token :refer [jws-backend]]
[buddy.auth.middleware :refer [wrap-authentication wrap-authorization]]
[cemerick.url :as url]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[clojure.string :as str]
[clojure.edn :as edn]
[com.brunobonacci.mulog :as mu]
[config.core :refer [env]]
[datomic.api :as dc]
[ring.middleware.edn :refer [wrap-edn-params]]
[ring.middleware.multipart-params :as mp]
[ring.middleware.params :refer [wrap-params]]
[ring.middleware.reload :refer [wrap-reload]]
[ring.middleware.session :refer [wrap-session]]
[ring.middleware.session.cookie :refer [cookie-store]]
[ring.util.response :as response]
[clojure.set :as set]))
(:require [amazonica.core :refer [defcredential]]
[auto-ap.client-routes :as client-routes]
[auto-ap.datomic :refer [conn pull-many]]
[auto-ap.datomic.clients :as d-clients]
[auto-ap.graphql.utils :refer [assert-can-see-client
limited-clients]]
[auto-ap.logging :as alog]
[auto-ap.routes.auth :as auth]
[auto-ap.routes.exports :as exports]
[auto-ap.routes.ezcater :as ezcater]
[auto-ap.routes.graphql :as graphql]
[auto-ap.routes.health :as health]
[auto-ap.routes.invoices :as invoices]
[auto-ap.routes.queries :as queries]
[auto-ap.routes.yodlee2 :as yodlee2]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.core :as ssr]
[bidi.bidi :as bidi]
[bidi.ring :refer [->ResourcesMaybe make-handler]]
[buddy.auth.backends.session :refer [session-backend]]
[buddy.auth.backends.token :refer [jws-backend]]
[buddy.auth.middleware :refer [wrap-authentication
wrap-authorization]]
[cemerick.url :as url]
[cheshire.core :as cheshire]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[clojure.edn :as edn]
[clojure.set :as set]
[clojure.string :as str]
[com.brunobonacci.mulog :as mu]
[config.core :refer [env]]
[datomic.api :as dc]
[hiccup2.core :as hiccup]
[ring.middleware.edn :refer [wrap-edn-params]]
[ring.middleware.multipart-params :as mp]
[ring.middleware.params :refer [wrap-params]]
[ring.middleware.session :refer [wrap-session]]
[ring.middleware.session.cookie :refer [cookie-store]]
[ring.util.response :as response]))
(when (:aws-access-key-id env)
(defcredential (:aws-access-key-id env) (:aws-secret-access-key env) (:aws-region env)))
@@ -102,14 +104,21 @@
match->handler))
(defn wrap-guess-route [handler]
(fn [{:keys [uri request-method] :as request}]
(fn [{:keys [uri request-method headers] :as request}]
(let [matched-route (:handler
(bidi.bidi/match-route all-routes
uri
:request-method request-method))]
:request-method request-method))
matched-hx-current-url-route (some->> (get headers "hx-current-url")
url/url
:path
(bidi/match-route ssr-routes/only-routes)
:handler)]
(handler (assoc request
:matched-route
matched-route)))))
matched-route
:matched-current-page-route
matched-hx-current-url-route)))))
(defn test-match-route [method uri]
(bidi.bidi/match-route all-routes
@@ -225,7 +234,13 @@
clients (some->> client-ids
seq
(pull-many (dc/db conn)
d-clients/full-read))]
'[:db/id :client/name :client/code :client/locations
:client/matches :client/feature-flags
{:client/bank-accounts [:db/id
{:bank-account/type [:db/ident]}
:bank-account/number
:bank-account/name
:bank-account/code]}]))]
(mu/with-context {:clients (take 10 (map :client/code clients))}
(handler (assoc request
@@ -284,6 +299,19 @@
(clojure.pprint/pprint (:session request))
(handler request)))
(defn wrap-error [handler]
(fn error-handling-request [request]
(try
(handler request)
(catch Throwable e
(if (= :notification (:type (ex-data e)))
{:status 200
:headers {"hx-trigger" (cheshire/generate-string
{"notification" (str (hiccup/html [:div (.getMessage e)]))})
"hx-reswap" "none"}}
{:status 500
:body (pr-str e)})))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defonce app
(-> route-handler
@@ -308,4 +336,5 @@
#_(wrap-reload)
(wrap-params)
(mp/wrap-multipart-params)
(wrap-edn-params)))
(wrap-edn-params)
(wrap-error)))

View File

@@ -25,20 +25,20 @@
(defn parse-sort [grid-spec q]
(if (not-empty q)
(->>
(str/split q #",")
(map (fn [k]
(let [[key asc?] (str/split k #":")
matching-header (first (filter #(= (str key) (:sort-key %)) (:headers grid-spec)))]
{:sort-key (str key)
:asc (boolean (= "asc" asc?))
:matching-header matching-header
:name (:name matching-header)
:sort-icon (if (= (boolean (= "asc" asc?)) true)
svg/sort-down
svg/sort-up)})))
(filter :matching-header)
(into []))
(->>
(str/split q #",")
(map (fn [k]
(let [[key asc?] (str/split k #":")
matching-header (first (filter #(= (str key) (:sort-key %)) (:headers grid-spec)))]
{:sort-key (str key)
:asc (boolean (= "asc" asc?))
:matching-header matching-header
:name (:name matching-header)
:sort-icon (if (= (boolean (= "asc" asc?)) true)
svg/sort-down
svg/sort-up)})))
(filter :matching-header)
(into []))
[]))
(defn parse-long [l]
@@ -55,30 +55,30 @@
(defn apply-date-range [source-key start-date-key end-date-key]
(fn [query-params]
(dissoc
(condp = (source-key query-params)
"week"
(assoc query-params
start-date-key (time/plus (time/now) (time/days -7))
end-date-key (time/now))
(dissoc
(condp = (source-key query-params)
"week"
(assoc query-params
start-date-key (time/plus (time/now) (time/days -7))
end-date-key (time/now))
"month"
(assoc query-params
start-date-key (time/plus (time/now) (time/months -1))
end-date-key (time/now))
"month"
(assoc query-params
start-date-key (time/plus (time/now) (time/months -1))
end-date-key (time/now))
"year"
(assoc query-params
start-date-key (time/plus (time/now) (time/years -1))
end-date-key (time/now))
"year"
(assoc query-params
start-date-key (time/plus (time/now) (time/years -1))
end-date-key (time/now))
"all"
(assoc query-params
start-date-key (time/plus (time/now) (time/years -3))
end-date-key (time/now))
"all"
(assoc query-params
start-date-key (time/plus (time/now) (time/years -3))
end-date-key (time/now))
query-params)
:date-range)))
query-params)
:date-range)))
(defn apply-toggle-sort [grid-spec]
(fn toggle-sort [query-params]

View File

@@ -660,7 +660,7 @@
:export-ntg-sales-snapshot (-> export-ntg-sales-snapshot wrap-csv-response
(wrap-schema-enforce :query-schema (mc/schema [:map
[:date {:required true
:decode/string #(try (atime/parse % atime/iso-date) (catch Exception e nil))} :some]]) )
:decode/string #(try (atime/parse % atime/iso-date) (catch Exception _ nil))} :some]]) )
(wrap-form-4xx-2 (fn [_] {:body "Invalid Date"}))
(wrap-predetermined-api-key "fd07755a-ed4c-4c9a-ad85-fbdd8af37206")
)

View File

@@ -1,3 +1,29 @@
(ns auto-ap.routes.exports
(:require
[auto-ap.datomic :refer [conn pull-attr pull-many]]
[auto-ap.datomic.accounts :as accounts]
[auto-ap.datomic.clients :as d-clients]
[auto-ap.datomic.transactions :as d-transactions]
[auto-ap.datomic.vendors :as vendor]
[auto-ap.graphql :as graphql]
[auto-ap.graphql.utils
:refer [->graphql <-graphql assert-admin assert-can-see-client]]
[auto-ap.logging :as alog]
[auto-ap.routes.utils :refer [wrap-secure]]
[auto-ap.ssr.utils :refer [wrap-schema-enforce wrap-form-4xx-2]]
[auto-ap.time :as atime]
[buddy.sign.jwt :as jwt]
[cheshire.generate :as generate]
[clj-time.coerce :as coerce :refer [to-date]]
[clj-time.core :as time]
[clojure.data.csv :as csv]
[clojure.edn :refer [read-string]]
[com.unbounce.dogstatsd.core :as statsd]
[config.core :refer [env]]
[datomic.api :as dc]
[malli.core :as mc]
[ring.middleware.json :refer [wrap-json-response]]
[venia.core :as venia]))
(let [query [[:all_payments
{:client-code "VS"

View File

@@ -192,10 +192,11 @@
(upload-xls request)
(base-page
request
(com/page {:nav (com/admin-aside-nav)
(com/page {:nav com/admin-aside-nav
:client-selection (:client-selection (:session request))
:client (:client request)
:identity (:identity request)
:request request
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
:admin-ezcater-xls)
:hx-trigger "clientSelected from:body"

View File

@@ -4,6 +4,7 @@
[auto-ap.graphql.utils
:refer [assert-can-see-client can-see-client? cleanse-query is-admin?]]
[auto-ap.solr :as solr]
[auto-ap.logging :as alog]
[auto-ap.ssr.utils
:refer [entity-id ref->enum-schema wrap-schema-enforce]]
[com.brunobonacci.mulog :as mu]
@@ -34,8 +35,7 @@
:name (first name)})))
(defn account-search [{{:keys [q client-id allowance vendor-id] :as qp} :query-params id :identity}]
(defn account-search [{{:keys [q client-id purpose vendor-id] :as qp} :query-params id :identity}]
(when client-id
(assert-can-see-client id client-id))
(let [num (some-> (re-find #"([0-9]+)" q)
@@ -46,9 +46,9 @@
valid-allowances (cond-> #{:allowance/allowed
:allowance/warn}
(is-admin? id) (conj :allowance/admin-only))
allowance (cond (= allowance :vendor)
allowance (cond (= purpose "vendor")
:account/vendor-allowance
(= allowance :invoice)
(= purpose "invoice")
:account/invoice-allowance
:else
:account/default-allowance)
@@ -99,6 +99,6 @@
[:maybe entity-id]]
[:vendor-id {:optional true}
[:maybe entity-id]]
[:allowance {:optional true}
[:maybe (ref->enum-schema "allowance")]]])))
[:purpose {:optional true}
[:maybe :string]]])))

View File

@@ -44,7 +44,7 @@
(defn page [request]
(base-page
request
(com/page {:nav (com/admin-aside-nav)
(com/page {:nav com/admin-aside-nav
:client-selection (:client-selection (:session request))
:clients (:clients request)
:client (:client request)

View File

@@ -132,12 +132,12 @@
(def grid-page
(helper/build {:id "entity-table"
:nav (com/admin-aside-nav)
: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))
(query-params/parse-key :code query-params/parse-long)
(helper/default-parse-query-params grid-page))
:action-buttons (fn [_]
[(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes
:admin-account-new-dialog))
@@ -145,8 +145,8 @@
"New Account")])
: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)))}
:admin-account-edit-dialog
:db/id (:db/id entity)))}
svg/pencil)])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:admin)}

View File

@@ -89,7 +89,7 @@
(def grid-page
(helper/build {:id "job-table"
:id-fn :arn
:nav (com/admin-aside-nav)
: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))
@@ -118,8 +118,8 @@
(when (and (:start-date e)
(:end-date e))
(str (time/in-minutes (time/interval
(:start-date e)
(:end-date e))) " minutes")))}
(:start-date e)
(:end-date e))) " minutes")))}
{:key "name"
:name "Name"
:render :name}

View File

@@ -172,7 +172,7 @@
(def grid-page
(helper/build {:id "entity-table"
:nav (com/admin-aside-nav)
:nav com/admin-aside-nav
:page-specific-nav filters
:fetch-page fetch-page
:parse-query-params (helper/default-parse-query-params grid-page)
@@ -1352,14 +1352,16 @@
:validation-route ::route/navigate))
mm/Initializable
(init-step-params
[_ request]
[_ multi-form-state request]
(let [bank-account-type (get-in request [:query-params :bank-account-type])]
(cond->
{:db/id (str (java.util.UUID/randomUUID))
:new? true}
(if (= {} (:step-params multi-form-state))
(cond->
{:db/id (str (java.util.UUID/randomUUID))
:new? true}
bank-account-type (assoc :bank-account/type (keyword "bank-account-type" bank-account-type)
:bank-account/visible true))))
bank-account-type (assoc :bank-account/type (keyword "bank-account-type" bank-account-type)
:bank-account/visible true))
(:step-params multi-form-state))))
mm/Discardable
(can-discard? [_ step-params]

View File

@@ -242,11 +242,12 @@
(defn page [{:keys [form-params form-errors] :as request}]
(base-page
request
(com/page {:nav (com/admin-aside-nav)
(com/page {:nav com/admin-aside-nav
:client-selection (:client-selection (:session request))
:clients (:clients request)
:client (:client request)
:identity (:identity request)}
:identity (:identity request)
:request request}
(com/breadcrumbs {} [:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
"Admin"])
[:div.flex.space-x-4

View File

@@ -165,10 +165,11 @@
(let [entity-id (or (some-> query-params (get "entity-id") Long/parseLong)
(some-> route-params (get :entity-id) Long/parseLong))]
(base-page request
(com/page {:nav (com/admin-aside-nav)
(com/page {:nav com/admin-aside-nav
:client-selection (:client-selection (:session request))
:client (:client request)
:identity (:identity request)
:request request
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
:admin-history)
:hx-trigger "clientSelected from:body"

View File

@@ -124,7 +124,7 @@
(def grid-page
(helper/build {:id "entity-table"
:id-fn :db/id
:nav (com/admin-aside-nav)
:nav com/admin-aside-nav
:fetch-page fetch-page
:page-specific-nav filters
:row-buttons (fn [_ entity]
@@ -138,7 +138,7 @@
:end (:end-date (:parsed-query-params request))}
:id "date-range"}) [1 :hx-swap-oob] true)])
:parse-query-params (comp
(helper/default-parse-query-params grid-page))
(helper/default-parse-query-params grid-page))
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:admin)}
"Admin"]

View File

@@ -118,7 +118,7 @@
(def grid-page
(helper/build {:id "entity-table"
:id-fn :db/id
:nav (com/admin-aside-nav)
:nav com/admin-aside-nav
:fetch-page fetch-page
:page-specific-nav filters
:row-buttons (fn [_ entity]
@@ -162,9 +162,9 @@
(reduce + 0.0 (map :sales-summary-item/discount x)))
(reduce + 0.0 (map :sales-summary-item/tax x))))])
[:li "Sales subtotal: " (format "$%,.2f" (- (+ (reduce + 0.0 (map :sales-summary-item/total (:sales-summary/sales-items ss)))
(reduce + 0.0 (map :sales-summary-item/discount (:sales-summary/sales-items ss))))
(reduce + 0.0 (map :sales-summary-item/discount (:sales-summary/sales-items ss))))
(reduce + 0.0 (map :sales-summary-item/tax (:sales-summary/sales-items ss)))))]
(reduce + 0.0 (map :sales-summary-item/tax (:sales-summary/sales-items ss)))))]
[:li "Tax: " (format "$%,.2f" (:sales-summary/total-tax ss))]
[:li "Tips: " (format "$%,.2f" (:sales-summary/total-tip ss))]
[:li (com/pill {:color (if (dollars= total-debits total-credits)

View File

@@ -176,7 +176,7 @@
(def grid-page
(helper/build {:id "entity-table"
:nav (com/admin-aside-nav)
:nav com/admin-aside-nav
:page-specific-nav filters
:fetch-page fetch-page
:parse-query-params (comp
@@ -470,6 +470,7 @@
(com/validated-field
{:errors (fc/field-errors)
:x-data (hx/json {:location (fc/field-value)})}
;; TODO make this thing into a component
[:div {:hx-trigger "changed"
:hx-target "next *"
:hx-swap "outerHTML"
@@ -815,7 +816,7 @@
(fc/with-field :transaction-rule/transaction-approval-status
(com/validated-field {:label "Approval status"
:errors (fc/field-errors)}
(com/radio {:options (ref->radio-options "transaction-approval-status")
(com/radio-card {:options (ref->radio-options "transaction-approval-status")
:value (fc/field-value)
:name (fc/field-name)
:size :small

View File

@@ -49,7 +49,7 @@
:placeholder "Cash"
:size :small}))
(com/field {:label "Type"}
(com/radio {:size :small
(com/radio-card {:size :small
:name "type"
:value (:type (:parsed-query-params request))
:options [{:value ""
@@ -136,7 +136,7 @@
(def grid-page
(helper/build {:id "entity-table"
:nav (com/admin-aside-nav)
:nav com/admin-aside-nav
:page-specific-nav filters
:fetch-page fetch-page
:parse-query-params (comp

View File

@@ -130,7 +130,7 @@
(defn page [{:keys [identity matched-route] :as request}]
(base-page
request
(com/page {:nav (com/company-aside-nav)
(com/page {:nav com/company-aside-nav
:client-selection (:client-selection (:session request))
:client (:client request)
:identity (:identity request)

View File

@@ -90,7 +90,7 @@
(def grid-page
(helper/build
{:id "entity-table"
:nav (com/company-aside-nav)
:nav com/company-aside-nav
:id-fn (comp :db/id second)
:fetch-page fetch-page
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
@@ -126,46 +126,46 @@
(-> vendor :vendor/legal-entity-last-name)))]]
(when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)]
(com/pill
{:class "text-xs font-medium"
:color :primary}
(str/capitalize t99-type))
{:class "text-xs font-medium"
:color :primary}
(str/capitalize t99-type))
)])}
{:key "tin"
:name "TIN"
:sort-key "tin"
:show-starting "md"
:render (fn [[_ vendor]]
[:div.flex.gap-4
(when-let [tin (-> vendor :vendor/legal-entity-tin)]
[:span {:class "text-xs font-medium py-0.5 "}
tin])
(when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)]
(com/pill {:class "text-xs font-medium"
:color :yellow}
(name tin-type)))]
)}
[:div.flex.gap-4
(when-let [tin (-> vendor :vendor/legal-entity-tin)]
[:span {:class "text-xs font-medium py-0.5 "}
tin])
(when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)]
(com/pill {:class "text-xs font-medium"
:color :yellow}
(name tin-type)))]
)}
{:key "address"
:name "Address"
:sort-key "address"
:show-starting "lg"
:render (fn [[_ vendor]]
(if (-> vendor :vendor/address :address/street1)
[:div
[:div (-> vendor :vendor/address :address/street1)] " "
[:div
(-> vendor :vendor/address :address/street2)] " "
[:div
(-> vendor :vendor/address :address/city) " "
(-> vendor :vendor/address :address/state) ","
(-> vendor :vendor/address :address/zip)]]
[:p.text-sm.italic.text-gray-400 "No address"]))}
(if (-> vendor :vendor/address :address/street1)
[:div
[:div (-> vendor :vendor/address :address/street1)] " "
[:div
(-> vendor :vendor/address :address/street2)] " "
[:div
(-> vendor :vendor/address :address/city) " "
(-> vendor :vendor/address :address/state) ","
(-> vendor :vendor/address :address/zip)]]
[:p.text-sm.italic.text-gray-400 "No address"]))}
{:key "paid"
:name "Paid"
:sort-key "paid"
:render (fn [[_ _ paid]]
(com/pill {:class "text-xs font-medium"
:color :primary}
"Paid $" (Math/round paid)))}]}))
"Paid $" (Math/round paid)))}]}))

View File

@@ -133,7 +133,7 @@
(def grid-page
(helper/build
{:id "plaid-table"
:nav (com/company-aside-nav)
:nav com/company-aside-nav
:fetch-page fetch-page
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:company)}
@@ -181,9 +181,9 @@
:name "Accounts"
:show-starting "md"
:render (fn [e]
[:ul
(for [a (:plaid-item/accounts e)]
[:li (:plaid-account/name a) " - " (:plaid-account/number a)])])}]}))
[:ul
(for [a (:plaid-item/accounts e)]
[:li (:plaid-account/name a) " - " (:plaid-account/number a)])])}]}))
(def page (helper/page-route grid-page))

View File

@@ -70,7 +70,7 @@
(def grid-page
(helper/build {:id "report-table"
:nav (com/company-aside-nav)
:nav com/company-aside-nav
:fetch-page fetch-page
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:company)}

View File

@@ -122,7 +122,7 @@ fastlink.open({fastLinkURL: '%s',
(def grid-page
(helper/build
{:id "yodlee-table"
:nav (com/company-aside-nav)
:nav com/company-aside-nav
:id-fn :db/id
:fetch-page fetch-page
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
@@ -136,19 +136,19 @@ fastlink.open({fastLinkURL: '%s',
:route :company-yodlee-table
:action-buttons (fn [request]
[[:div.flex.flex-col.flex-shrink
[:div.flex-shrink
(com/button {:color :primary
:on-click "openFastlink()"
:disabled (if (:client request)
false
true)
:hx-get (bidi/path-for ssr-routes/only-routes
:company-yodlee-fastlink-dialog)
:hx-target "#modal-holder"}
(com/button-icon {} svg/refresh)
"Link new account")]
(when-not (:client request)
[:div.text-xs "Note: please select a specific customer to link a new account."])]])
[:div.flex-shrink
(com/button {:color :primary
:on-click "openFastlink()"
:disabled (if (:client request)
false
true)
:hx-get (bidi/path-for ssr-routes/only-routes
:company-yodlee-fastlink-dialog)
:hx-target "#modal-holder"}
(com/button-icon {} svg/refresh)
"Link new account")]
(when-not (:client request)
[:div.text-xs "Note: please select a specific customer to link a new account."])]])
:row-buttons (fn [request _]
[
(com/button {:hx-put (bidi/path-for ssr-routes/only-routes
@@ -194,9 +194,9 @@ fastlink.open({fastLinkURL: '%s',
:name "Accounts"
:show-starting "md"
:render (fn [e]
[:ul
(for [a (:yodlee-provider-account/accounts e)]
[:li (:yodlee-account/name a) " - " (:yodlee-account/number a)])])}]}))
[:ul
(for [a (:yodlee-provider-account/accounts e)]
[:li (:yodlee-account/name a) " - " (:yodlee-account/number a)])])}]}))
(def page (helper/page-route grid-page))
(def table (helper/table-route grid-page))

View File

@@ -21,6 +21,8 @@
(def a-icon-button buttons/a-icon-button-)
(def button-group buttons/group-)
(def button-group-button buttons/group-button-)
(def navigation-button-list buttons/navigation-button-list-)
(def navigation-button buttons/navigation-button-)
(def modal dialog/modal-)
(def modal-card dialog/modal-card-)
(def modal-card-advanced dialog/modal-card-advanced-)
@@ -53,7 +55,8 @@
(def navbar navbar/navbar-)
(def page page/page-)
(def radio radio/radio-)
(def radio-card radio/radio-card-)
(def radio-list radio/radio-list-)
(def pill tags/pill-)
(def badge tags/badge-)

View File

@@ -1,231 +1,351 @@
(ns auto-ap.ssr.components.aside
(:require [auto-ap.ssr.svg :as svg]
[hiccup2.core :as hiccup]
[bidi.bidi :as bidi]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.client-routes :as client-routes]
[auto-ap.ssr.hx :as hx]
[auto-ap.routes.admin.transaction-rules :as transaction-rules]
[auto-ap.ssr.hiccup-helper :as hh]
[auto-ap.routes.admin.import-batch :as ib-routes]
(:require [auto-ap.client-routes :as client-routes]
[auto-ap.permissions :refer [can?]]
[auto-ap.routes.admin.clients :as ac-routes]
[auto-ap.routes.admin.excel-invoices :as ei-routes]
[auto-ap.routes.admin.import-batch :as ib-routes]
[auto-ap.routes.admin.transaction-rules :as transaction-rules]
[auto-ap.routes.admin.vendors :as v-routes]
[auto-ap.graphql.clients :as clients]))
[auto-ap.routes.invoice :as invoice-route]
[auto-ap.routes.outgoing-invoice :as oi-routes]
[auto-ap.routes.payments :as payment-routes]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.hiccup-helper :as hh]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.svg :as svg]
[bidi.bidi :as bidi]
[hiccup.util :as hu]))
(defn menu-button- [params & children]
[:div
[:a (-> params
(dissoc :icon)
(assoc :type "button")
(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")
(update :class (fn [c]
(cond-> (or c "cursor-pointer flex items-center p-2 w-full text-sm rounded-lg transition duration-75 group hover:bg-gray-100 dark:hover:bg-gray-700 select-none")
(:active? params) (hh/add-class "text-blue-600 font-extrabold dark:text-blue-100 bg-gray-100")
(not (:active? params)) (hh/add-class "text-gray-600 dark:text-white"))))
(assoc :hx-indicator "find .htmx-indicator")
(assoc :hx-boost "true")
(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 (get params "@click")
(into [:span {:class "flex-1 text-left whitespace-nowrap"}] children)
(when (get params "@click.prevent")
[: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 (update params
:class (fnil hh/add-class "")"py-2 space-y-1.5")
[:ul (cond-> (update params
:class (fnil hh/add-class "") "space-y-1.5 max-h-0 transition transition-all overflow-hidden")
true (assoc ":class" (format "selected == '%s' ? 'py-0.5' : 'py-0'" (:selector params))
:x-ref "submenu"
:style (cond-> {} (:active? params) (assoc "max-height" "400px"))
":style" (format "selected == '%s' ? 'max-height: ' + $refs.submenu.scrollHeight + 'px' : ''" (:selector params))))
(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")])])
(update-in c [1 1 :class ] (fn [c]
(hh/add-class (or c "") " flex items-center p-2 pl-11 w-full text-base font-normal 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",
:class "fixed top-0 left-0 pt-16 z-20 w-64 h-screen transition-transform",
"x-transition:enter" "transition duration-500"
"x-transition:enter-start" "lg:-translate-x-full"
"x-transition:enter-end" " lg:translate-x-0"
"x-transition:enter-start" "-translate-x-full"
"x-transition:enter-end" " 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"
"x-transition:leave-start" "translate-x-0"
"x-transition:leave-end" " -translate-x-full"
:aria-labelledby "left-nav"
:x-show "leftNavShow"
":aria-hidden" "leftNavShow ? 'false' : 'true'"}
[:template {:x-teleport "body"}
[:div.fixed.inset-0.lg:hidden {:x-show "leftNavShow" :x-transition:enter "transition duration-500" :x-transition:enter-start "opacity-0" :x-transition:enter-end "opacity-100"
:x-transition:leave "transition duration-500" :x-transition:leave-start "opacity-100" :x-transition:leave-end "opacity-0"
"@click.capture.prevent" "leftNavShow=false"}
[:div.fixed.inset-0.bg-gray-800.z-10.opacity-70]]]
[: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
(when page-specific
[:div {:class " pt-5 mt-5 space-y-2 border-t border-gray-200 dark:border-gray-700"}
page-specific]
)]])
page-specific])]])
(defn main-aside-nav- []
[:ul {:class "space-y-1"}
(defn main-aside-nav- [request]
(let [selected (cond
(#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page ::oi-routes/new} (:matched-route request))
"invoices"
[:li
(menu-button- {:icon svg/pie
:href "/"}
"Dashboard")]
[:li {:x-data (hx/json {:open false})}
(menu-button- {"@click" "open = !open"
:icon svg/accounting-invoice-mail}
"Invoices")
(sub-menu- (hx/alpine-appear {:x-show "open"})
(menu-button- {:href (bidi/path-for client-routes/routes
:invoices)}
"All")
(menu-button- {:href (bidi/path-for client-routes/routes
:paid-invoices)}
"Paid")
(menu-button- {:href (bidi/path-for client-routes/routes
:unpaid-invoices)}
"Unpaid")
(menu-button- {:href (bidi/path-for client-routes/routes
:voided-invoices)}
"Voided"))]
[:li {:x-data (hx/json {:open false})}
(menu-button- {:icon svg/receipt-register-1
"@click" "open = !open"}
"Sales")
(sub-menu- (hx/alpine-appear {:x-show "open"})
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-sales)
"?date-range=week")} "Sales")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-expected-deposits)
"?date-range=week")} "Expected Deposits")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-tenders)
"?date-range=week")} "Tenders")
(#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts} (:matched-route request))
"sales"
(#{::payment-routes/all-page ::payment-routes/pending-page ::payment-routes/cleared-page ::payment-routes/voided-page } (:matched-route request))
"payments"
:else
nil)]
[:ul {:class "space-y-1"
:x-data (hx/json {:selected selected})}
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-refunds)
"?date-range=week")} "Refunds")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-cash-drawer-shifts)
"?date-range=week")} "Cash drawer shifts")
#_(menu-button- {:href "Sales"} "Cash Shifts")
#_(menu-button- {:href "Sales"} "Tenders"))]
[:li {:x-data (hx/json {:open false})}
(menu-button- {"@click" "open = !open"
:icon svg/payments}
"Payments")
(sub-menu- (hx/alpine-appear {:x-show "open"})
(menu-button- {:href (bidi/path-for client-routes/routes
:payments)} "All")
(menu-button- {:href (bidi/path-for client-routes/routes
:payments)} "Pending")
(menu-button- {:href (bidi/path-for client-routes/routes
:payments)} "Cleared")
(menu-button- {:href (bidi/path-for client-routes/routes
:payments)} "Voided"))]
[:li
(menu-button- {:icon svg/pie
:href "/"}
"Dashboard")]
[:li {:x-data (hx/json {:open false})}
(menu-button- {"@click" "open = !open"
:icon svg/bank}
"Transactions")
(when (can? (:identity request)
{:subject :invoice-page})
(list
(menu-button- {"@click.prevent" "if (selected == 'invoices') {selected = null } else { selected = 'invoices'} "
:icon svg/accounting-invoice-mail}
"Invoices")
(sub-menu-
{:selector "invoices"
:active? (= "invoices" selected)}
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
::invoice-route/all-page)
{:date-range "month"})
:active? (= ::invoice-route/all-page (:matched-route request))
:hx-boost "true"}
(sub-menu- (hx/alpine-appear {:x-show "open"})
(menu-button- {:href (bidi/path-for client-routes/routes
:transactions)} "All")
(menu-button- {:href (bidi/path-for client-routes/routes
:unapproved-transactions)} "Unapproved")
(menu-button- {:href (bidi/path-for client-routes/routes
:requires-feedback-transactions)} "Client Review")
(menu-button- {:href (bidi/path-for client-routes/routes
:approved-transactions)} "Approved")
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
:transaction-insights)} "Insights"))]
[:li {:x-data (hx/json {:open false})}
(menu-button- {"@click" "open = !open"
:icon svg/receipt}
"Ledger")
(sub-menu- (hx/alpine-appear {:x-show "open"})
(menu-button- {:href (bidi/path-for client-routes/routes
:ledger)} "Register")
(menu-button- {:href (bidi/path-for client-routes/routes
:profit-and-loss)} "Profit & Loss")
(menu-button- {:href (bidi/path-for client-routes/routes
:profit-and-loss-detail)} "Profit & Loss Detail")
(menu-button- {:href (bidi/path-for client-routes/routes
:cash-flows)} "Cash Flows")
(menu-button- {:href (bidi/path-for client-routes/routes
:balance-sheet)} "Balance Sheet")
(menu-button- {:href (bidi/path-for client-routes/routes
:external-import-ledger)} "External Ledger Import"))]])
"All")
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
::invoice-route/paid-page)
{:date-range "month"})
:active? (= ::invoice-route/paid-page (:matched-route request))
:hx-boost "true"}
"Paid")
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
::invoice-route/unpaid-page)
{:date-range "month"})
:active? (= ::invoice-route/unpaid-page (:matched-route request))
:hx-boost "true"}
"Unpaid")
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
::invoice-route/voided-page)
{:date-range "month"})
:active? (= ::invoice-route/voided-page (:matched-route request))
:hx-boost "true"}
"Voided")
(menu-button- {:href (bidi/path-for client-routes/routes
:import-invoices)} "Import")
(when (can? (:identity request)
{:subject :ar-invoice
:activity :read})
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
::oi-routes/new)
:active? (= ::oi-routes/new (:matched-route request))
:hx-boost "true"}
"Create outgoing")))))
(when
(can? (:identity request) {:subject :sales :activity :read})
(list
(menu-button- {:icon svg/receipt-register-1
"@click.prevent" "if (selected == 'sales') {selected = null } else { selected = 'sales'} "}
"Sales")
(sub-menu- {:selector "sales"
:active? (= "sales" selected)}
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-sales)
"?date-range=week")
:active? (= :pos-sales (:matched-route request))
:hx-boost "true"}
"Sales")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-expected-deposits)
"?date-range=week")
:active? (= :pos-expected-deposits (:matched-route request))
:hx-boost "true"}
"Expected Deposits")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-tenders)
"?date-range=week")
:active? (= :pos-tenders (:matched-route request))
:hx-boost "true"}
"Tenders")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-refunds)
"?date-range=week")
:active? (= :pos-refunds (:matched-route request))
:hx-boost "true"}
"Refunds")
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
:pos-cash-drawer-shifts)
"?date-range=week")
:active? (= :cash-drawer-shifts (:matched-route request))
:hx-boost "true"}
"Cash drawer shifts"))))
(menu-button- {"@click.prevent" "if (selected == 'payments') {selected = null } else { selected = 'payments'} "
:icon svg/payments}
"Payments")
(sub-menu- {:selector "payments"
:active? (= "payments" selected)}
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
::payment-routes/all-page)
{:date-range "month"})
:active? (= ::payment-routes/all-page (:matched-route request))
:hx-boost "true"}
"All")
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
::payment-routes/pending-page)
{:date-range "month"})
:active? (= ::payment-routes/pending-page (:matched-route request))
:hx-boost "true"}
"Pending")
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
::payment-routes/cleared-page)
{:date-range "month"})
:active? (= ::payment-routes/cleared-page (:matched-route request))
:hx-boost "true"}
"Cleared")
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
::payment-routes/voided-page)
{:date-range "month"})
:active? (= ::payment-routes/voided-page (:matched-route request))
:hx-boost "true"}
"Voided"))
[:li {:x-data (hx/json {:open false})}
(menu-button- {"@click.prevent" "if (selected == 'transactions') {selected = null } else { selected = 'transactions'} "
:icon svg/bank}
"Transactions")
(sub-menu- {:selector "transactions"
:active? (= "transactions" selected)}
(menu-button- {:href (bidi/path-for client-routes/routes
:transactions)} "All")
(menu-button- {:href (bidi/path-for client-routes/routes
:unapproved-transactions)} "Unapproved")
(menu-button- {:href (bidi/path-for client-routes/routes
:requires-feedback-transactions)} "Client Review")
(menu-button- {:href (bidi/path-for client-routes/routes
:approved-transactions)} "Approved")
(when (can? (:identity request)
{:subject :transaction :activity :insights})
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
:transaction-insights)} "Insights")))]
(defn company-aside-nav- []
(when (can? (:identity request)
{:subject :ledger-page})
(list
(menu-button- {"@click.prevent" "if (selected == 'ledger') {selected = null } else { selected = 'ledger'} "
:icon svg/receipt}
"Ledger")
(sub-menu- {:selector "ledger"
:active? (= "ledger" selected)}
(menu-button- {:href (bidi/path-for client-routes/routes
:ledger)} "Register")
(menu-button- {:href (bidi/path-for client-routes/routes
:profit-and-loss)} "Profit & Loss")
(menu-button- {:href (bidi/path-for client-routes/routes
:profit-and-loss-detail)} "Profit & Loss Detail")
(menu-button- {:href (bidi/path-for client-routes/routes
:cash-flows)} "Cash Flows")
(menu-button- {:href (bidi/path-for client-routes/routes
:balance-sheet)} "Balance Sheet")
(when (can? (:identity request)
{:subject :ledger
:activity :import})
(menu-button- {:href (bidi/path-for client-routes/routes
:external-import-ledger)} "External Ledger Import")))))]))
(defn company-aside-nav- [_]
[:ul {:class "space-y-2" :hx-boost "true"}
[:li
(menu-button- {:icon svg/vendors
:href (bidi/path-for ssr-routes/only-routes
:company)}
:company)
:hx-boost true}
"My Company")]
[:li
(menu-button- {:icon svg/report
:href (bidi/path-for ssr-routes/only-routes
:company-reports)}
:company-reports)
:hx-boost true}
"Reports")]
[:li
(menu-button- {:icon svg/bank
:href (bidi/path-for ssr-routes/only-routes
:company-plaid)}
:company-plaid)
:hx-boost true}
"Plaid Link")]
[:li
(menu-button- {:icon svg/bank
:href (bidi/path-for ssr-routes/only-routes
:company-yodlee)}
:company-yodlee)
:hx-boost true}
"Yodlee Link")]
[:li
(menu-button- {:icon svg/government-building
:href (bidi/path-for ssr-routes/only-routes
:company-1099)}
:company-1099)
:hx-boost true}
"1099 Vendor Info"
)]])
(defn admin-aside-nav- []
[:ul {:class "space-y-2"}
(defn admin-aside-nav- [{:keys [matched-route] :as request}]
[:ul {:class "space-y-2" :x-data (hx/json {:selected "nil"})}
[:li
(menu-button- {:icon svg/dashboard
:href (bidi/path-for ssr-routes/only-routes :auto-ap.routes.admin/page)}
:active? (= :auto-ap.routes.admin/page matched-route)
:href (bidi/path-for ssr-routes/only-routes :auto-ap.routes.admin/page)
:hx-boost true}
"Dashboard")]
[:li
(menu-button- {:icon svg/restaurant
:href (bidi/path-for ssr-routes/only-routes ::ac-routes/page) }
:active? (= ::ac-routes/page matched-route)
:href (bidi/path-for ssr-routes/only-routes ::ac-routes/page)
:hx-boost true}
"Clients")]
[:li
(menu-button- {:icon svg/vendors
:active? (= ::v-routes/page matched-route)
:href (bidi/path-for ssr-routes/only-routes
::v-routes/page)}
::v-routes/page)
:hx-boost true}
"Vendors")]
[:li
(menu-button- {:icon svg/user
:active? (= :users matched-route)
:href (bidi/path-for ssr-routes/only-routes
:users)}
:users)
:hx-boost true}
"Users")]
[:li
(menu-button- {:icon svg/accounts
:active? (= :admin-accounts matched-route)
:href (bidi/path-for ssr-routes/only-routes
:admin-accounts)}
:admin-accounts)
:hx-boost true}
"Accounts")]
[:li
(menu-button- {:icon svg/cog
:href (bidi/path-for ssr-routes/only-routes ::transaction-rules/page)}
:active? (= ::transaction-rules/page matched-route)
:href (bidi/path-for ssr-routes/only-routes ::transaction-rules/page)
:hx-boost true}
"Rules")]
[:li
(menu-button- {:icon svg/question
:active? (= :admin-rules matched-route)
:href (bidi/path-for ssr-routes/only-routes
:admin-history)
:hx-boost "true"}
@@ -233,20 +353,32 @@
[:li
(menu-button- {:icon svg/rabbit
:active? (= :admin-jobs matched-route)
:href (bidi/path-for ssr-routes/only-routes
:admin-jobs)}
:admin-jobs)
:hx-boost true}
"Background Jobs")]
[:li {:x-data (hx/json {:open false})}
(menu-button- {:icon svg/arrow-in
"@click" "open = !open"}
"Import")
(sub-menu- (hx/alpine-appear
{:x-show "open"})
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
::ei-routes/page)} "Excel Invoices")
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
::ib-routes/page)} "Import Batches")
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
:admin-ezcater-xls)
:hx-boost "true"} "EZCater XLS Import"))]])
(when (can? (:identity request) {:subject :invoice :activity :import})
(menu-button- {:icon svg/arrow-in
"@click.prevent" "if (selected == 'import') {selected = null } else { selected = 'import'} "}
"Import"))
(sub-menu- {:selector "import"}
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
::ei-routes/page)
:active? (= ::ei-routes/page matched-route)
:hx-boost true}
"Excel Invoices")
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
::ib-routes/page)
:active? (= ::ib-routes/page matched-route)
:hx-boost true}
"Import Batches")
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
:admin-ezcater-xls)
:active? (= :admin-ezcater-xls matched-route)
:hx-boost "true"}
"EZCater XLS Import"))])

View File

@@ -0,0 +1,31 @@
(ns auto-ap.ssr.components.bank-account-icon
(:require [auto-ap.ssr.hiccup-helper :as hh]
[auto-ap.ssr.svg :as svg]))
(defmulti icon :bank-account/type)
(defmethod icon :bank-account-type/cash [_]
[:div.grow-0.flex.flex-col.justify-center
[:div.p-1.m-2.rounded-full
{:class
"bg-blue-50"}
[:div {:class
(hh/add-class "p-1.5 w-8 h-8" "text-green-600")}
svg/dollar]]])
(defmethod icon :bank-account-type/credit [_]
[:div.grow-0.flex.flex-col.justify-center
[:div.p-1.m-2.rounded-full
{:class
"bg-purple-50"}
[:div {:class
(hh/add-class "p-1.5 w-8 h-8" "text-purple-600")}
svg/credit-card]]])
(defmethod icon :bank-account-type/check [_]
[:div.grow-0.flex.flex-col.justify-center
[:div.p-1.m-2.rounded-full
{:class
"bg-blue-50"}
[:div {:class
(hh/add-class "p-1.5 w-8 h-8" "text-blue-600")}
svg/check]]])

View File

@@ -11,30 +11,37 @@
(= :secondary color)
"blue"
(= :red color)
"red"
(nil? color)
"white"
(sequential? color)
(first color)
:else
color)
base-weight (or (when (sequential? color)
(second color))
500)
disabled-weight (when disabled 400)]
(format " bg-%s-%d hover:bg-%s-%d focus:ring-%s-%d dark:bg-%s-%d dark:hover:bg-%s-%d "
base-color
(or disabled-weight 500)
(or disabled-weight (+ base-weight 0))
base-color
(or disabled-weight 600)
(or disabled-weight (+ base-weight 100))
base-color
(or disabled-weight 200)
(or disabled-weight (int (* base-weight 0.5)))
base-color
(or disabled-weight 600)
(or disabled-weight (+ base-weight 100))
base-color
(or disabled-weight 700)
)))
(or disabled-weight (+ base-weight 200)))))
(defn dark-color-weight [disabled]
(if disabled
@@ -48,7 +55,7 @@
(for [color ["green" "blue" "white"]
weight (range 100 900 100)]
(str "bg-" color "-" weight))
(str "bg-" color "-" weight))
;;ensuring these colors show up
;; => ("bg-green-100"
;; "bg-green-200"
@@ -76,10 +83,10 @@
;; "bg-white-800")
(defn button- [params & children]
[:button (update params
:class #(cond-> %
true (str " focus:ring-4 font-bold rounded-lg text-xs p-3 text-center mr-2 inline-flex items-center justify-center"
true (str " focus:ring-4 font-bold rounded-lg text-xs p-3 text-center mr-2 inline-flex items-center relative justify-center"
(bg-colors (:color params) (:disabled params)))
(not (:disabled params))
@@ -93,10 +100,11 @@
(nil? (:color params))
(str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 font-medium border border-gray-300 dark:border-gray-700")))
[:div.htmx-indicator.flex.items-center
[:div.htmx-indicator.flex.items-center.absolute.inset-0.justify-center
(svg/spinner {:class "inline w-4 h-4 text-white"})
[:div.ml-3 "Loading..."]]
(into [:div.htmx-indicator-hidden.inline-flex.gap-2.items-center.justify-center] children)])
(when (not (:minimal-loading? params))
[:div.ml-3 "Loading..."])]
(into [:div.htmx-indicator-invisible.inline-flex.gap-2.items-center.justify-center] children)])
(defn a-button- [params & children]
[:a (-> params
@@ -105,8 +113,7 @@
(= :secondary (:color params)) (str " text-white bg-blue-500 hover:bg-blue-600 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700")
(= :primary (:color params)) (str " text-white bg-green-500 hover:bg-green-600 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 ")
(nil? (:color params))
(str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 font-medium border border-gray-300 dark:border-gray-700")
))
(str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 font-medium border border-gray-300 dark:border-gray-700")))
(assoc :tabindex 0)
(assoc :href (:href params "#")))
[:div.htmx-indicator.flex.items-center
@@ -133,18 +140,17 @@
[:div.htmx-indicator-hidden.inline-flex.gap-2.items-center.justify-center (into [:div.h-4.w-4] children)]]))
(defn a-icon-button- [params & children]
(into
[:a (-> params (update :class str " inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center p-3 text-sm font-medium border border-gray-300 dark:border-gray-700 text-center text-gray-500 hover:text-gray-800 rounded-lg dark:text-gray-400 dark:hover:text-gray-100"
)
(update :href #(or % "")))
[:div.h-4.w-4 children]]))
(into
[:a (-> params (update :class str " inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center p-3 text-sm font-medium border border-gray-300 dark:border-gray-700 text-center text-gray-500 hover:text-gray-800 rounded-lg dark:text-gray-400 dark:hover:text-gray-100")
(update :href #(or % "")))
[:div.h-4.w-4 children]]))
(defn save-button- [params & children]
[:button { :class "text-white bg-green-500 hover:bg-green-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800 inline-flex items-center hover:scale-105 transition duration-300"}
[:button {:class "text-white bg-green-500 hover:bg-green-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800 inline-flex items-center hover:scale-105 transition duration-300"}
[:div.htmx-indicator.flex.items-center
(svg/spinner {:class "inline w-4 h-4 text-white"})
[:div.ml-3 "Loading..."]]
(into [:div.htmx-indicator-hidden ] children)])
(into [:div.htmx-indicator-hidden] children)])
@@ -159,10 +165,8 @@
(str " text-xs px-3 py-2")
(= :normal size)
(str " text-sm px-4 py-2")
)
))
true (dissoc :size))] children ))
(str " text-sm px-4 py-2"))))
true (dissoc :size))] children))
(defn group- [{:keys [name]} & children]
(let [children (-> children
@@ -174,6 +178,49 @@
[:input {:type "hidden" :name name}]]
children)))
(defn navigation-button- [{:keys [class next-arrow?] :or {next-arrow? true} :as params} & children]
[:button
(-> params
(update :class (fnil hh/add-class "")
"p-4 text-green-700 border border-gray-300 rounded-lg bg-gray-50
dark:bg-gray-800 dark:border-green-800 dark:text-green-400
focus:ring-green-400 focus:ring-2
hover:border-green-300 hover:bg-green-200 hover:dark:bg-gray-800 hover:dark:border-green-800
hover:dark:text-green-400")
(dissoc :next-arrow?))
[:div
{:class "flex items-center justify-between"}
[:span {:class "sr-only"} children]
[:h3 {:class "font-medium"} children]
(when next-arrow?
[:div.w-4.h-4
svg/arrow-right])]])
(defn navigation-button-list- [{:keys []} & children]
[:ol
{:class "space-y-4 w-72"}
(for [n children]
[:li n])
#_[:li
[:div
{:class
"w-full p-4 text-gray-900 bg-gray-100 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400",
:role "alert"}
[:div
{:class "flex items-center justify-between"}
[:span {:class "sr-only"} "Review"]
[:h3 {:class "font-medium"} "4. Review"]]]]
#_[:li
[:div
{:class
"w-full p-4 text-gray-900 bg-gray-100 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400",
:role "alert"}
[:div
{:class "flex items-center justify-between"}
[:span {:class "sr-only"} "Confirmation"]
[:h3 {:class "font-medium"} "5. Confirmation"]]]]])
(defn validated-save-button- [{:keys [errors class] :as params} & children]
(button- (-> {:color (or (:color params) :primary)

View File

@@ -12,7 +12,7 @@
(defn content-card- [params & children]
[:section (merge params {:class (hh/add-class " py-3 sm:py-5" (:class params))})
[:div {:class "max-w-screen-2xl"}
[:div {:class (:max-w params "max-w-screen-2xl")}
(into
[:div {:class "relative overflow-hidden shadow-md dark:bg-gray-800 sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 bg-white"}]
children)]])

View File

@@ -38,11 +38,12 @@
(defn checkbox-header- [params & rest]
[:th {:scope "col", :class "p-4"}
[:div {:class "flex items-center"}
[:input {:id "checkbox-all", :type "checkbox", :class inputs/default-checkbox-classes :name (:name params) :value (:value params)}]
[:input (merge {:id "checkbox-all", :type "checkbox", :class inputs/default-checkbox-classes :name (:name params) :value (:value params)} params)]
[:label {:for "checkbox-all", :class "sr-only"} "checkbox"]]])
(defn data-grid- [{:keys [headers thead-params id]} & rest]
[:table {:class "w-full text-sm text-left text-gray-500 dark:text-gray-400" :id id}
(defn data-grid- [{:keys [headers thead-params id] :as params} & rest]
[:table (merge {:class "w-full text-sm text-left text-gray-500 dark:text-gray-400"}
(dissoc params :headers :thead-params))
[:thead (assoc thead-params :class "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400")
(into
[:tr]
@@ -70,7 +71,7 @@
[:div {:hx-get (bidi/path-for ssr-routes/only-routes
route
:request-method :get)
:hx-trigger "clientSelected from:body"
:hx-trigger "clientSelected from:body, invalidated from:body"
:hx-swap "outerHTML swap:300ms"
:id id}
@@ -123,10 +124,11 @@
[: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]
(defn new-row- [{:keys [index colspan tr-params row-offset] :as params} & content]
(row-
(merge {:class "new-row"
:x-data (hx/json {:newRowIndex index})
:x-data (hx/json {:newRowIndex index
:offset (or row-offset 0)})
}
tr-params)
(cell- {:colspan colspan
@@ -135,10 +137,10 @@
(a-button- (merge
(dissoc params :index :colspan)
{
"@click" "$dispatch('newRow', {index: newRowIndex++})"
"@click" "$dispatch('newRow', {index: (newRowIndex++)})"
:color :secondary
:hx-trigger "newRow"
:hx-vals (hiccup/raw "js:{index: event.detail.index}")
:hx-vals (hiccup/raw "js:{index: event.detail.index }")
:hx-target "closest .new-row"
:hx-swap "beforebegin"})
content)])))

View File

@@ -16,7 +16,7 @@
[:div (-> params
(assoc "@click.outside" "open=false")
(dissoc :handle-unexpected-error?)
(update :class (fnil hh/add-class "") "w-full h-full modal-stack"))
(update :class (fnil hh/add-class "") ""))
children])
(defn modal-card- [params header content footer]
@@ -47,11 +47,11 @@
children])
(defn modal-body- [params & children]
[:div {:class "px-6 py-2 space-y-6 overflow-y-scroll w-full shrink"}
[:div {:class "px-6 py-2 space-y-6 overflow-y-scroll w-full shrink grow"}
children])
(defn modal-footer- [params & children]
[:div {:class "p-4"}
[:div {:class "p-4 border-t"}
[:span.items-center.bg-red-100.text-red-800.text-xs.font-medium.mb-2.p-1.rounded-full.inline-flex
(hx/alpine-appear {:x-show "unexpectedError" :class "dark:bg-red-900 dark:text-red-300"})
[:span {:class "w-2 h-2 bg-red-500 rounded-full"}]
@@ -61,5 +61,5 @@
(defn modal-card-advanced- [params & children]
[:div (merge params
{:class (hh/add-class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col h-full" (:class params "")) })
{:class (hh/add-class "modal-card bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col max-h-screen max-w-screen" (:class params "")) })
children])

View File

@@ -4,6 +4,7 @@
[auto-ap.ssr.hiccup-helper :as hh]
[clojure.string :as str]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.components.tags :as tags]
[auto-ap.ssr.hx :as hx]))
@@ -37,46 +38,59 @@
(defn typeahead- [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 identity) (:value params)) :label ((:content-fn params identity) (:value params))}
:search ""
:active -1
: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)
: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; popper.update()"
"@keydown.down.prevent.stop" "open = true; popper.update()"
"@keydown.backspace" "value = {value: '', label: '' }"
:tabindex 0
:x-init (:x-init params)
:x-ref "input"
}
[:input (-> params
(dissoc :class)
(dissoc :value-fn)
(dissoc :content-fn)
[:div.relative {: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 identity) (:value params)) :label ((:content-fn params identity) (:value params))}
:search ""
:active -1
:elements (if ((:value-fn params identity) (:value params))
[{:value ((:value-fn params identity) (:value params)) :label ((:content-fn params identity) (:value params))}]
[])
:popper nil
:warning_badge nil})
:x-modelable "value.value"
:x-model (:x-model params)
:x-init "popper = Popper.createPopper($refs.input, $refs.dropdown, {placement: 'bottom-start', strategy: 'fixed', modifiers: {name: 'offset', options: {offset: [0, 10]}}})
warning_badge = Popper.createPopper($refs.warning_badge, $refs.warning_pop, {placement: 'top', strategy: 'fixed', modifiers: {name: 'offset', options: {offset: [10,0 ]}}})"}
(if (:disabled params)
[:span {:x-text "value.label"}]
[:a {:class (-> (hh/add-class (or (:class params) "") default-input-classes)
(hh/add-class "cursor-pointer"))
"@click.prevent" "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-ref "input"}
[:input (-> params
(dissoc :class)
(dissoc :value-fn)
(dissoc :content-fn)
(dissoc :placeholder)
(dissoc :x-model)
(assoc
"x-ref" "hidden"
:type "hidden"
":value" "value.value"
:x-init (hiccup/raw (str "$watch('value', v => $dispatch('change')); "))))]
[:div.flex.w-full.justify-items-stretch
[:span.flex-grow.text-left {"x-text" "value.label"}]
[:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"}
svg/drop-down]]]
(dissoc :placeholder)
(dissoc :x-model)
(assoc
"x-ref" "hidden"
:type "hidden"
":value" "value.value"
:x-init (hiccup/raw (str "$watch('value', v => $dispatch('change')); "))))]
[:div.flex.w-full.justify-items-stretch
[:span.flex-grow.text-left {"x-text" "value.label"}]
[:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"}
svg/drop-down]
[:div {:x-show "value.warning"
:x-ref "warning_badge"
:x-effect "if (value.warning) { $nextTick(()=> warning_badge.update()) }"}
(tags/badge- {:class "peer"} "!")
[:div {:x-show "value.warning"
:x-ref "warning_pop"
:class "hidden peer-hover:block bg-red-50 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 p-4"
:x-text "value.warning"}]]]])
[:ul.dropdown-contents {:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 ring-1"
"x-ref" "dropdown"
@@ -90,12 +104,12 @@
"x-show " "open"
"x-trap" "open"
"@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"))
(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"
@@ -113,8 +127,7 @@
"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]
@@ -156,12 +169,14 @@
(dissoc :size))])
(defn date-input- [{:keys [size] :as params}]
[:div.shrink
[:input
[:div.shrink {:x-data (hx/json {:value (:value params)})}
[:input
(-> params
(update :class (fnil hh/add-class "") default-input-classes)
(assoc :x-modelable "value")
(assoc :type "text")
(assoc "_" (hiccup/raw "init initDatepicker(me)"))
(assoc "@change" "value = $event.target.value; console.log(value)")
(assoc "hx-on" (hiccup/raw "changeDate: htmx.trigger(this, \"change\")
htmx:beforeCleanupElement: this.dp.destroy()"))
(update :class #(str % (use-size size) " w-full"))
@@ -201,8 +216,8 @@
rest
(errors- {:errors (:errors params)})))
(defn hidden- [{:keys [name value]}]
[:input {:type "hidden" :value value :name name}])
(defn hidden- [{:keys [name value] :as params}]
[:input (merge {:type "hidden" :value value :name name} params)])
(defn checkbox- [params & rest]
(if (seq rest)

View File

@@ -0,0 +1,25 @@
(ns auto-ap.ssr.components.link-dropdown
(:require [auto-ap.ssr.components :as com]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.svg :as svg]))
(defn link-dropdown [links]
(if (> (count links) 0)
[:div {:x-data (hx/json {:popper nil
:show false})
"@click.outside" "show=false"
:x-init "popper = Popper.createPopper($refs.link, $refs.tooltip, {placement: 'bottom', strategy: 'fixed'})"}
(com/a-icon-button {:x-ref "link" "@click.prevent" "show=!show; $nextTick(() => popper.update());" :class "relative"}
svg/paperclip
(com/badge {} (count links)))
[:div.divide-y.divide-gray-200.bg-white.rounded-lg.shadow.z-50 (hx/alpine-appear {:x-ref "tooltip" :x-show "show" :data-key "show"})
[:div {:class "p-3 overflow-y-auto text-sm text-gray-700 dark:text-gray-200"}
[:div.flex.flex-col.gap-y-2
(for [l links]
[:div.flex-initial
[:a {:href (:link l)}
(com/pill {:color (or (:color l) :primary) :class "truncate block shrink grow-0"}
(:content l))]])]]]]))

View File

@@ -1,37 +1,26 @@
(ns auto-ap.ssr.components.multi-modal
(:require [auto-ap.ssr.components :as com]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
[auto-ap.ssr.utils
:refer [ html-response
assert-schema
main-transformer
modal-response
wrap-form-4xx-2
wrap-schema-enforce]]
(:require [auto-ap.cursor :as cursor]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.components.timeline :as timeline]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils
:refer [assert-schema html-response main-transformer
modal-response wrap-form-4xx-2 wrap-schema-enforce]]
[bidi.bidi :as bidi]
[hiccup.util :as hu]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.hx :as hx]
[malli.core :as mc]
[hiccup2.core :as hiccup2]
[hiccup2.core :as hiccup]
[auto-ap.cursor :as cursor]
[malli.core :as m]
[auto-ap.logging :as alog])
(:import [auto_ap.cursor VecCursor]))
[malli.core :as m]))
(def default-form-props {:hx-ext "response-targets"
:hx-swap "outerHTML"
:hx-target-400 "#form-errors .error-content"
:hx-trigger "submit"
:hx-target "this"
"x-trap" "true"
:class "h-full w-full" })
:hx-target "this" })
(defprotocol ModalWizardStep
(step-key [this])
@@ -41,23 +30,20 @@
(step-name [this]))
(defprotocol Initializable
(init-step-params [this request]))
(init-step-params [this multi-form-state request]))
(defprotocol CustomNext
(custom-next-handler [this request]))
(defprotocol Discardable
(can-discard? [this step-params])
(discard-changes [this request]))
(defn- init-step-params- [step request]
(if (satisfies? Initializable step)
(init-step-params step request)
{}))
(defprotocol LinearModalWizard
(hydrate-from-request [this request])
(get-current-step [this])
(navigate [this step-key])
(form-schema [this])
(steps [this])
(get-step [this step-key])
@@ -86,6 +72,11 @@
:edit-path []
:step-params @cursor)))
(defn get-mfs-field [mfs k]
(or (get (:step-params mfs) k)
(get-in (:snapshot mfs) (conj (or (:edit-path mfs) [])
k))))
(def step-key-schema (mc/schema [:orn {:decode/arbitrary clojure.edn/read-string
:encode/arbitrary pr-str}
[:sub-step [:cat :keyword [:or :int :string]]]
@@ -114,12 +105,13 @@
:to (encode-step-key (step-key (get-step linear-wizard n)))})}
(step-name (get-step linear-wizard n))])))))
(defn back-button [linear-wizard step validation-route]
[:a.cursor-pointer.whitespace-nowrap {:hx-put (hu/url (bidi/path-for ssr-routes/only-routes validation-route)
{:from (encode-step-key (step-key step))
:to (encode-step-key (->> (partition-all 2 1 (steps linear-wizard))
(filter (fn [[from to]]
(= to (step-key step))))
ffirst))})}
[:a.cursor-pointer.whitespace-nowrap.font-medium.text-blue-600 {:hx-put (hu/url (bidi/path-for ssr-routes/only-routes validation-route)
{:from (encode-step-key (step-key step))
:to (encode-step-key (->> (partition-all 2 1 (steps linear-wizard))
(filter (fn [[from to]]
(= to (step-key step))))
ffirst))})
:class "dark:text-blue-500"}
"Back"])
(defn default-next-button [linear-wizard step validation-route]
@@ -146,7 +138,7 @@
[:div.w-5.h-5 svg/arrow-right]))))
(defn default-step-body [params & children]
[:div.space-y-1 {:class "w-[600px] h-[700px]"}
[:div.space-y-1 {}
children])
(defn default-step-footer [linear-wizard step & {:keys [validation-route
@@ -173,12 +165,32 @@
(defn default-render-step [linear-wizard step & {:keys [head body footer validation-route discard-route]}]
(let [is-last? (= (step-key step) (last (steps linear-wizard)))]
(com/modal-card-advanced
{"@keydown.enter.prevent.stop" "$refs.next.click()"
:class (str (when is-last? "last-modal-step")
" transition duration-300 ease-in-out
{"@keydown.enter.prevent.stop" "if ($refs.next ) {$refs.next.click()}"
:class (str
"w-full h-full md:w-[750px] md:h-[600px]
group-[.forward]/transition:htmx-swapping:opacity-0
group-[.forward]/transition:htmx-swapping:-translate-x-1/4
group-[.forward]/transition:htmx-swapping:scale-75
group-[.forward]/transition:htmx-swapping:ease-in
group-[.forward]/transition:htmx-added:opacity-0
group-[.forward]/transition:htmx-added:scale-75
group-[.forward]/transition:htmx-added:translate-x-1/4
group-[.forward]/transition:htmx-added:ease-out
group-[.backward]/transition:htmx-swapping:opacity-0
group-[.backward]/transition:htmx-swapping:translate-x-1/4
group-[.backward]/transition:htmx-swapping:scale-75
group-[.backward]/transition:htmx-swapping:ease-in
group-[.backward]/transition:htmx-added:opacity-0
group-[.backward]/transition:htmx-added:scale-75
group-[.backward]/transition:htmx-added:-translate-x-1/4
group-[.backward]/transition:htmx-added:ease-out
opacity-100 translate-x-0 scale-100"
(when is-last? "last-modal-step")
" transition duration-150
")
":class" (hiccup/raw "{
\"htmx-swapping:-translate-x-2/3 htmx-swapping:opacity-0 htmx-swapping:scale-0 htmx-added:translate-x-2/3 htmx-added:opacity-0 htmx-added:scale-0 scale-100 translate-x-0 opacity-100\": $data.transitionType=='forward',
#_#_":class" (hiccup/raw "{
\"htmx-added:opacity-0 opacity-100\": $data.transitionType=='forward',
\"htmx-swapping:translate-x-2/3 htmx-swapping:opacity-0 htmx-swapping:scale-0 htmx-added:-translate-x-2/3 htmx-added:opacity-0 htmx-added:scale-0 scale-100 translate-x-0 opacity-100\": $data.transitionType=='backward'
}
")
@@ -186,8 +198,10 @@
(com/modal-header {}
head)
#_(com/modal-header-attachment {})
[:div.flex.shrink
[:div.grow-0.pr-6.pt-2.bg-gray-100.self-stretch #_{:style "margin-left:-20px"} (render-timeline linear-wizard step validation-route)]
[:div.flex.shrink.overflow-auto.grow
(when (:render-timeline? linear-wizard)
[:div.grow-0.pr-6.pt-2.bg-gray-100.self-stretch.hidden.md:block #_{:style "margin-left:-20px"}
(render-timeline linear-wizard step validation-route)])
(com/modal-body {}
body)]
@@ -221,27 +235,36 @@
:else
"forward")))
(defn navigate-handler [{{:keys [wizard] :as request} :request to-step :to-step oob :oob}]
(let [current-step (get-current-step wizard)
wizard (navigate wizard to-step)
new-step (get-current-step wizard)
transition-type (get-transition-type wizard (step-key current-step) to-step)]
(html-response
(render-wizard wizard
(-> request
(assoc :multi-form-state (-> (:multi-form-state request)
(merge-multi-form-state)
(select-state (edit-path new-step request) {})
(#(cond-> %
(satisfies? Initializable new-step)
(assoc :step-params
(init-step-params new-step % request))))))))
:headers {"HX-reswap" (when transition-type "outerHTML swap:0.16s")
"x-transition-type" (or transition-type "none")}
:oob (or oob []))))
(def next-handler
(-> (fn [{:keys [wizard] :as request}]
(let [current-step (get-current-step wizard)
to-step (:to (:query-params request))
wizard (navigate wizard to-step)
new-step (get-current-step wizard)
transition-type (get-transition-type wizard (step-key current-step) to-step)]
(html-response
(render-wizard wizard
(-> request
(assoc :multi-form-state (-> (:multi-form-state request)
(merge-multi-form-state)
(select-state
(edit-path new-step request)
(init-step-params- new-step request))))))
:headers {"HX-reswap" (when transition-type "outerHTML swap:0.15s")
"x-transition-type" (or transition-type "none")})))
(wrap-ensure-step)
(wrap-schema-enforce :query-schema
[:map
[:to step-key-schema]])))
(let [current-step (get-current-step wizard)]
(if (satisfies? CustomNext current-step)
(custom-next-handler current-step request)
(navigate-handler {:request request
:to-step (:to (:query-params request))}))))
(wrap-ensure-step)
(wrap-schema-enforce :query-schema
[:map
[:to {:optional true} [:maybe step-key-schema]]])))
(def discard-handler
(->
@@ -254,7 +277,7 @@
(render-wizard wizard
(-> request
(assoc :multi-form-state (discard-changes current-step multi-form-state))))
:headers {"HX-reswap" (when transition-type "outerHTML swap:0.15s")
:headers {"HX-reswap" (when transition-type "outerHTML swap:0.16s")
"x-transition-type" (or transition-type "none")})))
(wrap-schema-enforce :query-schema
[:map
@@ -263,11 +286,12 @@
(def submit-handler
(-> (fn [{:keys [wizard multi-form-state] :as request}]
(submit wizard (-> request
(assoc :multi-form-state (merge-multi-form-state multi-form-state)))))
(assoc :multi-form-state (merge-multi-form-state multi-form-state)))))
(wrap-ensure-step)))
(defn default-render-wizard [linear-wizard {:keys [multi-form-state form-errors snapshot current-step] :as request} & {:keys [form-params]}]
(let [current-step (get-current-step linear-wizard)
(defn default-render-wizard [linear-wizard {:keys [multi-form-state form-errors snapshot current-step] :as request} & {:keys [form-params render-timeline?]
:or {render-timeline? true}}]
(let [current-step (get-current-step (assoc linear-wizard :render-timeline? render-timeline?))
edit-path (edit-path current-step request)]
[:form#wizard-form form-params
(fc/start-form multi-form-state (when form-errors {:step-params form-errors})
@@ -311,15 +335,24 @@
(handler
(assoc request :wizard (hydrate-from-request linear-wizard request))))))
(defn open-wizard-handler [{:keys [wizard current-step] :as request}]
(modal-response
[:div {:x-data (hx/json {"transitionType" "none"
}
)
"@htmx:after-request" "if(event.detail.xhr.getResponseHeader('x-transition-type')) { $data.transitionType = event.detail.xhr.getResponseHeader('x-transition-type');}"
}
(render-wizard wizard request)]))
(defn open-wizard-handler [{:keys [wizard current-step query-params] :as request}]
(cond->
(modal-response
[:div#transitioner.flex-1 {:x-data (hx/json {"transitionType" "none"})
:x-ref "transitioner"
:class ""
"@htmx:after-request" "if(event.detail.xhr.getResponseHeader('x-transition-type')) {
$refs.transitioner.classList.remove('forward')
$refs.transitioner.classList.remove('backward');
$refs.transitioner.classList.add('group/transition')
$refs.transitioner.classList.add(event.detail.xhr.getResponseHeader('x-transition-type'));
} else {
$refs.transitioner.classList.remove('group/transition')
}
"}
(render-wizard wizard request)])
(get query-params :replace-modal) (assoc-in [:headers "hx-trigger"] "modalswap")))
@@ -346,4 +379,18 @@
[:maybe
:any]]]
(:form-params request)
main-transformer)))))
main-transformer)))))
#_(comment
(def f {"snapshot"
"{:invoices [{:invoice_id 17592297837035, :amount 23.0, :invoice {:db/id 17592297837035, :invoice/vendor {:db/id 17592186045722, :vendor/name \"Sysco\"}, :invoice/client {:db/id 17592232555238}, :invoice/outstanding-balance 23.0, :invoice/invoice-number \"702,34\"}} {:invoice_id 17592297837049, :amount 23.0, :invoice {:db/id 17592297837049, :invoice/vendor {:db/id 17592186045722, :vendor/name \"Sysco\"}, :invoice/client {:db/id 17592232555238}, :invoice/outstanding-balance 23.0, :invoice/invoice-number \"80[234234\"}}], :client 17592232555238}",
"edit-path" "[]",
"current-step" ":payment-details",
"mode" "advanced",
"step-params"
{"invoices"
{"0" {"invoice_id" "17592297837035", "amount" "1"},
"1" {"invoice_id" "17592297837049", "amount" "23.00"}}}})
(mc/decode [:map [:step-params {:optional true} [:maybe :any]]]
f
main-transformer))

View File

@@ -8,7 +8,7 @@
[auto-ap.ssr.svg :as svg]
[bidi.bidi :as bidi]))
(defn navbar- [{:keys [client-selection client identity clients]}]
(defn navbar- [{:keys [client-selection client identity clients dd-env]}]
[:nav {:class "fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700"}
[:div {:class "px-3 py-3 lg:px-5 lg:pl-3"}
[:div {:class "flex items-center justify-between"}
@@ -18,8 +18,9 @@
[: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"}]]]
[:a {:href "/" :class "flex ml-2 md:mr-24"}
[:img {:src "/img/logo-big2.png", :class "h-10 mr-16", :alt "Integreat logo"}]]]
[:a {:href "/" :class "flex ml-2 hidden md:mr-24 sm:inline"}
[:img {:src "/img/logo-big2.png", :class "h-10", :alt "Integreat logo"}]]
(when-not (= "prod" dd-env) [:div.rounded-full.bg-yellow-200.text-lg.text-yellow-800.px-4.hidden.md:block.mr-8 "environment: " dd-env])]
[:div {:class "flex items-center gap-4"}

View File

@@ -1,12 +1,12 @@
(ns auto-ap.ssr.components.page
(:require
[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.hx :as hx]))
(:require [auto-ap.ssr.components.aside :refer [left-aside-]]
[auto-ap.ssr.components.navbar :refer [navbar-]]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.svg :as svg]
[config.core :refer [env]]
[hiccup2.core :as hiccup]))
(defn page- [{:keys [nav page-specific client clients client-selection identity app-params] :or {app-params {}}} & children]
(defn page- [{:keys [nav page-specific client clients client-selection identity app-params request] :or {app-params {}} } & children]
[:div#app {"_" (hiccup/raw "
on notification from body 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"
@@ -15,11 +15,13 @@
(navbar- {:client-selection client-selection
:clients clients
:client client
:identity identity})
:identity identity
:dd-env (:dd-env env)})
[:div#app-contents.flex.pt-16.overflow-hidden (assoc app-params
:hx-disinherit "*"
:x-init "leftNavShow = true")
(left-aside- {:nav nav
(left-aside- {:nav (when nav
(nav request))
:page-specific page-specific})
[: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' : ''"

View File

@@ -2,7 +2,7 @@
(:require [auto-ap.ssr.hiccup-helper :as hh]
[auto-ap.ssr.hx :as hx]))
(defn radio- [{:keys [options name title size orientation] :or {size :medium} selected-value :value}]
(defn radio-card- [{:keys [options name title size orientation] :or {size :medium} selected-value :value}]
[:h3 {:class "mb-4 font-semibold text-gray-900 dark:text-white"} title]
[:ul {:class (cond-> "w-48 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white"
(= orientation :horizontal) (-> (hh/add-class "flex gap-2 flex-wrap")
@@ -26,6 +26,42 @@
(str " " "text-sm"))}
(= (cond-> selected-value (keyword? selected-value) clojure.core/name) value) (assoc :checked true))]
[:label {:for (str "list-" name "-" value)
:class
(cond-> "w-full ml-2 font-medium text-gray-900 dark:text-gray-300"
(= size :small)
(str " " "text-xs py-2")
(= size :medium)
(str " " "text-sm py-3")
(= orientation :horizontal)
(hh/remove-class "w-full"))} content]]])])
(defn radio-list- [{:keys [options name x-model title size orientation] :or {size :medium} selected-value :value}]
[:h3 {:class "mb-4 font-semibold text-gray-900 dark:text-white"} title]
[:ul {:class (cond-> "w-48 text-sm font-medium text-gray-900"
(= orientation :horizontal) (-> (hh/add-class "flex gap-2 flex-wrap")
(hh/remove-wildcard ["w-" "rounded-lg" "border" "bg-"])))}
(for [{:keys [value content]} options]
[:li {:class (cond-> "w-full"
(= orientation :horizontal) (-> (hh/remove-wildcard ["w-full" "rounded-"])
(hh/add-class "w-auto shrink-0 block px-3")))}
[:div {:class (cond-> "flex items-center"
(not= orientation :horizontal) (hh/add-class "pl-3"))}
[:input (cond-> {:id (str "list-" name "-" value)
:x-model x-model
:type "radio",
:value value
:name name
:class
(cond-> "w-4 h-4 text-blue-600"
(= size :small)
(str " " "text-xs")
(= size :medium)
(str " " "text-sm"))}
(= (cond-> selected-value (keyword? selected-value) clojure.core/name) value) (assoc :checked true))]
[:label {:for (str "list-" name "-" value)
:class
(cond-> "w-full ml-2 font-medium text-gray-900 dark:text-gray-300"
(= size :small)

View File

@@ -21,4 +21,4 @@
children))
(defn badge- [params & children]
[:div {:class (hh/add-class "absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-red-300 border-3 border-white rounded-full -top-2 -right-2 dark:border-gray-900" (:class params))} children])
[:div {:class (hh/add-class "absolute inline-flex items-center justify-center w-6 h-6 text-xs font-black text-white bg-red-300 border-3 border-white rounded-full -top-2 -right-2 dark:border-gray-900" (:class params))} children])

View File

@@ -40,7 +40,8 @@
children ]))
(defn vertical-timeline [params & children]
[:ol {:class "flex flex-col items-start space-y-2 text-xs text-center text-gray-500 bg-gray-100 dark:text-gray-400 sm:text-base dark:bg-gray-800 sm:space-y-4 px-2"}
[:ol {:class (hh/add-class "flex flex-col items-start space-y-2 text-xs text-center text-gray-500 bg-gray-100 dark:text-gray-400 sm:text-base dark:bg-gray-800 sm:space-y-4 px-2"
(:class params))}
children
#_[:li {:class "flex items-center"}
[:span {:class "flex items-center justify-center w-5 h-5 mr-2 text-xs border border-gray-500 rounded-full shrink-0 dark:border-gray-400"}]]])

View File

@@ -4,6 +4,7 @@
[auto-ap.routes.utils
:refer [wrap-admin wrap-client-redirect-unauthenticated wrap-secure]]
[auto-ap.ssr.account :as account]
[auto-ap.ssr.payments :as payments]
[auto-ap.ssr.admin :as admin]
[auto-ap.ssr.admin.accounts :as admin-accounts]
[auto-ap.ssr.admin.background-jobs :as admin-jobs]
@@ -28,6 +29,8 @@
[auto-ap.ssr.pos.refunds :as pos-refunds]
[auto-ap.ssr.pos.sales-orders :as pos-sales]
[auto-ap.ssr.pos.tenders :as pos-tenders]
[auto-ap.ssr.invoices :as invoice]
[auto-ap.ssr.outgoing-invoice.new :as oin]
[auto-ap.ssr.search :as search]
[auto-ap.ssr.transaction.insights :as insights]
[auto-ap.ssr.users :as users]
@@ -81,6 +84,7 @@
: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 invoice/key->handler)
(into import-batch/key->handler)
(into pos-sales/key->handler)
(into pos-expected-deposits/key->handler)
@@ -96,6 +100,7 @@
(into admin-vendors/key->handler)
(into admin-clients/key->handler)
(into admin-rules/key->handler)
(into indicators/key->handler)))
(into indicators/key->handler)
(into payments/key->handler)
(into oin/route->handler)))

View File

@@ -1,25 +1,32 @@
(ns auto-ap.ssr.grid-page-helper
(:require
[auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.query-params :as query-params]
[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.svg :as svg]
[auto-ap.ssr.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [html-response]]
[auto-ap.time :as atime]
[malli.core :as m]
[bidi.bidi :as bidi]
[cemerick.url :as url]
[clojure.string :as str]
[hiccup2.core :as hiccup]
[malli.transform :as mt2]
[auto-ap.ssr.hiccup-helper :as hh]))
(:require [auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.logging :as alog]
[auto-ap.query-params :as query-params]
[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.hiccup-helper :as hh]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [html-response main-transformer]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[cemerick.url :as url]
[clojure.string :as str]
[hiccup2.core :as hiccup]
[malli.core :as m]
[malli.transform :as mt2]
[malli.transform :as mt]
[taoensso.encore :refer [filter-vals]]))
(defn row* [gridspec user entity {:keys [flash? delete-after-settle? request class] :as options}]
(let [cells (->> gridspec
(let [cells (if (:check-boxes? gridspec)
[(com/data-grid-cell {} (com/checkbox {:name "id" :value ((:id-fn gridspec) entity)
:x-model "selected"}))]
[])
cells (->> gridspec
:headers
(filter (fn [h]
(if (and (:hide? h)
@@ -30,14 +37,15 @@
(com/data-grid-cell {:class (if-let [show-starting (:show-starting header)]
(format "hidden %s:table-cell" show-starting)
(:class header))}
((:render header) entity)))))
((:render header) entity))))
(into cells))
cells (conj cells (com/data-grid-right-stack-cell {}
(into [:form.flex.space-x-2
[:input {:type :hidden :name "id" :value ((:id-fn gridspec) entity)}]]
((:row-buttons gridspec) request entity))))] ;; TODO double check usage of row buttons user and identity in callers
(apply com/data-grid-row
(apply com/data-grid-row
{:class (cond-> (or class "")
flash? (hh/add-class "live-added"))
flash? (hh/add-class "live-added"))
"_" (hiccup/raw (when delete-after-settle?
" on htmx:afterSettle wait 400ms then remove me"))
@@ -52,24 +60,18 @@
(defn sort-by-list [grid-spec sort]
(if (seq sort)
(into
[:div.flex.gap-2.items-center
"sorted by"
(into
[:div.flex.gap-2.items-center
]
(for [{:keys [name sort-icon sort-key ]} sort]
[:div.py-1.px-3.text-sm.rounded.bg-gray-100.dark:bg-gray-600.flex.items-center.gap-2.relative name [:div.h-4.w-4.mr-3 sort-icon]
[:div {:class "absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white hover:scale-110 transition-all duration-300 bg-gray-400 border-2 border-white rounded-full -top-2 -right-2 dark:border-gray-900"}
[:a {:href (str (bidi/path-for ssr-routes/only-routes
(:route grid-spec)) "?remove-sort=" sort-key)
:hx-boost "true"
:hx-target (str "#" (:id grid-spec))
}
[:div.h-4.w-4 svg/x]]
]]
))
"sorted by"]
(for [{:keys [name sort-icon sort-key]} sort]
[:div.py-1.px-3.text-sm.rounded.bg-gray-100.dark:bg-gray-600.flex.items-center.gap-2.relative name [:div.h-4.w-4.mr-3 sort-icon]
[:div {:class "absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white hover:scale-110 transition-all duration-300 bg-gray-400 border-2 border-white rounded-full -top-2 -right-2 dark:border-gray-900"}
[:a {:hx-get (str (bidi/path-for ssr-routes/only-routes
(:route grid-spec)) "?remove-sort=" sort-key)
:href "#"
:hx-target (str "#" (:id grid-spec))}
[:div.h-4.w-4 svg/x]]]]))
"default sort"))
(defn table* [grid-spec user {{:keys [start per-page flash-id sort]} :parsed-query-params :as request}]
@@ -79,7 +81,9 @@
request)]
(com/data-grid-card {:id (:id grid-spec)
:title (:title grid-spec)
:title (if (string? (:title grid-spec))
(:title grid-spec)
((:title grid-spec) request))
:route (:route grid-spec)
:start start
:per-page per-page
@@ -87,7 +91,18 @@
:subtitle [:div.flex.items-center.gap-2
[:span (format "Total %s: %d, " (:entity-name grid-spec) total)]
(sort-by-list grid-spec sort)]
:action-buttons ((:action-buttons grid-spec) request)
:action-buttons (cond->> ((:action-buttons grid-spec) request)
(:check-boxes? grid-spec) (into [(com/pill {:color :primary
:x-show "selected.length > 0"}
[:div.flex.space-x-2.items-center
[:div
[:span {:x-text "selected.length" :x-show "!all_selected"}]
[:span {:x-show "all_selected"} "All"]
" selected"]
[:div.w-3.h-3
(com/link {"@click" "selected=[]; all_selected=false"}
svg/x)]])]))
:rows (for [entity entities]
(row* grid-spec user entity {:flash? (= flash-id ((:id-fn grid-spec) entity)) :request request}))
:thead-params {:hx-get (bidi/path-for ssr-routes/only-routes
@@ -97,37 +112,37 @@
:hx-trigger "sorted once"
:hx-vals "js:{\"toggle-sort\": event.detail.key || \"\"}"}
:headers
(conj
(->> grid-spec
:headers
(map
(fn [h]
(cond
(and (:hide? h)
((:hide? h) request))
nil
(conj
(->> grid-spec
:headers
(map
(fn [h]
(cond
(and (:hide? h)
((:hide? h) request))
nil
(:sort-key h)
(com/data-grid-sort-header {:class (if-let [show-starting (:show-starting h)]
(format "hidden %s:table-cell" show-starting)
(:class h))
:sort-key (:sort-key h)}
(:sort-key h)
(com/data-grid-sort-header {:class (if-let [show-starting (:show-starting h)]
(format "hidden %s:table-cell" show-starting)
(:class h))
:sort-key (:sort-key h)}
[:div.flex.gap-4.items-center
(:name h)
[:div.h-6.w-6.text-gray-400.dark:text-gray-500 (sort-icon sort (:sort-key h))]])
[:div.flex.gap-4.items-center
(:name h)
[:div.h-6.w-6.text-gray-400.dark:text-gray-500 (sort-icon sort (:sort-key h))]])
:else
(com/data-grid-header {:class (if-let [show-starting (:show-starting h)]
(format "hidden %s:table-cell" show-starting)
(:class h))
:sort-key (:sort-key h)}
(:name h))
)))
(filter identity)
(into []))
(com/data-grid-header {}))})))
:else
(com/data-grid-header {:class (if-let [show-starting (:show-starting h)]
(format "hidden %s:table-cell" show-starting)
(:class h))
:sort-key (:sort-key h)}
(:name h)))))
(filter identity)
(into (if (:check-boxes? grid-spec)
[(com/data-grid-checkbox-header {:name "all" :value "all" :x-model "all_selected"})]
[])))
(com/data-grid-header {}))})))
(defn sort->query [s]
@@ -137,47 +152,47 @@
s)))
(defn default-unparse-query-params [query-params]
(reduce
(fn [query-params [k value]]
(assoc query-params k
(cond (= k :sort)
(sort->query value)
(reduce
(fn [query-params [k value]]
(assoc query-params k
(cond (= k :sort)
(sort->query value)
(instance? org.joda.time.base.AbstractInstant value)
(atime/unparse-local value atime/normal-date)
(instance? org.joda.time.base.AbstractInstant value)
(atime/unparse-local value atime/normal-date)
(instance? Long value)
(str value)
(instance? Long value)
(str value)
(instance? Double value)
(format "%.2f" value)
(instance? Double value)
(format "%.2f" value)
(instance? Float value)
(format "%.2f" value)
(instance? Float value)
(format "%.2f" value)
(keyword? value)
(name value)
(keyword? value)
(name value)
(and (map? value)
(:db/id value))
(:db/id value)
(and (map? value)
(:db/id value))
(:db/id value)
:else
value)))
query-params
query-params))
:else
value)))
query-params
query-params))
(defn default-parse-query-params [grid-spec]
(comp
(query-params/apply-remove-sort)
(query-params/apply-toggle-sort grid-spec)
(query-params/apply-date-range :date-range :start-date :end-date)
(query-params/parse-key :exact-match-id query-params/parse-long)
(query-params/parse-key :sort #(query-params/parse-sort grid-spec %))
(query-params/parse-key :per-page query-params/parse-long)
(query-params/parse-key :start query-params/parse-long)
(query-params/parse-key :start-date query-params/parse-date)
(query-params/parse-key :end-date query-params/parse-date)))
(query-params/apply-remove-sort)
(query-params/apply-toggle-sort grid-spec)
(query-params/apply-date-range :date-range :start-date :end-date)
(query-params/parse-key :exact-match-id query-params/parse-long)
(query-params/parse-key :sort #(query-params/parse-sort grid-spec %))
(query-params/parse-key :per-page query-params/parse-long)
(query-params/parse-key :start query-params/parse-long)
(query-params/parse-key :start-date query-params/parse-date)
(query-params/parse-key :end-date query-params/parse-date)))
(defn wrap-trim-client-ids [handler]
(fn trim-client-ids [request]
@@ -196,10 +211,25 @@
(let [unparse-query-params (or (:unparse-query grid-spec)
default-unparse-query-params)]
(html-response (table*
grid-spec
identity
request)
:headers {"hx-push-url" (str "?" (url/map->query (unparse-query-params (:parsed-query-params request))))}
grid-spec
identity
request)
:headers {"hx-push-url" (str "?" (url/map->query
(dissoc (if (:query-schema grid-spec)
(do
(alog/peek ::setup4
(pr-str (update (filter-vals #(not (nil? %))
(m/encode (:query-schema grid-spec)
(:query-params request)
main-transformer))
"sort" sort->query)))
(update (filter-vals #(not (nil? %))
(m/encode (:query-schema grid-spec)
(:query-params request)
main-transformer))
"sort" sort->query))
(unparse-query-params (:parsed-query-params request)))
"selected" "all-selected")))} ;; TODO seems hacky to special case selected and all-selected here
:oob (when-let [oob-render (:oob-render grid-spec)]
(oob-render request)))))
(wrap-trim-client-ids)
@@ -208,22 +238,28 @@
(wrap-secure)
(wrap-client-redirect-unauthenticated)))
(defn page-route [grid-spec ]
(defn page-route [grid-spec]
(-> (fn page [{:keys [identity] :as request}]
(base-page
request
(com/page {:nav (:nav grid-spec)
:page-specific (when-let [page-specific-nav (:page-specific-nav grid-spec)]
[:div#page-specific-nav (page-specific-nav request)])
:client-selection (:client-selection (:session request))
:clients (:clients request)
:client (:client request)
:identity (:identity request)}
(apply com/breadcrumbs {} (:breadcrumbs grid-spec))
request
(com/page {:nav (:nav grid-spec)
:page-specific (when-let [page-specific-nav (:page-specific-nav grid-spec)]
[:div#page-specific-nav (page-specific-nav request)])
:client-selection (:client-selection (:session request))
:clients (:clients request)
:client (:client request)
:identity (:identity request)
:request request}
(apply com/breadcrumbs {} (:breadcrumbs grid-spec))
[:div {:x-data (hx/json {:selected [] :all_selected false})
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
:x-init "$watch('selected', s=> $dispatch('selectedChanged', {selected: s, all_selected: all_selected}) );
$watch('all_selected', a=>$dispatch('selectedChanged', {selected: selected, all_selected: a}))"}
(table* grid-spec
identity
request))
(:title grid-spec)))
request)])
(:title grid-spec)))
(wrap-trim-client-ids)
(query-params/wrap-parse-query-params (or (:parse-query-params grid-spec)
(default-parse-query-params grid-spec)))
@@ -235,12 +271,15 @@
(def header-spec (m/schema [:map
[:key :string]
[:name :string]
[:header-class {:optional true} [:maybe :string]]
[:sort-key {:optional true} :string]
[:render [:=> [:cat entity-spec] :any]]
[:hide? {:optional true} [:=> [:cat entity-spec] :boolean]]]))
(def grid-spec (m/schema [:map
[:id :string]
[:nav vector?]
[:nav [:=>
[:cat request-spec]
vector?]]
[:page-specific-nav
{:optional true
:default (fn [request])}
@@ -264,10 +303,12 @@
{:optional true
:default (fn [request])}
[:=>
[:cat request-spec]
vector?]]
[:cat request-spec]
vector?]]
[:breadcrumbs [:vector vector?]]
[:title :string]
[:title [:or :string
[:=> [:cat [:map-of :keyword :any]]
:string]]]
[:entity-name :string]
[:route :keyword]
[:action-buttons
@@ -290,4 +331,13 @@
(m/explain grid-spec grid-page))))
(m/decode grid-spec grid-page (mt2/default-value-transformer {::mt2/add-optional-keys true})))
(defn wrap-apply-sort [handler grid-spec]
(fn apply-sort [request]
(handler (update request :query-params
(fn [qp]
((comp
(query-params/apply-remove-sort)
(query-params/apply-toggle-sort grid-spec)
(query-params/parse-key :sort #(query-params/parse-sort grid-spec %)))
qp))))))

View File

@@ -20,13 +20,13 @@
(str/join ", " triggers))
(defn alpine-appear [m]
(assoc m
"x-transition:enter" "transition duration-500"
(assoc m
"x-transition:enter" "transition-opacity duration-500"
"x-transition:enter-start" "opacity-0"
"x-transition:enter-end" "opacity-100"))
(defn alpine-disappear [m]
(assoc m
(assoc m
"x-transition:leave" "transition duration-500"
"x-transition:leave-start" "opacity-100"
"x-transition:leave-end" "opacity-0"))
@@ -41,12 +41,15 @@
(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))
(fn [[field alpine-field]]
(format "\"%s\": $data.%s || ''" field alpine-field))
field->alpine-field)))))
field->alpine-field)))))
(defn trigger-click-or-enter [m]
(assoc m :hx-trigger "click, keyup[keyCode==13]"))

View File

@@ -1,8 +1,8 @@
(ns auto-ap.ssr.indicators
(:require [auto-ap.routes.indicators :as route]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.utils :refer [html-response wrap-schema-enforce]]
[auto-ap.time :as atime]
[auto-ap.ssr.utils :refer [clj-date-schema html-response
wrap-schema-enforce]]
[clj-time.coerce :as c]
[clj-time.core :as t]))
@@ -32,9 +32,5 @@
(def key->handler
{::route/days-ago (wrap-schema-enforce days-ago
:query-schema
[:map [:date {:optional false
:decode/arbitrary (fn [m]
(if (string? m)
(c/to-date (atime/parse m atime/normal-date))
m))}
inst?]])})
[:map [:date {:optional false}
clj-date-schema ]])})

View File

@@ -0,0 +1,25 @@
(ns auto-ap.ssr.invoice.common)
(def default-read '[:db/id
:invoice/invoice-number
:invoice/total
:invoice/outstanding-balance
:invoice/source-url
[:invoice/date :xform clj-time.coerce/from-date]
[:invoice/due :xform clj-time.coerce/from-date]
[:invoice/scheduled-payment :xform clj-time.coerce/from-date]
{:invoice/client [:client/code :db/id :client/name]
:invoice/expense-accounts [* {:invoice-expense-account/account [:account/name :db/id
:account/location
{:account/client-overrides [:account-client-override/name
{:account-client-override/client [:db/id]}]}]}]
[:transaction/_invoices :as :invoice/transaction] [:db/id]
[:journal-entry/_original-entity :as :invoice/journal-entry] [:db/id]
[:payment/_invoices :as :invoice/payments] [:db/id :payment/date :payment/amount
{[:transaction/_payment :as :payment/transaction] [:db/id]
[:payment/status :xform iol-ion.query/ident] [:db/ident]}]
[:invoice/status :xform iol-ion.query/ident] [:db/ident]
:invoice/vendor [:vendor/name :db/id]}])

View File

@@ -420,7 +420,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
:method request-method)
(base-page
request
(com/page {:nav (com/admin-aside-nav)
(com/page {:nav com/admin-aside-nav
:client-selection (:client-selection (:session request))
:client (:client request)
:identity (:identity request)

View File

@@ -0,0 +1,843 @@
(ns auto-ap.ssr.invoice.new-invoice-wizard
(:require [auto-ap.datomic
:refer [audit-transact conn pull-attr]]
[auto-ap.datomic.accounts :as d-accounts]
[auto-ap.datomic.invoices :as d-invoices]
[auto-ap.graphql.utils :refer [assert-can-see-client
assert-not-locked exception->4xx]]
[auto-ap.logging :as alog]
[auto-ap.routes.invoice :as route]
[auto-ap.routes.utils
:refer [wrap-client-redirect-unauthenticated]]
[auto-ap.rule-matching :as rm]
[auto-ap.solr :as solr]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.components.multi-modal :as mm]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.invoice.common :refer [default-read]]
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils
:refer [->db-id apply-middleware-to-all-handlers clj-date-schema
entity-id form-validation-error html-response money strip
wrap-schema-enforce]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[datomic.api :as dc]
[hiccup.util :as hu]
[iol-ion.query :refer [dollars=]]
[iol-ion.utils :refer [random-tempid]]
[malli.core :as mc]
[malli.util :as mut]))
(defn get-vendor [vendor-id]
(dc/pull
(dc/db conn)
[:vendor/terms
:vendor/automatically-paid-when-due
{:vendor/default-account d-accounts/default-read
:vendor/account-overrides
[:vendor-account-override/client
{:vendor-account-override/account d-accounts/default-read}]}
{:vendor/terms-overrides
[:vendor-terms-override/client :vendor-terms-override/terms]}]
vendor-id))
(defn check-invoice-expense-account-location [iea]
(let [account-location (pull-attr (dc/db conn) :account/location (:invoice-expense-account/account iea))]
(when (and (seq account-location)
(not= (:invoice-expense-account/location iea)
account-location))
(throw (ex-info "Exception." {:type (str "expected " account-location)})))
(when (and (empty? account-location)
(= "A" (:invoice-expense-account/location iea)))
(throw (ex-info "Exception." {:type "'A' not allowed"})))
true))
(defn check-allowance [account-id]
(let [allowance (:account/invoice-allowance (dc/pull (dc/db conn) '[{[:account/invoice-allowance :xform iol-ion.query/ident]
[:db/ident]}]
account-id))]
(not= :allowance/denied
allowance)))
(defn check-vendor-default-account [vendor-id]
(some? (:vendor/default-account (get-vendor vendor-id))))
(def new-form-schema
[:map
[:db/id {:optional true} [:maybe entity-id]]
[:customize-due-and-scheduled? {:optional true :default false :decode/arbitrary (fn [x] (if (= "" x)
false
x))} [:maybe :boolean]]
[:customize-accounts {:optional true :default :default} [:enum :default :customize]]
[:invoice/client {:optional true} [:maybe entity-id]]
[:invoice/date clj-date-schema]
[:invoice/due {:optional true} [:maybe clj-date-schema]]
[:invoice/scheduled-payment {:optional true} [:maybe clj-date-schema]]
[:invoice/vendor {:optional true}
[:and entity-id
[:fn {:error/message "Vendor is missing default expense account"}
check-vendor-default-account]]]
[:invoice/invoice-number {:optional true} [:string {:min 1 :decode/string strip}]]
[:invoice/total money]
[:invoice/expense-accounts
[:vector {:coerce? true}
[:and
[:map
[:invoice-expense-account/account [:and entity-id
[:fn {:error/message "Not an allowed account."}
check-allowance]]]
[:invoice-expense-account/location :string]
[:invoice-expense-account/amount :double]]
[:fn {:error/fn (fn [r x] (:type r))
:error/path [:invoice-expense-account/location]} check-invoice-expense-account-location]]]]])
(defn wrap-schema [s]
[:and s
[:fn (fn [{:keys [:db/id :invoice/invoice-number :invoice/vendor :invoice/client] :as z}]
(if id
true
(and invoice-number vendor client)))]])
(defn clientize-vendor [{:vendor/keys [terms-overrides automatically-paid-when-due default-account account-overrides] :as vendor} client-id]
(if (nil? vendor)
nil
(let [terms-override (->> terms-overrides
(filter (fn [to]
(= (->db-id (:vendor-terms-override/client to))
client-id)))
(map :vendor-terms-override/terms)
first)
account (or (->> account-overrides
(filter (fn [to]
(= (->db-id (:vendor-account-override/client to))
client-id)))
(map :vendor-account-override/account)
first)
default-account)
account (d-accounts/clientize account client-id)
automatically-paid-when-due (->> automatically-paid-when-due
(filter (fn [to]
(= (->db-id to)
client-id)))
seq
boolean)
vendor (cond-> vendor
terms-override (assoc :vendor/terms terms-override)
true (assoc :vendor/automatically-paid-when-due automatically-paid-when-due
:vendor/default-account account)
true (dissoc :vendor/account-overrides :vendor/terms-overrides))]
vendor)))
(defn account-prediction* [{:keys [multi-form-state]}]
(let [vendor (clientize-vendor (get-vendor (:invoice/vendor (:step-params multi-form-state)))
(->db-id (:invoice/client (:step-params multi-form-state))))
account-name (:account/name (:vendor/default-account vendor))
value (mm/get-mfs-field multi-form-state :customize-accounts)]
(when vendor
(com/radio-list {:name "step-params[customize-accounts]"
:value (name value)
:options (filter identity
[(when account-name {:value (name :default)
:content (com/pill {:color :primary} account-name)})
{:value (name :customize)
:content [:div "Customize accounts"]}])}))))
(defrecord BasicDetailsStep [linear-wizard]
mm/ModalWizardStep
(step-name [_]
"Basic Details")
(step-key [_]
:basic-details)
(edit-path [_ _]
[])
(step-schema [_]
(wrap-schema (mut/select-keys (mm/form-schema linear-wizard) #{:invoice/client :invoice/vendor :invoice/date :invoice/due :invoice/scheduled-payment :invoice/total :invoice/invoice-number :db/id :customize-due-and-scheduled? :customize-accounts})))
(render-step
[this {:keys [multi-form-state] :as request}]
(let [extant? (mm/get-mfs-field multi-form-state :db/id)]
(mm/default-render-step
linear-wizard this
:head [:div.p-2 (if extant?
"Edit invoice"
"New invoice")]
:body (mm/default-step-body
{}
[:div {:x-data (hx/json {:clientId (or (fc/field-value (:invoice/client fc/*current*))
(:db/id (:client request)))
:vendorId (fc/field-value (:invoice/vendor fc/*current*))
:date (-> (fc/field-value (:invoice/date fc/*current*))
(atime/unparse-local atime/normal-date))
:due (some-> (fc/field-value (:invoice/due fc/*current*))
(atime/unparse-local atime/normal-date))
:scheduledPayment (some-> (fc/field-value (:invoice/scheduled-payment fc/*current*))
(atime/unparse-local atime/normal-date))
:customizeDueAndScheduled (fc/field-value (:customize-due-and-scheduled? fc/*current*))})}
(fc/with-field :db/id
(when extant?
(com/hidden {:name (fc/field-name)
:value (fc/field-value)})))
(fc/with-field :customize-due-and-scheduled?
(com/hidden {:name (fc/field-name)
:value (fc/field-value)
:x-model "customizeDueAndScheduled"}))
(fc/with-field :invoice/client
(if (or (:client request) extant?)
(com/hidden {:name (fc/field-name)
:value (or (mm/get-mfs-field multi-form-state :invoice/client)
(:db/id (:client request)))})
(com/validated-field
{:label "Client"
:errors (fc/field-errors)}
[:div.w-96
(com/typeahead {:name (fc/field-name)
:error? (fc/error?)
:class "w-96"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :company-search)
:value (fc/field-value)
:content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))
:x-model "clientId"})])))
(fc/with-field :invoice/vendor
(com/validated-field
{:label "Vendor"
:errors (fc/field-errors)}
[:div.w-96
(com/typeahead {:name (fc/field-name)
:error? (fc/error?)
:disabled (boolean (-> request :multi-form-state :snapshot :db/id))
:class "w-96"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
:value (fc/field-value)
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))
:x-model "vendorId"})]))
[:div.flex.items-center.gap-2
(fc/with-field :invoice/date
(com/validated-field
{:label "Date"
:errors (fc/field-errors)}
[:div {:class "w-24"}
(com/date-input {:value (some-> (fc/field-value)
(atime/unparse-local atime/normal-date))
:name (fc/field-name)
:error? (fc/field-errors)
:x-model "date"
:placeholder "1/1/2024"})]))
[:div {:x-show "!customizeDueAndScheduled"}
(com/link {"@click" "customizeDueAndScheduled=true"
:x-show "!due && !scheduledPayment"}
"Add due / scheduled payment date")
(com/link {"@click" "customizeDueAndScheduled=true"
:x-show "due || scheduledPayment"}
"Change due / scheduled payment date")]]
(fc/with-field :invoice/due
(com/validated-field
(hx/alpine-appear {:label "Due (optional)"
:errors (fc/field-errors)
:x-show "customizeDueAndScheduled"})
[:div {:class "w-24"
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/due-date)
:x-dispatch:changed "[clientId, vendorId, date]"
:hx-trigger "changed"
:hx-target "this"
:hx-swap "innerHTML"}
(com/date-input {:value (some-> (fc/field-value)
(atime/unparse-local atime/normal-date))
:name (fc/field-name)
:x-model "due"
:error? (fc/field-errors)
:placeholder "1/1/2024"})]))
(fc/with-field :invoice/scheduled-payment
(com/validated-field
(hx/alpine-appear {:label "Scheduled payment (optional)"
:errors (fc/field-errors)
:x-show "customizeDueAndScheduled"})
[:div {:class "w-24"}
[:div {:class "w-24"
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/scheduled-payment-date)
:x-dispatch:changed "[clientId, vendorId, due]"
:hx-trigger "changed"
:hx-target "this"
:hx-swap "innerHTML"}
(com/date-input {:value (some-> (fc/field-value)
(atime/unparse-local atime/normal-date))
:name (fc/field-name)
:error? (fc/field-errors)
:placeholder "1/1/2024"})]]))
(fc/with-field :invoice/invoice-number
(com/validated-field
{:label "Invoice Number"
:errors (fc/field-errors)}
[:div {:class "w-24"}
(com/text-input {:value (-> (fc/field-value))
:disabled (boolean (-> request :multi-form-state :snapshot :db/id))
:name (fc/field-name)
:error? (fc/field-errors)
:placeholder "HA-123"})]))
(fc/with-field :invoice/total
(com/validated-field
{:label "Total"
:errors (fc/field-errors)}
[:div {:class "w-16"}
(com/money-input {:value (-> (fc/field-value))
:name (fc/field-name)
:class "w-24"
:error? (fc/field-errors)
:placeholder "212.44"})]))
[:div#expense-account-prediction
(hx/alpine-appear
{:x-dispatch:bryce "[vendorId]"
:hx-trigger "bryce"
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/account-prediction)
:hx-target "this"
:hx-swap "innerHTML"})
(account-prediction* request)]])
:footer
(mm/default-step-footer linear-wizard this :validation-route ::route/new-wizard-navigate
:next-button (com/button {:color :primary :x-ref "next" :class "w-32"
:hx-put (bidi.bidi/path-for ssr-routes/only-routes
::route/new-wizard-navigate)} "Save"))
:validation-route ::route/new-wizard-navigate)))
mm/CustomNext
(custom-next-handler
[_ request]
(if (= (get-in request [:multi-form-state :step-params :customize-accounts])
:customize)
(mm/navigate-handler {:request request
:to-step :accounts})
(html-response [:div]
:headers {"location" (bidi.bidi/path-for ssr-routes/only-routes ::route/new-invoice-submit)}
:status 308)
#_(mm/navigate-handler {:request request
:to-step :next-steps}))))
(defn- location-select*
[{:keys [name account-location client-locations value]}]
(let [options (into (cond account-location
[[account-location account-location]]
(seq client-locations)
(into [["Shared" "Shared"]]
(for [cl client-locations]
[cl cl]))
:else
[["Shared" "Shared"]]))]
(com/select {:options options
:name name
:value (ffirst options)
:class "w-full"})))
(defn- account-typeahead*
[{:keys [name value client-id x-model]}]
[:div.flex.flex-col
(com/typeahead {:name name
:placeholder "Search..."
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
{:client-id client-id
:purpose "invoice"})
: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- invoice-expense-account-row*
[{:keys [value client-id]}]
(com/data-grid-row
(-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? value))))
:accountId (fc/field-value (:invoice-expense-account/account value))})
:data-key "show"
:x-ref "p"}
hx/alpine-mount-then-appear)
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
:value (fc/field-value)}))
(fc/with-field :invoice-expense-account/account
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)}
(account-typeahead* {:value (fc/field-value)
:client-id client-id
:name (fc/field-name)
:x-model "accountId"}))))
(fc/with-field :invoice-expense-account/location
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)
:x-hx-val:account-id "accountId"
:hx-vals (hx/json {:name (fc/field-name)
:client-id client-id})
:x-dispatch:changed "accountId"
:hx-trigger "changed"
:hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select)
:hx-target "find *"
:hx-swap "outerHTML"}
(location-select* {:name (fc/field-name)
:account-location (:account/location (cond->> (:invoice-expense-account/account @value)
(nat-int? (:invoice-expense-account/account @value)) (dc/pull (dc/db conn)
'[:account/location])))
:client-locations (pull-attr (dc/db conn) :client/locations client-id)
:value (fc/field-value)}))))
(fc/with-field :invoice-expense-account/amount
(com/data-grid-cell
{}
(com/validated-field
{:errors (fc/field-errors)}
(com/money-input {:name (fc/field-name)
:class "w-16 amount-field"
:value (fc/field-value)}))))
(com/data-grid-cell {:class "align-top"}
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
(defn invoice-expense-account-total* [request]
(format "$%,.2f" (->> (-> request
:multi-form-state
:step-params
:invoice/expense-accounts)
(map (fnil :invoice-expense-account/amount 0.0))
(filter number?)
(reduce + 0.0))))
(defn invoice-expense-account-total [request]
(html-response (invoice-expense-account-total* request)))
(defrecord AccountsStep [linear-wizard]
mm/ModalWizardStep
(step-name [_]
"Expense Accounts")
(step-key [_]
:accounts)
(edit-path [_ _]
[])
(step-schema [_]
(mut/select-keys (mm/form-schema linear-wizard) #{:invoice/expense-accounts}))
(render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}]
(mm/default-render-step
linear-wizard this
:head [:div.p-2 "Invoice accounts "]
:body (mm/default-step-body
{}
[:div {}
(pull-attr (dc/db conn) :client/name (:invoice/client snapshot))
(fc/with-field :invoice/expense-accounts
(com/validated-field
{:errors (fc/field-errors)}
(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 #(invoice-expense-account-row* {:value %
:client-id (:invoice/client snapshot)}))
(com/data-grid-new-row {:colspan 4
:hx-get (bidi/path-for ssr-routes/only-routes
::route/new-wizard-new-account)
:row-offset 0
:index (count (fc/field-value))
:tr-params {:hx-vals (hx/json {:client-id (:invoice/client snapshot)})}}
"New account")
(com/data-grid-row {}
(com/data-grid-cell {})
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"])
(com/data-grid-cell {:id "total"
:class "text-right"
:hx-trigger "change from:closest form target:.amount-field"
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/expense-account-total)
:hx-target "this"
:hx-swap "innerHTML"}
(invoice-expense-account-total* request))
(com/data-grid-cell {}))
(com/data-grid-row {}
(com/data-grid-cell {})
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "INVOICE TOTAL"])
(com/data-grid-cell {:class "text-right"}
(format "$%,.2f" (:invoice/total snapshot)))
(com/data-grid-cell {})))))])
:footer
(mm/default-step-footer linear-wizard this :validation-route ::route/new-wizard-navigate
:next-button (com/button {:color :primary :x-ref "next" :class "w-32"} "Save"))
:validation-route ::route/new-wizard-navigate))
mm/Initializable
(init-step-params
[_ current request]
(if (not (seq (:invoice/expense-accounts (:step-params current))))
(assoc (:step-params current) :invoice/expense-accounts [{:db/id "123"
:invoice-expense-account/location "Shared"
:invoice-expense-account/account (:db/id (:vendor/default-account (clientize-vendor (get-vendor (->db-id (:invoice/vendor (:snapshot current))))
(->db-id (:invoice/client (:snapshot current))))))
:invoice-expense-account/amount (:invoice/total (:step-params current))}])
(:step-params current))))
(defrecord NextSteps [linear-wizard]
mm/ModalWizardStep
(step-name [_]
"Next Steps")
(step-key [_]
:next-steps)
(edit-path [_ _]
[])
(step-schema [_]
(mut/select-keys (mm/form-schema linear-wizard) #{}))
(render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}]
(mm/default-render-step
linear-wizard this
:head [:div.p-2 "Invoice accounts "]
:body (mm/default-step-body
{}
[:p.text-lg "Would you like to pay this invoice now?"]
(com/navigation-button-list {}
(com/navigation-button (-> {:class "w-48"
:hx-get (hu/url (bidi.bidi/path-for ssr-routes/only-routes ::route/pay-wizard)
{:selected (:db/id snapshot)
:replace-modal true})}
hx/trigger-click-or-enter) "Pay now")
(com/navigation-button (-> {:class "w-48"
:hx-get (hu/url (bidi.bidi/path-for ssr-routes/only-routes ::route/new-wizard)
{:replace-modal true})}
hx/trigger-click-or-enter) "Add another")
(com/navigation-button {:class "w-48" :next-arrow? false
"@click" "$dispatch('modalclose') "
"@keyup.enter.stop" "$dispatch('modalclose')"}
"Close")))
:footer
nil
:validation-route ::route/new-wizard-navigate)))
(defn assert-no-conflicting [{:invoice/keys [invoice-number client vendor]}]
(when (seq (d-invoices/find-conflicting {:invoice/invoice-number invoice-number
:invoice/vendor (->db-id vendor)
:invoice/client (->db-id client)}))
(form-validation-error (str "Invoice '" invoice-number "' already exists."))))
(defn assert-invoice-amounts-add-up [{:keys [:invoice/expense-accounts :invoice/total]}]
(let [expense-account-total (reduce + 0 (map (fn [x] (:invoice-expense-account/amount x)) expense-accounts))]
(when-not (dollars= total expense-account-total)
(form-validation-error (str "Expense account total (" expense-account-total ") does not equal invoice total (" total ")")))))
(defn- calculate-spread
"Helper function to calculate the amount to be assigned to each location"
[shared-amount total-locations]
(let [base-amount (int (/ shared-amount total-locations))
remainder (- shared-amount (* base-amount total-locations))]
{:base-amount base-amount
:remainder remainder}))
(defn- spread-expense-account
"Spreads the expense account amount across the given locations"
[locations expense-account]
(if (= "Shared" (:invoice-expense-account/location expense-account))
(let [{:keys [base-amount remainder]} (calculate-spread (:invoice-expense-account/amount expense-account) (count locations))]
(map-indexed (fn [idx _]
(assoc expense-account
:invoice-expense-account/amount (+ base-amount (if (< idx remainder) 1 0))
:invoice-expense-account/location (nth locations idx)))
locations))
[expense-account]))
(defn $->cents [x]
(int
(let [result (* 100M (bigdec x))]
(.setScale result 0 java.math.BigDecimal/ROUND_HALF_UP))))
(defn cents->$ [x]
(double
(let [result (* 0.01M (bigdec x))]
(.setScale result 2 java.math.BigDecimal/ROUND_HALF_UP))))
(defn- apply-total-delta-to-account [invoice-total eas]
(when (seq eas)
(let [leftover (- invoice-total (reduce + 0 (map :invoice-expense-account/amount eas)))
leftover-beyond-a-single-cent? (or (< leftover -1)
(> leftover 1))
leftover (if leftover-beyond-a-single-cent?
0
leftover)
[first-eas & rest] eas]
(cons
(update first-eas :invoice-expense-account/amount #(+ % leftover))
rest))))
(defn maybe-spread-locations
"Converts any expense account for a \"Shared\" location into a separate expense account for all valid locations for that client"
([invoice]
(maybe-spread-locations invoice (pull-attr (dc/db conn) :client/locations (:invoice/client invoice))))
([invoice locations]
(update-in invoice
[:invoice/expense-accounts]
(fn [expense-accounts]
(->> expense-accounts
(map (fn [ea] (update ea :invoice-expense-account/amount $->cents)))
(mapcat (partial spread-expense-account locations))
(apply-total-delta-to-account ($->cents (:invoice/total invoice)))
(map (fn [ea] (update ea :invoice-expense-account/amount cents->$))))))))
(defrecord NewWizard2 [_ current-step]
mm/LinearModalWizard
(hydrate-from-request
[this request]
this)
(navigate [this step-key]
(assoc this :current-step step-key))
(get-current-step [this]
(if current-step
(mm/get-step this current-step)
(mm/get-step this :basic-details)))
(render-wizard [this {:keys [multi-form-state] :as request}]
(mm/default-render-wizard
this request
:form-params
(-> mm/default-form-props
(assoc :hx-post
(str (bidi/path-for ssr-routes/only-routes ::route/new-invoice-submit))))
:render-timeline? false))
(steps [_]
[:basic-details
:accounts
:next-steps])
(get-step [this step-key]
(let [step-key-result (mc/parse mm/step-key-schema step-key)
[step-key-type step-key] step-key-result]
(get {:basic-details (->BasicDetailsStep this)
:accounts (->AccountsStep this)
:next-steps (->NextSteps this)}
step-key)))
(form-schema [_]
new-form-schema)
(submit [this {:keys [multi-form-state request-method identity] :as request}]
(let [invoice (:snapshot multi-form-state)
_ (alog/peek invoice)
extant? (:db/id invoice)
client-id (->db-id (:invoice/client invoice))
vendor-id (->db-id (:invoice/vendor invoice))
paid-amount (if-let [outstanding-balance
(and extant?
(-
(pull-attr (dc/db conn)
:invoice/total
(:db/id invoice))
(pull-attr (dc/db conn)
:invoice/outstanding-balance
(:db/id invoice))))]
outstanding-balance
0.0)
outstanding-balance (- (or
(:invoice/total (:step-params multi-form-state))
(:invoice/total (:snapshot multi-form-state)))
paid-amount)
transaction [:upsert-invoice (-> multi-form-state
:snapshot
(assoc :db/id (or (:db/id invoice) "invoice"))
(dissoc :customize-due-and-scheduled? :invoice/journal-entry :invoice/payments :customize-accounts)
(assoc :invoice/expense-accounts (if (= :customize (:customize-accounts invoice))
(-> multi-form-state :step-params :invoice/expense-accounts)
[{:db/id "123"
:invoice-expense-account/location "Shared"
:invoice-expense-account/account (:db/id (:vendor/default-account (clientize-vendor (get-vendor vendor-id)
client-id)))
:invoice-expense-account/amount (or (:invoice/total (:step-params multi-form-state))
(:invoice/total (:snapshot multi-form-state)))}]))
(assoc
:invoice/outstanding-balance outstanding-balance
:invoice/import-status :import-status/imported
:invoice/status (if (dollars= 0.0 outstanding-balance)
:invoice-status/paid
:invoice-status/unpaid))
(maybe-spread-locations)
(update :invoice/date coerce/to-date)
(update :invoice/due coerce/to-date)
(update :invoice/scheduled-payment coerce/to-date))]]
(assert-invoice-amounts-add-up (second transaction))
(when-not extant?
(assert-no-conflicting invoice))
(exception->4xx #(assert-can-see-client (:identity request) client-id))
(exception->4xx #(assert-not-locked client-id (:invoice/date invoice)))
(let [transaction-result (audit-transact [transaction] (:identity request))]
(solr/touch-with-ledger (get-in transaction-result [:tempids "invoice"]))
(if extant?
(html-response
(@(resolve 'auto-ap.ssr.invoices/row*) identity (dc/pull (dc/db conn) default-read (:db/id invoice)) {:flash? true
:request request})
:headers (cond-> {"hx-trigger" "modalclose"
"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id invoice))
"hx-reswap" "outerHTML"}))
(assoc-in (mm/navigate-handler {:request (assoc-in request [:multi-form-state :snapshot :db/id] (get-in transaction-result [:tempids "invoice"]))
:to-step :next-steps})
[:headers "hx-trigger"] "invalidated"))))))
(def new-wizard (->NewWizard2 nil nil))
(defn initial-new-wizard-state [request]
(mm/->MultiStepFormState {:invoice/date (time/now)
:customize-accounts :default}
[]
{:invoice/date (time/now)
:customize-accounts :default}))
(defn initial-edit-wizard-state [request]
(let [entity (dc/pull (dc/db conn) default-read (:db/id (:route-params request)))
entity (select-keys entity (mut/keys new-form-schema))]
(mm/->MultiStepFormState (assoc entity
:customize-accounts :customize)
[]
(assoc entity
:customize-accounts :customize))))
(defn location-select [{{:keys [name account-id client-id value] :as qp} :query-params}]
(html-response (location-select* {:name name
:value value
:account-location (some->> account-id
(pull-attr (dc/db conn) :account/location))
:client-locations (some->> client-id
(pull-attr (dc/db conn) :client/locations))})))
(defn due-date [{:keys [multi-form-state]}]
(let [vendor (clientize-vendor (get-vendor (:invoice/vendor (:step-params multi-form-state)))
(->db-id (:invoice/client (:step-params multi-form-state))))
good-date
(or (when (and (:invoice/date (:step-params multi-form-state))
(-> vendor :vendor/terms))
(time/plus (:invoice/date (:step-params multi-form-state))
(time/days (-> vendor :vendor/terms))))
(:invoice/due (:step-params multi-form-state)))]
(html-response
(com/date-input {:value (some-> good-date
(atime/unparse-local atime/normal-date))
:name "step-params[invoice/due]"
:x-init (format "due='%s'" (some-> good-date
(atime/unparse-local atime/normal-date)))
:x-model "due"
:error? false
:placeholder "1/1/2024"}))))
(defn scheduled-payment-date [{:keys [multi-form-state]}]
(let [vendor (clientize-vendor (get-vendor (:invoice/vendor (:step-params multi-form-state)))
(->db-id (:invoice/client (:step-params multi-form-state))))
good-date
(when (and (:invoice/due (:step-params multi-form-state))
(:vendor/automatically-paid-when-due vendor))
(:invoice/due (:step-params multi-form-state)))]
(html-response
(com/date-input {:value (some-> good-date
(atime/unparse-local atime/normal-date))
:name "step-params[invoice/scheduled-payment]"
:error? false
:placeholder "1/1/2024"}))))
(defn account-prediction [{:keys [multi-form-state form-errors] :as request}]
(html-response
(account-prediction* request)))
(def key->handler
(apply-middleware-to-all-handlers
{::route/new-wizard (-> mm/open-wizard-handler
(mm/wrap-wizard new-wizard)
(mm/wrap-init-multi-form-state initial-new-wizard-state))
::route/edit-wizard (-> mm/open-wizard-handler
(mm/wrap-wizard new-wizard)
(mm/wrap-init-multi-form-state initial-edit-wizard-state)
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
::route/due-date (-> due-date
(mm/wrap-wizard new-wizard)
(mm/wrap-decode-multi-form-state)
(wrap-nested-form-params))
::route/account-prediction (-> account-prediction
(mm/wrap-wizard new-wizard)
(mm/wrap-decode-multi-form-state)
(wrap-nested-form-params))
::route/scheduled-payment-date (-> scheduled-payment-date
(mm/wrap-wizard new-wizard)
(mm/wrap-decode-multi-form-state)
(wrap-nested-form-params))
::route/expense-account-total (-> invoice-expense-account-total
(mm/wrap-wizard new-wizard)
(mm/wrap-decode-multi-form-state))
::route/location-select (-> location-select
(wrap-schema-enforce :query-schema [:map
[:name :string]
[:client-id {:optional true}
[:maybe entity-id]]
[:account-id {:optional true}
[:maybe entity-id]]]))
::route/new-invoice-submit (-> mm/submit-handler
(mm/wrap-wizard new-wizard)
(mm/wrap-decode-multi-form-state))
::route/new-wizard-navigate (-> mm/next-handler
(mm/wrap-wizard new-wizard)
(mm/wrap-decode-multi-form-state))
::route/new-wizard-new-account (->
(add-new-entity-handler [:step-params :invoice/expense-accounts]
(fn render [cursor request]
(invoice-expense-account-row*
{:value cursor
:client-id (:client-id (:query-params request))}))
(fn build-new-row [base _]
(assoc base :invoice-expense-account/location "Shared")))
(wrap-schema-enforce :query-schema [:map
[:client-id {:optional true}
[:maybe entity-id]]]))}
(fn [h]
(-> h
(wrap-client-redirect-unauthenticated)))))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,285 @@
(ns auto-ap.ssr.outgoing-invoice.new
(:require [amazonica.aws.lambda :as lambda]
[auto-ap.datomic :refer [conn pull-attr]]
[auto-ap.routes.invoice :as invoice-route]
[auto-ap.routes.outgoing-invoice :as route]
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated
wrap-secure]]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.grid-page-helper :refer [wrap-trim-client-ids]]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [clj-date-schema modal-response money
percentage strip wrap-schema-decode]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[clojure.data.json :as json]
[datomic.api :as dc]
[malli.core :as mc]))
(def form-schema (mc/schema [:map
[:outgoing-invoice/client [:entity-map {:pull '[:client/name {:client/address [*]}]}]]
[:outgoing-invoice/date clj-date-schema]
[:outgoing-invoice/to :string]
[:outgoing-invoice/invoice-number :string]
[:outgoing-invoice/tax percentage]
[:outgoing-invoice/to-address [:map
[:street1 :string]
[:street2 {:optional true} [:maybe [:string {:decode/string strip}]]]
[:city :string]
[:state :string]
[:zip :string]]]
[:outgoing-invoice/line-items
[:vector {:coerce? true}
[:map
[:outgoing-invoice-line-item/description :string]
[:outgoing-invoice-line-item/unit-price money]
[:outgoing-invoice-line-item/quantity money]]]]]))
(defn line-item [z]
(com/data-grid-row
(hx/alpine-mount-then-appear {:x-data (hx/json {:show false})
:data-key "show"
:x-ref "p"})
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
:value (fc/field-value)}))
(com/data-grid-cell
{}
(fc/with-field :outgoing-invoice-line-item/description
(com/text-input {:name (fc/field-name)
:value (fc/field-value)
:class "w-full"
:placeholder "Catered sandwiches"})))
(com/data-grid-cell
{:class "w-8"}
(fc/with-field :outgoing-invoice-line-item/quantity
(com/money-input {:name (fc/field-name)
:value (fc/field-value)
:class "w-24"
:placeholder "20"})))
(com/data-grid-cell
{}
(fc/with-field :outgoing-invoice-line-item/unit-price
(com/money-input {:name (fc/field-name)
:value (fc/field-value)
:class "w-24"
:placeholder "23.50"})))
(com/data-grid-cell {:class "align-top"}
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
(defn form* [{:keys [form-params form-errors]}]
(fc/start-form
form-params
form-errors
[:form.flex.gap-4 {:hx-post (bidi.bidi/path-for ssr-routes/only-routes
::route/new-submit)}
(com/content-card {:max-w "max-w-screen-lg"}
[:div {:class "flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"}
[:div
[:h1.text-2xl.mb-3.font-bold "New outgoing invoice"]]
[:div {:class "flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"}]]
[:div.p-4
(fc/with-field :outgoing-invoice/client
(com/validated-field {:errors (fc/field-errors)
:label "From (client)"}
[:div.w-96
(com/typeahead {:name (fc/field-name)
:error? (fc/error?)
:autofocus true
:class "w-96"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes :company-search)
:value (fc/field-value)
:content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})]))
(fc/with-field :outgoing-invoice/invoice-number
(com/validated-field {:errors (fc/field-errors)
:label "Invoice #"}
[:div.w-96
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:class "w-96"
:placeholder "10000"
:value (fc/field-value)})]))
(fc/with-field :outgoing-invoice/date
(com/validated-field {:errors (fc/field-errors)
:label "Date"}
[:div.w-96
(com/date-input {:name (fc/field-name)
:error? (fc/error?)
:class "w-96"
:placeholder "10000"
:value (-> (fc/field-value)
(atime/unparse-local atime/normal-date))})]))
(fc/with-field-default :outgoing-invoice/line-items [{:db/id "first"}]
(com/validated-field {:errors (fc/field-errors)
:label "Line items"}
(com/data-grid {:headers [(com/data-grid-header {} "Description")
(com/data-grid-header {} "Quantity")
(com/data-grid-header {} "Unit Price")
(com/data-grid-header {} "")]}
(fc/cursor-map line-item)
(com/data-grid-new-row {:colspan 4
:index (count (fc/field-value))
:hx-get (bidi/path-for ssr-routes/only-routes
::route/new-line-item)} "Add line"))))
(fc/with-field-default :outgoing-invoice/tax 10.0
(com/validated-field {:errors (fc/field-errors)
:label "Tax %"}
(com/money-input {:name (fc/field-name)
:value (fc/field-value)})))
[:div.flex.flex-row-reverse (com/button {:color :primary :class "w-24"} "Generate")]])
(com/content-card {:max-w "max-w-24"}
[:div.p-4
[:h3.text-lg "Recipient details"]
[:div.flex.flex-col.gap-2
(fc/with-field :outgoing-invoice/to
(com/validated-field {:errors (fc/field-errors)
:label "To"}
[:div.w-96
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:class "w-96"
:placeholder "Hello Company"
:value (fc/field-value)})]))
(fc/with-field-default :outgoing-invoice/to-address {}
(list
(fc/with-field :address/street1
(com/validated-field {:label "Address"
:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:class "w-full"
:placeholder "1200 Pennsylvania Avenue"
:value (fc/field-value)})))
(fc/with-field :address/street2
(com/validated-field {:errors (fc/field-errors)}
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:class "w-full"
:placeholder "Suite 300"
:value (fc/field-value)})))
[:div.flex.w-full.space-x-4
(fc/with-field :address/city
(com/validated-field {:errors (fc/field-errors)
:class "w-full grow shrink"}
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:class "w-full"
:placeholder "Cupertino"
:value (fc/field-value)})))
(fc/with-field :address/state
(com/validated-field {:errors (fc/field-errors)
:class "w-16 shrink-0"}
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:class "w-full"
:placeholder "CA"
:value (fc/field-value)})))
(fc/with-field :address/zip
(com/validated-field {:errors (fc/field-errors)
:class "w-24 shrink-0"}
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:placeholder "98101"
:class "w-full"
:value (fc/field-value)})))]))]])]))
(defn- fmt-money [total]
(format "$%,.2f" (or total 0)))
(defn submit [{:keys [form-params]}]
(let [line-items (->> form-params
:outgoing-invoice/line-items
(filter (fn [li] (not-empty (:outgoing-invoice-line-item/description li))))
(mapv
#(assoc % :outgoing-invoice-line-item/total (* (:outgoing-invoice-line-item/unit-price %)
(:outgoing-invoice-line-item/quantity %)))))
subtotal (reduce + 0.0 (map :outgoing-invoice-line-item/total line-items))
tax (* subtotal (:outgoing-invoice/tax form-params))
total (+ subtotal tax)
final-outgoing-invoice (-> form-params
(assoc :outgoing-invoice/line-items
line-items
:outgoing-invoice/total total
:outgoing-invoice/tax tax))
result
(-> (lambda/invoke {:function-name "genpdf" :payload
(json/write-str
(-> final-outgoing-invoice
(update :outgoing-invoice/total fmt-money)
(update :outgoing-invoice/tax fmt-money)
(update :outgoing-invoice/line-items
(fn [lis]
(mapv
#(-> %
(update :outgoing-invoice-line-item/total fmt-money)
(update :outgoing-invoice-line-item/unit-price fmt-money))
lis)))
(update :outgoing-invoice/date
#(some-> % (atime/unparse-local atime/normal-date)))))})
:payload
slurp
json/read-str)]
(modal-response
(com/modal {}
(com/modal-card {} [:div "Download your invoice"] [:div
"click "
(com/link {:href (str "https://data.prod.app.integreatconsult.com/" result)}
"here")
" to download"] [:div])))))
(def page (-> (fn page [{:keys [identity] :as request}]
(base-page
request
(com/page {:nav com/main-aside-nav
:client-selection (:client-selection (:session request))
:clients (:clients request)
:client (:client request)
:identity (:identity request)
:request request}
(apply com/breadcrumbs {} [[:a {:href (bidi/path-for ssr-routes/only-routes
::invoice-route/all-page)}
"Invoices"]
[:a {:href "#"} ;; TODO
"Outgoing"]
[:a {:href (bidi/path-for ssr-routes/only-routes
::route/new)}
"New"]])
(form* request))
"New outgoing invoice"))
(wrap-trim-client-ids)
(wrap-secure)
(wrap-client-redirect-unauthenticated)))
(def route->handler
{::route/new page
::route/new-submit (-> submit
(wrap-schema-decode :form-schema form-schema)
(wrap-nested-form-params))
::route/new-line-item (->
(add-new-entity-handler [:outgoing-invoice/line-items]
(fn render [cursor request]
(line-item
{:value cursor }))
(fn build-new-row [base _]
base)))})

View File

@@ -0,0 +1,559 @@
(ns auto-ap.ssr.payments
(:require [auto-ap.client-routes :as client-routes]
[auto-ap.datomic
:refer [add-sorter-fields apply-pagination apply-sort-3
audit-transact conn merge-query observable-query
pull-many]]
[auto-ap.graphql.utils :refer [assert-can-see-client
exception->notification
extract-client-ids notify-if-locked]]
[auto-ap.logging :as alog]
[auto-ap.permissions :refer [can?]]
[auto-ap.routes.invoice :as invoice-route]
[auto-ap.routes.payments :as route]
[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.components.bank-account-icon :as bank-account-icon]
[auto-ap.ssr.components.link-dropdown :refer [link-dropdown]]
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.pos.common :refer [date-range-field*]]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils
:refer [apply-middleware-to-all-handlers clj-date-schema
dissoc-nil-transformer entity-id html-response
main-transformer modal-response ref->enum-schema strip
wrap-entity wrap-implied-route-param wrap-merge-prior-hx
wrap-schema-enforce]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[clj-time.coerce :as coerce]
[clojure.string :as str]
[datomic.api :as dc]
[hiccup.util :as hu]
[iol-ion.query :refer [dollars-0?]]
[malli.core :as mc]
[malli.transform :as mt]))
(defn exact-match-id* [request]
(if (nat-int? (:exact-match-id (:parsed-query-params request)))
[:div {:x-data (hx/json {:exact_match (:exact-match-id (:parsed-query-params request))}) :id "exact-match-id-tag"}
(com/hidden {:name "exact-match-id"
"x-model" "exact_match"})
(com/pill {:color :primary}
[:span.inline-flex.space-x-2.items-center
[:div "exact match"]
[:div.w-3.h-3
(com/link {"@click" "exact_match=null; $nextTick(() => $dispatch('change'))"}
svg/x)]])]
[:div {:id "exact-match-id-tag"}]))
;; TODO use query-params instead of parsed-query-params
(defn filters [request]
[:form#payment-filters {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
"hx-get" (bidi/path-for ssr-routes/only-routes
::route/table)
"hx-target" "#entity-table"
"hx-indicator" "#entity-table"}
(com/hidden {:name "status"
:value (some-> (:status (:query-params request)) name)})
[:fieldset.space-y-6
(com/field {:label "Vendor"}
(com/typeahead {:name "vendor"
:id "vendor"
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
:value (:vendor (:query-params request))
:value-fn :db/id
:content-fn :vendor/name}))
(date-range-field* request)
(com/field {:label "Check #"}
(com/text-input {:name "check-number"
:id "check-number"
:class "hot-filter"
:value (:check-number (:query-params request))
:placeholder "10001"
:size :small}))
(com/field {:label "Invoice #"}
(com/text-input {:name "invoice-number"
:id "invoice-number"
:class "hot-filter"
:value (:invoice-number (:query-params request))
:placeholder "10001"
:size :small}))
(com/field {:label "Amount"}
[:div.flex.space-x-4.items-baseline
(com/money-input {:name "amount-gte"
:id "amount-gte"
:hx-preserve "true"
:class "hot-filter w-20"
:value (:amount-gte (:query-params request))
:placeholder "0.01"
:size :small})
[:div.align-baseline
"to"]
(com/money-input {:name "amount-lte"
:hx-preserve "true"
:id "amount-lte"
:class "hot-filter w-20"
:value (:amount-lte (:query-params request))
:placeholder "9999.34"
:size :small})])
(com/field {:label "Payment Type"}
(com/radio-card {:size :small
:name "payment-type"
:value (:payment-type (:query-params request))
:options [{:value ""
:content "All"}
{:value "cash"
:content "Cash"}
{:value "check"
:content "Check"}
{:value "debit"
:content "Debit"}]}))
(exact-match-id* request)]])
(def default-read '[*
[:payment/date :xform clj-time.coerce/from-date]
{:invoice-payment/_payment [* {:invoice-payment/invoice [*]}]}
{:payment/client [:client/name :db/id :client/code]}
{:payment/bank-account [* {[:bank-account/type :xform iol-ion.query/ident] [:db/ident]}]}
{:payment/invoices [:db/id :invoice/invoice-number]}
{:payment/vendor [:vendor/name {:vendor/default-account
[:account/name :account/numeric-code :db/id]} :db/id {:vendor/primary-contact [*]} {:vendor/address [*]}]}
{[:payment/status :xform iol-ion.query/ident] [:db/ident]}
{[:payment/type :xform iol-ion.query/ident] [:db/ident]}
{:transaction/_payment [:db/id :transaction/date]}])
(defn fetch-ids [db {:keys [query-params route-params] :as request}]
(let [ valid-clients (extract-client-ids (:clients request)
(:client request)
(:client-id query-params)
(when (:client-code query-params)
[:client/code (:client-code query-params)]))
check-number-like (try (Long/parseLong (:check-number query-params)) (catch Exception _ nil))
query (if (:exact-match-id query-params)
{:query {:find '[?e]
:in '[$ ?e [?c ...]]
:where '[[?e :payment/client ?c]]}
:args [db
(:exact-match-id query-params)
valid-clients]}
(cond-> {:query {:find []
:in '[$ [?clients ?start ?end]]
:where '[[(iol-ion.query/scan-payments $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]]}
:args [db
[valid-clients
(some-> (:start-date query-params) coerce/to-date)
(some-> (:end-date query-params) coerce/to-date)]]}
(:sort query-params) (add-sorter-fields {"client" ['[?e :payment/client ?c]
'[?c :client/name ?sort-client]]
"vendor" ['[?e :payment/vendor ?v]
'[?v :vendor/name ?sort-vendor]]
"bank-account" ['[?e :payment/bank-account ?ba]
'[?ba :bank-account/name ?sort-bank-account]]
"check-number" ['[(get-else $ ?e :payment/check-number 0) ?sort-check-number]]
"date" ['[?e :payment/date ?sort-date]]
"amount" ['[?e :payment/amount ?sort-amount]]
"status" ['[?e :payment/status ?sort-status]]}
query-params)
(:exact-match-id query-params)
(merge-query {:query {:in ['?e]
:where []}
:args [(:exact-match-id query-params)]})
(:vendor query-params)
(merge-query {:query {:in ['?vendor-id]
:where ['[?e :payment/vendor ?vendor-id]]}
:args [(:db/id (:vendor query-params))]})
(:original-id query-params)
(merge-query {:query {:in ['?original-id]
:where ['[?e :payment/client ?c]
'[?c :client/original-id ?original-id]]}
:args [(:original-id query-params)]})
(:check-number-like query-params)
(merge-query {:query {:in ['?check-number]
:where ['[?e :payment/check-number ?check-number]]}
:args [(:check-number-like query-params)]})
(not-empty (:invoice-number query-params))
(merge-query {:query {:in ['?invoice-number]
:where ['[?e :payment/invoices ?i]
'[?i :invoice/invoice-number ?invoice-number]]}
:args [(:invoice-number query-params)]})
(:bank-account-id query-params)
(merge-query {:query {:in ['?bank-account-id]
:where ['[?e :payment/bank-account ?bank-account-id]]}
:args [(:bank-account-id query-params)]})
(:amount-gte query-params)
(merge-query {:query {:in ['?amount-gte]
:where ['[?e :payment/amount ?a]
'[(>= ?a ?amount-gte)]]}
:args [(:amount-gte query-params)]})
(:amount-lte query-params)
(merge-query {:query {:in ['?amount-lte]
:where ['[?e :payment/amount ?a]
'[(<= ?a ?amount-lte)]]}
:args [(:amount-lte query-params)]})
(:amount query-params)
(merge-query {:query {:in ['?amount]
:where ['[?e :payment/amount ?transaction-amount]
'[(iol-ion.query/dollars= ?transaction-amount ?amount)]]}
:args [(:amount query-params)]})
(:status route-params)
(merge-query {:query {:in ['?status]
:where ['[?e :payment/status ?status]]}
:args [(:status route-params)]})
(:payment-type query-params)
(merge-query {:query {:in '[?payment-type]
:where ['[?e :payment/type ?payment-type]]}
:args [(:payment-type query-params)]})
check-number-like
(merge-query {:query {:in '[?check-number-like]
:where ['[?e :payment/check-number ?check-number-like]]}
:args [check-number-like]})
true
(merge-query {:query {:find ['?sort-default '?e]}})))]
(cond->> (observable-query query)
true (apply-sort-3 query-params)
true (apply-pagination query-params))))
(defn hydrate-results [ids db _]
(let [results (->> (pull-many db default-read ids)
(group-by :db/id))
refunds (->> ids
(map results)
(map first))]
refunds))
(defn fetch-page [request]
(let [db (dc/db conn)
{ids-to-retrieve :ids matching-count :count} (fetch-ids db request)]
[(->> (hydrate-results ids-to-retrieve db request))
matching-count]))
(def query-schema (mc/schema
[:maybe [:map {:date-range [:date-range :start-date :end-date]}
[:sort {:optional true} [:maybe [:any]]]
[:per-page {:optional true :default 25} [:maybe :int]]
[:start {:optional true :default 0} [:maybe :int]]
[:amount-gte {:optional true} [:maybe :double]]
[:amount-lte {:optional true} [:maybe :double]]
[:payment-type {:optional true} [:maybe (ref->enum-schema "payment-type")]]
[:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]]
[:check-number {:optional true} [:maybe [:string {:decode/string strip}]]]
[:invoice-number {:optional true} [:maybe [:string {:decode/string strip}]]]
[:status {:optional true} [:maybe (ref->enum-schema "payment-status")]]
[:exact-match-id {:optional true} [:maybe entity-id]]
[:all-selected {:optional true :default nil} [:maybe :boolean]]
[:selected {:optional true :default nil} [:maybe [:vector {:coerce? true}
entity-id]]]
[:start-date {:optional true}
[:maybe clj-date-schema]]
[:end-date {:optional true}
[:maybe clj-date-schema]]]]))
(comment
(mc/decode query-schema
{:start " "}
main-transformer))
;; TODO fix parsing of query params
(def grid-page
(helper/build {:id "entity-table"
:nav com/main-aside-nav
:check-boxes? true
:page-specific-nav filters
:fetch-page fetch-page
:oob-render
(fn [request]
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)
(assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)])
:query-schema query-schema
:parse-query-params (fn [p]
(mc/decode query-schema p main-transformer))
:action-buttons (fn [request]
[(when (can? (:identity request) {:subject :payment :activity :bulk-delete})
(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/bulk-delete))
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
"hx-include" "#payment-filters"
:color :red}
"Void selected"))])
:row-buttons (fn [_ entity]
[(when (not= :payment-status/voided (:payment/status entity))
(com/icon-button {:hx-delete (bidi/path-for ssr-routes/only-routes
::route/delete
:db/id (:db/id entity))
:hx-confirm "Are you sure you want to void this payment?"}
svg/trash))])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
"Payments"]]
:title (fn [r]
(str
(some-> r :rout-params :status name str/capitalize (str " "))
"Payments"))
:entity-name "payments"
:route ::route/table
:headers [{:key "client"
:name "Client"
:sort-key "client"
:hide? (fn [args]
(= (count (:clients args)) 1))
:render #(-> % :payment/client :client/code)}
{:key "vendor"
:name "Vendor"
:sort-key "vendor"
:render #(-> % :payment/vendor :vendor/name)}
{:key "bank-account"
:name "Bank account"
:sort-key "bank-account"
:show-starting "xl"
:render (fn [p]
[:div.flex.items-center
(when (:payment/bank-account p)
(bank-account-icon/icon (:payment/bank-account p)))
[:div (-> p :payment/bank-account :bank-account/name)]])}
{:key "check-number"
:name "Check #"
:sort-key "check-number"
:render (fn [{:payment/keys [s3-url check-number]}]
(if s3-url
(com/link {:href s3-url :target "_new"} [:div.flex.items-center.gap-x-2 check-number [:div.w-4.h-4 svg/external-link]])
check-number))}
{:key "status"
:name "Status"
:render (fn [{:payment/keys [status]}]
(condp = status
:payment-status/cleared
(com/pill {:color :primary} "cleared")
:payment-status/pending
(com/pill {:color :secondary} "pending")
:payment-status/voided
(com/pill {:color :red} "voided")
nil
""))}
{:key "date"
:name "Date"
:show-starting "lg"
:render (fn [{:payment/keys [date]}]
(some-> date (atime/unparse-local atime/normal-date)))}
{:key "amount"
:name "Amount"
:render (fn [{:payment/keys [amount]}]
(some->> amount (format "$%.2f")))}
{:key "links"
:name "Links"
:class "w-8"
:render (fn [p]
(link-dropdown (concat (->> p :payment/invoices (map (fn [invoice]
{:link (hu/url (bidi/path-for ssr-routes/only-routes
::invoice-route/all-page)
{:exact-match-id (:db/id invoice)})
:content (str "Inv. " (:invoice/invoice-number invoice))})))
(some-> p :transaction/_payment ((fn [t]
[{:link (hu/url (bidi/path-for client-routes/routes
:transactions)
{:exact-match-id (:db/id (first t))})
:color :secondary
:content "Transaction"}]))))))}]}))
(def row* (partial helper/row* grid-page))
(comment
(mc/decode query-schema {"exact-match-id" "123"} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
(mc/decode query-schema {} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
(mc/decode query-schema {"exact-match-id" nil} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
(mc/decode query-schema {"exact-match-id" ""} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
(mc/decode query-schema {"start-date" "12/21/2023"} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
(mc/decode query-schema {"payment-type" "food"} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
(mc/decode query-schema {"vendor" "87"} (mt/transformer main-transformer mt/strip-extra-keys-transformer))
(mc/decode query-schema {"start-date" #inst "2023-12-21T08:00:00.000-00:00"} (mt/transformer main-transformer mt/strip-extra-keys-transformer)))
(defn delete [{check :entity :as request identity :identity}]
(alog/peek ::check-type check)
(exception->notification
#(when-not (or (= :payment-status/pending (:payment/status check))
(#{:payment-type/cash :payment-type/debit :payment-type/balance-credit} (:payment/type check)))
(throw (ex-info "Payment must be pending." {}))))
(exception->notification
#(assert-can-see-client identity (:db/id (:payment/client check))))
(notify-if-locked (:db/id (:payment/client check))
(:payment/date check))
(let [removing-payments (mapcat (fn [x]
(let [invoice (:invoice-payment/invoice x)
new-balance (+ (:invoice/outstanding-balance invoice)
(:invoice-payment/amount x))]
[[:db/retractEntity (:db/id x)]
[:upsert-invoice {:db/id (:db/id invoice)
:invoice/outstanding-balance new-balance
:invoice/status (if (dollars-0? new-balance)
(:invoice/status invoice)
:invoice-status/unpaid)}]]))
(:invoice-payment/_payment check))
updated-payment {:db/id (:db/id check)
:payment/amount 0.0
:payment/status :payment-status/voided}]
(audit-transact (conj removing-payments updated-payment)
identity)
(html-response (row* (:identity request) updated-payment {:delete-after-settle? true :class "live-removed"})
:headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id check))})))
;; TODO use decoding here
(defn bulk-delete-dialog [request]
(alog/peek :selected (pr-str (:selected (:query-params request))))
(let [all-selected (:all-selected (:query-params request))
selected (:selected (:query-params request))
ids (cond
all-selected
(:ids (fetch-ids (dc/db conn) (-> request
(assoc-in [:query-params :start] 0)
(assoc-in [:query-params :per-page] 250))))
:else
selected)]
(modal-response
(com/modal {}
(com/modal-card-advanced
{}
(com/modal-body {}
[:div.flex.flex-col.mt-4.space-y-4.items-center
[:div.w-24.h-24.bg-red-50.rounded-full.p-4.text-red-300
svg/alert]
[:div "You are about to void " (count ids) " payments. Are you sure you want to do this?"]])
(com/modal-footer {} [:div.flex.justify-end (com/button {:color :primary
:hx-vals (hx/json (mc/encode
query-schema
(dissoc (:query-params request) :sort)
(mt/transformer
main-transformer
dissoc-nil-transformer
mt/strip-extra-keys-transformer)))
:hx-delete (hu/url (bidi/path-for ssr-routes/only-routes
::route/bulk-delete-confirm))}
"Void payments")])))
:headers (-> {}
(assoc "hx-retarget" ".modal-stack")
(assoc "hx-reswap" "beforeend")))))
(defn void-payments-internal [all-ids id]
(let [payments-to-update (->> all-ids
(dc/q '[:find (pull ?p [:db/id
{:invoice-payment/_payment [:invoice-payment/amount
:db/id
{:invoice-payment/invoice [:db/id :invoice/outstanding-balance]}]}])
:in $ [?p ...]
:where
(not [_ :transaction/payment ?p])
(not [?p :payment/status :payment-status/voided])
[?p :payment/client ?c]
[(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu]
[?p :payment/date ?d]
[(>= ?d ?lu)]]
(dc/db conn))
(map first))]
(audit-transact (->> payments-to-update
(mapcat (fn [{:keys [:db/id]
invoices :invoice-payment/_payment}]
(into
[{:db/id id
:payment/amount 0.0
:payment/status :payment-status/voided}]
(->> invoices
(mapcat (fn [{:keys [:invoice-payment/invoice :db/id :invoice-payment/amount]}]
(let [new-balance (+ (:invoice/outstanding-balance invoice)
amount)]
[[:db.fn/retractEntity id]
[:upsert-invoice {:db/id (:db/id invoice)
:invoice/outstanding-balance new-balance
:invoice/status (if (dollars-0? new-balance)
(:invoice/status invoice)
:invoice-status/unpaid)}]]))))))))
id)
(count payments-to-update)))
(defn bulk-delete-dialog-confirm [request]
(alog/peek (:form-params request))
(let [all-selected (:all-selected (:form-params request))
selected (:selected (:form-params request))
ids (cond
all-selected
(:ids (fetch-ids (dc/db conn) (-> request
(assoc :query-params (:form-params request))
(assoc-in [:query-params :start] 0)
(assoc-in [:query-params :per-page] 250))))
:else
selected)
updated-count (void-payments-internal ids (:identity request))]
(html-response [:div]
:headers {"hx-trigger" (hx/json {:modalclose ""
:notification (format "Successfully voided %d of %d payments."
updated-count
(count ids))})})))
(defn wrap-status-from-source [handler]
(fn [{:keys [matched-current-page-route] :as request}]
(let [ request (cond-> request
(= ::route/cleared-page matched-current-page-route) (assoc-in [:route-params :status] :payment-status/cleared)
(= ::route/pending-page matched-current-page-route) (assoc-in [:route-params :status] :payment-status/pending)
(= ::route/voided-page matched-current-page-route) (assoc-in [:route-params :status] :payment-status/voided)
(= ::route/all-page matched-current-page-route) (assoc-in [:route-params :status] nil))]
(handler request))))
(def key->handler
(apply-middleware-to-all-handlers
{::route/cleared-page (-> (helper/page-route grid-page)
(wrap-implied-route-param :status :payment-status/cleared))
::route/pending-page (-> (helper/page-route grid-page)
(wrap-implied-route-param :status :payment-status/pending))
::route/voided-page (-> (helper/page-route grid-page)
(wrap-implied-route-param :status :payment-status/voided))
::route/all-page (-> (helper/page-route grid-page)
(wrap-implied-route-param :status nil))
::route/delete (-> delete
(wrap-entity [:route-params :db/id] default-read)
(wrap-schema-enforce :route-params [:map [:db/id entity-id]]))
::route/bulk-delete-confirm (-> bulk-delete-dialog-confirm
(wrap-schema-enforce :form-schema query-schema)
(wrap-admin))
::route/bulk-delete (-> bulk-delete-dialog
(wrap-admin))
::route/table (helper/table-route grid-page)}
(fn [h]
(-> h
(wrap-apply-sort grid-page)
(wrap-merge-prior-hx)
(wrap-status-from-source)
(wrap-schema-enforce :query-schema query-schema)
(wrap-schema-enforce :hx-schema query-schema)
(wrap-client-redirect-unauthenticated)))))

View File

@@ -84,10 +84,9 @@
matching-count]))
(def grid-page
{}
#_(helper/build
(helper/build
{:id "cash-drawer-shift-table"
:nav (com/main-aside-nav)
:nav com/main-aside-nav
:page-specific-nav filters
:fetch-page fetch-page
:oob-render

View File

@@ -10,7 +10,7 @@
(defn processor-field* [request]
(com/field {:label "Processor"}
(com/radio {:size :small
(com/radio-card {:size :small
:name "processor"
:value (:processor (:parsed-query-params request))
:options [{:value ""

View File

@@ -136,23 +136,23 @@
(def grid-page
(helper/build
{:id "expected-deposit-table"
:nav (com/main-aside-nav)
:nav com/main-aside-nav
:page-specific-nav filters
:fetch-page fetch-page
:parse-query-params (comp
(query-params/parse-key :total-gte query-params/parse-double)
(query-params/parse-key :total-lte query-params/parse-double)
(helper/default-parse-query-params grid-page))
(query-params/parse-key :total-gte query-params/parse-double)
(query-params/parse-key :total-lte query-params/parse-double)
(helper/default-parse-query-params grid-page))
:oob-render
(fn [request]
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:company)}
"POS"]
"POS"]
[:a {:href (bidi/path-for ssr-routes/only-routes
:pos-expected-deposits)}
"Expected deposits"]]
"Expected deposits"]]
:title "Expected deposits"
:entity-name "Expected deposit"
:route :pos-expected-deposit-table
@@ -170,7 +170,7 @@
:name "Client"
:sort-key "client"
:hide? (fn [args]
(= (count (:clients args)) 1))
(= (count (:clients args)) 1))
:render #(-> % :expected-deposit/client :client/code)}
{:key "date"
:name "Date"

View File

@@ -95,23 +95,23 @@
(def grid-page
(helper/build {:id "refund-table"
:nav (com/main-aside-nav)
:nav com/main-aside-nav
:page-specific-nav filters
:fetch-page fetch-page
:parse-query-params (comp
(query-params/parse-key :total-gte query-params/parse-double)
(query-params/parse-key :total-lte query-params/parse-double)
(helper/default-parse-query-params grid-page))
(query-params/parse-key :total-gte query-params/parse-double)
(query-params/parse-key :total-lte query-params/parse-double)
(helper/default-parse-query-params grid-page))
:oob-render
(fn [request]
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:company)}
"POS"]
"POS"]
[:a {:href (bidi/path-for ssr-routes/only-routes
:pos-refunds)}
"Refunds"]]
"Refunds"]]
:title "Refunds"
:entity-name "Refund"
:route :pos-refund-table
@@ -119,7 +119,7 @@
:name "Client"
:sort-key "client"
:hide? (fn [args]
(= (count (:clients args)) 1))
(= (count (:clients args)) 1))
:render #(-> % :sales-refund/client :client/code)}
{:key "date"
:name "Date"

View File

@@ -34,7 +34,7 @@
(total-field* request)
[:div
(com/field {:label "Payment Method"}
(com/radio {:size :small
(com/radio-card {:size :small
:name "payment-method"
:options [{:value ""
:content "All"}
@@ -169,14 +169,14 @@
(def grid-page
(helper/build
{:id "sales-table"
:nav (com/main-aside-nav)
:nav com/main-aside-nav
:page-specific-nav filters
:fetch-page fetch-page
:parse-query-params (comp
(query-params/parse-key :processor #(query-params/parse-keyword "ccp-processor" %))
(query-params/parse-key :total-gte query-params/parse-double)
(query-params/parse-key :total-lte query-params/parse-double)
(helper/default-parse-query-params grid-page))
(query-params/parse-key :processor #(query-params/parse-keyword "ccp-processor" %))
(query-params/parse-key :total-gte query-params/parse-double)
(query-params/parse-key :total-lte query-params/parse-double)
(helper/default-parse-query-params grid-page))
:oob-render
(fn [request]
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)])

View File

@@ -112,14 +112,14 @@
(def grid-page
(helper/build
{:id "tender-table"
:nav (com/main-aside-nav)
:nav com/main-aside-nav
:page-specific-nav filters
:fetch-page fetch-page
:parse-query-params (comp
(query-params/parse-key :processor #(query-params/parse-keyword "ccp-processor" %))
(query-params/parse-key :total-gte query-params/parse-double)
(query-params/parse-key :total-lte query-params/parse-double)
(helper/default-parse-query-params grid-page))
(query-params/parse-key :processor #(query-params/parse-keyword "ccp-processor" %))
(query-params/parse-key :total-gte query-params/parse-double)
(query-params/parse-key :total-lte query-params/parse-double)
(helper/default-parse-query-params grid-page))
:oob-render
(fn [request]
[(assoc-in (date-range-field* request) [1 :hx-swap-oob] true)])

View File

@@ -96,6 +96,14 @@
[:svg {:fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:d "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"}]])
(def three-dots
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24"}
[:defs]
[:title "navigation-menu-horizontal"]
[:path {:d "M0.5 12a2.5 2.5 0 1 0 5 0 2.5 2.5 0 1 0 -5 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "M9.5 12a2.5 2.5 0 1 0 5 0 2.5 2.5 0 1 0 -5 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "M18.5 12a2.5 2.5 0 1 0 5 0 2.5 2.5 0 1 0 -5 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]])
(def breadcrumb-component
[:svg {:fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
[:path {:fill-rule "evenodd", :d "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", :clip-rule "evenodd"}]])
@@ -487,4 +495,28 @@
[:path {:d "m19.025 2.677 1.293 -1.293", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "M6.5 9.616H1a0.5 0.5 0 0 0 -0.5 0.5v12a0.5 0.5 0 0 0 0.5 0.5h22a0.5 0.5 0 0 0 0.5 -0.5v-12a0.5 0.5 0 0 0 -0.5 -0.5h-5", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "M17.004 12.616h3s0.5 0 0.5 0.5v2s0 0.5 -0.5 0.5h-3s-0.5 0 -0.5 -0.5v-2s0 -0.5 0.5 -0.5", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "m16.757 2.823 -0.8 -0.8a1 1 0 0 0 -1.414 0L12.5 4.07", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]])
[:path {:d "m16.757 2.823 -0.8 -0.8a1 1 0 0 0 -1.414 0L12.5 4.07", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]])
(def paperclip
[:svg
{:xmlns "http://www.w3.org/2000/svg",
:viewBox "0 0 24 24",
:id "Attachment--Streamline-Streamline--3.0"}
[:desc "Attachment Streamline Icon: https://streamlinehq.com"]
[:defs]
[:title "attachment"]
[:path
{:d
"m7.618 15.345 8.666 -8.666a2.039 2.039 0 1 1 2.883 2.883L7.461 21.305a4.078 4.078 0 0 1 -5.767 -5.768L13.928 3.305a5.606 5.606 0 0 1 7.929 7.928L13.192 19.9",
:fill "none",
:stroke "currentcolor",
:stroke-linecap "round",
:stroke-linejoin "round",
:stroke-width "1"}]])
(def undo
[:svg {:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 24 24", :id "Undo--Streamline-Streamline--3.0"}
[:defs]
[:title "undo"]
[:path {:d "m1.5 0.498 0 7 7 0", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]
[:path {:d "M1.5 7.5a11.656 11.656 0 0 1 16.179 -2.647 11.508 11.508 0 0 1 0.11 18.645", :fill "none", :stroke "currentcolor", :stroke-linecap "round", :stroke-linejoin "round", :stroke-width "1"}]])

View File

@@ -317,7 +317,7 @@
(defn page [{:keys [identity matched-route session clients] :as request}]
(base-page
request
(com/page {:nav (com/main-aside-nav)
(com/page {:nav com/main-aside-nav
:client-selection (:client-selection (:session request))
:client (:client request)
:identity (:identity request)
@@ -325,7 +325,8 @@
:transaction-insights)
:hx-trigger "clientSelected from:body"
:hx-select "#app-contents"
:hx-swap "outerHTML swap:300ms"}}
:hx-swap "outerHTML swap:300ms"}
:request request}
(com/breadcrumbs {}
[:a {:href (bidi/path-for client-routes/routes
:transactions)}

View File

@@ -45,7 +45,6 @@
[:script {:src "/js/htmx-disable.js"}]
[:script {:type "text/javascript", :src "https://cdn.yodlee.com/fastlink/v4/initialize.js", :async "async"}]]
[:link {:rel "stylesheet" :href "https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.1.4/dist/css/datepicker.min.css"}]
[:script {:type "text/javascript" :src "https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.1.4/dist/js/datepicker-full.min.js"}]
[:link {:rel "stylesheet" :href "https://cdn.jsdelivr.net/npm/choices.js@9.0.1/public/assets/styles/choices.min.css"}]
@@ -54,6 +53,7 @@
[:script {:src "https://unpkg.com/dropzone@5.9.3/dist/min/dropzone.min.js"}]
[:link {:rel "stylesheet" :href "https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" :type "text/css"}]
[:script {:defer true :src "/js/alpine-vals.js"}]
[:script {:defer true :src "https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"}]
[:script {:defer true :src "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"}]
[:script {:src "https://cdn.jsdelivr.net/npm/signature_pad@4.1.7/dist/signature_pad.umd.min.js"}]
@@ -81,14 +81,16 @@ input[type=number] {
"x-show" "open"
":aria-hidden" "!open"
"x-data" (hx/json {"open" false
"forceBackground" false
"unexpectedError" false})
"x-on:htmx:response-error" "unexpectedError=true;"
"x-on:htmx:before-request" "unexpectedError=false"
"@modalopen.document" "open=true; unexpectedError=null"
"@modalclose.document" "open=false"}
"@modalclose.document" "open=false"
"@modalswap.document" "forceBackground=true; open=false; setTimeout(() => {open=true;forceBackground=false;}, 100)"}
[:div {:class "bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40 md:p-12"
"x-show" "open"
"x-show" "open || forceBackground"
":aria-hidden" "!open"
"x-transition:enter" "duration-300"
"x-transition:enter-start" "!bg-opacity-0"
@@ -108,7 +110,4 @@ input[type=number] {
"x-transition:leave-start" "!opacity-100 !translate-y-0"
"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
]]]]]]]))
[:div#modal-content.flex.items-center.justify-center {:class "md:p-12"}]]]]]]))

View File

@@ -66,7 +66,7 @@
:size :small}))
(com/field {:label "Role"}
(com/radio {:size :small
(com/radio-card {:size :small
:name "role"
:options [{:value ""
:content "All"}
@@ -205,20 +205,20 @@
(def grid-page
(helper/build {:id "user-table"
:nav (com/admin-aside-nav)
:nav com/admin-aside-nav
:page-specific-nav filters
:fetch-page fetch-page
:parse-query-params (comp
(query-params/parse-key :role #(query-params/parse-keyword "user-role" %))
(query-params/parse-key :client parse-client)
(helper/default-parse-query-params grid-page))
(query-params/parse-key :role #(query-params/parse-keyword "user-role" %))
(query-params/parse-key :client parse-client)
(helper/default-parse-query-params grid-page))
:row-buttons (fn [request entity]
[(com/button {:hx-post (str (bidi/path-for ssr-routes/only-routes
:user-impersonate))
: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)))}
:user-edit-dialog
:db/id (:db/id entity)))}
svg/pencil)])
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:admin)}
@@ -238,7 +238,7 @@
(when-let [profile-image (:user/profile-image-url user) ]
[:div.rounded-full.overflow-hidden.w-8.h-8.display-inline
[:img {:src profile-image }]])
[:span.inline-block ](:user/name user)])}
[:span.inline-block ] (:user/name user)])}
{:key "email"
:name "Email"
@@ -292,49 +292,50 @@
(defn dialog* [{:keys [form-params form-errors entity]}]
(println "FORM PARMS" form-params)
(fc/start-form
form-params form-errors
(com/modal
{:hx-target "this"
:hx-indicator "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)]]
[:div.space-y-6
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
:value (fc/field-value)}))
(fc/with-field :user/role
(com/validated-field {:label "Role"
:errors (fc/field-errors)}
(com/select {:name (fc/field-name)
:class "w-36"
:autofocus true
:value (some->> (fc/field-value) name)
:options (ref->select-options "user-role")})))
(fc/with-field :user/clients
(com/validated-field {:label "Clients"}
(com/data-grid {:headers [(com/data-grid-header {} "Client")
(com/data-grid-header {} )]
:id "client-table"}
(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
(com/form-errors {:errors (:errors fc/*form-errors*)})
(com/validated-save-button {:errors (seq form-errors)}
"Save user")])]])))
form-params form-errors
(com/modal
{:hx-target "this"
:hx-indicator "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)]]
[:div.space-y-6
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
:value (fc/field-value)}))
(fc/with-field :user/role
(com/validated-field {:label "Role"
:errors (fc/field-errors)}
(com/select {:name (fc/field-name)
:class "w-36"
:autofocus true
:value (some->> (fc/field-value) name)
:options (ref->select-options "user-role")})))
(fc/with-field :user/clients
(com/validated-field {:label "Clients"}
(com/data-grid {:headers [(com/data-grid-header {} "Client")
(com/data-grid-header {})]
:id "client-table"}
(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
(com/form-errors {:errors (:errors fc/*form-errors*)})
(com/validated-save-button {:errors (seq form-errors)}
"Save user")])]])))
(defn user-edit-save [{:keys [form-params identity] :as request}]
(let [_ @(dc/transact conn [[:upsert-entity form-params]])

View File

@@ -1,15 +1,25 @@
(ns auto-ap.ssr.utils
(:require
[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+]]))
(:require [auto-ap.datomic :refer [all-schema conn]]
[auto-ap.logging :as alog]
[auto-ap.time :as atime]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[clojure.string :as str]
[datomic.api :as dc]
[hiccup2.core :as hiccup]
[hiccup.compiler :refer [HtmlRenderer render-html]]
[malli.core :as mc]
[malli.core :as m]
[malli.error :as me]
[malli.registry :as mr]
[malli.transform :as mt2]
[slingshot.slingshot :refer [throw+ try+]]
[taoensso.encore :refer [filter-vals]]))
(defrecord OOBElements [elements]
HtmlRenderer
(render-html [this]
(str/join "\n" (map render-html elements))))
(defn html-response [hiccup & {:keys [status headers oob] :or {status 200 headers {} oob []}}]
{:status status
@@ -33,7 +43,17 @@
[hiccup]
(mapcat identity
(-> opts
(assoc-in [:headers "hx-trigger"] "modalopen")
(update-in [:headers "hx-trigger"] (fn [ht] (str/join ", " (filter identity [ht "modalopen"]))))
(assoc-in [:headers "hx-retarget"] "#modal-content")
(assoc-in [:headers "hx-reswap"] "innerHTML"))))))
(defn modal-replace-response [hiccup & {:as opts}]
(apply html-response
(into
[hiccup]
(mapcat identity
(-> opts
(assoc-in [:headers "hx-trigger"] "modalswap")
(assoc-in [:headers "hx-retarget"] "#modal-content")
(assoc-in [:headers "hx-reswap"] "innerHTML"))))))
@@ -96,11 +116,17 @@
:long empty->nil
'nat-int? empty->nil}}))
(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 raw-entity-id [nat-int? {:error/message "required"
:decode/arbitrary (fn [e]
(if (and (map? e) (:db/id e))
(:db/id e)
e))}])
(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 {:min 1}]))
(def money (mc/schema [:double]))
@@ -125,7 +151,7 @@
(if (sequential? x)
x
(into []
(for [[k v] (sort-by (comp #(Long/parseLong %) name first) x)]
(for [[k v] (sort-by (comp #(Long/parseLong %) first) x)]
v))))})
(defn many-entity [params & keys]
@@ -158,13 +184,173 @@
(throw+ (ex-info m (merge data {:type :form-validation
:form-validation-errors [m]}))))
(def clj-date-schema
(mc/schema [inst? {:date-format atime/normal-date}]))
(def date-transformer
(mt2/transformer
{:decoders
{'inst? {:compile (fn [schema _]
(let [properties (mc/properties schema)
format (:format properties atime/normal-date)]
(fn [m]
(if (string? m)
(coerce/to-date-time (atime/parse m format))
m))))}}
:encoders
{'inst?
{:compile (fn [schema _]
(let [properties (mc/properties schema)
format (:format properties atime/normal-date)]
(fn [m]
(cond
(inst? m)
(atime/unparse-local (coerce/to-date-time m) format)
(instance? org.joda.time.DateTime m)
(atime/unparse-local m format)
:else
m))))}}}))
(def date-range-transformer
(mt2/transformer {:decoders
{:map {:compile (fn [schema _]
(let [properties (mc/properties schema)]
(fn [m]
(if (:date-range properties)
(let [[date-range-key start-date-key end-date-key] (:date-range properties)
date-range-value (get m date-range-key)]
(if date-range-value
(-> (condp = date-range-value
"week"
(assoc m
start-date-key (time/plus (time/now) (time/days -7))
end-date-key (time/now))
"month"
(assoc m
start-date-key (time/plus (time/now) (time/months -1))
end-date-key (time/now))
"year"
(assoc m
start-date-key (time/plus (time/now) (time/years -1))
end-date-key (time/now))
"all"
(assoc m start-date-key (time/plus (time/now) (time/years -3))
end-date-key (time/now))
m)
(dissoc date-range-key))
m))
m))))}}}))
(defn ->db-id [m]
(cond
(map? m)
(:db/id m)
(nat-int? m)
m
(and (string? m) (not-empty m))
(Long/parseLong m)
:else
m))
(def pull-transformer
(mt2/transformer {:decoders
{:entity-map
{:compile (fn [schema _]
(let [pull-expr (:pull (mc/properties schema))]
(if pull-expr
(fn pull-data [m]
(cond
(nat-int? m)
(dc/pull (dc/db conn) pull-expr m)
(and (string? m) (not-empty m))
(dc/pull (dc/db conn) pull-expr (Long/parseLong m))
:else
nil))
identity)))}}
:encoders
{:entity-map
{:compile (fn [schema _]
(let [pull-expr (:pull (mc/properties schema))]
(if pull-expr
(fn pull-data [m]
(cond
(map? m)
(:db/id m)
(nat-int? m)
m
(and (string? m) (not-empty m))
(Long/parseLong m)
:else
m))
identity)))}}}))
(def coerce-vector
(mt2/transformer {:decoders {:vector {:compile (fn [schema _]
(when (:coerce? (m/properties schema))
(fn [data]
(cond
(vector? data)
data
(sequential? data)
data
(and (map? data)
(every? #(try (Long/parseLong %) true (catch Exception _ false)) (keys data)))
(into [] (->> (keys data)
sort
(map data)))
(nil? data)
nil
:else
[data]))))}}}))
(defn wrap-merge-prior-hx [handler]
(fn [{:keys [headers] :as request}]
(let [is-htmx-that-should-inherit-url-parameters? (and (not (get headers "hx-boosted"))
(get headers "hx-request"))]
(alog/peek ::check {:enabled? is-htmx-that-should-inherit-url-parameters?
:params (:query-params request)})
(if is-htmx-that-should-inherit-url-parameters?
(handler (update request :query-params (fn [qp]
(->> (concat (:hx-query-params request) qp)
(into {})))))
(handler request)))))
(def dissoc-nil-transformer
(let [e {:map {:compile (fn [schema _]
(fn [data]
(if (map? data)
(filter-vals
(fn [x]
(not (nil? x)))
data)
data)))}}]
(mt2/transformer {:encoders e
:decoders e})))
(def main-transformer
(mt2/transformer
parse-empty-as-nil
date-transformer
(mt2/key-transformer {:encode keyword->str :decode str->keyword})
mt2/string-transformer
mt2/json-transformer
(mt2/transformer {:name :arbitrary})
coerce-vector
date-range-transformer
pull-transformer
mt2/default-value-transformer))
(defn strip [s]
@@ -192,7 +378,7 @@
:error {:explain (mc/explain schema entity)}}))))
(defn schema-enforce-request [{:keys [form-params query-params params] :as request} & {:keys [form-schema query-schema route-schema params-schema]}]
(defn schema-enforce-request [{:keys [form-params query-params hx-query-params params] :as request} & {:keys [form-schema hx-schema query-schema route-schema params-schema]}]
(let [request (try
(cond-> request
(and (:params request) params-schema)
@@ -216,6 +402,14 @@
form-params
main-transformer))
(and hx-schema hx-query-params)
(assoc :hx-query-params
(mc/coerce
hx-schema
hx-query-params
main-transformer))
(and query-schema query-params)
(assoc :query-params
(mc/coerce
@@ -241,9 +435,10 @@
:error (:data (ex-data e))}))))]
request))
(defn wrap-schema-enforce [handler & {:keys [form-schema query-schema route-schema params-schema]}]
(defn wrap-schema-enforce [handler & {:keys [form-schema query-schema route-schema params-schema hx-schema]}]
(fn [request]
(handler (schema-enforce-request request
:hx-schema hx-schema
:form-schema form-schema
:query-schema query-schema
:route-schema route-schema
@@ -293,7 +488,10 @@
(into [:enum {:decode/string #(if (keyword? %)
%
(when (not-empty %)
(keyword n %)))}]
(keyword n %)))
:encode/string #(if (keyword? %)
(name %)
%)}]
(for [{:db/keys [ident]} (all-schema)
:when (= n (namespace ident))]
ident)))
@@ -375,4 +573,32 @@
(handler (if entity
(assoc request
:entity entity)
request)))))
request)))))
(mr/set-default-registry!
(mr/composite-registry
(mc/default-schemas)
{:entity-id entity-id
:entity-map
(mc/-simple-schema {:type :entity-map
:pred map?})
#_[:map {:name :entity-map} [:db/id nat-int?]]}))
(comment
(mc/coerce [:map [:x {:optional true} [:maybe [:entity-map {:pull '[:db/id]}]]]]
{:x nil :g 1}
main-transformer)
(mc/decode [:map [:x [:entity-map {:pull '[:db/id :db/ident]}]]]
{:x 87}
main-transformer))
(defn round-money [d]
(with-precision 2
(double (.setScale (bigdec d) 2 java.math.RoundingMode/HALF_UP))))
(defn wrap-implied-route-param [handler & {:as route-params}]
(fn [request]
(handler (update-in request [:route-params] merge route-params))))

View File

@@ -19,6 +19,7 @@
[com.brunobonacci.mulog.buffer :as rb]
[config.core :refer [env]]
[datomic.api :as dc]
[puget.printer :as puget]
[datomic.api :as d]
[figwheel.main.api]
[hawk.core]
@@ -27,30 +28,32 @@
(:import (org.apache.commons.io.input BOMInputStream)
[org.eclipse.jetty.server.handler.gzip GzipHandler]))
(defn println-event [item]
(printf "%s: %s - %s:%s by %s\n"
(str (c/to-date-time (:mulog/timestamp item)))
(:mulog/namespace item) (:mulog/event-name item)
(if (:mulog/duration item)
(str " " (int (/ (:mulog/duration item) 1000000)) "ms")
"")
(:user-name item))
(println (reduce
(fn [acc [k v]]
(assoc acc k v))
{}
(dissoc
item
:user)))
#_(puget/cprint (reduce
(fn [acc [k v]]
(assoc acc k v))
{}
(dissoc
item
:user))
{:seq-limit 10})
#_(printf "%s: %s - %s:%s by %s\n"
(str (c/to-date-time (:mulog/timestamp item)))
(:mulog/namespace item) (:mulog/event-name item)
(if (:mulog/duration item)
(str " " (int (/ (:mulog/duration item) 1000000)) "ms")
"")
(:user-name item))
#_(println (reduce
(fn [acc [k v]]
(assoc acc k v))
{}
(dissoc
item
:user)))
(when (= :auto-ap.logging/peek (:mulog/event-name item))
(println "\u001B[31mTEST")
)
(puget/cprint (reduce
(fn [acc [k v]]
(assoc acc k v))
{}
(dissoc
item
:user))
{:seq-limit 10})
(println))
@@ -354,7 +357,7 @@
`src` or `resources`."
[]
(println "starting auto reset")
(hawk.core/watch! [{:paths ["src/"]
(hawk.core/watch! [{:paths ["src/" "test/"]
:handler auto-reset-handler}]))

View File

@@ -150,7 +150,6 @@
{:db/id c :client/groups ["NTG"]}))))
(dc/q '[:find (count ?je)
:in $$
:where [$$ ?je :journal-entry/client 17592238607837]]

View File

@@ -25,6 +25,24 @@
(#{:invoice-page :payment-page :my-company-page :transaction-page :ledger-page} subject)
true
(= [:invoice :import] [subject activity])
true
(= [:invoice :create] [subject activity])
true
(= [:invoice :pay] [subject activity])
true
(= [:invoice :edit] [subject activity])
true
(= [:invoice :delete] [subject activity])
true
(= [:sales :read] [subject activity])
true
(= [:vendor :create] [subject activity])
true
@@ -44,6 +62,18 @@
(= [:vendor :edit] [subject activity])
true
(= [:invoice :create] [subject activity])
true
(= [:invoice :pay] [subject activity])
true
(= [:invoice :edit] [subject activity])
true
(= [:invoice :delete] [subject activity])
true
:else false)
(#{:user-role/read-only "read-only"} role)
@@ -66,6 +96,19 @@
(= [:signature :edit] [subject activity])
true
(= [:invoice :create] [subject activity])
true
(= [:invoice :pay] [subject activity])
true
(= [:invoice :edit] [subject activity])
true
(= [:invoice :delete] [subject activity])
true
:else false)
:else

View File

@@ -0,0 +1,32 @@
(ns auto-ap.routes.invoice)
(def routes {"" {:get ::all-page
"/unpaid" ::unpaid-page
"/paid" ::paid-page
"/voided" ::voided-page}
"/new" {:get ::new-wizard
:post ::new-invoice-submit
:put ::new-invoice-submit
"/due-date" ::due-date
"/scheduled-payment-date" ::scheduled-payment-date
"/navigate" ::new-wizard-navigate
"/account/new" ::new-wizard-new-account
"/account/location-select" ::location-select
"/account/prediction" ::account-prediction
"/total" ::expense-account-total}
"/pay-button" ::pay-button
"/pay" {:get ::pay-wizard
"/navigate" ::pay-wizard-navigate
:post ::pay-submit}
"/bulk-delete" {:get ::bulk-delete
:delete ::bulk-delete-confirm}
["/" [#"\d+" :db/id]] {:delete ::delete
"/unvoid" ::unvoid
"/edit" ::edit-wizard}
"/table" ::table
"/glimpse" {"" {:get :invoice-glimpse
:post :invoice-glimpse-upload
["/" [#"\w+" :textract-invoice-id]] {:get :invoice-glimpse-textract-invoice
"/create" {:post :invoice-glimpse-create-invoice}
"/update" {:patch :invoice-glimpse-update-textract-invoice}}}}})

View File

@@ -0,0 +1,4 @@
(ns auto-ap.routes.outgoing-invoice)
(def routes {"/" {"new" {:get ::new
:post ::new-submit}
"line-item/new" {:get ::new-line-item}}})

View File

@@ -0,0 +1,9 @@
(ns auto-ap.routes.payments)
(def routes {"" {:get ::all-page
"/pending" ::pending-page
"/voided" ::voided-page
"/cleared" ::cleared-page}
"/bulk-delete" {:get ::bulk-delete
:delete ::bulk-delete-confirm}
["/" [#"\d+" :db/id]] {:delete ::delete}
"/table" ::table})

View File

@@ -4,6 +4,9 @@
[auto-ap.routes.admin.import-batch :as ib-routes]
[auto-ap.routes.indicators :as indicator-routes]
[auto-ap.routes.admin.vendors :as v-routes]
[auto-ap.routes.outgoing-invoice :as oi-routes]
[auto-ap.routes.payments :as p-routes]
[auto-ap.routes.invoice :as i-routes]
[auto-ap.routes.admin.clients :as ac-routes]
[auto-ap.routes.admin.sales-summaries :as ss-routes]
[auto-ap.routes.admin.transaction-rules :as tr-routes]))
@@ -12,11 +15,7 @@
"logout" :logout
"search" :search
"indicators" indicator-routes/routes
"invoice" {"/glimpse" {"" {:get :invoice-glimpse
:post :invoice-glimpse-upload
["/" [#"\w+" :textract-invoice-id]] {:get :invoice-glimpse-textract-invoice
"/create" {:post :invoice-glimpse-create-invoice}
"/update" {:patch :invoice-glimpse-update-textract-invoice}}}}}
"account" {"/search" {:get :account-search}}
"admin" {"" :auto-ap.routes.admin/page
"/client" ac-routes/routes
@@ -66,6 +65,9 @@
"/cash-drawer-shifts" {"" {:get :pos-cash-drawer-shifts}
"/table" {:get :pos-cash-drawer-shift-table}}}
"outgoing-invoice" oi-routes/routes
"payment" p-routes/routes
"invoice" i-routes/routes
"vendor" {"/search" :vendor-search}
;; TODO Include IDS in routes for company-specific things, as opposed to headers
"company" {"" :company

View File

@@ -2,6 +2,7 @@
(:require [auto-ap.events :as events]
[auto-ap.routes :as routes]
[auto-ap.subs :as subs]
[auto-ap.routes.payments :as payment-routes]
[auto-ap.status :as status]
[auto-ap.views.components.buttons :as buttons]
[auto-ap.views.components.dropdown
@@ -17,7 +18,8 @@
[goog.string :as gstring]
[re-frame.core :as re-frame]
[auto-ap.views.components.expense-accounts-dialog :as expense-accounts-dialog]
[auto-ap.views.pages.data-page :as data-page]))
[auto-ap.views.pages.data-page :as data-page]
[auto-ap.ssr-routes :as ssr-routes]))
(defn data-params->query-params [params]
(if (:exact-match-id params)
@@ -195,7 +197,7 @@
[:td (:post-date (:transaction (:payment invoice-payment)))]
[:td
[buttons/fa-icon {:icon "fa-external-link"
:href (str (bidi/path-for routes/routes :payments )
:href (str (bidi/path-for ssr-routes/only-routes ::payment-routes/page )
"?"
(url/map->query {:exact-match-id (:id (:payment invoice-payment))}))}]]])
(when source-url

View File

@@ -6,6 +6,8 @@
[auto-ap.forms.builder :as form-builder]
[auto-ap.routes :as routes]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.routes.payments :as payment-routes]
[auto-ap.routes.invoice :as invoice-routes]
[auto-ap.subs :as subs]
[auto-ap.views.components.modal :as modal]
[auto-ap.views.components.search :as search]
@@ -175,11 +177,11 @@
"Home" ]
(when (p/can? @user {:subject :invoice-page})
[:a.navbar-item {:class [(active-when ap #{:unpaid-invoices :paid-invoices})]
:href (bidi/path-for routes/routes :unpaid-invoices)}
:href (str (bidi/path-for ssr-routes/only-routes ::invoice-routes/all-page) "?date-range=month")}
"Invoices" ])
(when (p/can? @user {:subject :payment-page})
[:a.navbar-item {:class [(active-when ap = :payments)]
:href (bidi/path-for routes/routes :payments)}
:href (str (bidi/path-for ssr-routes/only-routes ::payment-routes/all-page) "?date-range=month")}
"Payments" ])
(when (p/can? @user {:subject :pos-page})
[:a.navbar-item {:class [(active-when ap = :pos-sales)]

View File

@@ -1,25 +1,22 @@
(ns auto-ap.views.pages.transactions.table
(:require
[auto-ap.events :as events]
[auto-ap.routes :as routes]
[auto-ap.status :as status]
[auto-ap.subs :as subs]
[auto-ap.views.components.buttons :as buttons]
[auto-ap.views.components.dropdown
:refer [drop-down drop-down-contents]]
[auto-ap.views.components.grid :as grid]
[auto-ap.views.pages.data-page :as data-page]
[auto-ap.views.pages.transactions.form :as edit]
[auto-ap.views.utils
:refer [action-cell-width
date->str
dispatch-event-with-propagation
nf
pretty
with-role]]
[bidi.bidi :as bidi]
[cemerick.url :as url]
[re-frame.core :as re-frame]))
(:require [auto-ap.events :as events]
[auto-ap.routes :as routes]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.status :as status]
[auto-ap.routes.payments :as payment-route]
[auto-ap.subs :as subs]
[auto-ap.views.components.buttons :as buttons]
[auto-ap.views.components.dropdown
:refer [drop-down drop-down-contents]]
[auto-ap.views.components.grid :as grid]
[auto-ap.views.pages.data-page :as data-page]
[auto-ap.views.pages.transactions.form :as edit]
[auto-ap.views.utils
:refer [action-cell-width date->str dispatch-event-with-propagation nf
pretty with-role]]
[bidi.bidi :as bidi]
[cemerick.url :as url]
[re-frame.core :as re-frame]))
(re-frame/reg-event-fx
::editing-matches-found
@@ -133,7 +130,7 @@
[:td (date->str (:date payment) pretty)]
[:td
[buttons/fa-icon {:icon "fa-external-link"
:href (str (bidi/path-for routes/routes :payments)
:href (str (bidi/path-for ssr-routes/only-routes ::payment-route/page)
"?"
(url/map->query {:exact-match-id (:id payment)}))}]]])
(when expected-deposit