From 28915033777b97c7ee2b7c22f202b9d1331a0d07 Mon Sep 17 00:00:00 2001 From: Bryce Date: Wed, 20 Mar 2024 22:36:14 -0700 Subject: [PATCH] Makes beginning of new invoice --- src/clj/auto_ap/graphql/accounts.clj | 151 +++++---- src/clj/auto_ap/graphql/vendors.clj | 99 +++--- src/clj/auto_ap/ssr/invoices.clj | 440 ++++++++++++++++++++++++--- src/clj/auto_ap/ssr/utils.clj | 2 +- src/cljc/auto_ap/routes/invoice.cljc | 5 +- 5 files changed, 525 insertions(+), 172 deletions(-) diff --git a/src/clj/auto_ap/graphql/accounts.clj b/src/clj/auto_ap/graphql/accounts.clj index d2a53263..ffc03063 100644 --- a/src/clj/auto_ap/graphql/accounts.clj +++ b/src/clj/auto_ap/graphql/accounts.clj @@ -22,7 +22,7 @@ (defn get-all-graphql [context args _] (assert-admin (:id context)) (let [args (assoc args :id (:id context)) - [accounts _ ] (d-accounts/get-graphql (assoc (<-graphql args) :per-page Integer/MAX_VALUE))] + [accounts _] (d-accounts/get-graphql (assoc (<-graphql args) :per-page Integer/MAX_VALUE))] (map ->graphql accounts))) (defn default-for-vendor [context args _] @@ -31,34 +31,32 @@ (->graphql (d-accounts/clientize result (:client_id args))))) (def search-pattern [:db/id - :account/numeric-code - :account/location - {:account/vendor-allowance [:db/ident] - :account/default-allowance [:db/ident] - :account/invoice-allowance [:db/ident]}]) + :account/numeric-code + :account/location + {:account/vendor-allowance [:db/ident] + :account/default-allowance [:db/ident] + :account/invoice-allowance [:db/ident]}]) (defn search- [id query client] (let [client-part (if (some->> client (can-see-client? id)) (format "((applicability:(global OR optional) AND -client_id:*) OR (account_client_override_id:* AND client_id:%s))" client) - "(applicability:(global OR optional) AND -client_id:*)" - - ) + "(applicability:(global OR optional) AND -client_id:*)") query (format "_text_:(%s) AND %s" (cleanse-query query) client-part)] (mu/log ::searching :search-query query) (for [{:keys [account_id name] :as g} (solr/query solr/impl "accounts" - {"query" query - "fields" "id, name, client_id, numeric_code, applicability, account_id"})] - + {"query" query + "fields" "id, name, client_id, numeric_code, applicability, account_id"})] + {:account_id (first account_id) - :name (first name)}))) + :name (first name)}))) (defn search [context {query :query client :client_id allowance :allowance vendor-id :vendor_id} _] (when client (assert-can-see-client (:id context) client)) (let [num (some-> (re-find #"([0-9]+)" query) second - (not-empty ) + (not-empty) Integer/parseInt) - + valid-allowances (cond-> #{:allowance/allowed :allowance/warn} (is-admin? (:id context)) (conj :allowance/admin-only)) @@ -71,77 +69,76 @@ vendor-account (when vendor-id (-> (dc/q '[:find ?da - :in $ ?v - :where [?v :vendor/default-account ?da]] - (dc/db conn) - vendor-id) + :in $ ?v + :where [?v :vendor/default-account ?da]] + (dc/db conn) + vendor-id) ffirst)) xform (comp - (filter (fn [[_ a]] - (or - (valid-allowances (-> a allowance :db/ident)) - (= (:db/id a) vendor-account)))) - (map (fn [[n a]] - {:name (str (:account/numeric-code a) " - " n) - :id (:db/id a) - :location (:account/location a) - :warning (when (= :allowance/warn (-> a allowance :db/ident)) - "This account is not typically used for this purpose.")})))] - (if query + (filter (fn [[_ a]] + (or + (valid-allowances (-> a allowance :db/ident)) + (= (:db/id a) vendor-account)))) + (map (fn [[n a]] + {:name (str (:account/numeric-code a) " - " n) + :id (:db/id a) + :location (:account/location a) + :warning (when (= :allowance/warn (-> a allowance :db/ident)) + "This account is not typically used for this purpose.")})))] + (if query (if num (->> (dc/q '[:find ?n (pull ?i pattern) - :in $ ?numeric-code ?allowance pattern - :where [?i :account/numeric-code ?numeric-code] - [?i :account/name ?n] - (or [?i :account/applicability :account-applicability/global] - [?i :account/applicability :account-applicability/optional] - [?i :account/applicability :account-applicability/customized])] - (dc/db conn) - num - allowance - search-pattern) + :in $ ?numeric-code ?allowance pattern + :where [?i :account/numeric-code ?numeric-code] + [?i :account/name ?n] + (or [?i :account/applicability :account-applicability/global] + [?i :account/applicability :account-applicability/optional] + [?i :account/applicability :account-applicability/customized])] + (dc/db conn) + num + allowance + search-pattern) (sequence xform)) (->> (search- (:id context) query client) (sequence - (comp (map (fn [i] [(:name i) (dc/pull (dc/db conn) search-pattern (:account_id i))])) - xform)))) + (comp (map (fn [i] [(:name i) (dc/pull (dc/db conn) search-pattern (:account_id i))])) + xform)))) []))) (defn rebuild-search-index [] (solr/index-documents-raw - solr/impl - "accounts" - (for [result (map first (dc/qseq {:query '[:find (pull ?aco [:account-client-override/search-terms :account-client-override/client :db/id {:account/_client-overrides [:account/numeric-code :account/location :db/id {:account/applicability [:db/ident]}]}]) - :in $ - :where [?aco :account-client-override/client ] - [?aco :account-client-override/search-terms ] - [_ :account/client-overrides ?aco]] - :args [(dc/db conn)]})) - :when (:account/numeric-code (:account/_client-overrides result))] - {"id" (:db/id result) - "account_id" (:db/id (:account/_client-overrides result)) - "account_client_override_id" (str (:db/id result)) - "name" (:account-client-override/search-terms result) - "client_id" (str (:db/id (:account-client-override/client result))) - "numeric_code" (:account/numeric-code (:account/_client-overrides result)) - "location" (:account/location (:account/_client-overrides result)) - "applicability" (name (:db/ident (:account/applicability (:account/_client-overrides result))))})) + solr/impl + "accounts" + (for [result (map first (dc/qseq {:query '[:find (pull ?aco [:account-client-override/search-terms :account-client-override/client :db/id {:account/_client-overrides [:account/numeric-code :account/location :db/id {:account/applicability [:db/ident]}]}]) + :in $ + :where [?aco :account-client-override/client] + [?aco :account-client-override/search-terms] + [_ :account/client-overrides ?aco]] + :args [(dc/db conn)]})) + :when (:account/numeric-code (:account/_client-overrides result))] + {"id" (:db/id result) + "account_id" (:db/id (:account/_client-overrides result)) + "account_client_override_id" (str (:db/id result)) + "name" (:account-client-override/search-terms result) + "client_id" (str (:db/id (:account-client-override/client result))) + "numeric_code" (:account/numeric-code (:account/_client-overrides result)) + "location" (:account/location (:account/_client-overrides result)) + "applicability" (name (:db/ident (:account/applicability (:account/_client-overrides result))))})) (solr/index-documents-raw - solr/impl - "accounts" - (for [result (map first (dc/qseq {:query '[:find (pull ?a [:account/numeric-code - :account/search-terms - {:account/applicability [:db/ident]} - :db/id - :account/location]) - :in $ - :where [?a :account/search-terms ]] - :args [(dc/db conn)]})) - :when (:account/search-terms result) - ] - {"id" (:db/id result) - "account_id" (:db/id result) - "name" (:account/search-terms result) - "numeric_code" (:account/numeric-code result) - "location" (:account/location result) - "applicability" (name (:db/ident (:account/applicability result)))}))) + solr/impl + "accounts" + (for [result (map first (dc/qseq {:query '[:find (pull ?a [:account/numeric-code + :account/search-terms + {:account/applicability [:db/ident]} + :db/id + :account/location]) + :in $ + :where [?a :account/search-terms]] + :args [(dc/db conn)]})) + :when (:account/search-terms result)] + {"id" (:db/id result) + "account_id" (:db/id result) + "name" (:account/search-terms result) + "numeric_code" (:account/numeric-code result) + "location" (:account/location result) + "applicability" (name (:db/ident (:account/applicability result)))}))) \ No newline at end of file diff --git a/src/clj/auto_ap/graphql/vendors.clj b/src/clj/auto_ap/graphql/vendors.clj index 3da3fd85..413907a3 100644 --- a/src/clj/auto_ap/graphql/vendors.clj +++ b/src/clj/auto_ap/graphql/vendors.clj @@ -21,18 +21,18 @@ (defn can-user-edit-vendor? [vendor-id id] (if (is-admin? id) - true - (empty? - (set/difference (set (->> (dc/q '[:find ?c - :in $ ?v - :where [?vu :vendor-usage/vendor ?v] - [?vu :vendor-usage/client ?c] - [?vu :vendor-usage/count ?d] - [(>= ?d 0)]] - (dc/db conn) - vendor-id) - (map first))) - (set (map :db/id (:user/clients id))))))) + true + (empty? + (set/difference (set (->> (dc/q '[:find ?c + :in $ ?v + :where [?vu :vendor-usage/vendor ?v] + [?vu :vendor-usage/client ?c] + [?vu :vendor-usage/count ?d] + [(>= ?d 0)]] + (dc/db conn) + vendor-id) + (map first))) + (set (map :db/id (:user/clients id))))))) (defn upsert-vendor [context {{:keys [id name hidden terms code print_as primary_contact plaid_merchant secondary_contact address default_account_id invoice_reminder_schedule schedule_payment_dom terms_overrides account_overrides] :as in} :vendor} _] (when (and id (not (can-user-edit-vendor? id (:id context)))) @@ -63,11 +63,11 @@ hidden false) terms-overrides (mapv - (fn [to] - #:vendor-terms-override {:client (:client_id to) - :terms (:terms to) - :db/id (or (:id to) (random-tempid))}) - terms_overrides) + (fn [to] + #:vendor-terms-override {:client (:client_id to) + :terms (:terms to) + :db/id (or (:id to) (random-tempid))}) + terms_overrides) account-overrides (mapv (fn [ao] #:vendor-account-override {:client (:client_id ao) @@ -75,11 +75,11 @@ :db/id (or (:id ao) (random-tempid))}) account_overrides) schedule-payment-dom (mapv - (fn [ao] - #:vendor-schedule-payment-dom {:client (:client_id ao) - :dom (:dom ao) - :db/id (or (:id ao) (random-tempid))}) - schedule_payment_dom) + (fn [ao] + #:vendor-schedule-payment-dom {:client (:client_id ao) + :dom (:dom ao) + :db/id (or (:id ao) (random-tempid))}) + schedule_payment_dom) transaction [:upsert-entity (cond-> #:vendor {:db/id (if id id "vendor") @@ -114,41 +114,40 @@ "secondary") :name (:name secondary_contact) :phone (:phone secondary_contact) - :email (:email secondary_contact)}) - ) + :email (:email secondary_contact)})) :search-terms [name]} (is-admin? (:id context)) (assoc - :vendor/legal-entity-name (:legal_entity_name in) - :vendor/legal-entity-first-name (:legal_entity_first_name in) - :vendor/legal-entity-middle-name (:legal_entity_middle_name in) - :vendor/legal-entity-last-name (:legal_entity_last_name in) - :vendor/legal-entity-tin (:legal_entity_tin in) - :vendor/legal-entity-tin-type (enum->keyword (:legal_entity_tin_type in) "legal-entity-tin-type") - :vendor/legal-entity-1099-type (enum->keyword (:legal_entity_1099_type in) "legal-entity-1099-type") - :vendor/plaid-merchant plaid_merchant - :vendor/account-overrides account-overrides - :vendor/terms-overrides terms-overrides - :vendor/schedule-payment-dom schedule-payment-dom - :vendor/automatically-paid-when-due (:automatically_paid_when_due in)))] + :vendor/legal-entity-name (:legal_entity_name in) + :vendor/legal-entity-first-name (:legal_entity_first_name in) + :vendor/legal-entity-middle-name (:legal_entity_middle_name in) + :vendor/legal-entity-last-name (:legal_entity_last_name in) + :vendor/legal-entity-tin (:legal_entity_tin in) + :vendor/legal-entity-tin-type (enum->keyword (:legal_entity_tin_type in) "legal-entity-tin-type") + :vendor/legal-entity-1099-type (enum->keyword (:legal_entity_1099_type in) "legal-entity-1099-type") + :vendor/plaid-merchant plaid_merchant + :vendor/account-overrides account-overrides + :vendor/terms-overrides terms-overrides + :vendor/schedule-payment-dom schedule-payment-dom + :vendor/automatically-paid-when-due (:automatically_paid_when_due in)))] + - transaction-result (audit-transact [transaction] (:id context)) new-vendor (d-vendors/get-by-id (or (-> transaction-result :tempids (get "vendor")) id))] - + (auto-ap.solr/index-documents-raw - auto-ap.solr/impl - "vendors" - [{"id" (:db/id new-vendor) - "name" (:vendor/name new-vendor) - "hidden" (boolean (:vendor/hidden new-vendor))}]) - + auto-ap.solr/impl + "vendors" + [{"id" (:db/id new-vendor) + "name" (:vendor/name new-vendor) + "hidden" (boolean (:vendor/hidden new-vendor))}]) + (-> new-vendor (->graphql)))) (defn merge-vendors [context {:keys [from to]} _] (let [transaction (->> (dc/q {:find '[?x ?a2] - :in '[$ ?vendor-from ] + :in '[$ ?vendor-from] :where ['[?x ?a ?vendor-from] '[?a :db/ident ?a2]]} (dc/db conn) @@ -165,13 +164,13 @@ (defn get-graphql [context args _] (assert-admin (:id context)) (let [args (assoc args :id (:id context)) - [vendors vendors-count ] (d-vendors/get-graphql (<-graphql args))] + [vendors vendors-count] (d-vendors/get-graphql (<-graphql args))] (result->page vendors vendors-count :vendors args))) (defn get-by-id [context args _] (->graphql - (d-vendors/get-graphql-by-id (assoc args :id (:id context)) - (:id args)))) + (d-vendors/get-graphql-by-id (assoc args :id (:id context)) + (:id args)))) (defn partial-match-first [query matches] (if-let [best-match (->> matches @@ -187,7 +186,7 @@ (defn search [context args _] (if-let [query (not-empty (cleanse-query (:query args)))] (let [search-query (str "name:(" query ")")] - + (for [{:keys [id name]} (solr/query solr/impl "vendors" {"query" (cond-> search-query (not (is-admin? (:id context))) (str " hidden:false")) @@ -210,4 +209,4 @@ :args [(dc/db conn)]})] {"id" (:db/id result) "name" (:vendor/name result) - "hidden" (boolean (:vendor/hidden result))})))) + "hidden" (boolean (:vendor/hidden result))})))) \ No newline at end of file diff --git a/src/clj/auto_ap/ssr/invoices.clj b/src/clj/auto_ap/ssr/invoices.clj index ae9ee349..c9200ac1 100644 --- a/src/clj/auto_ap/ssr/invoices.clj +++ b/src/clj/auto_ap/ssr/invoices.clj @@ -3,7 +3,7 @@ [auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-3 audit-transact conn merge-query observable-query - pull-many]] + pull-attr pull-many]] [auto-ap.datomic.accounts :as d-accounts] [auto-ap.datomic.bank-accounts :as d-bank-accounts] [auto-ap.datomic.invoices :as d-invoices] @@ -23,6 +23,7 @@ :refer [wrap-admin wrap-client-redirect-unauthenticated]] [auto-ap.solr :as solr] [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] @@ -390,6 +391,7 @@ (pay-button* {:ids (selected->ids request (:query-params request))}))) +;; TODO test as a real user (def grid-page (helper/build {:id "entity-table" :nav com/main-aside-nav @@ -412,7 +414,10 @@ "Void selected")) (when (can? (:identity request) {:subject :invoice :activity :pay}) (pay-button* {:ids (selected->ids request - (:query-params request))}))]) + (:query-params request))})) + (when (can? (:identity request) {:subject :invoice :activity :create}) + (com/button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-wizard)} + "New invoice"))]) :row-buttons (fn [_ entity] [(when (= :invoice-status/unpaid (:invoice/status entity)) (com/icon-button {:hx-delete (bidi/path-for ssr-routes/only-routes @@ -923,7 +928,7 @@ :validation-route ::route/pay-wizard-navigate))) (defn add-handwritten-check [request wizard snapshot] - (let [invoices (d-invoices/get-multi (map :invoice-id (:invoices snapshot))) ;; TODO shouldn't need datomic + (let [invoices (d-invoices/get-multi (map :invoice-id (:invoices snapshot))) bank-account-id (:bank-account snapshot) bank-account (d-bank-accounts/get-by-id bank-account-id) _ (when-not (= 1 (count (set (map (comp :db/id :invoice/vendor) invoices)))) @@ -960,6 +965,7 @@ ;; Thought is to put it into the pay function itself ;; balance should only go to negative if total was negative ;; balance should stay positive if total was positive +;; NOTE: payable-ids function could be used. ;; TODO support crediting from balance (defrecord PayWizard [form-params current-step invoice-by-id] @@ -1057,7 +1063,7 @@ :payment-type/cash (= :credit (:method snapshot)) :payment-type/credit - :else :payment-type/debit) ;; TODO might not be right + :else :payment-type/debit) identity)))] (modal-response (com/modal {} @@ -1080,6 +1086,325 @@ (def pay-wizard (->PayWizard nil nil nil)) +(def new-form-schema + [:map + [:invoice/client [:entity-map {:pull [:db/id :client/name :client/accounts]}]] + [:invoice/vendor [:entity-map {:pull [:db/id :vendor/name]}]]]) + +(defrecord BasicDetailsStep [linear-wizard] + mm/ModalWizardStep + (step-name [_] + "Basic Details") + (step-key [_] + :basic-details) + + (edit-path [_ _] + []) + + (step-schema [_] + (mut/select-keys (mm/form-schema linear-wizard) #{:invoice/client :invoice/vendor})) + + (render-step [this request] + (mm/default-render-step + linear-wizard this + :head [:div.p-2 "New invoice"] + :body (mm/default-step-body + {} + [:div {} + (fc/with-field :invoice/client + (if (:client request) + (com/hidden {:name (fc/field-name) + :value (:db/id (:client request))}) + (com/validated-field + {:label "Client" + :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 :company-search) + :value (fc/field-value) + :content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})]))) + (fc/with-field :invoice/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))})])) + (fc/with-field :invoice/date + (com/validated-field + {:label "Date" + :errors (fc/field-errors)} + [:div {:class "w-24"} + (com/date-input {:value (-> (fc/field-value) + (atime/unparse-local atime/normal-date)) + :name (fc/field-name) + :error? (fc/field-errors) + :placeholder "1/1/2024"})])) + (fc/with-field :invoice/due + (com/validated-field + {:label "Due (optional)" + :errors (fc/field-errors)} + [:div {:class "w-24"} + (com/date-input {:value (-> (fc/field-value) + (atime/unparse-local atime/normal-date)) + :name (fc/field-name) + :error? (fc/field-errors) + :placeholder "1/1/2024"})])) + (fc/with-field :invoice/scheduled-payment + (com/validated-field + {:label "Scheduled payment (optional)" + :errors (fc/field-errors)} + [:div {:class "w-24"} + (com/date-input {:value (-> (fc/field-value) + (atime/unparse-local atime/normal-date)) + :name (fc/field-name) + :error? (fc/field-errors) + :placeholder "1/1/2024"})])) + + (fc/with-field :invoice/invoice-number + (com/validated-field + {:label "Invoice Number" + :errors (fc/field-errors)} + [:div {:class "w-24"} + (com/text-input {:value (-> (fc/field-value)) + :name (fc/field-name) + :error? (fc/field-errors) + :placeholder "HA-123"})])) + (fc/with-field :invoice/total + (com/validated-field + {:label "Total" + :errors (fc/field-errors)} + [:div {:class "w-16"} + (com/money-input {:value (-> (fc/field-value)) + :name (fc/field-name) + :class "w-24" + :error? (fc/field-errors) + :placeholder "212.44"})]))]) + + :footer + (mm/default-step-footer linear-wizard this :validation-route ::route/new-wizard-navigate) + :validation-route ::route/new-wizard-navigate))) + +(defn- location-select* + [{:keys [name account-location client-locations value]}] + (com/select {:options (into [["" ""]] + (cond account-location + [[account-location account-location]] + + (seq client-locations) + (into [["Shared" "Shared"]] + (for [cl client-locations] + [cl cl])) + :else + [["Shared" "Shared"]])) + :name name + :value value + :class "w-full"})) + +(defn- account-typeahead* + [{:keys [name value client-id x-model]}] + [:div.flex.flex-col + (com/typeahead {:name name + :placeholder "Search..." + :url (str (bidi/path-for ssr-routes/only-routes :account-search) "?client-id=" client-id) + :id name + :x-model x-model + :value value + :content-fn (fn [value] + (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) + client-id)))})]) + +(defn- transaction-rule-account-row* + [account client-id client-locations] + (com/data-grid-row + (-> {:x-data (hx/json {:accountId (or (:db/id (fc/field-value (:transaction-rule-account/account account))) + (fc/field-value (:transaction-rule-account/account account))) + :location (fc/field-value (:transaction-rule-account/location account)) + :show (boolean (not (fc/field-value (:new? account))))}) + :data-key "show" + :x-ref "p"} + hx/alpine-mount-then-appear) + (let [account-name (fc/field-name (:transaction-rule-account/account account))] + (list + + (fc/with-field :db/id + (com/hidden {:name (fc/field-name) + :value (fc/field-value)})) + (fc/with-field :transaction-rule-account/account + (com/data-grid-cell + {} + (com/validated-field + {:errors (fc/field-errors)} + [:div {:hx-trigger "changed" + :hx-target "next div" + :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', value: event.detail.accountId || ''}" account-name) + :hx-get (str (bidi/path-for ssr-routes/only-routes ::route/account-typeahead)) + :x-init "$watch('clientId', cid => $dispatch('changed', $data));"}] + (account-typeahead* {:value (fc/field-value) + :client-id client-id + :name (fc/field-name) + :x-model "accountId"})))) + (fc/with-field :transaction-rule-account/location + (com/data-grid-cell + {} + (com/validated-field + {:errors (fc/field-errors) + :x-data (hx/json {:location (fc/field-value)})} + [:div {:hx-trigger "changed" + :hx-target "next *" + :hx-swap "outerHTML" + :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || '', value: event.detail.location || ''}" (fc/field-name)) + :hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select) + :x-init "$watch('clientId', cid => $dispatch('changed', $data)); $watch('accountId', cid => $dispatch('changed', $data) )"}] + (location-select* {:name (fc/field-name) + :account-location (:account/location (cond->> (:transaction-rule-account/account @account) + (nat-int? (:transaction-rule-account/account @account)) (dc/pull (dc/db conn) + '[:account/location]))) + :client-locations client-locations + :x-model "location" + :value (fc/field-value)})))) + (fc/with-field :transaction-rule-account/percentage + (com/data-grid-cell + {} + (com/validated-field + {:errors (fc/field-errors)} + (com/money-input {:name (fc/field-name) + :class "w-16" + :value (some-> (fc/field-value) + (* 100) + (long))})))))) + (com/data-grid-cell {:class "align-top"} + (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) + +(defrecord AccountsStep [linear-wizard] + mm/ModalWizardStep + (step-name [_] + "Details") + (step-key [_] + :accounts) + + (edit-path [_ _] + []) + + (step-schema [_] + (mut/select-keys (mm/form-schema linear-wizard) #{})) + + (render-step [this {{:keys [snapshot]} :multi-form-state :as request}] + (mm/default-render-step + linear-wizard this + :head [:div.p-2 "Invoice accounts "] + :body (mm/default-step-body + {} + [:div {} + (:client/name (:invoice/client snapshot)) + (fc/with-field :invoice/expense-accounts + (com/validated-field + {:errors (fc/field-errors)} + (com/data-grid {:headers [(com/data-grid-header {} "Account") + (com/data-grid-header {:class "w-32"} "Location") + (com/data-grid-header {:class "w-16"} "%") + (com/data-grid-header {:class "w-16"})]} + (fc/cursor-map #(transaction-rule-account-row* % + (some->> snapshot :invoice/client :db/id) + (some->> snapshot :invoice/client :client/locations))) + (com/data-grid-new-row {:colspan 4 + :hx-get (bidi/path-for ssr-routes/only-routes + ::route/new-wizard-new-account) + :index (count (fc/field-value)) + :tr-params (hx/bind-alpine-vals {} {"client-id" "clientId"})} + "New account")))) ]) + :footer + (mm/default-step-footer linear-wizard this :validation-route ::route/new-wizard-navigate) + :validation-route ::route/new-wizard-navigate))) + + + +(defrecord NewWizard2 [_ current-step] + mm/LinearModalWizard + (hydrate-from-request + [this request] + this) + (navigate [this step-key] + (assoc this :current-step step-key)) + (get-current-step [this] + (if current-step + (mm/get-step this current-step) + (mm/get-step this :basic-details))) + (render-wizard [this {:keys [multi-form-state] :as request}] + (alog/peek ::MFS multi-form-state) + (mm/default-render-wizard + this request + :form-params + (-> mm/default-form-props + (assoc :hx-post + (str (bidi/path-for ssr-routes/only-routes ::route/pay-submit)))))) + (steps [_] + [:basic-details + :accounts]) + + (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) + :accounts (->AccountsStep this)} + step-key))) + (form-schema [_] new-form-schema) + (submit [this {:keys [multi-form-state request-method identity] :as request}] + #_(let [snapshot (mc/decode + payment-form-schema + (:snapshot multi-form-state) + mt/strip-extra-keys-transformer) + _ (exception->4xx + #(if (= :handwrite-check (:method snapshot)) + (when (or (not (some? (:check-number snapshot))) + (= "" (:check-number snapshot))) + (throw (Exception. "Check number is required"))) + true)) + result (exception->4xx + #(if (= :handwrite-check (:method snapshot)) + (add-handwritten-check request this snapshot) + (print-checks-internal (map (fn [i] {:invoice-id (:invoice-id i) + :amount (:amount i)}) + (:invoices snapshot)) + (:client snapshot) + (:bank-account snapshot) + (cond (= :print-check (:method snapshot)) + :payment-type/check + (= :debit (:method snapshot)) + :payment-type/debit + (= :cash (:method snapshot)) + :payment-type/cash + (= :credit (:method snapshot)) + :payment-type/credit + :else :payment-type/debit) + identity)))] + (modal-response + (com/modal {} + (com/modal-card-advanced + {:class "transition duration-300 ease-in-out 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"} + (com/modal-body {} + [:div.flex.flex-col.mt-4.space-y-4.items-center + [:div.w-24.h-24.bg-green-50.rounded-full.p-4.text-green-300.animate-gg + svg/thumbs-up] + (when-not (:pdf-url result) + [:div "That's a wrap. Your payment is complete."]) + (when (:pdf-url result) + [:div "Your checks are ready. Click " + (com/link {:href (:pdf-url result) :target "_new"} "here") + " to download and print."]) + (when (:pdf-url result) + [:div.text-xs.italic [:em "Remember to turn off all scaling and margins."]])]))) + :headers {"hx-trigger" "invalidated"})))) + +(def new-wizard (->NewWizard2 nil nil)) (defn wrap-status-from-source [handler] @@ -1091,6 +1416,55 @@ (= ::route/all-page matched-current-page-route) (assoc-in [:route-params :status] nil))] (handler request)))) +(defn payable-ids [ids] + (->> (dc/q '[:find ?i + :in $ [?i ...] + :where [?i :invoice/status :invoice-status/unpaid] + [?i :invoice/client ?c] + [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu] + [?i :invoice/date ?d] + [(>= ?d ?lu)]] + (dc/db conn) + ids) + (map first))) + +(defn initial-pay-wizard-state [request] + (exception->notification + #(let [selected-ids (selected->ids request (:query-params request)) + selected-ids (payable-ids selected-ids) + _ (when (= 0 (count selected-ids)) + (throw (ex-info "No selected invoices are applicable for payment" {:type :notification}))) + + has-warning? (and (:selected (:query-params request)) + (not= (count selected-ids) + (count (:selected (:query-params request))))) + invoices (->> (dc/q '[:find (pull ?i [{:invoice/vendor [:vendor/name :db/id] + :invoice/client [:db/id]} + :invoice/outstanding-balance + :invoice/invoice-number + :db/id]) + :in $ [?i ...]] + (dc/db conn) + selected-ids) + (map first) + (sort-by (juxt (comp :invoice/vendor :vendor/name) + :invoice/invoice-number)))] + (mm/->MultiStepFormState {:invoices (mapv (fn [i] {:invoice-id (:db/id i) + :amount (:invoice/outstanding-balance i)}) + invoices) + :mode :simple + :client (-> invoices first :invoice/client :db/id) + :has-warning? (boolean has-warning?) + :handwritten-date (time/now)} + [] + {:mode :simple + :has-warning? (boolean has-warning?)})))) + +(defn initial-new-wizard-state [request] + (mm/->MultiStepFormState {:TODO nil} + [] + {})) + (def key->handler (apply-middleware-to-all-handlers {::route/all-page (-> (helper/page-route grid-page) @@ -1114,46 +1488,26 @@ ::route/pay-wizard (-> mm/open-wizard-handler (mm/wrap-wizard pay-wizard) - (mm/wrap-init-multi-form-state (fn [request] - (exception->notification - #(let [selected-ids (selected->ids request (:query-params request)) - selected-ids (->> (dc/q '[:find ?i - :in $ [?i ...] - :where [?i :invoice/status :invoice-status/unpaid] - [?i :invoice/client ?c] - [(get-else $ ?c :client/locked-until #inst "2000-01-01") ?lu] - [?i :invoice/date ?d] - [(>= ?d ?lu)]] - (dc/db conn) - selected-ids) - (map first)) - _ (when (= 0 (count selected-ids)) - (throw (ex-info "No selected invoices are applicable for payment" {:type :notification}))) + (mm/wrap-init-multi-form-state initial-pay-wizard-state)) + ::route/new-wizard (-> mm/open-wizard-handler - has-warning? (and (:selected (:query-params request)) - (not= (count selected-ids) - (count (:selected (:query-params request))))) - invoices (->> (dc/q '[:find (pull ?i [{:invoice/vendor [:vendor/name :db/id] - :invoice/client [:db/id]} - :invoice/outstanding-balance - :invoice/invoice-number - :db/id]) - :in $ [?i ...]] - (dc/db conn) - selected-ids) - (map first) - (sort-by (juxt (comp :invoice/vendor :vendor/name) - :invoice/invoice-number)))] - (mm/->MultiStepFormState {:invoices (mapv (fn [i] {:invoice-id (:db/id i) - :amount (:invoice/outstanding-balance i)}) - invoices) - :mode :simple - :client (-> invoices first :invoice/client :db/id) - :has-warning? (boolean has-warning?) - :handwritten-date (time/now)} - [] - {:mode :simple - :has-warning? (boolean has-warning?)})))))) + (mm/wrap-wizard new-wizard) + (mm/wrap-init-multi-form-state initial-new-wizard-state)) + ::route/new-wizard-navigate (-> mm/next-handler + (mm/wrap-wizard new-wizard) + (mm/wrap-decode-multi-form-state)) + ::route/new-wizard-new-account (-> + (add-new-entity-handler [:step-params :invoice/expense-accounts] + (fn render [cursor request] + (transaction-rule-account-row* + cursor + (:client-id (:query-params request)) + (some->> (:client-id (:query-params request)) (pull-attr (dc/db conn) :client/locations)))) + (fn build-new-row [base _] + (assoc base :transaction-rule-account/location "Shared"))) + (wrap-schema-enforce :query-schema [:map + [:client-id {:optional true} + [:maybe entity-id]]])) ::route/pay-submit (-> mm/submit-handler (mm/wrap-wizard pay-wizard) diff --git a/src/clj/auto_ap/ssr/utils.clj b/src/clj/auto_ap/ssr/utils.clj index 0ce6edab..dc2ac50c 100644 --- a/src/clj/auto_ap/ssr/utils.clj +++ b/src/clj/auto_ap/ssr/utils.clj @@ -312,7 +312,7 @@ (def dissoc-nil-transformer - (let [e {:map {:compile (fn [schema _] + (let [e {:map {:compile (fn [schema _] (fn [data] (if (map? data) (filter-vals diff --git a/src/cljc/auto_ap/routes/invoice.cljc b/src/cljc/auto_ap/routes/invoice.cljc index 6a0e400f..da2b5a71 100644 --- a/src/cljc/auto_ap/routes/invoice.cljc +++ b/src/cljc/auto_ap/routes/invoice.cljc @@ -2,7 +2,10 @@ (def routes {"" {:get ::all-page "/unpaid" ::unpaid-page "/paid" ::paid-page - "/voided" ::voided-page } + "/voided" ::voided-page} + "/new" {:get ::new-wizard + "/navigate" ::new-wizard-navigate + "/account/new" ::new-wizard-new-account} "/pay-button" ::pay-button "/pay" {:get ::pay-wizard "/navigate" ::pay-wizard-navigate