2 Commits

Author SHA1 Message Date
df85e30bf6 merged 2026-05-17 08:17:51 -07:00
31179278e4 Adds a more actionable view 2026-05-17 08:16:11 -07:00
2 changed files with 234 additions and 173 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +1,17 @@
(ns auto-ap.ssr.pos.sales-summaries
(:require
[auto-ap.datomic
:refer [apply-pagination apply-sort-3 conn merge-query pull-many
query2]]
: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.query-params :refer [wrap-copy-qp-pqp]]
[auto-ap.client-routes :as client-routes]
[auto-ap.routes.pos.sales-summaries :as route]
[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.link-dropdown :refer [link-dropdown]]
[auto-ap.ssr.components.multi-modal :as mm]
[auto-ap.ssr.form-cursor :as fc]
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
@@ -18,35 +20,35 @@
:refer [date-range-field*]]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils
:refer [apply-middleware-to-all-handlers clj-date-schema
default-grid-fields-schema entity-id html-response money
strip temp-id wrap-merge-prior-hx wrap-schema-enforce]]
:refer [apply-middleware-to-all-handlers clj-date-schema
default-grid-fields-schema entity-id html-response money
strip temp-id wrap-merge-prior-hx 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]
[hiccup.util :as hu]
[iol-ion.query :refer [dollars=]]
[iol-ion.query :refer [dollars= dollars-0?]]
[malli.core :as mc]
[malli.util :as mut]))
(def query-schema (mc/schema
[:maybe
(into [:map {:date-range [:date-range :start-date :end-date]}
[:start-date {:optional true}
[:maybe clj-date-schema]]
[:end-date {:optional true}
[:maybe clj-date-schema]] ]
[:maybe clj-date-schema]]]
default-grid-fields-schema)]))
(defn filters [request]
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
"hx-get" (bidi/path-for ssr-routes/only-routes
::route/table)
"hx-target" "#entity-table"
"hx-indicator" "#entity-table"}
[:form {"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"}
[:fieldset.space-y-6
(date-range-field* request)]])
@@ -55,15 +57,14 @@
*
[:sales-summary/date :xform clj-time.coerce/from-date]
{:sales-summary/client [:client/code :client/name :db/id]}
{:sales-summary/items [{[:ledger-mapped/ledger-side :xform iol-ion.query/ident] [:db/ident]
}
:ledger-mapped/account
:ledger-mapped/amount
:sales-summary-item/category
:sales-summary-item/sort-order
:db/id
:sales-summary-item/manual?]
} ])
{:sales-summary/items [{[:ledger-mapped/ledger-side :xform iol-ion.query/ident] [:db/ident]}
:ledger-mapped/account
:ledger-mapped/amount
:sales-summary-item/category
:sales-summary-item/sort-order
:db/id
:sales-summary-item/manual?]}
{:journal-entry/original-entity [:db/id]}])
(defn fetch-ids [db request]
(let [query-params (:query-params request)
@@ -72,27 +73,26 @@
(:client-id query-params)
(when (:client-code query-params)
[:client/code (:client-code query-params)]))
query (cond-> {:query {:find []
:in '[$ [?client ...]]
query (cond-> {:query {:find []
:in '[$ [?client ...]]
:where '[[?e :sales-summary/client ?client]]}
:args [db valid-clients]}
:args [db valid-clients]}
(or (:start-date query-params)
(:end-date query-params))
(merge-query {:query '{:where [[?e :sales-summary/date ?d]]}})
(:start-date query-params)
(merge-query {:query '{:in [?start-date]
(merge-query {:query '{:in [?start-date]
:where [[(>= ?d ?start-date)]]}
:args [(-> query-params :start-date c/to-date)]})
:args [(-> query-params :start-date c/to-date)]})
(:end-date query-params)
(merge-query {:query '{:in [?end-date]
(merge-query {:query '{:in [?end-date]
:where [[(< ?d ?end-date)]]}
:args [(-> query-params :end-date c/to-date)]})
:args [(-> query-params :end-date c/to-date)]})
true
(merge-query {:query {:find ['?sort-default '?e]
(merge-query {:query {:find ['?sort-default '?e]
:where ['[?e :sales-summary/date ?sort-default]]}}))]
(cond->> (query2 query)
true (apply-sort-3 query-params)
@@ -107,7 +107,7 @@
refunds))
(defn fetch-page [request]
(let [db (dc/db conn)
(let [db (dc/db conn)
{ids-to-retrieve :ids matching-count :count} (fetch-ids db request)]
[(->> (hydrate-results ids-to-retrieve db request))
@@ -116,27 +116,29 @@
(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
(->> items
(filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)))
(map #(:ledger-mapped/amount % 0.0))
(reduce + 0.0)))
(defn total-credits [items]
(->> items
(->> items
(filter #(= :ledger-side/credit (:ledger-mapped/ledger-side %)))
(map #(:ledger-mapped/amount % 0.0))
(reduce + 0.0)))
(defn truncate [s max-len]
(if (> (count s) max-len)
(str (subs s 0 (- max-len 3)) "...")
s))
(def grid-page
(helper/build {:id "entity-table"
(helper/build {:id "entity-table"
:id-fn :db/id
:nav com/main-aside-nav
:fetch-page fetch-page
:page-specific-nav filters
:nav com/main-aside-nav
:fetch-page fetch-page
:page-specific-nav filters
:query-schema query-schema
:row-buttons (fn [_ entity]
[(com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes
@@ -144,84 +146,145 @@
:db/id (:db/id entity))}
svg/pencil)])
: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"]
(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"]
[:a {:href (bidi/path-for ssr-routes/only-routes
::route/page)}
"Sales Summaries"]]
:title "Sales Summaries"
[:a {:href (bidi/path-for ssr-routes/only-routes
::route/page)}
"Sales Summaries"]]
:title "Sales Summaries"
:entity-name "Daily Summary"
:route ::route/table
:headers [{:key "client"
:name "Client"
:sort-key "client"
:hide? (fn [args]
(= (count (:clients args)) 1))
:render #(-> % :sales-summary/client :client/code)}
:route ::route/table
: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 "date"
:name "Date"
:sort-key "date"
:render #(some-> % :sales-summary/date (atime/unparse-local atime/normal-date))}
{:key "debits"
:name "Debits"
:sort-key "debits"
:class "w-64 align-top"
:render (fn [ss]
(let [items (:sales-summary/items ss)
debit-items (filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)) (sort-items items))
credit-count (count (filter #(= :ledger-side/credit (:ledger-mapped/ledger-side %)) items))
total-debits (total-debits items)
total-credits (total-credits items)
delta (- total-debits total-credits)]
[:div.flex.flex-col.h-full
[:div.font-semibold.text-sm "Debits"]
[:ul.space-y-0.5.flex-grow
(for [si debit-items]
[:li.text-sm.text-gray-700
[:span.text-gray-500 (truncate (:sales-summary-item/category si) 30)]
[:span.ml-2 "→"]
[:span.ml-2.font-mono (format "$%,.2f" (:ledger-mapped/amount si))]
(when-not (:ledger-mapped/account si)
[:span.ml-2 (com/pill {:color :red} "?")])])
(for [_ (range (max 0 (- credit-count (count debit-items))))]
[:li "\u00a0"])]
[:div.border-t.mt-1.pt-1.flex.justify-between.items-center.w-full
[:span.font-semibold "Total"]
[:span.font-mono.font-semibold (format "$%,.2f" total-debits)]]
(when-not (dollars-0? delta)
[:div.text-xs.text-red-600.flex.justify-between.w-full.mt-1
[:span "Delta:"]
[:span.font-mono (format "$%,.2f" delta)]])]))}
{:key "credits"
:name "Credits"
:sort-key "credits"
:class "w-64 align-top"
:render (fn [ss]
(let [items (:sales-summary/items ss)
credit-items (filter #(= :ledger-side/credit (:ledger-mapped/ledger-side %)) (sort-items items))
debit-count (count (filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)) items))
total-debits (total-debits items)
total-credits (total-credits items)
delta (- total-credits total-debits)]
[:div.flex.flex-col.h-full
[:div.font-semibold.text-sm "Credits"]
[:ul.space-y-0.5.flex-grow
(for [si credit-items]
[:li.text-sm.text-gray-700
[:span.text-gray-500 (truncate (:sales-summary-item/category si) 30)]
[:span.ml-2 "→"]
[:span.ml-2.font-mono (format "$%,.2f" (:ledger-mapped/amount si))]
(when-not (:ledger-mapped/account si)
[:span.ml-2 (com/pill {:color :red} "?")])])
(for [_ (range (max 0 (- debit-count (count credit-items))))]
[:li "\u00a0"])]
[:div.border-t.mt-1.pt-1.flex.justify-between.items-center.w-full
[:span.font-semibold "Total"]
[:span.font-mono.font-semibold (format "$%,.2f" total-credits)]]
(when-not (dollars-0? delta)
[:div.text-xs.text-green-600.flex.justify-between.w-full.mt-1
[:span "Delta:"]
[:span.font-mono (format "$%,.2f" delta)]])]))}
{:key "debits"
:name "debits"
:sort-key "debits"
: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/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))]]))}
{: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))]]))}]}))
{:key "balance"
:name "Balance"
:sort-key "balance"
:class "w-24"
:render (fn [ss]
(let [items (:sales-summary/items ss)
total-debits (total-debits items)
total-credits (total-credits items)
delta (- total-debits total-credits)
balanced? (dollars= total-debits total-credits)
missing-account? (some #(not (:ledger-mapped/account %)) items)]
[:div.flex.flex-col.items-center.pt-2
(when missing-account?
[:span.text-red-600.font-bold.text-xs "Missing acct"])
(if balanced?
(when-not missing-account?
[:span.text-green-600.font-bold "✓ Balanced"])
(list
[:span.text-red-600.font-mono (format "$%,.2f" (Math/abs delta))]
[:span.text-xs.text-gray-500.mt-1
(if (> total-debits total-credits) "Debit over" "Credit over")]))]))}
{:key "links"
:name "Links"
:show-starting "lg"
:class "w-8"
:render (fn [ss]
(let [ledger-entry (:journal-entry/original-entity ss)]
(when (seq ledger-entry)
(link-dropdown
[{:link (hu/url (bidi/path-for client-routes/routes :ledger)
{:exact-match-id (:db/id (first ledger-entry))})
:color :yellow
:content "Ledger entry"}]))))}]}))
(def row* (partial helper/row* grid-page))
(def table* (partial helper/table* grid-page))
(def edit-schema
[:map
[:db/id entity-id]
[:db/id entity-id]
[:sales-summary/client [:map [:db/id entity-id]]]
[:sales-summary/items
[:sales-summary/items
[:vector {:coerce? true}
[:and
[:map
[: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
[:sales-summary-item/manual? {:default false :decode/arbitrary (fn [x] (cond
(boolean? x)
x
(nil? x)
false
false
(str/blank? x)
false
:else
@@ -230,11 +293,10 @@
[:credit {:optional true} [:maybe money]]
[:debit {:optional true} [:maybe money]]]
[:fn {:error/message "Must choose one of credit/debit"
:error/path [:credit]}
:error/path [:credit]}
(fn [x]
(not (and (:credit x)
(:debit x))))]]]] ])
(:debit x))))]]]]])
(defn summary-total-row* [request]
(let [total-credits (-> request
@@ -293,21 +355,21 @@
(defn- account-typeahead*
[{:keys [name value client-id]}]
[:div.flex.flex-col
(com/typeahead {:name name
(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)))})])
: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"
(com/data-grid-row (cond-> {:x-ref "p"
:x-data (hx/json {})}
(fc/field-value (:new? value)) (hx/htmx-transition-appear ))
(fc/field-value (:new? value)) (hx/htmx-transition-appear))
(fc/with-field :db/id
(com/hidden {:name (fc/field-name)
:value (fc/field-value)}))
@@ -319,9 +381,9 @@
(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)}))
(com/text-input {:placeholder "Category/Explanation"
:name (fc/field-name)
:value (fc/field-value)}))
(list
(com/hidden {:name (fc/field-name)
@@ -330,9 +392,9 @@
(com/data-grid-cell {}
(fc/with-field :ledger-mapped/account
(com/validated-field {:errors (fc/field-errors)}
(account-typeahead* {:value (fc/field-value)
(account-typeahead* {:value (fc/field-value)
:client-id client-id
:name (fc/field-name)}))))
:name (fc/field-name)}))))
(com/data-grid-cell {:class "text-right"}
(if manual?
@@ -356,7 +418,7 @@
:ledger-side/credit)
(format "$%,.2f" (fc/field-value (:ledger-mapped/amount value))))))
(com/data-grid-cell {:class "align-top"}
(when manual?
(when manual?
(com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x))))))
(defrecord MainStep [linear-wizard]
@@ -370,45 +432,45 @@
[])
(step-schema [_]
(mut/select-keys (mm/form-schema linear-wizard) #{:db/id :sales-summary/items}))
(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))) }))
(com/data-grid-new-row {:colspan 5
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item)
[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)))}))
(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))
:index (count (fc/field-value))
:tr-params {:hx-vals (hx/json {:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))})}}
"New Summary Item")))
(summary-total-row* request)
(unbalanced-row* request)) ])
(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]")))
: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
(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
@@ -424,8 +486,8 @@
(navigate [this step-key]
(assoc this :current-step step-key))
(get-current-step
[this]
(mm/get-step this :main))
[this]
(mm/get-step this :main))
(render-wizard [this {:keys [multi-form-state] :as request}]
(mm/default-render-wizard
this request
@@ -437,29 +499,28 @@
(steps [_]
[:main])
(get-step [this step-key]
(let [step-key-result (mc/parse mm/step-key-schema 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-sales-summary {: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))}]]
(let [result (:snapshot multi-form-state)
transaction [:upsert-sales-summary {: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])
@(dc/transact conn [transaction])
(html-response
(row* identity (dc/pull (dc/db conn) default-read (:db/id result))
(row* identity (dc/pull (dc/db conn) default-read (:db/id result))
{:flash? true
:request request})
:headers (cond-> {"hx-trigger" "modalclose"
:request request})
:headers (cond-> {"hx-trigger" "modalclose"
"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id result))
"hx-reswap" "outerHTML"})))))
@@ -479,8 +540,8 @@
(def key->handler
(apply-middleware-to-all-handlers
(->>
{::route/page (helper/page-route grid-page)
::route/table (helper/table-route grid-page)
{::route/page (helper/page-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)
@@ -498,9 +559,9 @@
(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))})
::route/edit-wizard-submit (-> mm/submit-handler
(mm/wrap-wizard edit-wizard)
(mm/wrap-decode-multi-form-state))})
(fn [h]
(-> h
(wrap-copy-qp-pqp)