Files
integreat/src/cljs/auto_ap/views/pages/transactions/form.cljs
Bryce Covert 8f47b4bb2b minor bugfix.
2022-05-08 16:16:38 -07:00

467 lines
24 KiB
Clojure

(ns auto-ap.views.pages.transactions.form
(:require
[auto-ap.forms :as forms]
[auto-ap.status :as status]
[auto-ap.subs :as subs]
[auto-ap.views.components.button-radio :refer [button-radio]]
[auto-ap.views.components.expense-accounts-field
:as expense-accounts-field
:refer [expense-accounts-field]]
[auto-ap.views.components.layouts :as layouts]
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
[auto-ap.views.components.typeahead.vendor
:refer [search-backed-typeahead]]
[auto-ap.views.pages.transactions.common :refer [transaction-read]]
[auto-ap.views.utils
:refer [->$ date->str dispatch-event pretty with-user]]
[clojure.string :as str]
[re-frame.core :as re-frame]
[react :as react]
[reagent.core :as r]
[vimsical.re-frame.fx.track :as track]
[auto-ap.events :as events]))
;; SUBS
(re-frame/reg-sub
::submit-query
:<- [::forms/form ::form]
(fn [{{:keys [id vendor accounts approval-status forecast-match]} :data}]
{:venia/operation {:operation/type :mutation
:operation/name "EditTransaction"}
:venia/queries [{:query/data
[:edit-transaction
{:transaction {:id id
:vendor-id (:id vendor)
:approval-status approval-status
:forecast-match (:id forecast-match)
:accounts (map
(fn [{:keys [id account amount location]}]
{:id (when-not (str/starts-with? id "new-")
id)
:account-id (:id account)
:location location
:amount amount})
accounts)}}
transaction-read]}]}))
(re-frame/reg-sub
::can-submit
:<- [::forms/form ::form]
(fn [{:keys [status]} _]
(not= :loading status)))
;; EVENTS
(re-frame/reg-event-db
::editing
(fn [db [_ which potential-payment-matches potential-autopay-invoices-matches potential-unpaid-invoices-matches potential-transaction-rule-matches]]
(let [locations @(re-frame/subscribe [::subs/locations-for-client (:id (:client which))])]
(forms/start-form db ::form
(-> which
(select-keys [:vendor :amount :payment :client :description-original
:yodlee-merchant :id :potential-payment-matches
:forecast-match :date
:location :accounts :approval-status
:matched-rule])
(update :date #(date->str % pretty))
(assoc :original-status (:approval-status which))
(assoc :potential-payment-matches potential-payment-matches)
(assoc :potential-transaction-rule-matches potential-transaction-rule-matches)
(assoc :potential-autopay-invoices-matches potential-autopay-invoices-matches)
(assoc :potential-unpaid-invoices-matches potential-unpaid-invoices-matches)
(update :accounts expense-accounts-field/from-graphql (:amount which) locations))))))
(re-frame/reg-event-db
::changed
(forms/change-handler ::form
(fn [data field value]
(let [locations @(re-frame/subscribe [::subs/locations-for-client (:id (:client data))])]
(if (and (= [:vendor-preferences] field)
value
(expense-accounts-field/can-replace-with-default? (:accounts data)))
[[:accounts] (expense-accounts-field/default-account (:accounts data)
(:default-account (:vendor-preferences data))
(:amount data)
locations)]
[])))))
(re-frame/reg-event-fx
::saving
[with-user (forms/triggers-loading ::form) (forms/in-form ::form)]
(fn [{:keys [user]} [_]]
{:graphql
{:token user
:query-obj @(re-frame/subscribe [::submit-query])
:on-success (fn [result]
[::edited (:edit-transaction result)])
:on-error [::forms/save-error ::form]}}))
(re-frame/reg-event-fx
::matching-payment
[with-user (forms/triggers-loading ::form) (forms/in-form ::form)]
(fn [{{{:keys [id]} :data} :db user :user} [_ payment-id]]
{:graphql
{:token user
:query-obj {:venia/operation {:operation/type :mutation
:operation/name "MatchTransaction"}
:venia/queries [{:query/data [:match-transaction
{:transaction_id id
:payment-id payment-id}
transaction-read]}]}
:on-success (fn [result]
[::edited (:match-transaction result)])
:on-error [::forms/save-error ::form]}}))
(re-frame/reg-event-fx
::matching-autopay-invoices
[with-user (forms/triggers-loading ::form) (forms/in-form ::form)]
(fn [{{{:keys [id]} :data} :db user :user} [_ invoice-ids]]
{:graphql
{:token user
:query-obj {:venia/operation {:operation/type :mutation
:operation/name "MatchTransactionAutopayInvoices"}
:venia/queries [{:query/data [:match-transaction-autopay-invoices
{:transaction_id id
:autopay-invoice-ids invoice-ids}
transaction-read]}]}
:owns-state {:multi ::matching
:which [:autopay-invoices invoice-ids]}
:on-success (fn [result]
[::edited (:match-transaction-autopay-invoices result)])}}))
(re-frame/reg-event-fx
::matching-unpaid-invoices
[with-user (forms/triggers-loading ::form) (forms/in-form ::form)]
(fn [{{{:keys [id]} :data} :db user :user} [_ invoice-ids]]
{:graphql
{:token user
:query-obj {:venia/operation {:operation/type :mutation
:operation/name "MatchTransactionUnpaidInvoices"}
:venia/queries [{:query/data [:match-transaction-unpaid-invoices
{:transaction_id id
:unpaid-invoice-ids invoice-ids}
transaction-read]}]}
:owns-state {:multi ::matching
:which [:unpaid-invoices invoice-ids]}
:on-success (fn [result]
[::edited (:match-transaction-unpaid-invoices result)])}}))
(re-frame/reg-event-fx
::matching-rule
[with-user (forms/triggers-loading ::form) (forms/in-form ::form)]
(fn [{{{:keys [id]} :data} :db user :user} [_ transaction-rule-id]]
{:graphql
{:token user
:query-obj {:venia/operation {:operation/type :mutation
:operation/name "MatchTransactionRules"}
:venia/queries [{:query/data [:match-transaction-rules
{:transaction-ids [id]
:transaction-rule-id transaction-rule-id}
transaction-read]}]}
:on-success (fn [result]
[::edited (first (:match-transaction-rules result))])
:on-error [::forms/save-error ::form]}}))
(re-frame/reg-event-fx
::unlink
[with-user (forms/triggers-loading ::form) (forms/in-form ::form)]
(fn [{{{:keys [id]} :data} :db user :user} [_]]
{:graphql
{:token user
:query-obj {:venia/operation {:operation/type :mutation
:operation/name "UnlinkTransaction"}
:venia/queries [{:query/data [:unlink-transaction
{:transaction-id id}
transaction-read]}]}
:on-success (fn [result]
[::edited (:unlink-transaction result)])
:on-error [::forms/save-error ::form]}}))
(re-frame/reg-event-fx
::edited
[(forms/triggers-stop ::form)]
(fn [_ [_ _]]
{}))
(re-frame/reg-event-fx
::changed-vendor
[(forms/in-form ::form)]
(fn [{{{:keys [client]} :data} :db} [_ vendor]]
(when (and (:id client) (:id vendor))
{:dispatch [::events/vendor-preferences-requested {:client-id (:id client)
:vendor-id (:id vendor)
:on-success [::changed [:vendor-preferences]]
:on-failure [:hello]}]})))
(re-frame/reg-event-fx
::mounted
(fn []
{::track/register {:id ::vendor-change
:subscription [::forms/field ::form [:vendor]]
:event-fn (fn [v]
[::changed-vendor v])}}))
(re-frame/reg-event-fx
::unmounted
(fn []
{::track/dispose {:id ::vendor-change}}))
;; VIEWS
(def transaction-form (forms/vertical-form {:can-submit [::can-submit]
:change-event [::changed]
:submit-event [::saving ]
:id ::form}))
(defn potential-transaction-rule-matches-box [{:keys [potential-transaction-rule-matches]}]
(let [states @(re-frame/subscribe [::status/multi ::matching])]
[:div.box
[:div.columns
[:div.column
[:h1.subtitle.is-5 "Matching Rules:"]]]
[:table.table.compact.is-borderless
(for [{:keys [note id]} potential-transaction-rule-matches]
^{:key id}
[:tr
[:td.no-border note]
[:td.no-border
[:a.button.is-primary.is-small {:on-click (dispatch-event [::matching-rule id])
:class (status/class-for (get states [:transaction-rule id]))
:disabled (status/disabled-if-any states)}
"Use this rule"]]])]]))
(defn potential-payment-matches-box [{:keys [potential-payment-matches]}]
(let [states @(re-frame/subscribe [::status/multi ::matching])]
[:div
[:h1.subtitle.is-5 "Potentially matching payments:"]
[:table.table.compact.is-borderless
(list
(for [{:keys [memo check-number vendor id]} potential-payment-matches]
[:tr
[:td.no-border (:name vendor)]
[:td.no-border (when check-number (str "Check " check-number " ")) memo]
[:td.no-border
[:a.button.is-primary.is-small {:on-click (dispatch-event [::matching-payment id])
:class (status/class-for (get states [:payment id]))
:disabled (status/disabled-if-any states)}
"Match"]]]))]]))
(defn potential-autopay-invoices-matches-box [{:keys [potential-autopay-invoices-matches]}]
(let [states @(re-frame/subscribe [::status/multi ::matching])]
[:div
[:div.notification.is-light.is-info "This transaction may match the following autopay invoice(s)."]
[:table.table.grid.is-fullwidth
(list
(for [invoices potential-autopay-invoices-matches]
^{:key (str invoices)}
[:tr
[:td {:style {:width "30%"}} (:name (:vendor (first invoices)))]
[:td {:style {:overflow "visible" :width "60%"}} [:span.has-tooltip-arrow.has-tooltip-bottom {:data-tooltip (str/join "\n"
(for [i invoices]
(str (:invoice-number i) " (" (->$ (:total i)) ")"))
)}
(count invoices) " invoices" (if (> (count invoices) 1)
(str " from " (date->str (:date (first invoices))) " - " (date->str (:date (last invoices))))
(str " on " (date->str (:date (first invoices)))))]]
[:td {:style {:width "6em"}}
[:a.button.is-primary.is-small {:on-click (dispatch-event [::matching-autopay-invoices (map :id invoices)])
:class (status/class-for (get states [:autopay-invoices (map :id invoices)]))
:disabled (status/disabled-if-any states)}
"Match"]]]))]]))
(defn potential-unpaid-invoices-matches-box [{:keys [potential-unpaid-invoices-matches]}]
(let [states @(re-frame/subscribe [::status/multi ::matching])]
[:div
[:div.notification.is-light.is-info "This transaction may match the following unpaid invoice(s)."]
[:table.table.grid.is-fullwidth
(list
(for [invoices potential-unpaid-invoices-matches]
^{:key (str invoices)}
[:tr
[:td {:style {:width "30%"}} (:name (:vendor (first invoices)))]
[:td {:style {:overflow "visible" :width "60%"}} [:span.has-tooltip-arrow.has-tooltip-bottom {:data-tooltip (str/join "\n"
(for [i invoices]
(str (:invoice-number i) " (" (->$ (:total i)) ")"))
)}
(count invoices) " invoices" (if (> (count invoices) 1)
(str " from " (date->str (:date (first invoices))) " - " (date->str (:date (last invoices))))
(str " on " (date->str (:date (first invoices)))))]]
[:td {:style {:width "6em"}}
[:a.button.is-primary.is-small {:on-click (dispatch-event [::matching-unpaid-invoices (map :id invoices)])
:class (status/class-for (get states [:unpaid-invoices (map :id invoices)]))
:disabled (status/disabled-if-any states)}
"Match"]]]))]]))
(defonce ^js/React.Context current-tab-context ( react/createContext "default"))
(def ^js/React.Provider CurrentTabProvider (. current-tab-context -Provider))
#_(println "Provider is" Provider)
(def ^js/React.Consumer CurrentTabConsumer (. current-tab-context -Consumer))
(defn tabs [props & _]
(let [current-tab (r/atom (:default-tab props))]
(fn [_ & _]
(let [current-tab-v @current-tab]
(r/create-element CurrentTabProvider #js {:value #js {:current-tab current-tab-v
:on-tab-clicked (fn [new]
(reset! current-tab new))}}
(r/as-element
[:<>
[:div.tabs
(into [:ul]
(r/children (r/current-component)))]
(into [:div]
(->> (r/children (r/current-component))
(filter identity)
(filter #(= (:key (second %)) current-tab-v) )
first
(drop 2)))]))))))
(defn tab [props & _]
[:> CurrentTabConsumer {}
(fn [consume]
(let [{:strs [current-tab on-tab-clicked]} (js->clj consume)]
(r/as-element
[:li (when (= (:key props)
current-tab)
{:class "is-active"})
[:a {:on-click (fn [] (on-tab-clicked (:key props)))} (:title props)]])))])
(defn form-content [_]
[layouts/side-bar {:on-close (dispatch-event [::forms/form-closing ::form])}
(let [{:keys [data] } @(re-frame/subscribe [::forms/form ::form])
locations @(re-frame/subscribe [::subs/locations-for-client (:id (:client data))])
{:keys [form-inline field error-notification submit-button ]} transaction-form
is-admin? @(re-frame/subscribe [::subs/is-admin?])
is-power-user? @(re-frame/subscribe [::subs/is-power-user?])
should-disable-for-client? (and (not (or is-admin? is-power-user?))
(not= :requires-feedback (:original-status data)))
is-already-matched? (:payment data)]
(with-meta
(form-inline {:title "Transaction"}
[:<>
(when (and @(re-frame/subscribe [::subs/is-admin?])
(get-in data [:yodlee-merchant]))
[:div.control
[:p.help "Merchant"]
[:input.input {:type "text"
:disabled true
:value (str (get-in data [:yodlee-merchant :name])
" - "
(get-in data [:yodlee-merchant :yodlee-id]))}]])
(when is-admin?
(field "Matched Rule"
[:input.input {:type "text"
:field [:matched-rule :note]
:disabled "disabled"}]))
(field "Amount"
[:input.input {:type "text"
:field [:amount]
:disabled "disabled"}])
(field "Description"
[:input.input {:type "text"
:field [:description-original]
:disabled "disabled"}])
(field "Date"
[:input.input {:type "text"
:field [:date]
:disabled "disabled"}])
(when (and (:payment data)
(or is-admin? is-power-user?))
[:p.notification.is-info.is-light>div.level>div.level-left
[:div.level-item "This transaction is linked to a payment "]
[:div.level-item [:button.button.is-warning {:on-click (dispatch-event [::unlink])} "Unlink"]]])
[tabs {:default-tab :details}
(when
(and (seq (:potential-transaction-rule-matches data))
(not (:matched-rule data))
is-admin?)
[tab {:title "Transaction Rule" :key :transaction-rule}
[potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data)}]])
(when
(and (seq (:potential-autopay-invoices-matches data))
(not is-already-matched?)
(or is-admin? is-power-user?))
[tab {:title "Autopay Invoices" :key :autopay-invoices}
[potential-autopay-invoices-matches-box {:potential-autopay-invoices-matches (:potential-autopay-invoices-matches data)}]])
(when
(and (seq (:potential-unpaid-invoices-matches data))
(not is-already-matched?)
(or is-admin? is-power-user?))
[tab {:title "Unpaid Invoices" :key :unpaid-invoices}
[potential-unpaid-invoices-matches-box {:potential-unpaid-invoices-matches (:potential-unpaid-invoices-matches data)}]])
(when
(and (seq (:potential-payment-matches data))
(not is-already-matched?)
(or is-admin? is-power-user?))
[tab {:title "Payment" :key :payment}
[potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data)}]])
[tab {:title "Details" :key :details}
[:div
(field "Vendor"
[search-backed-typeahead {:search-query (fn [i]
[:search_vendor
{:query i}
[:name :id]])
:type "typeahead-v3"
:auto-focus true
:field [:vendor]
:disabled (or (boolean (:payment data))
should-disable-for-client?)}])
(with-meta
(field nil
[expense-accounts-field
{:type "expense-accounts"
:field [:accounts]
:max (Math/abs (js/parseFloat (:amount data)))
:descriptor "credit account"
:client (:client data)
:disabled (or (boolean (:payment data))
should-disable-for-client?)
:locations locations}])
{:key (str (:id (:vendor data)))})
(field "Approval Status"
[button-radio
{:type "button-radio"
:field [:approval-status]
:options [[:unapproved "Unapproved"]
[:requires-feedback "Client Review"]
[:approved "Approved"]
[:excluded "Excluded from Ledger"]]
:disabled should-disable-for-client?}])
(field "Forecasted-transaction"
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))])
:entity->text :identifier
:type "typeahead-v3"
:field [:forecast-match]}])
(error-notification)
(when-not should-disable-for-client?
(submit-button "Save"))]]]])
{:key (:id data)}))])
(defn form [_]
(r/create-class
{:display-name "transaction-form"
:component-did-mount #(re-frame/dispatch [::mounted])
:component-will-unmount #(re-frame/dispatch [::unmounted])
:reagent-render (fn [p]
[form-content p])}))