(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.components :as com] [auto-ap.ssr.components.link-dropdown :refer [link-dropdown]] [auto-ap.ssr.components.selmer :as sc] [auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]] [auto-ap.ssr.hx :as hx] [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.pos.common :refer [date-range-field*]] [auto-ap.ssr.selmer :as sel] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils :refer [->db-id apply-middleware-to-all-handlers assert-schema clj-date-schema default-grid-fields-schema entity-id html-response main-transformer modal-response money path->name2 strip temp-id wrap-form-4xx-2 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=]] [malli.core :as mc])) (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" "datesApplied, 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)) (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))))]]]]]) ;; --------------------------------------------------------------------------- ;; Field-name / error helpers (no step-params[...] prefix) + item helpers. ;; Mirrors transaction/edit.clj and transaction/bulk_code.clj. ;; --------------------------------------------------------------------------- (def ^:dynamic *errors* "Humanized form errors for the current render, keyed by edit-schema paths. Bound by render-form from the request's :form-errors. Plain map -- no wizard, no cursor." {}) (defn- ferr [& path] (get-in *errors* (vec path))) (defn- item-field-name [index field] (path->name2 :sales-summary/items index field)) (defn- item-field-errors [index field] (ferr :sales-summary/items index field)) (defn- item-side "Which column an item belongs to: its persisted ledger-side for auto items, else the filled-in credit/debit for a manual row (nil for a brand-new blank manual row)." [item] (cond (= :ledger-side/debit (:ledger-mapped/ledger-side item)) :debit (= :ledger-side/credit (:ledger-mapped/ledger-side item)) :credit (:debit item) :debit (:credit item) :credit :else nil)) (defn- sum-debits [items] (->> items (keep :debit) (filter number?) (reduce + 0.0))) (defn- sum-credits [items] (->> items (keep :credit) (filter number?) (reduce + 0.0))) ;; --------------------------------------------------------------------------- ;; Render (Selmer): account typeahead, inline account cell (display/edit), ;; the read-only auto rows, the editable manual rows, totals/balance. ;; --------------------------------------------------------------------------- (defn account-typeahead* [{:keys [name value client-id]}] (sc/typeahead {:name name :id 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* "Account name + inline-edit pencil. The pencil swaps just this `.account-cell` (#account-cell, Rule 2) into the edit cell." [{:keys [index account-id client-id]}] (let [account-id (when (and account-id (not= account-id "")) (->db-id account-id)) account-name (when account-id (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read account-id) client-id)))] (str "
" (str (sc/hidden {:name (item-field-name index :ledger-mapped/account) :value (or account-id "")})) (if account-name (str "" (hu/escape-html account-name) "") (str (sel/hiccup->html (com/pill {:color :red} "Missing acct")))) (str (sc/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 index :client-id client-id :current-account-id (or account-id "")})} svg/pencil)) "
"))) (defn account-edit-cell* "The account typeahead + check (save) / cancel buttons. Each swaps just the `.account-cell` back to the display cell." [{:keys [index account-id client-id]}] (str "
" (str (account-typeahead* {:name (item-field-name index :ledger-mapped/account) :value account-id :client-id client-id})) "
" (str (sc/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 {:item-index index :client-id client-id})} svg/check)) (str (sc/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 {:item-index index :client-id client-id :current-account-id (or account-id "")})} svg/x)) "
")) (defn- auto-item-row* "A read-only auto item in its Debits/Credits column: category + inline-editable account cell + the (read-only) amount. Posts db/id, category, and account." [index item client-id] (let [side (item-side item) amount (if (= side :debit) (:debit item) (:credit item))] (str "
" (str (sc/hidden {:name (item-field-name index :db/id) :value (:db/id item)})) (str (sc/hidden {:name (item-field-name index :sales-summary-item/category) :value (:sales-summary-item/category item)})) "" (hu/escape-html (str (:sales-summary-item/category item))) "" (str (account-display-cell* {:index index :account-id (:ledger-mapped/account item) :client-id client-id})) "" (format "$%,.2f" (or amount 0.0)) "" "
"))) (defn- manual-amount-input* [index field item] (sc/money-input {:name (item-field-name index field) :value (get item field) :class "w-24 text-right font-mono tabular-nums" :placeholder (str/capitalize (clojure.core/name field)) :hx-post (bidi/path-for ssr-routes/only-routes ::route/form-changed) :hx-target "#summary-totals" :hx-select "#summary-totals" :hx-swap "outerHTML" :hx-trigger "keyup changed delay:300ms" :hx-include "closest form"})) (defn- manual-item-row* "An editable manual item: category + account typeahead + debit + credit money inputs + remove. The amount inputs swap the totals block (Rule 4); remove swaps the whole form." [index item client-id] (str "
" (str (sc/hidden {:name (item-field-name index :db/id) :value (:db/id item)})) (str (sc/hidden {:name (item-field-name index :sales-summary-item/manual?) :value "true"})) (str (sc/validated-field {:errors (item-field-errors index :sales-summary-item/category) :class "flex-1"} (sc/text-input {:name (item-field-name index :sales-summary-item/category) :value (:sales-summary-item/category item) :placeholder "Category/Explanation"}))) (str (sc/validated-field {:errors (item-field-errors index :ledger-mapped/account)} (account-typeahead* {:name (item-field-name index :ledger-mapped/account) :value (:ledger-mapped/account item) :client-id client-id}))) (str (manual-amount-input* index :debit item)) (str (manual-amount-input* index :credit item)) (str (sc/a-icon-button {:class "p-1 account-remove-action" :hx-post (bidi/path-for ssr-routes/only-routes ::route/form-changed) :hx-vals (hx/json {:op "remove-item" :row-index index}) :hx-target "#summary-edit-form" :hx-select "#summary-edit-form" :hx-swap "outerHTML" :hx-include "closest form"} svg/x)) "
")) (defn- totals* "Swappable totals + balance block (#summary-totals). Fixes the previously-dead total / balance display: shows the running debit/credit totals and a Balanced / Unbalanced indicator." [items] (let [td (sum-debits items) tc (sum-credits items) balanced? (dollars= td tc) delta (- td tc)] (str "
" "
Total" "
" (format "$%,.2f" td) "" "" (format "$%,.2f" tc) "
" (if balanced? "
Balanced
" (str "
Unbalanced" "" (format "$%,.2f" (Math/abs delta)) " " (if (pos? delta) "Debit over" "Credit over") "
")) "
"))) (defn- new-item-button* [] (sc/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/form-changed) :hx-vals (hx/json {:op "new-item"}) :hx-target "#summary-edit-form" :hx-select "#summary-edit-form" :hx-swap "outerHTML" :hx-include "closest form" :color :secondary} "New Summary Item")) (defn- form-errors-html [errors] (str "
" (when (seq errors) (str "

" (str/join ", " (filter string? errors)) "

")) "
")) (defn- footer* [request] (sel/raw (str "
" (form-errors-html (:errors (:form-errors request))) (str (sc/button {:color :primary :x-ref "next" :class "w-32" :type "submit"} "Save")) "
"))) (defn render-form "Renders the whole plain sales-summary edit form (no wizard). Binds *errors* so the field-level lookups (item-field-errors) resolve. Reuses the edit modal chrome." [request] (binding [*errors* (or (:form-errors request) {})] (let [{tx-id :db/id client :sales-summary/client items :sales-summary/items} (:edit-state request) client-id (:db/id client) indexed (map-indexed vector items) auto (remove (fn [[_ it]] (:sales-summary-item/manual? it)) indexed) manual (filter (fn [[_ it]] (:sales-summary-item/manual? it)) indexed) debit-rows (->> auto (filter (fn [[_ it]] (= :debit (item-side it)))) (map (fn [[i it]] (auto-item-row* i it client-id))) (apply str)) credit-rows (->> auto (filter (fn [[_ it]] (= :credit (item-side it)))) (map (fn [[i it]] (auto-item-row* i it client-id))) (apply str)) manual-rows (->> manual (map (fn [[i it]] (manual-item-row* i it client-id))) (apply str)) body (sel/render "templates/sales-summary/summary-body.html" {:debit_rows debit-rows :credit_rows credit-rows :totals (totals* items) :manual_rows manual-rows :new_item_button (str (new-item-button*))}) modal-card (sel/render "templates/transaction-edit/edit-modal.html" {:head "
Edit Summary
" :side_panel nil :body body :footer (str (footer* request))})] (sel/render->hiccup "templates/sales-summary/edit-form.html" {:db_id tx-id :form_attrs (sc/attrs->str {:hx-ext "response-targets" :hx-swap "outerHTML" :hx-target-400 "#form-errors .error-content" :hx-trigger "submit" :hx-target "this" :hx-put (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-submit)}) :modal (str (sc/modal {:id "wizardmodal"} (sel/raw modal-card)))})))) ;; --------------------------------------------------------------------------- ;; State: derive the flat edit-state from the entity overlaid with the posted ;; form (replaces MultiStepFormState + the EDN snapshot round-trip). ;; --------------------------------------------------------------------------- (defn entity->edit-state "The persisted sales summary, shaped like the form's state: each item gets a :credit or :debit field derived from its ledger-side/amount (what initial-edit-wizard-state did)." [tx-id] (let [e (dc/pull (dc/db conn) default-read tx-id) items (->> (:sales-summary/items e) sort-items (mapv (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))))))] {:db/id (:db/id e) :sales-summary/client (:sales-summary/client e) :sales-summary/items items})) (defn- merge-items "Overlay the posted items onto the persisted items by :db/id, so read-only fields the form doesn't post (ledger-side, amount, the credit/debit shaping for auto items) survive while edited fields (category, account, manual credit/debit) win. New manual rows (temp db/id) have no persisted match and ride through as-is." [entity-items posted-items] (let [by-id (into {} (map (juxt :db/id identity)) entity-items)] (mapv (fn [pi] (merge (get by-id (:db/id pi)) pi)) posted-items))) (defn wrap-decode "Parses the posted (nested) form params and decodes them straight into edit-schema -- no step-params[...] prefix. Strips to the editable top-level keys." [handler] (-> (fn [request] (let [decoded (mc/decode edit-schema (:form-params request) main-transformer) decoded (if (map? decoded) (select-keys decoded [:db/id :sales-summary/items]) {})] (handler (assoc request :posted decoded)))) (wrap-nested-form-params))) (defn wrap-derive-state "Builds :edit-state from the entity (db/id hidden, or the route on initial open) overlaid with the live posted items -- no serialized snapshot. db/id + client always come from the entity; items are the merged posted items when present, else the entity's." [handler] (fn [request] (let [tx-id (->db-id (or (some-> request :form-params (get "db/id")) (-> request :route-params :db/id))) base (entity->edit-state tx-id) posted (:posted request) items (if (contains? posted :sales-summary/items) (merge-items (:sales-summary/items base) (:sales-summary/items posted)) (:sales-summary/items base))] (handler (assoc request :edit-state (assoc base :sales-summary/items items)))))) (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 :new? :item-index) true (assoc :sales-summary-item/manual? true))) ;; --------------------------------------------------------------------------- ;; form-changed ops (whole-form re-render): add / remove a manual item. A bare ;; re-render (no op) refreshes the totals block (manual amount edits). ;; --------------------------------------------------------------------------- (defn apply-new-item [request] (let [items (vec (:sales-summary/items (:edit-state request))) new-item {:db/id (str (java.util.UUID/randomUUID)) :new? true :sales-summary-item/manual? true :sales-summary-item/category ""}] (assoc-in request [:edit-state :sales-summary/items] (conj items new-item)))) (defn apply-remove-item [request] (let [row-index (some-> request :form-params (get "row-index") Integer/parseInt) items (vec (:sales-summary/items (:edit-state request))) updated (if (and row-index (< row-index (count items))) (vec (concat (subvec items 0 row-index) (subvec items (inc row-index)))) items)] (assoc-in request [:edit-state :sales-summary/items] updated))) (defn form-changed-handler "Single whole-form re-render endpoint. Dispatches on `op` (add/remove a manual item); a missing op (a manual amount keyup) just re-renders (hx-select picks #summary-totals)." [request] (let [op (get-in request [:form-params "op"]) request' (case op "new-item" (apply-new-item request) "remove-item" (apply-remove-item request) request)] (html-response (render-form request')))) ;; --------------------------------------------------------------------------- ;; Inline account editor (targeted .account-cell swaps -- a distinct click-to-edit ;; feature, kept as its own three small stateless routes). ;; --------------------------------------------------------------------------- (defn edit-item-account [request] (let [{:keys [item-index client-id current-account-id]} (:query-params request) idx (if (string? item-index) (Integer/parseInt item-index) item-index) account-id (when (and current-account-id (not= current-account-id "")) (->db-id current-account-id))] (html-response (sel/raw (account-edit-cell* {:index idx :account-id account-id :client-id (->db-id client-id)}))))) (defn save-item-account [request] (let [item-index (get-in request [:params "item-index"]) idx (if (string? item-index) (Integer/parseInt item-index) item-index) client-id (->db-id (get-in request [:params "client-id"])) account-id-str (get-in request [:form-params (item-field-name idx :ledger-mapped/account)]) account-id (when (and account-id-str (not= account-id-str "")) (->db-id account-id-str))] (html-response (sel/raw (account-display-cell* {:index idx :account-id account-id :client-id client-id}))))) (defn cancel-item-account [request] (let [{:keys [item-index client-id current-account-id]} (:query-params request) idx (if (string? item-index) (Integer/parseInt item-index) item-index) account-id (when (and current-account-id (not= current-account-id "")) (->db-id current-account-id))] (html-response (sel/raw (account-display-cell* {:index idx :account-id account-id :client-id (->db-id client-id)}))))) ;; --------------------------------------------------------------------------- ;; Open + submit ;; --------------------------------------------------------------------------- (defn open-handler [request] (modal-response (sel/render->hiccup "templates/transaction-edit/transitioner.html" {:body (str (render-form request))}))) (defn- render-form-response [request] (html-response (render-form request) :headers {"HX-reswap" "outerHTML"})) (defn submit "Validates the posted edit-state against edit-schema (field errors via wrap-form-4xx-2), then upserts the sales summary: manual items attach-ledger (credit/debit -> ledger side+amount), auto items update only their account." [request] (let [{tx-id :db/id items :sales-summary/items :as edit-state} (:edit-state request)] (assert-schema edit-schema edit-state) (let [transaction [:upsert-sales-summary {:db/id tx-id :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)})) items)}]] @(dc/transact conn [transaction]) (html-response (row* (:identity request) (dc/pull (dc/db conn) default-read tx-id) {:flash? true :request request}) :headers {"hx-trigger" "modalclose" "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" tx-id) "hx-reswap" "outerHTML"})))) (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 (-> open-handler (wrap-derive-state) (wrap-decode) (wrap-schema-enforce :route-schema [:map [:db/id entity-id]])) ::route/form-changed (-> form-changed-handler (wrap-derive-state) (wrap-decode)) ::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 (-> submit (wrap-form-4xx-2 render-form-response) (wrap-derive-state) (wrap-decode))} (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)))))