791 lines
45 KiB
Clojure
791 lines
45 KiB
Clojure
(ns auto-ap.ssr.pos.sales-summaries
|
||
(: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.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]]
|
||
[auto-ap.ssr.hx :as hx]
|
||
[auto-ap.ssr.pos.common
|
||
:refer [date-range-field*]]
|
||
[auto-ap.ssr.svg :as svg]
|
||
[auto-ap.ssr.utils
|
||
:refer [apply-middleware-to-all-handlers clj-date-schema
|
||
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= 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]]]
|
||
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"}
|
||
|
||
[:fieldset.space-y-6
|
||
(date-range-field* request)]])
|
||
|
||
(def default-read '[:db/id
|
||
*
|
||
[: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?]}
|
||
{:journal-entry/original-entity [:db/id]}])
|
||
|
||
(defn fetch-ids [db request]
|
||
(let [query-params (:query-params request)
|
||
valid-clients (extract-client-ids (:clients request)
|
||
(:client request)
|
||
(:client-id query-params)
|
||
(when (:client-code query-params)
|
||
[:client/code (:client-code query-params)]))
|
||
query (cond-> {:query {:find []
|
||
:in '[$ [?client ...]]
|
||
:where '[[?e :sales-summary/client ?client]]}
|
||
: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]
|
||
:where [[(>= ?d ?start-date)]]}
|
||
:args [(-> query-params :start-date c/to-date)]})
|
||
|
||
(:end-date query-params)
|
||
(merge-query {:query '{:in [?end-date]
|
||
:where [[(< ?d ?end-date)]]}
|
||
:args [(-> query-params :end-date c/to-date)]})
|
||
|
||
true
|
||
(merge-query {:query {:find ['?sort-default '?e]
|
||
:where ['[?e :sales-summary/date ?sort-default]]}}))]
|
||
(cond->> (query2 query)
|
||
true (apply-sort-3 query-params)
|
||
true (apply-pagination query-params))))
|
||
|
||
(defn hydrate-results [ids db _]
|
||
(let [results (->> (pull-many db default-read ids)
|
||
(group-by :db/id))
|
||
refunds (->> ids
|
||
(map results)
|
||
(map first))]
|
||
refunds))
|
||
|
||
(defn fetch-page [request]
|
||
(let [db (dc/db conn)
|
||
{ids-to-retrieve :ids matching-count :count} (fetch-ids db request)]
|
||
|
||
[(->> (hydrate-results ids-to-retrieve db request))
|
||
matching-count]))
|
||
|
||
(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)))
|
||
|
||
(defn truncate [s max-len]
|
||
(if (> (count s) max-len)
|
||
(str (subs s 0 (- max-len 3)) "...")
|
||
s))
|
||
|
||
(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 account-display-cell [{:keys [item field-name-prefix client-id]}]
|
||
(let [account-id (:ledger-mapped/account item)
|
||
account-name (when account-id
|
||
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read account-id)
|
||
client-id)))]
|
||
[:div.account-cell.flex.items-center.gap-2
|
||
(com/hidden {:name (str field-name-prefix "[ledger-mapped/account]")
|
||
:value (or account-id "")})
|
||
(if account-id
|
||
[:span.text-sm account-name]
|
||
(com/pill {:color :red} "Missing acct"))
|
||
(com/a-icon-button {:class "p-1"
|
||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-item-account)
|
||
:hx-target "closest .account-cell"
|
||
:hx-swap "outerHTML"
|
||
:hx-vals (hx/json {:item-index (or (:item-index item) 0)
|
||
:client-id client-id
|
||
:current-account-id (or account-id "")})}
|
||
svg/pencil)]))
|
||
|
||
(defn account-edit-cell [{:keys [field-name-prefix client-id current-account-id]}]
|
||
(let [account-input-name (str field-name-prefix "[ledger-mapped/account]")]
|
||
[:div.account-cell.flex.flex-col.gap-2
|
||
(account-typeahead* {:name account-input-name
|
||
:value current-account-id
|
||
:client-id client-id})
|
||
[:div.flex.gap-1
|
||
(com/a-icon-button {:class "p-1"
|
||
:hx-put (bidi/path-for ssr-routes/only-routes ::route/save-item-account)
|
||
:hx-target "closest .account-cell"
|
||
:hx-swap "outerHTML"
|
||
:hx-include "closest .account-cell"
|
||
:hx-vals (hx/json {:field-name-prefix field-name-prefix
|
||
:client-id client-id})}
|
||
svg/check)
|
||
(com/a-icon-button {:class "p-1"
|
||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/cancel-item-account)
|
||
:hx-target "closest .account-cell"
|
||
:hx-swap "outerHTML"
|
||
:hx-vals (hx/json {:field-name-prefix field-name-prefix
|
||
:client-id client-id
|
||
:current-account-id (or current-account-id "")})}
|
||
svg/x)]]))
|
||
|
||
(def grid-page
|
||
(helper/build {:id "entity-table"
|
||
:id-fn :db/id
|
||
: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
|
||
::route/edit-wizard
|
||
: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"]
|
||
|
||
[: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)}
|
||
|
||
{: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-72 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)]
|
||
[:div.flex.flex-col.h-full
|
||
[:ul.flex-grow
|
||
(for [si debit-items]
|
||
[:li.flex.items-baseline.gap-2.py-0.5.text-sm.text-gray-700
|
||
[:span.flex-1.min-w-0.truncate.text-gray-600
|
||
(:sales-summary-item/category si)]
|
||
(when-not (:ledger-mapped/account si)
|
||
[:span.shrink-0 (com/pill {:color :red} "?")])
|
||
[:span.shrink-0.font-mono.tabular-nums.text-right.text-gray-900.whitespace-nowrap
|
||
(format "$%,.2f" (:ledger-mapped/amount si))]])
|
||
(for [_ (range (max 0 (- credit-count (count debit-items))))]
|
||
[:li.py-0.5.text-sm " "])]
|
||
[:div.border-t-2.border-gray-300.mt-1.pt-1.flex.justify-between.items-baseline
|
||
[:span.text-xs.uppercase.tracking-wider.font-semibold.text-gray-500 "Total"]
|
||
[:span.font-mono.tabular-nums.font-bold.text-gray-900
|
||
(format "$%,.2f" total-debits)]]]))}
|
||
|
||
{:key "credits"
|
||
:name "Credits"
|
||
:sort-key "credits"
|
||
:class "w-72 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-credits (total-credits items)]
|
||
[:div.flex.flex-col.h-full
|
||
[:ul.flex-grow
|
||
(for [si credit-items]
|
||
[:li.flex.items-baseline.gap-2.py-0.5.text-sm.text-gray-700
|
||
[:span.flex-1.min-w-0.truncate.text-gray-600
|
||
(:sales-summary-item/category si)]
|
||
(when-not (:ledger-mapped/account si)
|
||
[:span.shrink-0 (com/pill {:color :red} "?")])
|
||
[:span.shrink-0.font-mono.tabular-nums.text-right.text-gray-900.whitespace-nowrap
|
||
(format "$%,.2f" (:ledger-mapped/amount si))]])
|
||
(for [_ (range (max 0 (- debit-count (count credit-items))))]
|
||
[:li.py-0.5.text-sm " "])]
|
||
[:div.border-t-2.border-gray-300.mt-1.pt-1.flex.justify-between.items-baseline
|
||
[:span.text-xs.uppercase.tracking-wider.font-semibold.text-gray-500 "Total"]
|
||
[:span.font-mono.tabular-nums.font-bold.text-gray-900
|
||
(format "$%,.2f" total-credits)]]]))}
|
||
|
||
{:key "balance"
|
||
:name "Status"
|
||
:sort-key "balance"
|
||
:class "w-28 align-top"
|
||
: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.gap-1.pt-2
|
||
(when missing-account?
|
||
[:span.inline-block.text-xs.font-semibold.uppercase.tracking-wider.text-amber-800.bg-amber-100.border.border-amber-300.rounded-sm.px-1.5.py-0.5
|
||
"Missing acct"])
|
||
(if balanced?
|
||
(when-not missing-account?
|
||
[:span.inline-block.text-xs.font-semibold.uppercase.tracking-wider.text-emerald-800.bg-emerald-100.border.border-emerald-300.rounded-sm.px-1.5.py-0.5
|
||
"Balanced"])
|
||
[:div.flex.flex-col.items-center
|
||
[:span.font-mono.tabular-nums.text-red-700.font-bold.text-sm
|
||
(format "$%,.2f" (Math/abs delta))]
|
||
[:span.text-xs.uppercase.tracking-wider.text-red-600.font-medium.mt-0.5
|
||
(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]
|
||
[: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"
|
||
:class "bg-slate-50 border-t-2 border-slate-300"}
|
||
(com/data-grid-cell {})
|
||
(com/data-grid-cell {:class "text-right"}
|
||
[:span.text-xs.uppercase.tracking-wider.font-semibold.text-slate-600
|
||
"Total"])
|
||
(com/data-grid-cell {:class "text-right"}
|
||
[:span.font-mono.tabular-nums.font-bold.text-slate-900
|
||
(format "$%,.2f" total-debits)])
|
||
(com/data-grid-cell {:class "text-right"}
|
||
[:span.font-mono.tabular-nums.font-bold.text-slate-900
|
||
(format "$%,.2f" total-credits)])
|
||
(com/data-grid-cell {}))))
|
||
|
||
(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))
|
||
unbalanced? (not (dollars= total-credits total-debits))
|
||
debit-over? (and unbalanced? (> total-debits total-credits))
|
||
credit-over? (and unbalanced? (> total-credits total-debits))]
|
||
|
||
(com/data-grid-row {:id "unbalanced-row"
|
||
:class (when unbalanced? "bg-red-50 border-t border-red-200")}
|
||
(com/data-grid-cell {})
|
||
(com/data-grid-cell {:class "text-right"}
|
||
(when unbalanced?
|
||
[:span.text-xs.uppercase.tracking-wider.font-semibold.text-red-700
|
||
"Out of balance"]))
|
||
(com/data-grid-cell {:class "text-right"}
|
||
(when debit-over?
|
||
[:span.font-mono.tabular-nums.font-bold.text-red-700
|
||
(format "$%,.2f" (- total-debits total-credits))]))
|
||
(com/data-grid-cell {:class "text-right"}
|
||
(when credit-over?
|
||
[:span.font-mono.tabular-nums.font-bold.text-red-700
|
||
(format "$%,.2f" (- total-credits total-debits))]))
|
||
(com/data-grid-cell {}))))
|
||
|
||
(defn summary-total-display [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))]
|
||
[:div.flex.justify-between.text-sm.py-1.border-t.mt-1
|
||
{:id "total-display"}]
|
||
[:span.font-semibold "Total"]
|
||
[:div.flex.gap-8
|
||
[:span.font-mono (format "$%,.2f" total-debits)]
|
||
[:span.font-mono (format "$%,.2f" total-credits)]]))
|
||
|
||
(defn unbalanced-display [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))
|
||
delta (- total-debits total-credits)]
|
||
(when-not (dollars-0? delta)
|
||
[:div.flex.justify-between.text-sm.py-1
|
||
{:id "unbalanced-display"}
|
||
[:span.font-semibold.text-red-600 "Unbalanced"]
|
||
[:div.flex.gap-8
|
||
[:span.font-mono (when (pos? delta) (format "$%,.2f" delta))
|
||
[:span.font-mono (when (neg? delta) (format "$%,.2f" (Math/abs delta)))]]]])))
|
||
|
||
(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 {})
|
||
:class (when manual?
|
||
"bg-indigo-50/40 border-l-2 border-indigo-300")}
|
||
(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 {:class "align-top"}
|
||
(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)})
|
||
[:span.text-sm.text-gray-700
|
||
(fc/field-value (:sales-summary-item/category value))]))))
|
||
(com/data-grid-cell {:class "align-top"}
|
||
(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 align-top"}
|
||
|
||
(if manual?
|
||
(fc/with-field :debit
|
||
(com/validated-field {:errors (fc/field-errors)}
|
||
(com/money-input {:class "w-24 text-right font-mono tabular-nums"
|
||
:name (fc/field-name)
|
||
:value (fc/field-value)})))
|
||
(when (= (fc/field-value (:ledger-mapped/ledger-side value))
|
||
:ledger-side/debit)
|
||
[:span.font-mono.tabular-nums.text-gray-900.text-sm.whitespace-nowrap
|
||
(format "$%,.2f" (fc/field-value (:ledger-mapped/amount value)))])))
|
||
(com/data-grid-cell {:class "text-right align-top"}
|
||
|
||
(if manual?
|
||
(fc/with-field :credit
|
||
(com/validated-field {:errors (fc/field-errors)}
|
||
(com/money-input {:class "w-24 text-right font-mono tabular-nums"
|
||
:name (fc/field-name)
|
||
:value (fc/field-value)})))
|
||
(when (= (fc/field-value (:ledger-mapped/ledger-side value))
|
||
:ledger-side/credit)
|
||
[:span.font-mono.tabular-nums.text-gray-900.text-sm.whitespace-nowrap
|
||
(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}]
|
||
(let [client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))
|
||
items (:sales-summary/items (:step-params multi-form-state))
|
||
sorted-items (sort-items items)
|
||
indexed-items (map-indexed vector sorted-items)
|
||
debit-items (filter #(= :ledger-side/debit (:ledger-mapped/ledger-side (second %))) indexed-items)
|
||
credit-items (filter #(= :ledger-side/credit (:ledger-mapped/ledger-side (second %))) indexed-items)
|
||
max-rows (max (count debit-items) (count credit-items))
|
||
padded-debits (concat debit-items (repeat (- max-rows (count debit-items)) nil))
|
||
padded-credits (concat credit-items (repeat (- max-rows (count credit-items)) nil))]
|
||
(mm/default-render-step
|
||
linear-wizard this
|
||
:head [:div.p-2 "Edit Summary"]
|
||
:body (mm/default-step-body
|
||
{}
|
||
[:div
|
||
(fc/with-field :db/id
|
||
(com/hidden {:name (fc/field-name)
|
||
:value (fc/field-value)}))
|
||
[:div.grid.grid-cols-2.gap-6
|
||
[:div
|
||
[:div.font-semibold.text-sm.mb-2 "Debits"]
|
||
[:div.space-y-1
|
||
(for [[actual-idx item] padded-debits]
|
||
(if item
|
||
(let [manual? (:sales-summary-item/manual? item)]
|
||
(if manual?
|
||
[:div.flex.items-center.gap-2.text-sm {:x-ref "p" :x-data (hx/json {})}
|
||
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][db/id]")
|
||
:value (:db/id item)})
|
||
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][sales-summary-item/manual?]")
|
||
:value "true"})
|
||
(fc/start-form-with-prefix [(str "step-params[sales-summary/items][" actual-idx "]")]
|
||
item []
|
||
(fc/with-field :sales-summary-item/category
|
||
(com/text-input {:placeholder "Category"
|
||
:name (fc/field-name)
|
||
:value (fc/field-value)
|
||
:class "w-32 text-sm"})))
|
||
(account-typeahead* {:name (str "step-params[sales-summary/items][" actual-idx "][ledger-mapped/account]")
|
||
:value (:ledger-mapped/account item)
|
||
:client-id client-id})
|
||
(fc/start-form-with-prefix [(str "step-params[sales-summary/items][" actual-idx "]")]
|
||
item []
|
||
(fc/with-field :debit
|
||
(com/money-input {:class "w-24 text-right font-mono tabular-nums"
|
||
:name (fc/field-name)
|
||
:value (fc/field-value)})))
|
||
(com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x)]
|
||
[:div.flex.items-center.gap-2.text-sm
|
||
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][db/id]")
|
||
:value (:db/id item)})
|
||
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][sales-summary-item/category]")
|
||
:value (:sales-summary-item/category item)})
|
||
[:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)]
|
||
(account-display-cell {:item (assoc item :item-index actual-idx)
|
||
:field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]")
|
||
:client-id client-id})
|
||
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
|
||
[:div.h-6]))]
|
||
[:div.mt-2.border-t.pt-1
|
||
(summary-total-display request)
|
||
(unbalanced-display request)]]
|
||
[:div
|
||
[:div.font-semibold.text-sm.mb-2 "Credits"]
|
||
[:div.space-y-1
|
||
(for [[actual-idx item] padded-credits]
|
||
(if item
|
||
(let [manual? (:sales-summary-item/manual? item)]
|
||
(if manual?
|
||
[:div.flex.items-center.gap-2.text-sm {:x-ref "p" :x-data (hx/json {})}
|
||
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][db/id]")
|
||
:value (:db/id item)})
|
||
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][sales-summary-item/manual?]")
|
||
:value "true"})
|
||
(fc/start-form-with-prefix [(str "step-params[sales-summary/items][" actual-idx "]")]
|
||
item []
|
||
(fc/with-field :sales-summary-item/category
|
||
(com/text-input {:placeholder "Category"
|
||
:name (fc/field-name)
|
||
:value (fc/field-value)
|
||
:class "w-32 text-sm"})))
|
||
(account-typeahead* {:name (str "step-params[sales-summary/items][" actual-idx "][ledger-mapped/account]")
|
||
:value (:ledger-mapped/account item)
|
||
:client-id client-id})
|
||
(fc/start-form-with-prefix [(str "step-params[sales-summary/items][" actual-idx "]")]
|
||
item []
|
||
(fc/with-field :credit
|
||
(com/money-input {:class "w-24 text-right font-mono tabular-nums"
|
||
:name (fc/field-name)
|
||
:value (fc/field-value)})))
|
||
(com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x)]
|
||
[:div.flex.items-center.gap-2.text-sm
|
||
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][db/id]")
|
||
:value (:db/id item)})
|
||
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][sales-summary-item/category]")
|
||
:value (:sales-summary-item/category item)})
|
||
[:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)]
|
||
(account-display-cell {:item (assoc item :item-index actual-idx)
|
||
:field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]")
|
||
:client-id client-id})
|
||
[:span.ml-auto.font-mono.tabular-nums.text-gray-900 (format "$%,.2f" (:ledger-mapped/amount item))]]))
|
||
[:div.h-6]))]
|
||
[:div.mt-2.border-t.pt-1
|
||
(summary-total-display request)
|
||
(unbalanced-display request)]]]
|
||
[:div.mt-4.border-t.pt-2
|
||
(fc/with-field :sales-summary/items
|
||
(com/data-grid-new-row {:colspan 2
|
||
: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 client-id})}}
|
||
"New Summary Item"))]])
|
||
|
||
: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-[900px] lg:h-[600px]"))))
|
||
|
||
(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-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))}]]
|
||
@(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)))
|
||
|
||
(defn edit-item-account [request]
|
||
(let [{:keys [item-index client-id current-account-id]} (:query-params request)
|
||
item-index (if (string? item-index) (Integer/parseInt item-index) item-index)
|
||
field-name-prefix (str "step-params[sales-summary/items][" item-index "]")
|
||
current-account-id (when (and current-account-id (not= current-account-id ""))
|
||
(if (string? current-account-id)
|
||
(Long/parseLong current-account-id)
|
||
current-account-id))
|
||
client-id (if (string? client-id) (Long/parseLong client-id) client-id)]
|
||
(html-response
|
||
(account-edit-cell {:field-name-prefix field-name-prefix
|
||
:client-id client-id
|
||
:current-account-id current-account-id}))))
|
||
|
||
(defn save-item-account [request]
|
||
(let [field-name-prefix (get-in request [:params "field-name-prefix"])
|
||
client-id (get-in request [:params "client-id"])
|
||
account-input-name (str field-name-prefix "[ledger-mapped/account]")
|
||
account-id-str (get-in request [:form-params account-input-name])
|
||
account-id (when (and account-id-str (not= account-id-str ""))
|
||
(Long/parseLong account-id-str))
|
||
item {:ledger-mapped/account account-id
|
||
:item-index (second (re-find #"\[(\d+)\]" (or field-name-prefix "")))}
|
||
client-id (if (string? client-id) (Long/parseLong client-id) client-id)]
|
||
(html-response
|
||
(account-display-cell {:item item
|
||
:field-name-prefix field-name-prefix
|
||
:client-id client-id}))))
|
||
|
||
(defn cancel-item-account [request]
|
||
(let [{:keys [field-name-prefix client-id current-account-id]} (:query-params request)
|
||
account-id (when (and current-account-id (not= current-account-id ""))
|
||
(if (string? current-account-id)
|
||
(Long/parseLong current-account-id)
|
||
current-account-id))
|
||
item {:ledger-mapped/account account-id
|
||
:item-index (second (re-find #"\[(\d+)\]" (or field-name-prefix "")))}
|
||
client-id (if (string? client-id) (Long/parseLong client-id) client-id)]
|
||
(html-response
|
||
(account-display-cell {:item item
|
||
:field-name-prefix field-name-prefix
|
||
:client-id client-id}))))
|
||
|
||
(def key->handler
|
||
(apply-middleware-to-all-handlers
|
||
(->>
|
||
{::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)
|
||
(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-item-account (-> edit-item-account
|
||
(wrap-schema-enforce :query-schema [:map
|
||
[:item-index nat-int?]
|
||
[:client-id {:optional true} [:maybe entity-id]]
|
||
[:current-account-id {:optional true} [:maybe :string]]]))
|
||
::route/save-item-account save-item-account
|
||
::route/cancel-item-account cancel-item-account
|
||
::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)
|
||
(wrap-apply-sort grid-page)
|
||
(wrap-merge-prior-hx)
|
||
(wrap-schema-enforce :query-schema query-schema)
|
||
(wrap-schema-enforce :hx-schema query-schema)))))
|