Files
integreat/src/cljs/auto_ap/views/pages/invoices/form.cljs

460 lines
23 KiB
Clojure

(ns auto-ap.views.pages.invoices.form
(:require
[auto-ap.events :as events]
[auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.schema :as schema]
[auto-ap.status :as status]
[auto-ap.subs :as subs]
[auto-ap.time-utils :refer [next-dom]]
[auto-ap.utils :refer [dollars=]]
[auto-ap.views.components :as com]
[auto-ap.views.components.dropdown :refer [drop-down]]
[auto-ap.views.components.expense-accounts-field
:as eaf
:refer [expense-accounts-field-v2 recalculate-amounts]]
[auto-ap.views.components.layouts :as layouts]
[auto-ap.views.components.level :refer [left-stack]]
[auto-ap.views.components.modal :as modal]
[auto-ap.views.pages.invoices.common :refer [invoice-read]]
[auto-ap.views.utils :refer [date-picker dispatch-event with-user date->str]]
[cljs-time.core :as c]
[clojure.string :as str]
[malli.core :as m]
[re-frame.core :as re-frame]
[reagent.core :as r]
[vimsical.re-frame.cofx.inject :as inject]
[vimsical.re-frame.fx.track :as track]))
(def schema (m/schema
[:map
[:client schema/reference]
[:vendor schema/reference]
[:date schema/date]
[:due {:optional true} [:maybe schema/date]]
[:scheduled-payment {:optional true} [:maybe schema/date]]
[:invoice-number schema/not-empty-string]
[:total schema/money]
[:expense-accounts eaf/schema]]))
;; SUBS
(re-frame/reg-sub
::can-submit
:<- [::forms/form ::form]
(fn [{:keys [data]} _]
(let [min-total (if (= (:total (:original data)) (:outstanding-balance (:original data)))
nil
(- (:total (:original data)) (:outstanding-balance (:original data))))
account-total (reduce + 0 (map :amount (:expense-accounts data)))]
(and
(or (not min-total) (>= (:total data) min-total))
(or (not (:id data))
(dollars= (Math/abs (:total data)) (Math/abs account-total)))))))
(re-frame/reg-sub
::create-query
:<- [::forms/form ::form]
(fn [{{:keys [invoice-number date due scheduled-payment location total expense-accounts vendor client]} :data}]
{:venia/operation {:operation/type :mutation
:operation/name "AddInvoice"}
:venia/queries [{:query/data [:add-invoice
{:invoice {
:vendor-id (:id vendor)
:client-id (:id client)
:date date
:scheduled-payment scheduled-payment
:due due
:invoice-number (some-> invoice-number str/trim)
:location location
:total total
:expense-accounts (map (fn [ea]
{:id (when-not (str/starts-with? (:id ea) "new-")
(:id ea))
:account_id (:id (:account ea))
:location (:location ea)
:amount (:amount ea)})
expense-accounts)}}
invoice-read]}]}))
(re-frame/reg-sub
::edit-query
:<- [::forms/form ::form]
(fn [{{:keys [id invoice-number date due scheduled-payment total expense-accounts]} :data}]
{:venia/operation {:operation/type :mutation
:operation/name "EditInvoice"}
:venia/queries [{:query/data [:edit-invoice
{:invoice {:id id
:invoice-number invoice-number
:date date
:scheduled-payment scheduled-payment
:due due
:total total
:expense-accounts (map (fn [ea]
{:id (when-not (str/starts-with? (:id ea) "new-")
(:id ea))
:account_id (:id (:account ea))
:location (:location ea)
:amount (:amount ea)})
expense-accounts)}}
invoice-read]}]}))
;; EVENTS
(re-frame/reg-event-fx
::updated
[
(re-frame/inject-cofx ::inject/sub (fn [[_ _ _ client]]
[::subs/locations-for-client (:id client)]))]
(fn [{:keys [db] ::subs/keys [locations-for-client]} [_ _ command client]]
(when (= :create command)
{:db
(-> db
(forms/stop-form ::form )
(forms/start-form ::form {:client client
:status :unpaid
:date (c/now)
:expense-accounts
(eaf/from-graphql []
0.0
locations-for-client)}))})))
(re-frame/reg-event-fx
::adding
[(re-frame/inject-cofx ::inject/sub (fn [[_ which _]]
[::subs/locations-for-client (:id (:client which))]))]
(fn [{:keys [db] ::subs/keys [locations-for-client]} [_ new]]
{:db
(-> db (forms/start-form ::form (assoc new :expense-accounts
(eaf/from-graphql (:expense-accounts new)
0.0
locations-for-client))))}))
(re-frame/reg-event-fx
::editing
[(re-frame/inject-cofx ::inject/sub (fn [[_ which _]]
[::subs/locations-for-client (:id (:client which))]))]
(fn [{:keys [db] ::subs/keys [locations-for-client]} [_ which vendor-preferences]]
(let [edit-invoice which]
{:db
(-> db
(forms/start-form ::form {:id (:id edit-invoice)
:payments (:payments edit-invoice)
:status (:status edit-invoice)
:date (:date edit-invoice)
:due (:due edit-invoice)
:vendor-preferences vendor-preferences
:scheduled-payment (:scheduled-payment edit-invoice)
:invoice-number (:invoice-number edit-invoice)
:total (cond-> (:total edit-invoice)
(not (str/blank? (:total edit-invoice))) (js/parseFloat ))
:original which
:vendor (:vendor edit-invoice)
:client (:client edit-invoice)
:expense-accounts (eaf/from-graphql (:expense-accounts which)
(:total which)
locations-for-client)}))})))
(re-frame/reg-event-db
::changed
(forms/change-handler ::form
(fn [data field value]
(cond
(= [:vendor-preferences] field)
(cond-> []
(eaf/can-replace-with-default? (:expense-accounts data))
(into [[:expense-accounts] (eaf/default-account (:expense-accounts data)
(:default-account value)
(:total data)
(:locations (:client data)))])
(:automatically-paid-when-due value)
(into [[:scheduled-payment] (:due data)
[:schedule-when-due] true])
(:schedule-payment-dom value)
(into [[:scheduled-payment] (next-dom (:date data) (:schedule-payment-dom value))]))
(= [:total] field)
[[:expense-accounts] (recalculate-amounts (:expense-accounts data) value)]
(and (= [:date] field)
(:schedule-payment-dom (:vendor-preferences data)))
[[:scheduled-payment] (next-dom value (:schedule-payment-dom (:vendor-preferences data))) ]
(and (= [:schedule-when-due] field) value)
[[:scheduled-payment] (:due data)]
(and (= [:due] field) (:schedule-when-due data))
[[:scheduled-payment] value]
:else
[]))))
(re-frame/reg-event-db
::maybe-change-client
[ (forms/in-form ::form) ]
(fn [{:keys [data] :as f} [_ c]]
(if (and (not (:id data))
(:id c))
(assoc-in f [:data :client] c)
f)))
(re-frame/reg-event-fx
::add-and-print
[with-user (forms/in-form ::form)]
(fn [{:keys [user]
{{:keys [invoice-number date location total expense-accounts scheduled-payment vendor client]
:as data} :data} :db} [_ bank-account-id type]]
(if (not (m/validate schema data))
{:dispatch-n [[::status/error ::form [{:message "Please fix the errors and try again."}]]
[::forms/attempted-submit ::form]]}
{:graphql
{:token user
:owns-state {:single ::form}
:query-obj {:venia/operation {:operation/type :mutation
:operation/name "AddAndPrintInvoice"}
:venia/queries [{:query/data [:add-and-print-invoice
{:invoice {:date date
:vendor-id (:id vendor)
:client-id (:id client)
:scheduled-payment scheduled-payment
:invoice-number invoice-number
:location location
:total total
:expense-accounts (map (fn [ea]
{:id (when-not (str/starts-with? (:id ea) "new-")
(:id ea))
:account_id (:id (:account ea))
:location (:location ea)
:amount (:amount ea)})
expense-accounts)}
:bank-account-id bank-account-id
:type type}
[:pdf-url [:invoices invoice-read]]]}]}
:on-success [::added-and-printed]
:on-error [::forms/save-error ::form]}})
))
(re-frame/reg-event-fx
::saving
[with-user (forms/in-form ::form) (re-frame/inject-cofx ::inject/sub [::edit-query]) (re-frame/inject-cofx ::inject/sub [::create-query])]
(fn [{:keys [user] {:keys [data]} :db ::keys [edit-query create-query]} _]
(if (not (m/validate schema data))
{:dispatch-n [[::status/error ::form [{:message "Please fix the errors and try again."}]]
[::forms/attempted-submit ::form]]}
{:graphql
{:token user
:owns-state {:single ::form}
:query-obj (if (:id data)
edit-query
create-query)
:on-success (fn [result]
[::updated
(assoc (if (:id data)
(:edit-invoice result)
(:add-invoice result))
:class "live-added")
(if (:id data)
:edit
:create)
(:client data)])
:on-error [::forms/save-error ::form]}})))
(re-frame/reg-event-fx
::save-requested
[with-user (forms/in-form ::form)]
(fn [{:keys [db]} [_ fwd-event]]
(if (and (:scheduled-payment (:data db))
(not (:vendor-autopay? (:vendor-preferences (:data db)))))
{:dispatch
[::modal/modal-requested {:title "Scheduled payment date"
:body [:div "This vendor isn't set up to be automatically paid. On "
(date->str (:scheduled-payment (:data db)))
" the invoice will be marked as paid, but no payment will be made to the vendor. "
"Are you sure you want to continue?"]
:confirm {:value "Save"
:class "is-warning"
:on-click #(do (re-frame/dispatch [::modal/modal-closed])
(re-frame/dispatch fwd-event))}
:cancel? true}]}
{:dispatch fwd-event})))
(re-frame/reg-event-fx
::added-and-printed
(fn [_ [_ result]]
(let [invoice (first (:invoices (:add-and-print-invoice result)))]
{:dispatch-n [[::updated (assoc invoice :class "live-added") :create]
[::checks-printed [invoice] (:pdf-url (:add-and-print-invoice result))]]})))
(re-frame/reg-event-db
::checks-printed
(fn [db [_]]
db))
(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]}]})))
;; VIEWS
(re-frame/reg-event-fx
::mounted
(fn []
{::track/register [{:id ::client
:subscription [::subs/client]
:event-fn (fn [c]
[::maybe-change-client c])}
{: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 ::client}
{:id ::vendor-change}]}))
(defn form-content []
[layouts/side-bar {:on-close (dispatch-event [::forms/form-closing ::form ])}
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])
can-submit? (boolean @(re-frame/subscribe [::can-submit]))
status @(re-frame/subscribe [::status/single ::form])
active-client @(re-frame/subscribe [::subs/client])
exists? (:id data)
can-change-amount? (#{:unpaid ":unpaid"} (:status data))
min-total (if (= (:total (:original data)) (:outstanding-balance (:original data)))
nil
(- (:total (:original data)) (:outstanding-balance (:original data))))]
[form-builder/builder {:can-submit [::can-submit]
:change-event [::changed]
:submit-event [::save-requested [::saving ]]
:id ::form
:schema schema}
[form-builder/section {:title [:div "New Invoice "
(cond
(#{:unpaid ":unpaid"} (:status data))
nil
(#{:voided ":voided"} (:status data))
[:div.tag.is-info.is-light "Voided"]
(and (#{:paid ":paid"} (:status data))
(not (seq (:payments data))))
[:div.tag.is-info.is-light "Automatically paid"]
(#{:paid ":paid"} (:status data))
(if-let [check-number (:check-number (:payment (first (:payments data))))]
[:div.tag.is-info.is-light "Paid by check #" check-number ]
[:div.tag.is-info.is-light "Paid"])
:else
nil)]}
(when-not active-client
[form-builder/field-v2 {:required? true
:field [:client]}
"Client"
[com/entity-typeahead {:entities @(re-frame/subscribe [::subs/clients])
:entity->text :name
:style {:width "18em"}
:auto-focus (if active-client false true)
:disabled exists?}]])
[form-builder/field-v2 {:required? true
:field [:vendor]}
"Vendor"
[com/search-backed-typeahead {:disabled exists?
:search-query (fn [i]
[:search_vendor
{:query i}
[:name :id]])
:style {:width "18em"}
:auto-focus (if active-client true false)}]]
[form-builder/field-v2 {:required? true
:field :date}
"Date"
[date-picker {:output :cljs-date}]]
[form-builder/field-v2 {:field [:due]}
"Due (optional)"
[date-picker {:output :cljs-date}]]
[form-builder/vertical-control
"Scheduled payment (optional)"
[left-stack
[:div.control
[form-builder/raw-field-v2 {:field :scheduled-payment}
[date-picker {:output :cljs-date}]]
[form-builder/raw-error-v2 {:field :scheduled-payment}]]
[:div.control
[form-builder/raw-field-v2 {:field :schedule-when-due}
[com/switch-input {:id "schedule-when-due"
:label "Same as due date"}]]]]]
[form-builder/field-v2 {:required? true
:field :invoice-number}
"Invoice #"
[:input.input {:style {:width "12em"}}]]
[form-builder/field-v2 {:required? true
:field :total}
"Total"
[com/money-input {:disabled (if can-change-amount? "" "disabled")
:style {:max-width "8em"}
:min min-total}]]]
[form-builder/field-v2 {:field :expense-accounts}
"Expense Accounts"
[expense-accounts-field-v2 {:descriptor "expense account"
:vendor-id (:id (:vendor data))
:allowance :invoice
:locations (:locations (:client data))
:max (:total data)
:client (or (:client data) active-client)}]]
[form-builder/error-notification]
[:div {:style {:margin-bottom "1em"}}]
[:div.columns
(when-not exists?
[:div.column
[drop-down {:header [:button.button.is-primary-two.is-medium.is-fullwidth {:aria-haspopup true
:type "button"
:on-click (dispatch-event [::events/toggle-menu ::add-and-print-invoice ])
:disabled (or (status/disabled-for status)
(not can-submit?))
:class (status/class-for status)}
"Pay "
[:span " "]
[:span.icon [:i.fa.fa-angle-down {:aria-hidden "true"}]]]
:class "is-fullwidth"
:id ::add-and-print-invoice}
[:div
(list
(for [{:keys [id name type]} (->> (:bank-accounts (:client data)) (filter :visible) (sort-by :sort-order))]
(if (= :cash type)
^{:key id} [:a.dropdown-item {:on-click (dispatch-event [::save-requested [::add-and-print id :cash]])} "With cash"]
(list
^{:key (str id "-check")} [:a.dropdown-item {:on-click (dispatch-event [::save-requested [::add-and-print id :check]])} "Print checks from " name]
^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event [::save-requested [::add-and-print id :debit]])} "Debit from " name]))))]]])
[:div.column
[form-builder/submit-button {:class ["is-fullwidth"]}
"Save"]]]])])
(defn form [_]
(r/create-class
{:display-name "invoice-form"
:component-did-mount #(re-frame/dispatch [::mounted])
:component-will-unmount #(re-frame/dispatch [::unmounted])
:reagent-render (fn [p]
[form-content p])}))