(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])}))