diff --git a/src/clj/auto_ap/ssr/components/multi_modal.clj b/src/clj/auto_ap/ssr/components/multi_modal.clj index 1928c0b3..898343bb 100644 --- a/src/clj/auto_ap/ssr/components/multi_modal.clj +++ b/src/clj/auto_ap/ssr/components/multi_modal.clj @@ -163,7 +163,7 @@ :else [:div "No action possible."])]]) -(defn default-render-step [linear-wizard step & {:keys [head body footer validation-route discard-route width-height-class]}] +(defn default-render-step [linear-wizard step & {:keys [head body footer validation-route discard-route width-height-class side-panel]}] (let [is-last? (= (step-key step) (last (steps linear-wizard)))] (com/modal-card-advanced {"@keydown.enter.prevent.stop" "if ($refs.next ) {$refs.next.click()}" @@ -195,12 +195,16 @@ \"htmx-added:opacity-0 opacity-100\": $data.transitionType=='forward', \"htmx-swapping:translate-x-2/3 htmx-swapping:opacity-0 htmx-swapping:scale-0 htmx-added:-translate-x-2/3 htmx-added:opacity-0 htmx-added:scale-0 scale-100 translate-x-0 opacity-100\": $data.transitionType=='backward' } - ") + ") "x-data" ""} (com/modal-header {} head) #_(com/modal-header-attachment {}) [:div.flex.shrink.overflow-auto.grow + (when side-panel + [:div.grow-0.w-64.bg-gray-50.border-r.hidden.md:block.overflow-y-auto + {:class "max-h-full"} + side-panel]) (when (:render-timeline? linear-wizard) [:div.grow-0.pr-6.pt-2.bg-gray-100.self-stretch.hidden.md:block #_{:style "margin-left:-20px"} (render-timeline linear-wizard step validation-route)]) @@ -215,11 +219,11 @@ (fn [{:keys [wizard multi-form-state] :as request}] (assert-schema (step-schema (get-current-step wizard)) (:step-params multi-form-state)) (handler request)) - (wrap-form-4xx-2 (fn [{:keys [wizard] :as request}] ;; THIS MAY BE BETTER TO JUST MAKE THE LINEAR WIZARD POPULATE FROM THE REQUEST - (html-response - (render-wizard wizard request) - :headers {"x-transition-type" "none" - "HX-reswap" "outerHTML"}))))) + (wrap-form-4xx-2 (fn [{:keys [wizard] :as request}] ;; THIS MAY BE BETTER TO JUST MAKE THE LINEAR WIZARD POPULATE FROM THE REQUEST + (html-response + (render-wizard wizard request) + :headers {"x-transition-type" "none" + "HX-reswap" "outerHTML"}))))) (defn get-transition-type [wizard from-step-key to-step-key] (let [to-step-index (.indexOf (steps wizard) to-step-key) diff --git a/src/clj/auto_ap/ssr/transaction/edit.clj b/src/clj/auto_ap/ssr/transaction/edit.clj index 01b1a087..615b3437 100644 --- a/src/clj/auto_ap/ssr/transaction/edit.clj +++ b/src/clj/auto_ap/ssr/transaction/edit.clj @@ -484,135 +484,36 @@ (format "$%,.2f" total)) (com/data-grid-cell {})))]))) -(defrecord BasicDetailsStep [linear-wizard] - mm/ModalWizardStep - (step-name [_] - "Transaction Details") - (step-key [_] - :basic-details) - - (edit-path [_ _] - []) - - (step-schema [_] - (mc/schema [:map - [:db/id {:optional true} [:maybe entity-id]]])) - - (render-step - [this {:keys [multi-form-state] :as request}] - (let [tx-id (mm/get-mfs-field multi-form-state :db/id) - tx (d-transactions/get-by-id tx-id)] - (alog/info ::TRANSACTION :i multi-form-state) - (mm/default-render-step - linear-wizard this - :head [:div.p-2 "Edit transaction"] - :body (mm/default-step-body - {} - [:div {:x-data (hx/json {:clientId (or (fc/field-value (:transaction/client fc/*current*)) - (:db/id (:client request)))})} - - ;; Read-only transaction details - [:div.mb-6.border.rounded-lg.p-4.bg-gray-50 - [:h3.text-lg.font-semibold.mb-2 "Transaction Details"] - [:div.grid.grid-cols-2.gap-4 - [:div - [:div.text-sm.font-medium.text-gray-500 "Amount"] - [:div.text-base (format "$%,.2f" (Math/abs (:transaction/amount tx)))]] - - [:div - [:div.text-sm.font-medium.text-gray-500 "Date"] - [:div.text-base (some-> tx :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date))]] - - [:div - [:div.text-sm.font-medium.text-gray-500 "Bank Account"] - [:div.text-base (or (-> tx :transaction/bank-account :bank-account/name) "-")]] - - [:div - [:div.text-sm.font-medium.text-gray-500 "Post Date"] - [:div.text-base (some-> tx :transaction/post-date coerce/to-date-time (atime/unparse-local atime/normal-date))]] - - [:div - [:div.text-sm.font-medium.text-gray-500 "Original Description"] - [:div.text-base (or (:transaction/description-original tx) "-")]] - - [:div - [:div.text-sm.font-medium.text-gray-500 "Simplified Description"] - [:div.text-base (or (:transaction/description-simple tx) "-")]] - - [:div - [:div.text-sm.font-medium.text-gray-500 "Check Number"] - [:div.text-base (or (:transaction/check-number tx) "-")]] - - [:div - [:div.text-sm.font-medium.text-gray-500 "Status"] - [:div.text-base (or (some-> tx :transaction/status) "-")]] - - [:div - [:div.text-sm.font-medium.text-gray-500 "Transaction Type"] - [:div.text-base (or (some-> tx :transaction/type) "-")]]]] - -;; Transaction Links Section - #_[:div.mb-6.border.rounded-lg.p-4.bg-gray-50 - [:h3.text-lg.font-semibold.mb-2 "Transaction Links"] - (let [tx-id (mm/get-mfs-field multi-form-state :db/id) - db-history (dc/history (dc/db conn)) - - ;; Get current and historical payments linked to this transaction - current-payment (when-let [payment-id (-> (dc/pull (dc/db conn) - '[:transaction/payment] - tx-id) - :transaction/payment - :db/id)] - {:entity-id payment-id :active true}) - historical-payments (when tx-id - (->> (dc/q '[:find ?payment ?inst ?added - :in $ ?e - :where - [?e :transaction/payment ?payment ?tx ?added] - [?tx :db/txInstant ?inst]] - db-history tx-id) - (map (fn [[id date op]] {:entity-id id - :date date - :op op})))) - - all-payments historical-payments] - - [:div - ;; Payments section - [:div.mb-3 - [:h4.font-medium.text-gray-700 "Linked Payments:"] - (if (seq all-payments) - [:ul.list-disc.pl-6.mt-1 - (for [{:keys [entity-id date op]} all-payments - :let [payment (dc/pull (dc/db conn) - '[:db/id :payment/invoice-number - [:payment/date :xform clj-time.coerce/from-date] - {:payment/vendor [:vendor/name]}] - entity-id)]] - [:li.text-sm.text-gray-600 {:class (when-not op "line-through")} - (if op [:span.text-green-600 "✓ "] "") - (str "Payment #" (:payment/invoice-number payment) " - " - (-> payment :payment/vendor :vendor/name))])] - [:p.text-sm.text-gray-500.italic "No payments linked to this transaction"])]])]] - - ;; Invoices section - -;; Hidden ID field - (fc/with-field :db/id - (com/hidden {:name (fc/field-name) - :value (fc/field-value)})) - -;; Editable fields section - ;; Vendor field - ) - :footer - (mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate) - :validation-route ::route/edit-wizard-navigate))) - - mm/Initializable - (init-step-params - [_ current request] - (:step-params current))) +(defn transaction-details-panel [tx] + [:div.p-4.space-y-4 + [:h3.text-sm.font-semibold.text-gray-900.uppercase.tracking-wider "Details"] + [:div.space-y-3 + [:div + [:div.text-xs.font-medium.text-gray-500 "Amount"] + [:div.text-sm.font-medium.text-gray-900 (format "$%,.2f" (Math/abs (:transaction/amount tx)))]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Date"] + [:div.text-sm.text-gray-900 (some-> tx :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date))]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Bank Account"] + [:div.text-sm.text-gray-900 (or (-> tx :transaction/bank-account :bank-account/name) "-")]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Post Date"] + [:div.text-sm.text-gray-900 (some-> tx :transaction/post-date coerce/to-date-time (atime/unparse-local atime/normal-date))]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Description"] + [:div.text-sm.text-gray-900.truncate.cursor-help + {:title (or (:transaction/description-original tx) "No original description")} + (or (:transaction/description-simple tx) "-")]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Check Number"] + [:div.text-sm.text-gray-900 (or (:transaction/check-number tx) "-")]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Status"] + [:div.text-sm.text-gray-900 (or (some-> tx :transaction/status) "-")]] + [:div + [:div.text-xs.font-medium.text-gray-500 "Transaction Type"] + [:div.text-sm.text-gray-900 (or (some-> tx :transaction/type) "-")]]]]) (defn get-available-payments [request] (let [tx-id (or (get-in request [:form-params :transaction-id]) @@ -856,121 +757,126 @@ (mm/form-schema linear-wizard)) (render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}] - (mm/default-render-step - linear-wizard this - :head [:div.p-2 "Transaction Actions"] - :body (mm/default-step-body - {} - [:div - (fc/with-field :transaction/memo - (com/validated-field - {:label "Memo" - :errors (fc/field-errors)} - [:div.w-96 - (com/text-input {:value (-> (fc/field-value)) - :name (fc/field-name) - :error? (fc/field-errors) - :placeholder "Optional note"})])) - [:div {:x-data (hx/json {:activeForm (if (:transaction/payment (:entity request)) - "link-payment" - (or (fc/with-field :action (fc/field-value)) - "manual")) - :canChange (boolean (not (:transaction/payment (:entity request))))}) - "@unlinked" "canChange=true"} - [:div {:class "flex space-x-2 mb-4"} - (fc/with-field :action - (com/hidden {:name (fc/field-name) - :value (fc/field-value) - ":value" "activeForm"})) - (com/button-group {:name "method"} - (com/button-group-button {"@click" "activeForm = 'link-payment'" :value "payment" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'link-payment'}" :class "relative"} - (let [count (count-payment-matches request)] - (when (> count 0) - (com/badge {:color "green"} (str count)))) - "Link to payment") - (com/button-group-button {"@click" "activeForm = 'link-unpaid-invoices'" :value "unpaid" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'link-unpaid-invoices'}" :class "relative" - ":disabled" "!canChange"} - (let [count (count-unpaid-invoice-matches request)] - (when (> count 0) - (com/badge {:color "green"} (str count)))) - "Link to unpaid invoices") - (com/button-group-button {"@click" "activeForm = 'link-autopay-invoices'" :value "autopay" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'link-autopay-invoices'}" :class "relative" - ":disabled" "!canChange"} - (let [count (count-autopay-invoice-matches request)] - (when (> count 0) - (com/badge {:color "green"} (str count)))) - "Link to autopay invoices") - (com/button-group-button {"@click" "activeForm = 'apply-rule'" :value "rule" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'apply-rule'}" :class "relative" - ":disabled" "!canChange"} - (let [count (count-rule-matches request)] - (when (> count 0) - (com/badge {:color "green"} (str count)))) - "Apply rule") - (com/button-group-button {"@click" "activeForm = 'manual'" :value "manual" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'manual'}" - ":disabled" "!canChange"} - "Manual"))] - [:div {:x-show "activeForm === 'link-payment'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"} + (let [tx-id (mm/get-mfs-field multi-form-state :db/id) + tx (d-transactions/get-by-id tx-id)] + (mm/default-render-step + linear-wizard this + :head [:div.p-2 "Edit Transaction"] + :width-height-class " md:w-[950px] md:h-[650px] " + :side-panel (transaction-details-panel tx) + :body (mm/default-step-body + {} + [:div + (fc/with-field :transaction/memo + (com/validated-field + {:label "Memo" + :errors (fc/field-errors)} + [:div.w-96 + (com/text-input {:value (-> (fc/field-value)) + :name (fc/field-name) + :error? (fc/field-errors) + :placeholder "Optional note"})])) + [:div {:x-data (hx/json {:activeForm (if (:transaction/payment (:entity request)) + "link-payment" + (or (fc/with-field :action (fc/field-value)) + "manual")) + :canChange (boolean (not (:transaction/payment (:entity request))))}) + "@unlinked" "canChange=true"} + [:div {:class "flex space-x-2 mb-4"} + (fc/with-field :action + (com/hidden {:name (fc/field-name) + :value (fc/field-value) + ":value" "activeForm"})) + (com/button-group {:name "method"} + (com/button-group-button {"@click" "activeForm = 'link-payment'" :value "payment" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'link-payment'}" :class "relative"} + (let [count (count-payment-matches request)] + (when (> count 0) + (com/badge {:color "green"} (str count)))) + "Link to payment") + (com/button-group-button {"@click" "activeForm = 'link-unpaid-invoices'" :value "unpaid" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'link-unpaid-invoices'}" :class "relative" + ":disabled" "!canChange"} + (let [count (count-unpaid-invoice-matches request)] + (when (> count 0) + (com/badge {:color "green"} (str count)))) + "Link to unpaid invoices") + (com/button-group-button {"@click" "activeForm = 'link-autopay-invoices'" :value "autopay" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'link-autopay-invoices'}" :class "relative" + ":disabled" "!canChange"} + (let [count (count-autopay-invoice-matches request)] + (when (> count 0) + (com/badge {:color "green"} (str count)))) + "Link to autopay invoices") + (com/button-group-button {"@click" "activeForm = 'apply-rule'" :value "rule" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'apply-rule'}" :class "relative" + ":disabled" "!canChange"} + (let [count (count-rule-matches request)] + (when (> count 0) + (com/badge {:color "green"} (str count)))) + "Apply rule") + (com/button-group-button {"@click" "activeForm = 'manual'" :value "manual" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'manual'}" + ":disabled" "!canChange"} + "Manual"))] + [:div {:x-show "activeForm === 'link-payment'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"} - (payment-matches-view request)] - [:div {:x-show "activeForm === 'link-unpaid-invoices'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"} - (unpaid-invoices-view request)] - [:div {:x-show "activeForm === 'link-autopay-invoices'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"} - (autopay-invoices-view request)] - [:div {:x-show "activeForm === 'apply-rule'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"} - (transaction-rules-view request)] - [:div {:x-show "activeForm === 'manual'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"} - [:div {} - [:div {:hx-trigger "change" - :hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-vendor-changed) - :hx-target "#account-grid-body" - :hx-swap "outerHTML" - :hx-include "closest form"} - (fc/with-field :transaction/vendor - (com/validated-field - {:label "Vendor" - :errors (fc/field-errors)} - [:div.w-96 - (com/typeahead {:name (fc/field-name) - :error? (fc/error?) - :class "w-96" - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes :vendor-search) - :value (fc/field-value) - :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})]))] + (payment-matches-view request)] + [:div {:x-show "activeForm === 'link-unpaid-invoices'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"} + (unpaid-invoices-view request)] + [:div {:x-show "activeForm === 'link-autopay-invoices'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"} + (autopay-invoices-view request)] + [:div {:x-show "activeForm === 'apply-rule'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"} + (transaction-rules-view request)] + [:div {:x-show "activeForm === 'manual'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"} + [:div {} + [:div {:hx-trigger "change" + :hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-vendor-changed) + :hx-target "#account-grid-body" + :hx-swap "outerHTML" + :hx-include "closest form"} + (fc/with-field :transaction/vendor + (com/validated-field + {:label "Vendor" + :errors (fc/field-errors)} + [:div.w-96 + (com/typeahead {:name (fc/field-name) + :error? (fc/error?) + :class "w-96" + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes :vendor-search) + :value (fc/field-value) + :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})]))] - ;; Memo field + ;; Memo field - ;; Approval status field - (fc/with-field :transaction/approval-status - (com/validated-field - {:label "Status" - :errors (fc/field-errors)} - (let [current-value (name (or (fc/field-value) :transaction-approval-status/unapproved))] - [:div {:x-data (hx/json {:approvalStatus current-value})} - (com/hidden {:name (fc/field-name) - :value current-value - ":value" "approvalStatus"}) - [:div {:class "inline-flex rounded-md shadow-sm", :role "group"} - (com/button-group-button {"@click" "approvalStatus = 'approved'" - ":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'approved' }" - :class "rounded-l-lg"} - "Approved") - (com/button-group-button {"@click" "approvalStatus = 'unapproved'" - ":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'unapproved' }"} - "Unapproved") - (com/button-group-button {"@click" "approvalStatus = 'suppressed'" - ":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'suppressed' }" - :class "rounded-r-lg"} - "Client Review")]]))) - (fc/with-field :transaction/accounts - (com/validated-field - {:errors (fc/field-errors)} - [:div#account-grid-body - (account-grid-body* request)]))]]]]) - :footer - (mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate - :next-button (com/button {:color :primary :x-ref "next" :class "w-32 wizard-save-action"} "Done")) - :validation-route ::route/edit-wizard-navigate))) + ;; Approval status field + (fc/with-field :transaction/approval-status + (com/validated-field + {:label "Status" + :errors (fc/field-errors)} + (let [current-value (name (or (fc/field-value) :transaction-approval-status/unapproved))] + [:div {:x-data (hx/json {:approvalStatus current-value})} + (com/hidden {:name (fc/field-name) + :value current-value + ":value" "approvalStatus"}) + [:div {:class "inline-flex rounded-md shadow-sm", :role "group"} + (com/button-group-button {"@click" "approvalStatus = 'approved'" + ":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'approved' }" + :class "rounded-l-lg"} + "Approved") + (com/button-group-button {"@click" "approvalStatus = 'unapproved'" + ":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'unapproved' }" + :class "rounded-r-lg"} + "Unapproved") + (com/button-group-button {"@click" "approvalStatus = 'suppressed'" + ":class" "{ '!bg-primary-200 text-primary-800': approvalStatus === 'suppressed' }" + :class "rounded-r-lg"} + "Client Review")]]))) + (fc/with-field :transaction/accounts + (com/validated-field + {:errors (fc/field-errors)} + [:div#account-grid-body + (account-grid-body* request)]))]]]]) + :footer + (mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate + :next-button (com/button {:color :primary :x-ref "next" :class "w-32 wizard-save-action"} "Done")) + :validation-route ::route/edit-wizard-navigate)))) (defmulti save-handler (fn [request] (-> request :multi-form-state :snapshot :action))) @@ -1369,7 +1275,7 @@ (get-current-step [this] (if current-step (mm/get-step this current-step) - (mm/get-step this :basic-details))) + (mm/get-step this :links))) (render-wizard [this {:keys [multi-form-state] :as request}] (mm/default-render-wizard this request @@ -1379,13 +1285,11 @@ (str (bidi/path-for ssr-routes/only-routes ::route/edit-submit)))) :render-timeline? false)) (steps [_] - [:basic-details - :links]) + [:links]) (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] - (get {:basic-details (->BasicDetailsStep this) - :links (->LinksStep this)} + (get {:links (->LinksStep this)} step-key))) (form-schema [_] edit-form-schema)