(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.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.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=]] [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?] } ]) (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))) (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" :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))]]))}]})) (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) (def edit-schema [:map [:db/id entity-id] [:sales-summary/client [:map [:db/id entity-id]]] [:sales-summary/items [:vector {:coerce? true} [:and [:map [:db/id [:or entity-id temp-id]] [:sales-summary-item/category [:string {:decode/string strip}]] [:sales-summary-item/manual? {:default false :decode/arbitrary (fn [x] (cond (boolean? x) x (nil? x) false (str/blank? x) false :else true))} :boolean] [:ledger-mapped/account entity-id] [:credit {:optional true} [:maybe money]] [:debit {:optional true} [:maybe money]]] [:fn {:error/message "Must choose one of credit/debit" :error/path [:credit]} (fn [x] (not (and (:credit x) (:debit x))))]]]] ]) (defn summary-total-row* [request] (let [total-credits (-> request :multi-form-state :step-params :sales-summary/items (total-credits)) total-debits (-> request :multi-form-state :step-params :sales-summary/items (total-debits))] (com/data-grid-row {:id "total-row" :hx-trigger "change from:closest form target:.amount-field" :hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/expense-account-total) :hx-target "this" :hx-swap "innerHTML"} (com/data-grid-cell {}) (com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"]) (com/data-grid-cell {:class "text-right"} (format "$%,.2f" total-debits)) (com/data-grid-cell {:class "text-right"} (format "$%,.2f" total-credits))))) (defn unbalanced-row* [request] (let [total-credits (-> request :multi-form-state :step-params :sales-summary/items (total-credits)) total-debits (-> request :multi-form-state :step-params :sales-summary/items (total-debits))] (com/data-grid-row {:id "total-row" :hx-trigger "change from:closest form target:.amount-field" :hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/expense-account-total) :hx-target "this" :hx-swap "innerHTML"} (com/data-grid-cell {}) (com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "UNBALANCED"]) (com/data-grid-cell {:class "text-right"} (when (and (not (dollars= total-credits total-debits)) (> total-debits total-credits)) (format "$%,.2f" (- total-debits total-credits)))) (com/data-grid-cell {:class "text-right"} (when (and (not (dollars= total-credits total-debits)) (> total-credits total-debits)) (format "$%,.2f" (- total-credits total-debits))))))) (defn- account-typeahead* [{:keys [name value client-id]}] [:div.flex.flex-col (com/typeahead {:name name :placeholder "Search..." :url (hu/url (bidi/path-for ssr-routes/only-routes :account-search) {:client-id client-id :purpose "invoice"}) :value value :content-fn (fn [value] (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) client-id)))})]) (defn sales-summary-item-row* [{:keys [value client-id]}] (let [manual? (fc/field-value (:sales-summary-item/manual? value))] (com/data-grid-row (cond-> {:x-ref "p" :x-data (hx/json {})} (fc/field-value (:new? value)) (hx/htmx-transition-appear )) (fc/with-field :db/id (com/hidden {:name (fc/field-name) :value (fc/field-value)})) (when manual? (fc/with-field :sales-summary-item/manual? (com/hidden {:name (fc/field-name) :value true}))) (com/data-grid-cell {} (fc/with-field :sales-summary-item/category (if manual? (com/validated-field {:errors (fc/field-errors)} (com/text-input {:placeholder "Category/Explanation" :name (fc/field-name) :value (fc/field-value)})) (list (com/hidden {:name (fc/field-name) :value (fc/field-value)}) (fc/field-value (:sales-summary-item/category value)))))) (com/data-grid-cell {} (fc/with-field :ledger-mapped/account (com/validated-field {:errors (fc/field-errors)} (account-typeahead* {:value (fc/field-value) :client-id client-id :name (fc/field-name)})))) (com/data-grid-cell {:class "text-right"} (if manual? (fc/with-field :debit (com/validated-field {:errors (fc/field-errors)} (com/money-input {:class "w-24" :name (fc/field-name) :value (fc/field-value)}))) (when (= (fc/field-value (:ledger-mapped/ledger-side value)) :ledger-side/debit) (format "$%,.2f" (fc/field-value (:ledger-mapped/amount value)))))) (com/data-grid-cell {:class "text-right"} (if manual? (fc/with-field :credit (com/validated-field {:errors (fc/field-errors)} (com/money-input {:class "w-24" :name (fc/field-name) :value (fc/field-value)}))) (when (= (fc/field-value (:ledger-mapped/ledger-side value)) :ledger-side/credit) (format "$%,.2f" (fc/field-value (:ledger-mapped/amount value)))))) (com/data-grid-cell {:class "align-top"} (when manual? (com/a-icon-button {"@click.prevent.stop" "$refs.p.remove()"} svg/x)))))) (defrecord MainStep [linear-wizard] mm/ModalWizardStep (step-name [_] "Main") (step-key [_] :main) (edit-path [_ _] []) (step-schema [_] (mut/select-keys (mm/form-schema linear-wizard) #{:db/id :sales-summary/items})) (render-step [this {:keys [multi-form-state] :as request}] (mm/default-render-step linear-wizard this :head [:div.p-2 "New invoice"] :body (mm/default-step-body {} [:div (fc/with-field :db/id (com/hidden {:name (fc/field-name) :value (fc/field-value)})) (com/data-grid {:headers [(com/data-grid-header {} "Category") (com/data-grid-header {} "Account") (com/data-grid-header {} "Debits") (com/data-grid-header {} "Credits") (com/data-grid-header {} "")]} (fc/with-field :sales-summary/items (list (fc/cursor-map #(sales-summary-item-row* {:value % :client-id (:db/id (:sales-summary/client (:snapshot multi-form-state))) })) (com/data-grid-new-row {:colspan 5 :hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item) :row-offset 0 :index (count (fc/field-value)) :tr-params {:hx-vals (hx/json {:client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))})}} "New Summary Item"))) (summary-total-row* request) (unbalanced-row* request)) ]) :footer (mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate) :validation-route ::route/edit-wizard-navigate :width-height-class "lg:w-[850px] lg:h-[900px]"))) (defn attach-ledger [i] (cond-> i (:credit i) (assoc :ledger-mapped/ledger-side :ledger-side/credit :ledger-mapped/amount (:credit i)) (:debit i) (assoc :ledger-mapped/ledger-side :ledger-side/debit :ledger-mapped/amount (:debit i)) true (dissoc :credit :debit) true (assoc :sales-summary-item/manual? true))) (defrecord EditWizard [_ current-step] mm/LinearModalWizard (hydrate-from-request [this request] this) (navigate [this step-key] (assoc this :current-step step-key)) (get-current-step [this] (mm/get-step this :main)) (render-wizard [this {:keys [multi-form-state] :as request}] (mm/default-render-wizard this request :form-params (-> mm/default-form-props (assoc :hx-put (str (bidi/path-for ssr-routes/only-routes ::route/edit-wizard-submit)))) :render-timeline? false)) (steps [_] [:main]) (get-step [this step-key] (let [step-key-result (mc/parse mm/step-key-schema step-key) [step-key-type step-key] step-key-result] (->MainStep this))) (form-schema [_] edit-schema) (submit [this {:keys [multi-form-state request-method identity] :as request}] (let [result (:snapshot multi-form-state ) transaction [:upsert-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]) (html-response (row* identity (dc/pull (dc/db conn) default-read (:db/id result)) {:flash? true :request request}) :headers (cond-> {"hx-trigger" "modalclose" "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id result)) "hx-reswap" "outerHTML"}))))) (def edit-wizard (->EditWizard nil nil)) (defn initial-edit-wizard-state [request] (let [entity (dc/pull (dc/db conn) default-read (:db/id (:route-params request))) entity (select-keys entity (mut/keys edit-schema)) entity (update entity :sales-summary/items (comp #(map (fn [x] (if (= :ledger-side/debit (:ledger-mapped/ledger-side x)) (assoc x :debit (:ledger-mapped/amount x)) (assoc x :credit (:ledger-mapped/amount x)))) %) sort-items))] (mm/->MultiStepFormState entity [] entity))) (def key->handler (apply-middleware-to-all-handlers (->> {::route/page (helper/page-route grid-page) ::route/table (helper/table-route grid-page) ::route/edit-wizard (-> mm/open-wizard-handler (mm/wrap-wizard edit-wizard) (mm/wrap-init-multi-form-state initial-edit-wizard-state) (wrap-schema-enforce :route-schema [:map [:db/id entity-id]])) ::route/edit-wizard-navigate (-> mm/next-handler (mm/wrap-wizard edit-wizard) (mm/wrap-decode-multi-form-state)) ::route/new-summary-item (-> (add-new-entity-handler [:step-params :sales-summary/items] (fn render [cursor request] (sales-summary-item-row* {:value cursor :client-id (:client-id (:query-params request))})) (fn build-new-row [base _] (assoc base :sales-summary-item/manual? true))) (wrap-schema-enforce :query-schema [:map [:client-id {:optional true} [:maybe entity-id]]])) ::route/edit-wizard-submit (-> mm/submit-handler (mm/wrap-wizard edit-wizard) (mm/wrap-decode-multi-form-state))}) (fn [h] (-> h (wrap-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)))))