466 lines
24 KiB
Clojure
466 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"
|
|
: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])}))
|