This commit is contained in:
2024-05-26 20:22:13 -07:00
111 changed files with 10708 additions and 5015 deletions

View File

@@ -35,61 +35,62 @@
:name (first name)})))
(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)
second
(not-empty)
Integer/parseInt)
(defn account-search [{{:keys [q client-id purpose vendor-id] :as qp} :query-params id :identity :as request}]
(let [client-id (or client-id (:db/id (:client request)))]
(when client-id
(assert-can-see-client id client-id))
(let [num (some-> (re-find #"([0-9]+)" q)
second
(not-empty)
Integer/parseInt)
valid-allowances (cond-> #{:allowance/allowed
:allowance/warn}
(is-admin? id) (conj :allowance/admin-only))
allowance (cond (= purpose "vendor")
:account/vendor-allowance
(= purpose "invoice")
:account/invoice-allowance
:else
:account/default-allowance)
valid-allowances (cond-> #{:allowance/allowed
:allowance/warn}
(is-admin? id) (conj :allowance/admin-only))
allowance (cond (= purpose "vendor")
:account/vendor-allowance
(= purpose "invoice")
:account/invoice-allowance
:else
:account/default-allowance)
vendor-account (when vendor-id
(-> (dc/q '[:find ?da
: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]]
{:label (str (:account/numeric-code a) " - " n)
:value (:db/id a)
:location (:account/location a)
:warning (when (= :allowance/warn (-> a allowance :db/ident))
"This account is not typically used for this purpose.")})))]
{:body (take 10 (if q
(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)
(sequence xform))
(->> (search- id q client-id)
(sequence
(comp (map (fn [i] [(:name i) (dc/pull (dc/db conn) search-pattern (:account_id i))]))
xform))))
[]))}))
vendor-account (when vendor-id
(-> (dc/q '[:find ?da
: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]]
{:label (str (:account/numeric-code a) " - " n)
:value (:db/id a)
:location (:account/location a)
:warning (when (= :allowance/warn (-> a allowance :db/ident))
"This account is not typically used for this purpose.")})))]
{:body (take 10 (if q
(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)
(sequence xform))
(->> (search- id q client-id)
(sequence
(comp (map (fn [i] [(:name i) (dc/pull (dc/db conn) search-pattern (:account_id i))]))
xform))))
[]))})))
(def account-search (wrap-json-response (wrap-schema-enforce account-search
:query-schema [:map

View File

@@ -64,7 +64,26 @@
:class "hot-filter"
:value (:code (:parsed-query-params request))
:placeholder "11101"
:size :small}))]])
:size :small}))
(com/field {:label "Type"}
(com/radio-card {:size :small
:name "type"
:options [{:value ""
:content "All"}
{:value "dividend"
:content "Dividend"}
{:value "asset"
:content "Asset"}
{:value "equity"
:content "Equity"}
{:value "liability"
:content "Liability"}
{:value "expense"
:content "Expense"}
{:value "revenue"
:content "Revenue"}
{:value "none"
:content "None"}]}))]])
(def default-read '[:db/id
:account/code
@@ -82,9 +101,9 @@
(defn fetch-ids [db request]
(let [query-params (:parsed-query-params request)
query (cond-> {:query {:find []
:in '[$ ]
:in '[$]
:where '[]}
:args [db ]}
:args [db]}
(:sort query-params) (add-sorter-fields {"name" ['[?e :account/name ?n]
'[(clojure.string/upper-case ?n) ?sort-name]]
"code" ['[(get-else $ ?e :account/numeric-code 0) ?sort-code]]
@@ -96,17 +115,24 @@
(merge-query {:query {:find []
:in ['?ns]
:where ['[?e :account/name ?an]
'[(clojure.string/upper-case ?an) ?upper-an]
'[(clojure.string/includes? ?upper-an ?ns)]]}
'[(clojure.string/upper-case ?an) ?upper-an]
'[(clojure.string/includes? ?upper-an ?ns)]]}
:args [(str/upper-case (:name query-params))]})
(some->> query-params :code)
(merge-query {:query {:find []
:in ['?nc]
:where ['[?e :account/numeric-code ?nc]
]}
:where ['[?e :account/numeric-code ?nc]]}
:args [(:code query-params)]})
(some->> query-params :type)
(merge-query {:query {:find []
:in ['?rir]
:where ['[?e :account/type ?r]
'[?r :db/ident ?ri]
'[(name ?ri) ?rir] ]}
:args [(some->> query-params :type)]})
true
(merge-query {:query {:find ['?sort-default '?e]
:where ['[?e :account/numeric-code ?un]

View File

@@ -497,7 +497,9 @@
:value (fc/field-value)
:options [["new-square" "New Square+Ezcater (no effect)"]
["manually-pay-cintas" "Manually Pay Cintas"]
["include-in-ntg-corp-reports" "Include in NTG Corporate reports"]]})))
["include-in-ntg-corp-reports" "Include in NTG Corporate reports"]
["import-custom-amount" "Import Custom Amount Line Items from Square"]
["code-sysco-items" "Code individual sysco line items"]]})))
(com/data-grid-cell {:class "align-top"}
(com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x))))
@@ -1282,11 +1284,10 @@
[:td (fc/with-field :square-location/client-location
(com/text-input {:name (fc/field-name)
:value (fc/field-value)}))]]))]]]])
(defn refresh-square-locations [request]
#_(alog/peek (keys (:query-params request)))
(defn refresh-square-locations [request]
(let [locations @(de/timeout!
(de/chain (square/client-locations {:client/square-auth-token (get-in request [:query-params "step-params[client/square-auth-token]"])})
(de/chain (square/client-locations {:client/square-auth-token (get-in request [:query-params (keyword "step-params[client/square-auth-token]")])})
(fn [client-locations]
(into []
(for [square-location client-locations]
@@ -1324,19 +1325,23 @@
:body (mm/default-step-body
{}
[:div
(fc/with-field :client/square-auth-token
(com/validated-field
{:errors (fc/field-errors)
:label "Square Auth Token"}
(com/text-input {:name (fc/field-name)
:error? (fc/error?)
:hx-get (bidi/path-for ssr-routes/only-routes ::route/refresh-square-locations)
:hx-trigger "keyup changed delay:1s queue:none"
:hx-indicator "#square-locations"
:hx-target "#square-locations"
:placeholder "Token from square"
:class "w-64"
:value (fc/field-value)})))
[:div.flex.gap-2.items-center
(fc/with-field :client/square-auth-token
(com/validated-field
{:errors (fc/field-errors)
:label "Square Auth Token"}
(com/text-input {:name (fc/field-name)
:id "square-token"
:error? (fc/error?)
:placeholder "Token from square"
:class "w-64"
:value (fc/field-value)})))
(com/button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/refresh-square-locations)
:hx-include "#square-token"
:hx-trigger "click"
:hx-indicator "#square-locations"
:hx-target "#square-locations" }
"Refresh")]
(fc/with-field :client/square-locations
(square-location-table))])

View File

@@ -2,20 +2,31 @@
(:require [auto-ap.datomic
:refer [apply-pagination apply-sort-3 conn merge-query pull-many
query2]]
[auto-ap.datomic.accounts :as d-accounts]
[auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.routes.admin.sales-summaries :as route]
[auto-ap.routes.utils
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
[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.grid-page-helper :as helper]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils
:refer [apply-middleware-to-all-handlers]]
:refer [apply-middleware-to-all-handlers entity-id html-response
money strip temp-id wrap-schema-enforce]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[clj-time.coerce :as c]
[clojure.string :as str]
[datomic.api :as dc]
[iol-ion.query :refer [dollars=]]))
[hiccup.util :as hu]
[iol-ion.query :refer [dollars=]]
[malli.core :as mc]
[malli.util :as mut]))
(defn filters [request]
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
@@ -44,8 +55,18 @@
:size :small}))]])
(def default-read '[:db/id
*
[:sales-summary/date :xform clj-time.coerce/from-date]
*]) ;; TODO
{:sales-summary/client [:client/code :client/name :db/id]}
{:sales-summary/items [{[:ledger-mapped/ledger-side :xform iol-ion.query/ident] [:db/ident]
} ;; TODO clientize
:ledger-mapped/account
:ledger-mapped/amount
:sales-summary-item/category
:sales-summary-item/sort-order
:db/id
:sales-summary-item/manual?]
} ]) ;; TODO
(defn fetch-ids [db request]
(let [query-params (:parsed-query-params request)
@@ -95,7 +116,7 @@
[(->> (hydrate-results ids-to-retrieve db request))
matching-count]))
(defn get-debits [ss]
#_(defn get-debits [ss]
{:card-payments (+ (:sales-summary/total-card-payments ss 0.0)
(:sales-summary/total-card-fees ss 0.0)
(- (:sales-summary/total-card-refunds ss 0.0)))
@@ -103,8 +124,8 @@
(:sales-summary/total-food-app-fees ss 0.0)
(- (:sales-summary/total-food-app-refunds ss 0.0)))
:gift-card-payments (+ (:sales-summary/total-gift-card-payments ss 0.0)
(:sales-summary/total-gift-card-fees ss 0.0)
(- (:sales-summary/total-gift-card-refunds ss 0.0)))
(:sales-summary/total-gift-card-fees ss 0.0)
(- (:sales-summary/total-gift-card-refunds ss 0.0)))
#_#_:refunds (+ (:sales-summary/total-food-app-refunds ss 0.0)
(:sales-summary/total-card-refunds ss 0.0)
(:sales-summary/total-cash-refunds ss 0.0))
@@ -112,8 +133,25 @@
:fees (- (:sales-summary/total-card-fees ss 0.0))
:cash-payments (+ (:sales-summary/total-cash-payments ss 0.0)
(- (:sales-summary/total-cash-refunds ss 0.0)))
:total-unknown-processor-payments (:sales-summary/total-unknown-processor-payments ss 0.0)
:discounts (+ (:sales-summary/discount ss 0.0))
:returns (+ (:sales-summary/total-returns ss 0.0))})
(defn sort-items [ss]
(sort-by (juxt :ledger-mapped/ledger-side :sales-summary-item/sort-order :sales-summary-item/category) ss))
(defn total-debits [items]
(->> items
(filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)))
(map #(:ledger-mapped/amount % 0.0))
(reduce + 0.0)))
(defn total-credits [items]
(->> items
(filter #(= :ledger-side/credit (:ledger-mapped/ledger-side %)))
(map #(:ledger-mapped/amount % 0.0))
(reduce + 0.0)))
(def grid-page
(helper/build {:id "entity-table"
@@ -122,7 +160,10 @@
:fetch-page fetch-page
:page-specific-nav filters
:row-buttons (fn [_ entity]
[])
[(com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes
::route/edit-wizard
:db/id (:db/id entity))}
svg/pencil)])
:oob-render
(fn [request]
[#_(assoc-in (date-range-field {:value {:start (:start-date (:parsed-query-params request))
@@ -140,86 +181,53 @@
:title "Sales Summaries"
:entity-name "Daily Summary"
:route ::route/table
:headers [{:key "date"
:headers [{:key "client"
:name "Client"
:sort-key "client"
:hide? (fn [args]
(= (count (:clients args)) 1))
:render #(-> % :sales-summary/client :client/code)}
{:key "date"
:name "Date"
:sort-key "date"
:render #(some-> % :sales-summary/date (atime/unparse-local atime/normal-date))}
{:key "credits"
:name "credits"
:sort-key "credits"
:render (fn [ss]
(let [total-debits (reduce + 0.0 (vals (get-debits ss)))
total-credits (+ (- (+ (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/tax (:sales-summary/sales-items ss))))
(:sales-summary/total-tax ss 0.0)
(:sales-summary/total-tip ss 0.0))]
[:ul
(for [[n x] (group-by :sales-summary-item/category (:sales-summary/sales-items ss))]
[:li n ": " (format "$%,.2f" (- (+ (reduce + 0.0 (map :sales-summary-item/total x))
(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/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)
:primary
:red)} "Total: " (format "$%,.2f" total-credits))]])
#_(count))}
{:key "debits"
:name "debits"
:sort-key "debits"
:render (fn [ss]
(let [{:keys [card-payments food-app-payments
cash-payments discounts fees
gift-card-payments
returns refunds] :as debits} (get-debits ss)
total-debits (reduce + 0.0 (vals debits))
total-credits (+ (- (+ (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/tax (:sales-summary/sales-items ss))))
(:sales-summary/total-tax ss 0.0)
(:sales-summary/total-tip ss 0.0))]
(let [total-debits (total-debits (:sales-summary/items ss))
total-credits (total-credits (:sales-summary/items ss))]
[:ul
[:li "Card Payments: "
(format "$%,.2f" card-payments)]
[:li "Food App Payments: "
(format "$%,.2f" food-app-payments)]
[:li "Gift Card Payments"
(format "$%,.2f" gift-card-payments)]
[:li "Cash Payments: "
(format "$%,.2f" cash-payments)]
[:li "Discounts: "
(format "$%,.2f" discounts)]
[:li "Fees: "
(format "$%,.2f" fees)]
[:li "Returns: "
(format "$%,.2f" returns)]
#_[:li "Refunds: "
(format "$%,.2f" refunds)]
(for [si (sort-items (:sales-summary/items ss))
:when (= :ledger-side/debit (:ledger-mapped/ledger-side si))]
[:li (:sales-summary-item/category si) ": " (format "$%,.2f" (:ledger-mapped/amount si))
(when-not (:ledger-mapped/account si)
[:span.pl-4 (com/pill {:color :red}
"missing account")])]
)
[:li (com/pill {:color (if (dollars= total-debits total-credits)
:primary
:red)} "Total: " (format "$%,.2f" total-debits))]])
#_(count))}]}))
:red)} "Total: " (format "$%,.2f" total-debits))]]))}
{:key "credits"
:name "credits"
:sort-key "credits"
:render (fn [ss]
(let [total-debits (total-debits (:sales-summary/items ss))
total-credits (total-credits (:sales-summary/items ss))]
[:ul
(for [si (sort-items (:sales-summary/items ss))
:when (= :ledger-side/credit (:ledger-mapped/ledger-side si))]
[:li (:sales-summary-item/category si) ": " (format "$%,.2f" (:ledger-mapped/amount si))
(when-not (:ledger-mapped/account si)
[:span.pl-4 (com/pill {:color :red}
"missing account")])])
[:li (com/pill {:color (if (dollars= total-debits total-credits)
:primary
:red)} "Total: " (format "$%,.2f" total-credits))]]))}]}))
;; TODO schema cleanup
;; Decide on what should be calculated as generating ledger entries, and what should be calculated
@@ -231,11 +239,302 @@
(def row* (partial helper/row* grid-page))
(def table* (partial helper/table* grid-page))
(def edit-schema
[:map
[:db/id entity-id]
[:sales-summary/client [:map [:db/id entity-id]]]
[:sales-summary/items
[:vector {:coerce? true}
[:and
[:map
[:db/id [:or entity-id temp-id]]
[:sales-summary-item/category [:string {:decode/string strip}]]
[:sales-summary-item/manual? {:default false :decode/arbitrary (fn [x] (cond
(boolean? x)
x
(nil? x)
false
(str/blank? x)
false
:else
true))} :boolean]
[:ledger-mapped/account entity-id]
[:credit {:optional true} [:maybe money]]
[:debit {:optional true} [:maybe money]]]
[:fn {:error/message "Must choose one of credit/debit"
:error/path [:credit]}
(fn [x]
(not (and (:credit x)
(:debit x))))]]]] ])
(defn summary-total-row* [request]
(let [total-credits (-> request
:multi-form-state
:step-params
:sales-summary/items
(total-credits))
total-debits (-> request
:multi-form-state
:step-params
:sales-summary/items
(total-debits))]
(com/data-grid-row {:id "total-row"
: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"}
(com/data-grid-cell {})
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"])
(com/data-grid-cell {:class "text-right"}
(format "$%,.2f" total-debits))
(com/data-grid-cell {:class "text-right"}
(format "$%,.2f" total-credits)))))
(defn unbalanced-row* [request]
(let [total-credits (-> request
:multi-form-state
:step-params
:sales-summary/items
(total-credits))
total-debits (-> request
:multi-form-state
:step-params
:sales-summary/items
(total-debits))]
(com/data-grid-row {:id "total-row"
: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"}
(com/data-grid-cell {})
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "UNBALANCED"])
(com/data-grid-cell {:class "text-right"}
(when (and
(not (dollars= total-credits total-debits))
(> total-debits total-credits))
(format "$%,.2f" (- total-debits total-credits))))
(com/data-grid-cell {:class "text-right"}
(when
(and (not (dollars= total-credits total-debits))
(> total-credits total-debits))
(format "$%,.2f" (- total-credits total-debits)))))))
(defn- account-typeahead*
[{:keys [name value client-id]}]
[: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"})
:value value
:content-fn (fn [value]
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
client-id)))})])
(defn sales-summary-item-row* [{:keys [value client-id]}]
(let [manual? (fc/field-value (:sales-summary-item/manual? value))]
(com/data-grid-row (cond-> {:x-ref "p"
:x-data (hx/json {})}
(fc/field-value (:new? value)) (hx/htmx-transition-appear ))
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
:value (fc/field-value)}))
(when manual?
(fc/with-field :sales-summary-item/manual?
(com/hidden {:name (fc/field-name)
:value true})))
(com/data-grid-cell {}
(fc/with-field :sales-summary-item/category
(if manual?
(com/validated-field {:errors (fc/field-errors)}
(com/text-input {:placeholder "Category/Explanation"
:name (fc/field-name)
:value (fc/field-value)}))
(list
(com/hidden {:name (fc/field-name)
:value (fc/field-value)})
(fc/field-value (:sales-summary-item/category value))))))
(com/data-grid-cell {}
(fc/with-field :ledger-mapped/account
(com/validated-field {:errors (fc/field-errors)}
(account-typeahead* {:value (fc/field-value)
:client-id client-id
:name (fc/field-name)}))))
(com/data-grid-cell {:class "text-right"}
(if manual?
(fc/with-field :debit
(com/validated-field {:errors (fc/field-errors)}
(com/money-input {:class "w-24"
:name (fc/field-name)
:value (fc/field-value)})))
(when (= (fc/field-value (:ledger-mapped/ledger-side value))
:ledger-side/debit)
(format "$%,.2f" (fc/field-value (:ledger-mapped/amount value))))))
(com/data-grid-cell {:class "text-right"}
(if manual?
(fc/with-field :credit
(com/validated-field {:errors (fc/field-errors)}
(com/money-input {:class "w-24"
:name (fc/field-name)
:value (fc/field-value)})))
(when (= (fc/field-value (:ledger-mapped/ledger-side value))
:ledger-side/credit)
(format "$%,.2f" (fc/field-value (:ledger-mapped/amount value))))))
(com/data-grid-cell {:class "align-top"}
(when manual?
(com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x))))))
(defrecord MainStep [linear-wizard]
mm/ModalWizardStep
(step-name [_]
"Main")
(step-key [_]
:main)
(edit-path [_ _]
[])
(step-schema [_]
(mut/select-keys (mm/form-schema linear-wizard) #{:db/id :sales-summary/items}))
(render-step
[this {:keys [multi-form-state] :as request}]
(mm/default-render-step
linear-wizard this
:head [:div.p-2 "New invoice"]
:body (mm/default-step-body
{}
[:div
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
:value (fc/field-value)}))
(com/data-grid {:headers
[(com/data-grid-header {} "Category")
(com/data-grid-header {} "Account")
(com/data-grid-header {} "Debits")
(com/data-grid-header {} "Credits")
(com/data-grid-header {} "")]}
(fc/with-field :sales-summary/items
(list
(fc/cursor-map #(sales-summary-item-row* {:value %
:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state))) }))
;; TODO
(com/data-grid-new-row {:colspan 5
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item)
:row-offset 0
:index (count (fc/field-value))
:tr-params {:hx-vals (hx/json {:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))})}} ;; TODO
"New Summary Item")))
(summary-total-row* request)
(unbalanced-row* request)) ])
:footer
(mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate)
:validation-route ::route/edit-wizard-navigate
:width-height-class "lg:w-[850px] lg:h-[900px]")))
(defn attach-ledger [i]
(cond-> i
(:credit i) (assoc :ledger-mapped/ledger-side :ledger-side/credit
:ledger-mapped/amount (:credit i))
(:debit i) (assoc :ledger-mapped/ledger-side :ledger-side/debit
:ledger-mapped/amount (:debit i))
true (dissoc :credit :debit)
true (assoc :sales-summary-item/manual? true)))
(defrecord EditWizard [_ current-step]
mm/LinearModalWizard
(hydrate-from-request
[this request]
this)
(navigate [this step-key]
(assoc this :current-step step-key))
(get-current-step
[this]
(mm/get-step this :main))
(render-wizard [this {:keys [multi-form-state] :as request}]
(mm/default-render-wizard
this request
:form-params
(-> mm/default-form-props
(assoc :hx-put
(str (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-submit))))
:render-timeline? false))
(steps [_]
[:main])
(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]
(->MainStep this)))
(form-schema [_]
edit-schema)
(submit [this {:keys [multi-form-state request-method identity] :as request}]
(let [result (:snapshot multi-form-state )
transaction [:upsert-entity {:db/id (:db/id result)
:sales-summary/items (map
(fn [i]
(if (:sales-summary-item/manual? i)
(attach-ledger i)
{:db/id (:db/id i)
:ledger-mapped/account (:ledger-mapped/account i)
}))
(:sales-summary/items result))}]]
(clojure.pprint/pprint (:sales-summary/items result))
@(dc/transact conn [ transaction])
(html-response
(row* identity (dc/pull (dc/db conn) default-read (:db/id result))
{:flash? true
:request request})
:headers (cond-> {"hx-trigger" "modalclose"
"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id result))
"hx-reswap" "outerHTML"})))))
(def edit-wizard (->EditWizard nil nil))
(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 edit-schema))
entity (update entity :sales-summary/items (comp #(map (fn [x]
(if (= :ledger-side/debit (:ledger-mapped/ledger-side x))
(assoc x :debit (:ledger-mapped/amount x))
(assoc x :credit (:ledger-mapped/amount x))))
%) sort-items))]
(mm/->MultiStepFormState entity [] entity)))
(def key->handler
(apply-middleware-to-all-handlers
(->>
{::route/page (helper/page-route grid-page)
::route/table (helper/table-route grid-page)})
::route/table (helper/table-route grid-page)
::route/edit-wizard (-> mm/open-wizard-handler
(mm/wrap-wizard edit-wizard)
(mm/wrap-init-multi-form-state initial-edit-wizard-state)
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
::route/edit-wizard-navigate (-> mm/next-handler
(mm/wrap-wizard edit-wizard)
(mm/wrap-decode-multi-form-state))
::route/new-summary-item (-> (add-new-entity-handler [:step-params :sales-summary/items]
(fn render [cursor request]
(sales-summary-item-row*
{:value cursor
:client-id (:client-id (:query-params request)) }))
(fn build-new-row [base _]
(assoc base :sales-summary-item/manual? true)))
(wrap-schema-enforce :query-schema [:map
[:client-id {:optional true}
[:maybe entity-id]]]))
::route/edit-wizard-submit (-> mm/submit-handler
(mm/wrap-wizard edit-wizard)
(mm/wrap-decode-multi-form-state))})
(fn [h]
(-> h
(wrap-admin)

View File

@@ -132,6 +132,7 @@
request
(com/page {:nav com/company-aside-nav
:client-selection (:client-selection request)
:request request
:client (:client request)
:clients (:clients request)
:identity (:identity request)

View File

@@ -1,25 +1,24 @@
(ns auto-ap.ssr.company.reports
(:require
[amazonica.aws.s3 :as s3]
[auto-ap.datomic
:refer [add-sorter-fields
apply-pagination
apply-sort-3
conn
merge-query
pull-many
query2]]
[auto-ap.graphql.utils :refer [assert-can-see-client is-admin?]]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.grid-page-helper :as helper]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils :refer [html-response]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[clojure.set :as set]
[config.core :refer [env]]
[datomic.api :as dc]))
(:require [amazonica.aws.s3 :as s3]
[auto-ap.datomic
:refer [add-sorter-fields apply-pagination apply-sort-3 conn merge-query
pull-many query2]]
[auto-ap.graphql.utils :refer [assert-can-see-client is-admin?]]
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated
wrap-secure]]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.company.reports.expense :as company-expense-report]
[auto-ap.ssr.company.reports.reconciliation :as company-reconciliation-report]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.grid-page-helper :as helper]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers
html-response]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[clojure.set :as set]
[config.core :refer [env]]
[datomic.api :as dc]))
(def default-read '[:db/id :report/client [:report/created :xform clj-time.coerce/from-date] :report/url :report/name :report/creator])
@@ -132,3 +131,17 @@
{:flash? true
:delete-after-settle? true}))))
(def key->handler
(apply-middleware-to-all-handlers
(->>
(into
{:company-reports page
:company-reports-table table
:company-reports-delete delete-report}
company-expense-report/key->handler)
(into company-reconciliation-report/key->handler))
(fn [h]
(-> h
(wrap-secure)
(wrap-client-redirect-unauthenticated)))))

View File

@@ -0,0 +1,295 @@
(ns auto-ap.ssr.company.reports.expense
(:require [auto-ap.datomic :refer [conn merge-query]]
[auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.logging :as alog]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers
clj-date-schema html-response
wrap-schema-enforce]]
[auto-ap.time :as atime]
[auto-ap.utils :refer [by]]
[bidi.bidi :as bidi]
[cemerick.url :as url]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[datomic.api :as dc]
[hiccup2.core :as hiccup]))
(defn lookup-breakdown-data [request]
(let [query (cond-> {:query '{:find [?cn ?user-date (sum ?amt)]
:with [?e]
:in [$ [?clients ?start ?end]]
:where
[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]
(not [?e :invoice/status :invoice-status/voided])
[?e :invoice/date ?d]
[?e :invoice/client ?c]
[?e :invoice/expense-accounts ?iea]
[?iea :invoice-expense-account/amount ?amt]
[?c :client/name ?cn]
[(clj-time.coerce/to-date-time ?d) ?user-date]]}
:args
[(dc/db conn)
[(extract-client-ids (:clients request)
(:client-id request)
(when (:client-code request)
[:client/code (:client-code request)]))
(some-> (time/plus (time/now) (time/days -65)) coerce/to-date)
(some-> (time/now) coerce/to-date)]]}
(:vendor-id (:query-params request))
(merge-query {:query '{:in [?v]
:where [ [?e :invoice/vendor ?v]]}
:args [ (:db/id (:vendor-id (:query-params request)))]})
(:account-id (:query-params request))
(merge-query {:query '{:in [?a]
:where [ [?iea :invoice-expense-account/account ?a]]}
:args [ (:db/id (:account-id (:query-params request)))]}))]
(dc/query query)))
(defn lookup-invoice-total-data [request]
(let [start (:start-date (:query-params request) (time/plus (time/now) (time/days -30)))
end (:end-date (:query-params request) (time/now))
query (cond-> {:query '{:find [?cn ?vn (sum ?t)]
:with [ ?e]
:in [$ [?clients ?start ?end]]
:where
[[(iol-ion.query/scan-invoices $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]
(not [?e :invoice/status :invoice-status/voided])
[?e :invoice/client ?c]
[?e :invoice/total ?t]
[?e :invoice/vendor ?v]
[?v :vendor/name ?vn]
[?c :client/name ?cn]
]}
:args
[(dc/db conn)
[(extract-client-ids (:clients request)
(:client-id request)
(when (:client-code request)
[:client/code (:client-code request)]))
(some-> start coerce/to-date)
(some-> end coerce/to-date)]]})]
(dc/query query)))
(defn week-seq
([c] (week-seq c (atime/last-monday)))
([c starting] (reverse (for [n (range c)
:let [start (time/minus starting (time/weeks n))
end (time/minus starting (time/weeks (dec n)))]]
[(atime/as-local-time (coerce/to-date-time start)) (atime/as-local-time (coerce/to-date-time end))]))))
(defn- best-week [d weeks]
(reduce
(fn [acc [start end]]
(if (and (time/after? d start)
(time/before? d end))
(reduced [start end])
nil))
nil
weeks))
(defn expense-breakdown-card* [request]
(com/card {:class "w-full h-full" :id "expense-breakdown-report"}
[:div {:class "flex flex-col px-8 py-8 space-y-3 w-full h-full"}
[:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-breakdown-card)
:hx-trigger "change"
:hx-target "#expense-breakdown-report"
:hx-swap "outerHTML"}
(fc/start-form
(:query-params request)
(:form-errors request)
[:div.flex.justify-between
[:h1.text-2xl.mb-3.font-bold "Expense breakdown report, last 8 weeks"]
[:div.flex.gap-2
(fc/with-field :vendor-id
(com/validated-field {:label "Vendor"
:errors (fc/field-errors)}
(com/typeahead {:name (fc/field-name)
:class "w-64"
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
:value (fc/field-value)
:value-fn :db/id
:content-fn :vendor/name})))
(fc/with-field :account-id
(com/validated-field {:label "Account"
:errors (fc/field-errors)}
(com/typeahead {:name (fc/field-name)
:class "w-64"
:url (bidi/path-for ssr-routes/only-routes :account-search)
:value (fc/field-value)
:value-fn :db/id
:content-fn :account/name})))]])]
[:div.flex-grow
(let [data (lookup-breakdown-data request)
distinct-accounts (->> data
(reduce
(fn [acc [an _ amount]]
(update acc an (fnil + 0.0) amount))
{})
(sort-by last)
(reverse)
(map first)
(take 20))
weeks (week-seq 8)
x-axis (for [[start end] weeks]
(str (iol-ion.query/excel-date (coerce/to-date start))
" - "
(iol-ion.query/excel-date (coerce/to-date end))))
lookup (->>
(reduce
(fn [acc [a d v]]
(update-in acc [a (best-week d weeks)] (fnil + 0.0) v))
{}
data))
series (for [ea distinct-accounts]
(for [d weeks]
(get-in lookup [ea d] 0)))]
[:canvas {:x-data (hx/json {:chart nil
:labels x-axis
:datasets (map (fn [s a] {:label a
:data s
:borderWidth 1})
series
distinct-accounts)})
:x-init
"new Chart($el, {
type: 'bar',
data: {
labels: labels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});"}])]]))
(defn vendor-invoice-total-card* [request]
(com/content-card {:class "w-full" :id "invoice-totals-report"}
[:div {:class "flex flex-col px-8 py-8 space-y-3"}
[:div
[:h1.text-2xl.mb-3.font-bold "Invoice totals by vendor"]
[:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-invoice-total-card )
:hx-trigger "change"
:hx-target "#invoice-totals-report"
:hx-swap "outerHTML"}
(fc/start-form
(:query-params request)
(:form-errors request)
[:div.flex.gap-2
(fc/with-field :start-date
(com/validated-field {:label "Start"
:errors (fc/field-errors)}
[:div {:class "w-64"}
(com/date-input {:name (fc/field-name)
:class "w-64"
:value (some-> (fc/field-value)
(atime/unparse-local atime/normal-date)) })]))
(fc/with-field :end-date
(com/validated-field {:label "End"
:errors (fc/field-errors)}
[:div {:class "w-64"}
(com/date-input {:name (fc/field-name)
:class "w-64"
:value (some-> (fc/field-value)
(atime/unparse-local atime/normal-date)) })]))])]
[:div {:class "overflow-scroll min-w-full max-h-[700px]"}
(let [data (lookup-invoice-total-data request)
companies (sort (set (map first data)))
vendors (sort (set (map second data)))
result (by (juxt first second) last data)
]
(com/data-grid
{:headers (into
[(com/data-grid-header {:class "sticky left-0 z-60 bg-gray-100"} "Vendor")]
(for [company companies]
(com/data-grid-header {} company)))
:thead-params {:class "sticky top-0 z-50"}}
(for [vendor vendors]
(com/data-grid-row
{}
(com/data-grid-cell {:class "sticky left-0 z-0 bg-gray-100"}
vendor)
(for [company companies]
(com/data-grid-cell
{}
(or (some->> (get result [company vendor])
(format "$%,.2f" ))
[:span.text-gray-200 "-"])))))))]]]))
(defn page [request]
(base-page
request
(com/page {:nav com/company-aside-nav
:client-selection (:client-selection request)
:client (:client request)
:clients (:clients request)
:identity (:identity request)
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes :company-expense-report)
:hx-trigger "clientSelected from:body"
:hx-select "#app-contents"
:hx-swap "outerHTML swap:300ms"}}
(com/breadcrumbs {}
[:a {:href (bidi/path-for ssr-routes/only-routes :company)}
"My Company"]
[:a {:href (bidi/path-for ssr-routes/only-routes :company-expense-report)}
"Expense Report"])
(expense-breakdown-card* request)
(vendor-invoice-total-card* request))
"My Company"))
(defn normalize-query-params [request]
(-> request
:query-params
(update :vendor-id :db/id)
(update :account-id :db/id)
(update :start-date #(atime/unparse-local % atime/normal-date))
(update :end-date #(atime/unparse-local % atime/normal-date))
url/map->query))
(defn expense-breakdown-card [request]
(html-response
(expense-breakdown-card* request)
:headers {"hx-push-url" (str "?" (normalize-query-params request))}))
(defn invoice-total-card [request]
(html-response
(vendor-invoice-total-card* request)
:headers {"hx-push-url" (str "?" (normalize-query-params request))}))
(def key->handler
(apply-middleware-to-all-handlers
{:company-expense-report page
:company-expense-report-breakdown-card expense-breakdown-card
:company-expense-report-invoice-total-card invoice-total-card}
(fn [h]
(-> h
(wrap-schema-enforce :query-schema
[:map {:default {}}
[:start-date {:optional true}
[:maybe clj-date-schema]]
[:end-date {:optional true}
[:maybe clj-date-schema]]
[:vendor-id {:optional true}
[:maybe
[:entity-map {:pull [:vendor/name :db/id]}]]]
[:account-id {:optional true}
[:maybe
[:entity-map {:pull [:account/name :db/id]}]]]])))))

View File

@@ -0,0 +1,201 @@
(ns auto-ap.ssr.company.reports.reconciliation
(:require [auto-ap.datomic :refer [conn pull-attr]]
[auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.import.intuit :refer [get-intuit-bank-accounts
intuits->transactions]]
[auto-ap.intuit.core :refer [get-transactions]]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers
clj-date-schema html-response
wrap-schema-enforce]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[cemerick.url :as url]
[clj-time.coerce :as coerce]
[datomic.api :as dc]
[auto-ap.ssr.svg :as svg]))
(defn report* [{:keys [request report]}]
[:div #_{:class "overflow-scroll min-w-full max-h-[700px]"}
(com/data-grid
{:headers (into
[(com/data-grid-header {} "Bank Account")
(com/data-grid-header {} "Source count")
(com/data-grid-header {} "Synced count")
(com/data-grid-header {} "Approved transactions")
(com/data-grid-header {} "Unapproved transactions")
(com/data-grid-header {} "Requires feedback transactions")
(com/data-grid-header {} "Missing transactions")])
#_#_:thead-params {:class "sticky top-0 z-50"}}
(for [row report]
(let [matches? (= (:external-transaction-count row)
(:integreat-transaction-count row))
class (if matches? "bg-primary-200 text-primary-900"
"bg-red-200 text-red-900")]
(com/data-grid-row
{}
(com/data-grid-cell {:class class}
(:bank-account/name row))
(com/data-grid-cell {:class class}
(:external-transaction-count row))
(com/data-grid-cell {:class class}
(:integreat-transaction-count row))
(com/data-grid-cell {:class class}
(:approved-count row))
(com/data-grid-cell {:class class}
(:unapproved-count row))
(com/data-grid-cell {:class class}
(:requires-feedback-count row))
(com/data-grid-cell {:class class}
(when (> (count (:missing-transactions row)) 0)
[:div { :x-data (hx/json {:popper nil
:hovering false})
"x-init" "popper = Popper.createPopper($refs.hover_target, $refs.tooltip, {placement: 'bottom', strategy:'fixed', modifiers: [{name: 'preventOverflow'}, {name: 'offset', options: {offset: [0, 10]}}]});"}
(com/button {"x-ref" "hover_target"
"@click.prevent" "hovering=!hovering; $nextTick(() => popper.update())"}
[:div.flex.gap-2.items-center
(count (:missing-transactions row))
[:div.w-4.h-4 svg/question]
])
[:div (hx/alpine-appear {:x-ref "tooltip"
:x-show "hovering"
:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 p-4"})
(com/data-grid {:headers [(com/data-grid-header {} "Date")
(com/data-grid-header {} "Amount")]}
(for [r (:missing-transactions row)]
(com/data-grid-row {}
(com/data-grid-cell {}
(atime/unparse-local (coerce/to-date-time (:transaction/date r)) atime/normal-date))
(com/data-grid-cell {}
(format "$%,.2f" (:transaction/amount r)))))) ] ]))))))])
(defn reconciliation-card* [{:keys [request report]}]
(com/content-card {:class "w-full" :id "reconciliation-report"}
[:div {:class "flex flex-col px-8 py-8 space-y-3"}
[:div
[:h1.text-2xl.mb-3.font-bold "Bank Reconciliation Report"]
[:form {:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-reconciliation-report-card)
:hx-target "#reconciliation-report"
:hx-swap "outerHTML"}
(fc/start-form
(:query-params request)
(:form-errors request)
[:div.flex.gap-2
(fc/with-field :start-date
(com/validated-field {:label "Start"
:errors (fc/field-errors)}
[:div {:class "w-64"}
(com/date-input {:name (fc/field-name)
:class "w-64"
:value (some-> (fc/field-value)
(atime/unparse-local atime/normal-date)) })]))
(fc/with-field :end-date
(com/validated-field {:label "End"
:errors (fc/field-errors)}
[:div {:class "w-64"}
(com/date-input {:name (fc/field-name)
:class "w-64"
:value (some-> (fc/field-value)
(atime/unparse-local atime/normal-date)) })]))
(com/button {:color :primary :class "self-center w-24"} "Run")])]
(if report
(report* {:request request :report report})
[:div "Please choose a time range to run the report"])
]]))
(defn page [request]
(base-page
request
(com/page {:nav com/company-aside-nav
:client-selection (:client-selection request)
:client (:client request)
:clients (:clients request)
:identity (:identity request)
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes :company-reconciliation-report)
:hx-trigger "clientSelected from:body"
:hx-select "#app-contents"
:hx-swap "outerHTML swap:300ms"}}
(com/breadcrumbs {}
[:a {:href (bidi/path-for ssr-routes/only-routes :company)}
"My Company"]
[:a {:href (bidi/path-for ssr-routes/only-routes :company-reconciliation-report)}
"Reconciliation Report"])
(reconciliation-card* {:request request :report nil}))
"My Company"))
(defn normalize-query-params [request]
(-> request
:query-params
(update :vendor-id :db/id)
(update :account-id :db/id)
(update :start-date #(atime/unparse-local % atime/normal-date))
(update :end-date #(atime/unparse-local % atime/normal-date))
url/map->query))
(defn get-report-data [start-date end-date client-ids]
(let [client-codes (map first (dc/q '[:find ?cc :in $ [?c ...] :where [?c :client/code ?cc]] (dc/db conn ) client-ids))]
(for [[ib ba c] (seq (apply get-intuit-bank-accounts (dc/db conn) client-codes))
:let [raw-transactions (get-transactions (atime/unparse-local start-date atime/iso-date)
(atime/unparse-local end-date atime/iso-date)
ib)
ideal-transactions (intuits->transactions raw-transactions ba c)
found-transactions (when (seq ideal-transactions)
(into {} (dc/q '[:find ?si (count ?t)
:in $ [?eid ...]
:where
[?t :transaction/id ?eid]
[?t :transaction/approval-status ?s]
[?s :db/ident ?si]]
(dc/db conn)
(map :transaction/id ideal-transactions))))
missing-transaction-ids (when (seq ideal-transactions)
(->>
(dc/q '[:find ?eid
:in $ [?eid ...]
:where (not [_ :transaction/id ?eid])]
(dc/db conn)
(map :transaction/id ideal-transactions))
(map first)
(into #{})))
missing-transactions (filter (comp missing-transaction-ids :transaction/id) ideal-transactions)]]
{:bank-account/name (pull-attr (dc/db conn) :bank-account/name ba)
:external-transaction-count (count raw-transactions)
:integreat-transaction-count (reduce + 0 (vals found-transactions))
:approved-count (:transaction-approval-status/approved found-transactions 0)
:unapproved-count (:transaction-approval-status/unapproved found-transactions 0)
:requires-feedback-count (:transaction-approval-status/requires-feedback found-transactions 0)
:missing-transactions missing-transactions})))
(defn card [{ {:keys [start-date end-date]} :query-params :as request}]
(let [client-ids (extract-client-ids (:clients request)
(:client-id request)
(when (:client-code request)
[:client/code (:client-code request)]))
report (get-report-data start-date end-date client-ids)]
(html-response
(reconciliation-card* {:request request
:report report})
:headers {"hx-push-url" (str "?" (normalize-query-params request))})))
(def key->handler
(apply-middleware-to-all-handlers
{:company-reconciliation-report page
:company-reconciliation-report-card card}
(fn [h]
(-> h
(wrap-schema-enforce :query-schema
[:map {:default {}}
[:start-date {:optional true}
[:maybe clj-date-schema]]
[:end-date {:optional true}
[:maybe clj-date-schema]] ])))))

View File

@@ -10,6 +10,7 @@
[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.components.tags :as tags]
[auto-ap.ssr.hiccup-helper :as hh]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.svg :as svg]
@@ -45,14 +46,14 @@
: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))))
:style (cond-> {} (:active? params) (assoc "max-height" "900px"))
":style" (format "selected == '%s' ? 'max-height: ' + $el.scrollHeight + 'px' : ''" (:selector params))))
(for [c children]
[:li
(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]
(defn left-aside- [{:keys [nav page-specific]} & _]
[:aside {:id "left-nav",
:class "fixed top-0 left-0 pt-16 z-20 w-64 h-screen transition-transform",
"x-transition:enter" "transition duration-500"
@@ -66,12 +67,13 @@
:x-show "leftNavShow"
":aria-hidden" "leftNavShow ? 'false' : 'true'"}
;; TODO this causes a leftNavShow error when hitting back button. maybe amke a container
[: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.fixed.inset-0.bg-gray-800.z-100.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
@@ -83,12 +85,12 @@
(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 ::invoice-route/import-page} (:matched-route request))
(#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page ::oi-routes/new ::invoice-route/import-page :invoice-glimpse :invoice-glimpse-textract-invoice} (:matched-route request))
"invoices"
(#{: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))
(#{::payment-routes/all-page ::payment-routes/pending-page ::payment-routes/cleared-page ::payment-routes/voided-page} (:matched-route request))
"payments"
:else
nil)]
@@ -102,7 +104,7 @@
(when (can? (:identity request)
{:subject :invoice-page})
(list
(list
(menu-button- {"@click.prevent" "if (selected == 'invoices') {selected = null } else { selected = 'invoices'} "
:icon svg/accounting-invoice-mail}
"Invoices")
@@ -135,13 +137,26 @@
:hx-boost "true"}
"Voided")
(when (can? (:identity request)
{:subject :invoice
:activity :import})
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
::invoice-route/import-page)
:active? (= ::invoice-route/import-page (:matched-route request))
:hx-boost "true"} "Import"))
:hx-boost "true"} "Import"))
(when (can? (:identity request)
{:subject :invoice
:activity :import})
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
:invoice-glimpse))
:active? (= :invoice-glimpse (:matched-route request))
:hx-boost "true"}
[:div.flex.gap-2
"Glimpse"
(tags/pill- {:color :secondary} "Beta")]))
(when (can? (:identity request)
@@ -272,42 +287,62 @@
:external-import-ledger)} "External Ledger Import")))))]))
(defn company-aside-nav- [_]
(defn company-aside-nav- [request]
[:ul {:class "space-y-2" :hx-boost "true"}
[:li
(menu-button- {:icon svg/vendors
:active? (= :company (:matched-route request))
:href (bidi/path-for ssr-routes/only-routes
:company)
:hx-boost true}
"My Company")]
[:li
(menu-button- {:icon svg/report
:active? (= :company-reports (:matched-route request))
:href (bidi/path-for ssr-routes/only-routes
:company-reports)
:hx-boost true}
"Reports")]
[:li
(menu-button- {:icon svg/report
:active? (= :company-expense-report (:matched-route request))
:href (bidi/path-for ssr-routes/only-routes
:company-expense-report)
:hx-boost true}
"Expense Report")]
(when (can? (:identity request)
{:subject :reconciliation-report})
[:li
(menu-button- {:icon svg/report
:active? (= :company-reconciliation-report (:matched-route request))
:href (bidi/path-for ssr-routes/only-routes
:company-reports)
:company-reconciliation-report)
:hx-boost true}
"Reports")]
"Bank Sync Report")])
[:li
(menu-button- {:icon svg/bank
:active? (= :company-plaid (:matched-route request))
:href (bidi/path-for ssr-routes/only-routes
:company-plaid)
:hx-boost true}
"Plaid Link")]
[:li
(menu-button- {:icon svg/bank
:active? (= :company-yodlee (:matched-route request))
:href (bidi/path-for ssr-routes/only-routes
:company-yodlee)
:hx-boost true}
"Yodlee Link")]
[:li
(menu-button- {:icon svg/government-building
:active? (= :company-1099 (:matched-route request))
:href (bidi/path-for ssr-routes/only-routes
:company-1099)
:hx-boost true}
"1099 Vendor Info"
)]])
"1099 Vendor Info")]])
(defn admin-aside-nav- [{:keys [matched-route] :as request}]
(defn admin-aside-nav- [{:keys [matched-route]}]
[:ul {:class "space-y-2" :x-data (hx/json {:selected "nil"})}
[:li
(menu-button- {:icon svg/dashboard

View File

@@ -44,7 +44,8 @@
(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")
[:thead (update thead-params :class #(-> "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"
(hh/add-class (or % ""))))
(into
[:tr]
headers)]
@@ -137,7 +138,7 @@
(a-button- (merge
(dissoc params :index :colspan)
{
"@click" "$dispatch('newRow', {index: (newRowIndex++)})"
"@click.prevent" "$dispatch('newRow', {index: (newRowIndex++)})"
:color :secondary
:hx-trigger "newRow"
:hx-vals (hiccup/raw "js:{index: event.detail.index }")

View File

@@ -175,10 +175,12 @@
(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()"))
(assoc :x-data (hx/json {:dp nil}) )
(assoc :x-init " dp = initDatepicker($el);")
(assoc "@htmx:before-history-save" "destroyDatepicker(dp)" )
(assoc "@htmx:before-cleanup-element" "destroyDatepicker(dp)" )
(assoc "@change" "value = $event.target.value;")
(assoc "hx-on" (hiccup/raw "changeDate: htmx.trigger(this, \"change\") "))
(update :class #(str % (use-size size) " w-full"))
(dissoc :size))]])

View File

@@ -165,12 +165,13 @@
:else
[:div "No action possible."])]])
(defn default-render-step [linear-wizard step & {:keys [head body footer validation-route discard-route]}]
(defn default-render-step [linear-wizard step & {:keys [head body footer validation-route discard-route width-height-class]}]
(let [is-last? (= (step-key step) (last (steps linear-wizard)))]
(com/modal-card-advanced
{"@keydown.enter.prevent.stop" "if ($refs.next ) {$refs.next.click()}"
:class (str
"w-full h-full md:w-[750px] md:h-[600px]
(or width-height-class " md:w-[750px] md:h-[600px] ")
" w-full h-full
group-[.forward]/transition:htmx-swapping:opacity-0
group-[.forward]/transition:htmx-swapping:-translate-x-1/4
group-[.forward]/transition:htmx-swapping:scale-75

View File

@@ -1,12 +1,11 @@
(ns auto-ap.ssr.components.navbar
(:require
[auto-ap.graphql.utils :refer [is-admin?]]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.company-dropdown :as cd]
[auto-ap.ssr.components.buttons :refer [icon-button-]]
[auto-ap.ssr.components.user-dropdown :as user-dropdown]
[auto-ap.ssr.svg :as svg]
[bidi.bidi :as bidi]))
(:require [auto-ap.graphql.utils :refer [is-admin? limited-clients]]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.company-dropdown :as cd]
[auto-ap.ssr.components.buttons :refer [icon-button-]]
[auto-ap.ssr.components.user-dropdown :as user-dropdown]
[auto-ap.ssr.svg :as svg]
[bidi.bidi :as bidi]))
(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"}
@@ -39,7 +38,10 @@
:hx-target "#modal-holder"
:hx-swap "outerHTML"}
svg/search)
(cd/dropdown {:client-selection client-selection :client client :identity identity
:clients clients})
(let [limited-clients (limited-clients identity)]
(when (or (nil? limited-clients)
(> (count limited-clients) 1))
(cd/dropdown {:client-selection client-selection :client client :identity identity
:clients clients})))
(user-dropdown/dropdown {:identity identity})]]]])

View File

@@ -11,7 +11,8 @@
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"
)
:x-data (hx/json {:leftNavShow true})}
:x-data (hx/json {:leftNavShow true})
}
(navbar- {:client-selection client-selection
:clients clients
:client client

View File

@@ -1,41 +1,42 @@
(ns auto-ap.ssr.core
(:require
[auto-ap.routes.ezcater-xls :as ezcater-xls]
[auto-ap.routes.utils
(:require [auto-ap.permissions :refer [wrap-must]]
[auto-ap.routes.ezcater-xls :as ezcater-xls]
[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]
[auto-ap.ssr.admin.excel-invoice :as admin-excel-invoices]
[auto-ap.ssr.admin.history :as history]
[auto-ap.ssr.admin.import-batch :as import-batch]
[auto-ap.ssr.admin.transaction-rules :as admin-rules]
[auto-ap.ssr.admin.vendors :as admin-vendors]
[auto-ap.ssr.admin.clients :as admin-clients]
[auto-ap.ssr.admin.sales-summaries :as admin-sales-summaries]
[auto-ap.ssr.auth :as auth]
[auto-ap.ssr.indicators :as indicators]
[auto-ap.ssr.company :as company]
[auto-ap.ssr.company-dropdown :as company-dropdown]
[auto-ap.ssr.company.company-1099 :as company-1099]
[auto-ap.ssr.company.plaid :as company-plaid]
[auto-ap.ssr.company.reports :as company-reports]
[auto-ap.ssr.company.yodlee :as company-yodlee]
[auto-ap.ssr.invoice.glimpse :as invoice-glimpse]
[auto-ap.ssr.pos.cash-drawer-shifts :as pos-cash-drawer-shifts]
[auto-ap.ssr.pos.expected-deposits :as pos-expected-deposits]
[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]
[auto-ap.ssr.vendor :as vendors]
[ring.middleware.json :refer [wrap-json-response]]))
[auto-ap.ssr.account :as account]
[auto-ap.ssr.admin :as admin]
[auto-ap.ssr.admin.accounts :as admin-accounts]
[auto-ap.ssr.admin.background-jobs :as admin-jobs]
[auto-ap.ssr.admin.clients :as admin-clients]
[auto-ap.ssr.admin.excel-invoice :as admin-excel-invoices]
[auto-ap.ssr.admin.history :as history]
[auto-ap.ssr.admin.import-batch :as import-batch]
[auto-ap.ssr.admin.sales-summaries :as admin-sales-summaries]
[auto-ap.ssr.admin.transaction-rules :as admin-rules]
[auto-ap.ssr.admin.vendors :as admin-vendors]
[auto-ap.ssr.auth :as auth]
[auto-ap.ssr.dashboard :as dashboard]
[auto-ap.ssr.company :as company]
[auto-ap.ssr.company-dropdown :as company-dropdown]
[auto-ap.ssr.company.company-1099 :as company-1099]
[auto-ap.ssr.company.plaid :as company-plaid]
[auto-ap.ssr.company.reports :as company-reports]
[auto-ap.ssr.company.yodlee :as company-yodlee]
[auto-ap.ssr.indicators :as indicators]
[auto-ap.ssr.invoice.glimpse :as invoice-glimpse]
[auto-ap.ssr.invoices :as invoice]
[auto-ap.ssr.outgoing-invoice.new :as oin]
[auto-ap.ssr.payments :as payments]
[auto-ap.ssr.pos.cash-drawer-shifts :as pos-cash-drawer-shifts]
[auto-ap.ssr.pos.expected-deposits :as pos-expected-deposits]
[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.search :as search]
[auto-ap.ssr.transaction.insights :as insights]
[auto-ap.ssr.users :as users]
[auto-ap.ssr.vendor :as vendors]
[ring.middleware.json :refer [wrap-json-response]]))
;; from auto-ap.ssr-routes, because they're shared
@@ -66,14 +67,11 @@
:company-yodlee-fastlink-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/fastlink-dialog))
:company-yodlee-provider-account-refresh (wrap-client-redirect-unauthenticated (wrap-admin company-yodlee/refresh-provider-account))
:company-yodlee-provider-account-reauthenticate (wrap-client-redirect-unauthenticated (wrap-secure company-yodlee/reauthenticate))
:company-reports (wrap-client-redirect-unauthenticated (wrap-secure company-reports/page))
:company-reports-table (wrap-client-redirect-unauthenticated (wrap-secure company-reports/table))
:company-reports-delete (wrap-client-redirect-unauthenticated (wrap-admin company-reports/delete-report))
:invoice-glimpse (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/page))
:invoice-glimpse-upload (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/upload))
:invoice-glimpse-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/textract-invoice))
:invoice-glimpse-create-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/create-invoice))
:invoice-glimpse-update-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/update-textract-invoice))
:invoice-glimpse (wrap-client-redirect-unauthenticated (wrap-must (wrap-secure invoice-glimpse/page) {:activity :import :subject :invoice}))
:invoice-glimpse-upload (wrap-client-redirect-unauthenticated (wrap-must (wrap-secure invoice-glimpse/upload) {:activity :import :subject :invoice}))
:invoice-glimpse-textract-invoice (wrap-client-redirect-unauthenticated (wrap-must (wrap-secure invoice-glimpse/textract-invoice) {:activity :import :subject :invoice}))
:invoice-glimpse-create-invoice (wrap-client-redirect-unauthenticated (wrap-must (wrap-secure invoice-glimpse/create-invoice) {:activity :import :subject :invoice}))
:invoice-glimpse-update-textract-invoice (wrap-client-redirect-unauthenticated (wrap-must (wrap-secure invoice-glimpse/update-textract-invoice) {:activity :import :subject :invoice}))
:vendor-search (wrap-client-redirect-unauthenticated (wrap-secure vendors/search))
:transaction-insights (wrap-client-redirect-unauthenticated (wrap-admin insights/page))
:transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-admin insights/insight-table))
@@ -83,6 +81,7 @@
:transaction-insight-explain (wrap-client-redirect-unauthenticated (wrap-admin insights/explain))
:admin-ezcater-xls (wrap-client-redirect-unauthenticated (wrap-admin ezcater-xls/page))
:search (wrap-client-redirect-unauthenticated (wrap-secure search/dialog-contents))}
(into company-reports/key->handler)
(into company-1099/key->handler)
(into invoice/key->handler)
(into import-batch/key->handler)
@@ -100,6 +99,7 @@
(into admin-vendors/key->handler)
(into admin-clients/key->handler)
(into admin-rules/key->handler)
(into dashboard/key->handler)
(into indicators/key->handler)
(into payments/key->handler)
(into oin/route->handler)))

View File

@@ -0,0 +1,340 @@
(ns auto-ap.ssr.dashboard
(:require [auto-ap.client-routes :as client-routes]
[auto-ap.datomic :refer [conn]]
[auto-ap.graphql.ledger :refer [get-profit-and-loss-raw]]
[auto-ap.graphql.utils :refer [<-graphql]]
[auto-ap.ledger.reports :as r]
[auto-ap.routes.dashboard :as d-routes]
[auto-ap.routes.invoice :as i-routes]
[auto-ap.routes.utils :refer [wrap-admin
wrap-client-redirect-unauthenticated]]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.company.reports.expense :refer [expense-breakdown-card]]
[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 [apply-middleware-to-all-handlers
html-response]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[cemerick.url :as url]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[datomic.api :as dc]
[hiccup.util :as hu]))
(defn bank-accounts-card [request]
(html-response
(com/card {:class "h-full"}
[:div.p-4.h-full
[:h1.text-2xl.font-bold "Bank Accounts"]
[:div (hx/htmx-transition-appear {:class "h-full overflow-scroll" })
(for [c (:valid-trimmed-client-ids request)
b (:client/bank-accounts (dc/pull (dc/db conn) '[{:client/bank-accounts
[:bank-account/current-balance
{[:bank-account/type :xform iol-ion.query/ident] [:db/ident]}
[:bank-account/current-balance-synced :xform clj-time.coerce/from-date]
:bank-account/name
{:bank-account/intuit-bank-account [:intuit-bank-account/current-balance
[:intuit-bank-account/last-synced :xform clj-time.coerce/from-date]]}
{:bank-account/yodlee-account [:yodlee-account/available-balance
[:yodlee-account/last-synced :xform clj-time.coerce/from-date]]}
{:bank-account/plaid-account [:plaid-account/balance
[:plaid-account/last-synced :xform clj-time.coerce/from-date]]}]}]
c))
:when (not= :bank-account-type/cash (:bank-account/type b))]
[:div.flex.flex-col.p-4.border-b-2.border-gray-200
[:div.font-bold.text-gray-700 (:client/name c)]
[:div (:bank-account/name b)]
[:div.grid.grid-cols-3.gap-x-2.items-baseline
[:div "Ledger Balance"]
[:div.text-right (format "$%,.2f" (or (:bank-account/current-balance b) 0.0))]
[:div.text-xs.text-gray-400.text-right (some-> (:bank-account/current-balance-synced b)
(atime/unparse-local atime/standard-time)
(#(str "Synced " %)))]
(when-let [n (cond (-> b :bank-account/intuit-bank-account)
"Intuit"
(-> b :bank-account/yodlee-account)
"Yodlee"
(-> b :bank-account/plaid-account)
"Plaid"
:else
nil)]
(list
[:div (str n " Balance")]
[:div.text-right (format "$%,.2f" (or (-> b :bank-account/intuit-bank-account :intuit-bank-account/current-balance)
(-> b :bank-account/yodlee-account :yodlee-account/available-balance)
(-> b :bank-account/plaid-account :plaid-account/balance)
0.0))]
[:div.text-xs.text-gray-400.text-right (or (some-> (:bank-account/intuit-bank-account b)
(:intuit-bank-account/last-synced)
(atime/unparse-local atime/standard-time)
(#(str "Synced " %)))
(some-> (:bank-account/yodlee-account b)
(:yodlee-account/last-synced)
(atime/unparse-local atime/standard-time)
(#(str "Synced " %)))
(some-> (:bank-account/plaid-account b)
(:plaid-account/last-synced)
(atime/unparse-local atime/standard-time)
(#(str "Synced " %))))]
[:div.inline-flex.justify-end.text-xs.text-gray-400.it]))
#_[:div.inline-flex.justify-between.items-baseline]]])]])))
(defn sales-chart-card [request]
(html-response
(let [ totals
(->> (dc/q '[:find ?sd (sum ?total)
:with ?e
:in $ [?clients ?start-date ?end-date]
:where [(iol-ion.query/scan-sales-orders $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
[?e :sales-order/date ?d]
[(iol-ion.query/iso-date ?d) ?sd]
[?e :sales-order/total ?total]]
(dc/db conn)
[(:valid-trimmed-client-ids request)
(coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/days -14))))
(coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/days 1))))])
(sort-by first))]
(com/card {:class "w-full h-full p-4"}
[:h1.text-2xl.font-bold.text-slate-700 "Gross sales, last 14 days"]
[:div.w-full.h-full
[:canvas.w-full.h-full.p-8 {:x-data (hx/json {:chart nil
:labels (map first totals)
:data (map second totals)})
:x-init
"new Chart($el, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Gross sales',
data: data,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});"}]]))))
(defn expense-pie-card [request]
(html-response
(let [ totals
(->> (dc/q '[:find ?an (sum ?amt)
:with ?iea
:in $ [?clients ?start-date ?end-date]
:where [(iol-ion.query/scan-invoices $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
[?e :invoice/total ?total]
[?e :invoice/expense-accounts ?iea]
[?iea :invoice-expense-account/account ?ea]
[?iea :invoice-expense-account/amount ?amt]
[?ea :account/name ?an]]
(dc/db conn)
[(:valid-trimmed-client-ids request)
(coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/months -1))))
(coerce/to-date (time/plus (time/with-time-at-start-of-day (time/now)) (time/days 1)))])
(sort-by last)
(reverse)
(take 5))]
(com/card {:class "w-full h-full p-4"}
[:h1.text-2xl.font-bold.text-slate-700
"Expenses, last month"]
[:div.w-full.h-full
[:canvas.w-full.h-full.p-8 {:x-data (hx/json {:chart nil
:labels (map first totals)
:data (map second totals)})
:x-init " new Chart($el, {
type: 'pie',
data: {
labels: labels,
datasets: [{
label: 'Total invoices',
data: data,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
}
});"}]]))))
(defn pnl-card [request]
(html-response
(com/card {:class "w-full h-full p-4"}
[:h1.text-2xl.font-bold.text-gray-700
"Profit and Loss, last month" ]
(let [ data (<-graphql (get-profit-and-loss-raw (:valid-trimmed-client-ids request)
[{:start (time/plus (time/now) (time/months -1))
:end (time/now)}]))
data (r/->PNLData {} (:accounts (first (:periods data))) {})
sales (r/aggregate-accounts (r/filter-categories data [ :sales]))
expenses (r/aggregate-accounts (r/filter-categories data [ :cogs :payroll :controllable :fixed-overhead :ownership-controllable ]))]
(list
#_(when (not= (count all-clients) (count clients))
)
[:canvas.w-full.h-full.p-8 {:x-data (hx/json {:chart nil
:labels [(format "Income $%,.2f" sales) (format "Expenses $%,.2f" expenses)]
:data [sales expenses]})
:x-init
"new Chart($el, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Dollars',
data: data,
borderWidth: 1
}]
},
options: {
responsive: true,
indexAxis: 'y',
maintainAspectRatio: false,
scales: {
x: {
beginAtZero: true
}
}
}
});"}]
[:div
"Income: " (format "$%,.2f" sales)]
[:div
"Expenses: " (format "$%,.2f" expenses)])))))
(defn tasks-card [request]
(html-response
(com/card {:class "w-full h-full p-4"}
[:h1.text-2xl.font-bold.text-gray-700
"Tasks"]
[:div (hx/htmx-transition-appear {:class "space-y-2"})
(let [[unpaid-invoice-count unpaid-invoice-amount]
(first (dc/q '[:find (count ?e) (sum ?ab)
:in $ [?clients ?start-date ?end-date]
:where [(iol-ion.query/scan-invoices $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
[?e :invoice/status :invoice-status/unpaid]
[?e :invoice/outstanding-balance ?ab]]
(dc/db conn)
[(:valid-trimmed-client-ids request)
(coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/years -1))))
nil]))
[uncategorized-transaction-count uncategorized-transaction-amount]
(first (dc/q '[:find (count ?e) (sum ?am)
:in $ [?clients ?start-date ?end-date]
:where [(iol-ion.query/scan-transactions $ ?clients ?start-date ?end-date) [[?e _ ?sort-default] ...]]
[?e :transaction/approval-status :transaction-approval-status/requires-feedback]
[?e :transaction/amount ?am]]
(dc/db conn)
[(:valid-trimmed-client-ids request)
(coerce/to-date (time/with-time-at-start-of-day (time/plus (time/now) (time/years -1))))
nil]))]
(list
(when (not= 0 (or unpaid-invoice-count 0))
[:div.bg-gray-50.rounded.p-4
[:span "You have " (str unpaid-invoice-count) " unpaid invoices with an outstanding balance of " (format "$%,.2f" unpaid-invoice-amount) ". " ]
(com/link {:href (hu/url (bidi.bidi/path-for ssr-routes/only-routes ::i-routes/unpaid-page)
{:date-range "year"})
}
"Pay now")
])
(when (not= 0 (or uncategorized-transaction-count 0))
[:div.bg-gray-50.rounded.p-4
[:span "You have " (str uncategorized-transaction-count) " transactions needing your feedback. " ]
(com/link {:href (str (bidi.bidi/path-for client-routes/routes :requires-feedback-transactions)
"?date-range="
(url/url-encode (pr-str {:start (atime/unparse-local (time/plus (time/now) (time/years -1)) atime/iso-date) :end (atime/unparse-local (time/now) atime/iso-date)}))) }
"Review now")])))])))
(defn stub-card [params & children]
(com/card (-> params
(dissoc :title)
(update :class #(hh/add-class (or % "") "w-full h-full p-4 space-y-2"))
(assoc :hx-swap "outerHTML"))
[:h1.text-2xl.font-bold.text-gray-700
(:title params)]
[:div.w-full.h-full.flex.justify-center.items-center
[:div.htmx-indicator (svg/spinner {:class "inline w-32 h-32 text-green-500"})]]))
(defn- page-contents [request]
[:div.mb-8
[:div {:class "grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-3 gap-4 mb-8"}
[:div.h-96 (stub-card {:title "Expenses"
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/expense-card)
:hx-trigger "load"} )]
[:div.h-96
(stub-card {:title "Tasks"
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/tasks-card)
:hx-trigger "load"} )]
[:div {:class " row-span-2 h-[49rem]"}
(stub-card {:title "Bank Accounts"
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/bank-accounts-card)
:hx-trigger "load"} )
]
[:div.h-96
(stub-card {:title "Gross Sales, last 14 days"
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/sales-card)
:hx-trigger "load"})
]
[:div.h-96
(stub-card {:title "Profit and Loss, last month"
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::d-routes/pnl-card)
:hx-trigger "load"}) ]
[:div.col-span-2.h-96
(stub-card {:title "Expense breakdown"
:hx-get (bidi.bidi/path-for ssr-routes/only-routes :company-expense-report-breakdown-card)
:hx-trigger "load"} )]
[:div]] ])
(defn page [request]
(base-page
request
(com/page {:nav com/main-aside-nav
:client-selection (:client-selection request)
:clients (:clients request)
:client (:client request)
:identity (:identity request)
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
::d-routes/page)
:hx-trigger "clientSelected from:body"
:hx-select "#app-contents"
:hx-swap "outerHTML swap:300ms"}
:request request}
(com/breadcrumbs {}
[:a {:href (bidi/path-for ssr-routes/only-routes ::d-routes/page)}
"Dashboard"])
(when (:clients-trimmed? request)
[:div.bg-yellow-100.rounded-lg.p-4.my-2.text-yellow-900.border-1 "Warning: These reports are only for twenty of the selected customers. Please select a specific customer to see more detail."])
(page-contents request))
"Dashboard"))
(def key->handler
( apply-middleware-to-all-handlers
{::d-routes/page page
::d-routes/expense-card expense-pie-card
::d-routes/pnl-card pnl-card
::d-routes/sales-card sales-chart-card
::d-routes/bank-accounts-card bank-accounts-card
::d-routes/tasks-card tasks-card }
(fn [h]
(wrap-client-redirect-unauthenticated (wrap-admin h)))))

View File

@@ -213,6 +213,7 @@
set)]
(handler (assoc request :trimmed-clients valid-clients)))))
(defn table-route [grid-spec & {:keys [parse-query-params?] :or {parse-query-params? true}}]
(cond-> (fn table [{:keys [identity] :as request}]

View File

@@ -1,6 +1,7 @@
(ns auto-ap.ssr.hx
(:require [cheshire.core :as cheshire]
[clojure.string :as str]))
[clojure.string :as str]
[auto-ap.ssr.hiccup-helper :as hh]))
(defn vals [m]
@@ -53,3 +54,9 @@
(defn trigger-click-or-enter [m]
(assoc m :hx-trigger "click, keyup[keyCode==13]"))
(defn htmx-transition-appear [params]
(-> params
(update :class (fn [c]
(-> (or c "")
(hh/add-class "opacity-100 transition htmx-added:opacity-0 duration-300")))))
)

View File

@@ -5,8 +5,6 @@
: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]
@@ -17,9 +15,10 @@
{: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
[:invoice-payment/_invoice :as :invoice/payments] [{:invoice-payment/payment [:db/id :payment/date :payment/amount
{[:transaction/_payment :as :payment/transaction] [:db/id]
[:payment/status :xform iol-ion.query/ident] [:db/ident]}]
{[:transaction/_payment :as :payment/transaction] [:db/id]
[:payment/status :xform iol-ion.query/ident] [:db/ident]}]}]
#_[:payment/_invoices :as :invoice/payments]
[:invoice/status :xform iol-ion.query/ident] [:db/ident]
:invoice/vendor [:vendor/name :db/id]}])

View File

@@ -1,33 +1,32 @@
(ns auto-ap.ssr.invoice.glimpse
(:require
[amazonica.aws.s3 :as s3]
[amazonica.aws.textract :as textract]
[auto-ap.datomic :refer [conn pull-attr pull-id]]
[auto-ap.datomic.clients :as d-clients]
[auto-ap.logging :as alog]
[auto-ap.solr :as solr]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [html-response path->name]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[cemerick.url :as url]
[clj-time.coerce :as coerce]
[cheshire.core :as cheshire]
[clojure.java.io :as io]
[clojure.string :as str]
[com.brunobonacci.mulog :as mu]
[config.core :refer [env]]
[datomic.api :as dc]
[hiccup2.core :as hiccup]
[iol-ion.tx :refer [random-tempid]]
[auto-ap.client-routes :as client-routes]
[auto-ap.datomic.vendors :as d-vendors]
[clj-time.core :as time])
(:import
(java.util UUID)))
(:require [amazonica.aws.s3 :as s3]
[amazonica.aws.textract :as textract]
[auto-ap.client-routes :as client-routes]
[auto-ap.datomic :refer [conn pull-attr]]
[auto-ap.datomic.clients :as d-clients]
[auto-ap.datomic.vendors :as d-vendors]
[auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.logging :as alog]
[auto-ap.solr :as solr]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.ui :refer [base-page]]
[auto-ap.ssr.utils :refer [html-response path->name]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[cemerick.url :as url]
[cheshire.core :as cheshire]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[clojure.java.io :as io]
[clojure.string :as str]
[com.brunobonacci.mulog :as mu]
[config.core :refer [env]]
[datomic.api :as dc]
[hiccup2.core :as hiccup]
[iol-ion.tx :refer [random-tempid]])
(:import (java.util UUID)))
(def bucket-name (:data-bucket env))
@@ -63,8 +62,12 @@
[[] #{}]
xs)))
(defn textract->textract-invoice [id tx]
(defn textract->textract-invoice [request id tx]
(let [lookup (lookup tx)
valid-client-ids (extract-client-ids (:clients request)
(:client-id request)
(when (:client-code request)
[:client/code (:client-code request)]))
total-options (->> (stack-rank #{"AMOUNT_DUE"} lookup)
(map (fn [t]
[t (some->> t
@@ -87,8 +90,16 @@
[t (->> (solr/query solr/impl "clients" {"query" (format "name:(%s) ", (clean-customer t)) "fields" "score, *"})
#_(filter (fn [d] (> (:score d) 4.0)))
(map (comp #(Long/parseLong %) :id))
first)]))))
first)]))
(filter (fn [[t id]]
(valid-client-ids id)))))
deduplicate)
customer-identifier-options (if (seq customer-identifier-options)
customer-identifier-options
(->> valid-client-ids
(take 10)
(map (fn [c]
[(pull-attr (dc/db conn) :client/name c) c]))))
vendor-name-options (->> (stack-rank #{"VENDOR_NAME"} lookup)
(mapcat (fn [t]
(for [m (->> (solr/query solr/impl "vendors" {"query" (format "name:(%s) ", t) "fields" "score, *"})
@@ -161,13 +172,13 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
(update :textract-invoice/vendor-name vendor-name-tuple->vendor-tuple)
(update :textract-invoice/vendor-name-options #(map vendor-name-tuple->vendor-tuple %) )))
(defn refresh-job [id]
(defn refresh-job [request id]
(let [{:keys [:db/id :textract-invoice/job-id :textract-invoice/textract-status]} (get-job id)]
(when (and job-id (= "IN_PROGRESS" textract-status))
(let [result (textract/get-expense-analysis {:job-id job-id})
new-status (:job-status result)]
(cond (= "SUCCEEDED" new-status)
@(dc/transact conn [[:upsert-entity (textract->textract-invoice id result)]])
@(dc/transact conn [[:upsert-entity (textract->textract-invoice request id result)]])
:else
@(dc/transact conn [{:db/id id :textract-invoice/textract-status new-status}]))))
(get-job id)))
@@ -198,6 +209,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
(com/field {:label "Client"}
(com/text-input {:name (path->name [:invoice/client])
:value (-> textract-invoice :textract-invoice/customer-identifier second second)
:class "w-96"
:placeholder "Client"
:disabled true
:autofocus true}))]
@@ -213,6 +225,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
[:div.col-span-6
(com/field {:label "Vendor"}
(com/text-input {:name (path->name [:invoice/vendor])
:class "w-96"
:value (-> textract-invoice :textract-invoice/vendor-name second second)
:disabled true
:placeholder "Vendor"}))]
@@ -270,8 +283,8 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
(str invoice-number))})]
(com/button {:color :primary} "Save")])
(defn job-progress* [id]
(let [textract-invoice (refresh-job id)]
(defn job-progress* [request id]
(let [textract-invoice (refresh-job request id)]
(cond
(= "IN_PROGRESS" (:textract-invoice/textract-status textract-invoice))
[:div.bg-blue-100.border-2.border-dashed.rounded-lg.border-blue-300.p-4.max-w-md.w-md.text-center.cursor-pointer
@@ -290,12 +303,12 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
[:div {:style {:width "805"}}
(com/card {}
[:iframe.p-4 {:src (:textract-invoice/pdf-url textract-invoice) :width 791 :height 700}])]
[:div {:class "basis-1/4"}
[:div {:class "basis-1/2"}
(com/card {}
[:div.p-4
(textract->invoice-form* textract-invoice)])]]])))
(defn page* [id]
(defn page* [request id]
[:div#invoice-glimpse-content.mt-4
(com/card {}
[:div.px-4.py-3.space-y-4.flex.flex-col
@@ -307,7 +320,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
[:p.text-sm.italic "Import your invoices with the power of AI. Please only use PDFs with a single invoice in them."]
(when id
(job-progress* id))
(job-progress* request id))
(when-not id
(upload-form*))])])
@@ -402,7 +415,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
_ (when new-invoice-id @(dc/transact conn [{:db/id (:db/id current-job)
:textract-invoice/invoice new-invoice-id}]))]
(if new-invoice-id
(html-response (page* nil)
(html-response (page* request nil)
:headers {"hx-push-url" (bidi/path-for ssr-routes/only-routes :invoice-glimpse)
"hx-retarget" "#invoice-glimpse-content"
"hx-trigger" (cheshire/generate-string {"notification" (str (hiccup/html [:div "Successfully created "
@@ -420,10 +433,11 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
:method request-method)
(base-page
request
(com/page {:nav com/admin-aside-nav
(com/page {:nav com/main-aside-nav
:client-selection (:client-selection request)
:client (:client request)
:clients (:clients request)
:request request
:identity (:identity request)
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
:invoice-glimpse)
@@ -437,7 +451,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
[:a {:href (bidi/path-for ssr-routes/only-routes
:invoice-glimpse)}
"Glimpse"])
(page* (some-> request
(page* request (some-> request
:route-params
:textract-invoice-id
Long/parseLong)))
@@ -446,7 +460,7 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
(defn textract-invoice [request]
(if (get-in request [:headers "hx-request"])
(html-response (job-progress* (some-> request
(html-response (job-progress* request (some-> request
:route-params
:textract-invoice-id
Long/parseLong)))

View File

@@ -379,8 +379,7 @@
(: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]}]
(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))})
@@ -743,7 +742,13 @@
(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"]))
(try
(solr/touch-with-ledger (get-in transaction-result [:tempids "invoice"]))
(catch Exception e
(alog/error ::cant-save-solr
:error e
))
)
(if extant?
(html-response

View File

@@ -85,6 +85,14 @@
:value (:vendor (:query-params request))
:value-fn :db/id
:content-fn :vendor/name}))
(com/field {:label "Account"}
(com/typeahead {:name "account"
:id "account"
:url (bidi/path-for ssr-routes/only-routes :account-search)
:value (:account (:query-params request))
:value-fn :db/id
:content-fn #(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read (:db/id %))
(:db/id (:client request))))}))
(date-range-field* request)
(com/field {:label "Check #"}
(com/text-input {:name "check-number"
@@ -100,7 +108,7 @@
:value (:invoice-number (:query-params request))
:placeholder "e.g., ABC-456"
:size :small}))
(com/field {:label "Amount"}
[:div.flex.space-x-4.items-baseline
(com/money-input {:name "amount-gte"
@@ -171,6 +179,10 @@
(merge-query {:query {:in ['?import-status]
:where ['[?e :invoice/import-status ?import-status]]}
:args [(:import-status query-params)]})
(not (:import-status query-params))
(merge-query {:query { :where ['[?e :invoice/import-status :import-status/imported]]} })
(:status route-params)
(merge-query {:query {:in ['?status]
:where ['[?e :invoice/status ?status]]}
@@ -180,11 +192,11 @@
:where ['[?e :invoice/vendor ?vendor-id]]}
:args [(:db/id (:vendor query-params))]})
(:account-id query-params)
(:account query-params)
(merge-query {:query {:in ['?account-id]
:where ['[?e :invoice/expense-accounts ?iea ?]
'[?iea :invoice-expense-account/account ?account-id]]}
:args [(:account-id query-params)]})
:args [(:db/id (:account query-params))]})
(:amount-gte query-params)
(merge-query {:query {:in ['?amount-gte]
@@ -297,6 +309,7 @@
[:amount-gte {:optional true} [:maybe :double]]
[:amount-lte {:optional true} [:maybe :double]]
[:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]]
[:account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :account/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 "invoice-status")]]
@@ -539,6 +552,7 @@
(link-dropdown
(concat (->> i
:invoice/payments
(map :invoice-payment/payment)
(filter (fn [p]
(not= :payment-status/voided
(:payment/status p))))

View File

@@ -364,7 +364,10 @@
:name "Links"
:class "w-8"
:render (fn [p]
(link-dropdown (concat (->> p :payment/invoices (map (fn [invoice]
(link-dropdown (concat (->> p :invoice-payment/_payment
(map :invoice-payment/invoice)
(filter identity)
(map (fn [invoice]
{:link (hu/url (bidi/path-for ssr-routes/only-routes
::invoice-route/all-page)
{:exact-match-id (:db/id invoice)})

View File

@@ -50,6 +50,7 @@
[:link {:rel "stylesheet" :href "https://cdn.jsdelivr.net/npm/choices.js@9.0.1/public/assets/styles/choices.min.css"}]
[:script {:src "https://cdn.jsdelivr.net/npm/choices.js@9.0.1/public/assets/scripts/choices.min.js"}]
[:script {:src "https://unpkg.com/htmx.org/dist/ext/response-targets.js"}]
[:script {:src "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js" :integrity "sha512-CQBWl4fJHWbryGE+Pc7UAxWMUMNMWzWxF4SQo9CgkJIN1kx6djDQZjh3Y8SZ1d+6I+1zze6Z7kHXO7q3UyZAWw==" :crossorigin "anonymous" :referrerpolicy "no-referrer"}]
[: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"}]
@@ -69,9 +70,9 @@ input::-webkit-inner-spin-button {
input[type=number] {
-moz-appearance:textfield; /* Firefox */
} "]
[:body {:hx-ext "disable-submit, class-tools"
[:body {:hx-ext "disable-submit, class-tools"
:x-data (hx/json {:globalClientSelection (or (:client-selection request)
:all )}) ;; TODO remove once session is used
:all)}) ;; TODO remove once session is used
:x-hx-header:x-clients "JSON.stringify(globalClientSelection)"}
contents
[:script {:src "/js/flowbite.min.js"}]

View File

@@ -240,7 +240,7 @@
(if date-range-value
(-> (condp = date-range-value
"week"
(let [last-monday (atime/last-monday)]
(let [last-monday (coerce/to-date-time (atime/last-monday))]
(assoc m
start-date-key (time/plus last-monday (time/days -7))
end-date-key last-monday))
@@ -248,16 +248,16 @@
"month"
(assoc m
start-date-key (time/plus (time/now) (time/months -1))
end-date-key (time/now))
end-date-key nil)
"year"
(assoc m
start-date-key (time/plus (time/now) (time/years -1))
end-date-key (time/now))
end-date-key nil)
"all"
(assoc m start-date-key (time/plus (time/now) (time/years -6))
end-date-key (time/now))
end-date-key nil)
m)
(dissoc date-range-key))
@@ -439,10 +439,14 @@
:explain
(me/humanize {:errors (assoc me/default-errors
::mc/missing-key {:error/message {:en "required"}})}))
(map (fn [[k v]]
(str (if (keyword? k)
(name k)
k) ": " (str/join ", " v))))
(map (fn [x]
(if (and (sequential? x)
(= (count x) 2))
(let [[k v] x]
(str (if (keyword? k)
(name k)
k) ": " (str/join ", " v))
(str x)))))
(str/join ", "))
{:type :schema-validation
:decoded (:value (:data (ex-data e)))
@@ -539,7 +543,8 @@
{:path (:in e)
:message (get-in humanized (:in e))})
(:errors (:explain (:error e))))]
(alog/warn ::form-4xx :errors errors)
(alog/warn ::form-4xx :errors errors
:data e)
(form-handler (assoc request
:form-params (:decoded e)
:field-validation-errors errors