460 lines
23 KiB
Clojure
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])}))
|