(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)))))