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

480 lines
26 KiB
Clojure

(ns auto-ap.views.pages.invoices.form
(:require [auto-ap.entities.invoice :as invoice]
[auto-ap.events :as events]
[auto-ap.forms :as forms]
[auto-ap.status :as status]
[auto-ap.subs :as subs]
[auto-ap.time-utils :refer [next-dom]]
[auto-ap.utils :refer [by dollars=]]
[auto-ap.views.components.dropdown :refer [drop-down]]
[auto-ap.views.components.expense-accounts-field
:as
expense-accounts-field
:refer
[expense-accounts-field recalculate-amounts]]
[auto-ap.views.components.layouts :as layouts]
[auto-ap.views.components.modal :as modal]
[auto-ap.views.components.money-field :refer [money-field]]
[auto-ap.views.components.switch-field :refer [switch-field]]
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
[auto-ap.views.pages.invoices.common :refer [invoice-read]]
[auto-ap.views.utils
:refer
[date->str date-picker dispatch-event standard str->date with-user]]
[cljs-time.core :as c]
[clojure.spec.alpha :as s]
[clojure.string :as str]
[re-frame.core :as re-frame]
[reagent.core :as r]
[vimsical.re-frame.fx.track :as track]))
;; SUBS
(re-frame/reg-sub
::can-submit
:<- [::forms/form ::form]
(fn [{:keys [data status]} _]
(let [min-total (if (= (:total (:original data)) (:outstanding-balance (:original data)))
nil
(- (:total (:original data)) (:outstanding-balance (:original data))))]
(and (s/valid? ::invoice/invoice data)
(or (not min-total) (>= (:total data) min-total))
(or (not (:id data))
(dollars= (js/parseFloat (:total data)) (reduce + 0 (map (fn [ea] (js/parseFloat (:amount ea))) (:expense-accounts data)))))))))
(re-frame/reg-sub
::create-query
:<- [::forms/form ::form]
(fn [{:keys [data] {:keys [id 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 {:date date
:due due
: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)}}
invoice-read]}]}))
(re-frame/reg-sub
::edit-query
:<- [::forms/form ::form]
(fn [{:keys [data] {:keys [id invoice-number date due scheduled-payment location total expense-accounts vendor client]} :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]}]}))
(re-frame/reg-sub
::add-and-print-query
(fn [db [_ bank-account-id type]]
(let [{:keys [data] {:keys [id invoice-number date location total expense-accounts scheduled-payment vendor client]} :data} @(re-frame/subscribe [::forms/form ::form])]
{: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]]]}]})))
;; EVENTS
(re-frame/reg-event-db
::updated
(fn [db [_ invoice command]]
(if (= :create command)
(-> db
(forms/stop-form ::form )
(forms/start-form ::form {:client @(re-frame/subscribe [::subs/client])
:status :unpaid
:date (date->str (c/now) standard)}))
db)))
(re-frame/reg-event-db
::adding
(fn [db [_ new]]
(let [locations @(re-frame/subscribe [::subs/locations-for-client (:client new)])
accounts-by-id @(re-frame/subscribe [::subs/accounts-by-id (:client new)])]
(-> db (forms/start-form ::form (assoc new :expense-accounts
(expense-accounts-field/from-graphql (:expense-accounts new)
accounts-by-id
0.0
locations)))))))
(re-frame/reg-event-db
::editing
(fn [db [_ which]]
(let [accounts-by-id @(re-frame/subscribe [::subs/accounts-by-id (:client which)])
vendor (get @(re-frame/subscribe [::subs/vendors-by-id]) (:id (:vendor which)))
edit-invoice (update which :date #(date->str % standard))
edit-invoice (update edit-invoice :due #(date->str % standard))
edit-invoice (update edit-invoice :scheduled-payment #(date->str % standard))
edit-invoice (assoc edit-invoice :original edit-invoice)
edit-invoice (assoc edit-invoice :vendor-autopay? (boolean ((set (map :id (:automatically-paid-when-due vendor)))
(:id (:client which)))))
locations @(re-frame/subscribe [::subs/locations-for-client (:id (:client which))])
]
(-> 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-autopay? (boolean ((set (map :id (:automatically-paid-when-due vendor)))
(:id (:client which))))
:scheduled-payment (:scheduled-payment edit-invoice)
:invoice-number (:invoice-number edit-invoice)
:total (:total edit-invoice)
:original edit-invoice
:vendor (:vendor edit-invoice)
:client (:client edit-invoice)
:expense-accounts (expense-accounts-field/from-graphql (:expense-accounts which)
accounts-by-id
(:total 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))])]
(cond (and (= [:vendor] field)
value)
(let [schedule-payment-dom (get (by (comp :id :client ) :dom (:schedule-payment-dom value))
(:id (:client data)))]
(cond-> []
(expense-accounts-field/can-replace-with-default? (:expense-accounts data))
(into [[:expense-accounts] (expense-accounts-field/default-account (:expense-accounts data)
@(re-frame/subscribe [::subs/vendor-default-account value (:client data)])
(:total data)
locations)])
(boolean ((set (map :id (:automatically-paid-when-due value))) (:id (:client data))))
(into [[:scheduled-payment] (:due data)
[:schedule-when-due] true
[:vendor-autopay? ] true])
schedule-payment-dom
(into [[:scheduled-payment] (date->str (next-dom (str->date (:date data) standard) schedule-payment-dom) standard)
[:vendor-autopay?] true])
true
(into [[:schedule-payment-dom] schedule-payment-dom])))
(= [:total] field)
[[:expense-accounts] (recalculate-amounts (:expense-accounts data) value)]
(and (= [:date] field)
(:schedule-payment-dom data))
[[:scheduled-payment] (date->str (next-dom (str->date value standard) (:schedule-payment-dom data)) standard) ]
(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 (:id data)
f
(assoc-in f [:data :client] c))))
(re-frame/reg-event-fx
::add-and-print
[with-user (forms/in-form ::form)]
(fn [{:keys [user] {:keys [data]} :db} [_ bank-account-id type]]
{:graphql
{:token user
:owns-state {:single ::form}
:query-obj @(re-frame/subscribe [::add-and-print-query bank-account-id type])
:on-success [::added-and-printed]
:on-error [::forms/save-error ::form]}}))
(re-frame/reg-event-fx
::saving
[with-user (forms/in-form ::form)]
(fn [{:keys [user] {:keys [data]} :db} _]
{:graphql
{:token user
:owns-state {:single ::form}
:query-obj (if (:id data)
@(re-frame/subscribe [::edit-query])
@(re-frame/subscribe [::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)])
:on-error [::forms/save-error ::form]}}))
(re-frame/reg-event-fx
::save-requested
[with-user (forms/in-form ::form)]
(fn [{:keys [user db]} [_ fwd-event]]
(if (and (:scheduled-payment (:data db))
(not (:vendor-autopay? (:data db))))
{:dispatch
[::modal/modal-requested {:title "Scheduled payment date"
:body [:div "This vendor isn't set up to be automatically paid. On "
(: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 [{:keys [db]} [_ 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 [_ invoices pdf-url]]
db))
;; VIEWS
(def invoice-form (forms/vertical-form {:can-submit [::can-submit]
:change-event [::changed]
:submit-event [::save-requested [::saving ]]
:id ::form}))
(re-frame/reg-event-fx
::mounted
(fn []
{::track/register [{:id ::client
:subscription [::subs/client]
:event-fn (fn [c]
[::maybe-change-client c])}]}))
(re-frame/reg-event-fx
::unmounted
(fn []
{::track/dispose [{:id ::client}]}))
(defn form-content [{:keys [can-change-amount?] :as params}]
[layouts/side-bar {:on-close (dispatch-event [::forms/form-closing ::form ])}
(let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form])
{:keys [form-inline field raw-field error-notification submit-button ]} invoice-form
can-submit? (boolean @(re-frame/subscribe [::can-submit]))
status @(re-frame/subscribe [::status/single ::form])
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))))]
(with-meta
(form-inline (assoc params :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"]
(and (#{: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 @(re-frame/subscribe [::subs/client])
(field [:span "Client"
[:span.has-text-danger " *"]]
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients])
:entity->text :name
:type "typeahead-v3"
:auto-focus (if @(re-frame/subscribe [::subs/client]) false true)
:field [:client]
:disabled exists?
:spec ::invoice/client}]))
(field [:span "Vendor"
[:span.has-text-danger " *"]]
[typeahead-v3 {:entities-by-id @(re-frame/subscribe [::subs/vendors-by-id])
:entity-index @(re-frame/subscribe [::subs/searchable-vendors-index])
:entity->text :name
:type "typeahead-v3"
:disabled exists?
:auto-focus (if @(re-frame/subscribe [::subs/client]) true false)
:field [:vendor]}])
(field [:span "Date"
[:span.has-text-danger " *"]]
[date-picker {:class-name "input"
:class "input"
:format-week-number (fn [] "")
:previous-month-button-label ""
:placeholder "mm/dd/yyyy"
:next-month-button-label ""
:next-month-label ""
:type "date"
:field [:date]
:spec ::invoice/date}])
(field "Due (optional)"
[date-picker {:class-name "input"
:class "input"
:format-week-number (fn [] "")
:previous-month-button-label ""
:placeholder "mm/dd/yyyy"
:next-month-button-label ""
:next-month-label ""
:type "date"
:field [:due]
:spec ::invoice/due}])
[:p.help "Scheduled payment (optional)"]
[:div.level
[:div.level-left
[:div.level-item
[:div.control
(raw-field
[date-picker {:class-name "input"
:class "input"
:disabled (boolean (:schedule-when-due data))
:format-week-number (fn [] "")
:previous-month-button-label ""
:placeholder "mm/dd/yyyy"
:next-month-button-label ""
:next-month-label ""
:type "date"
:field [:scheduled-payment]
:spec ::invoice/scheduled-payment}])]]
[:div.level-item [:div.control
(raw-field
[switch-field {:id "schedule-when-due"
:field [:schedule-when-due]
:label "Same as due date"
:type "checkbox"}])]]]]
(field [:span "Invoice #"
[:span.has-text-danger " *"]]
[:input.input {:type "text"
:field [:invoice-number]
:spec ::invoice/invoice-number}])
(field [:span "Total"
[:span.has-text-danger " *"]]
[money-field {:type "money"
:field [:total]
:disabled (if can-change-amount? "" "disabled")
:min min-total
:spec ::invoice/total
:step "0.01"}])
(field nil
[expense-accounts-field {:type "expense-accounts"
:descriptor "expense account"
:locations (:locations (:client data))
:max (:total data)
:client (or (:client data) @(re-frame/subscribe [::subs/client]))
:field [:expense-accounts]}])
(error-notification)
[: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 number 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
(submit-button "Save")]]])
{:key id}))])
(defn form [p]
(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])}))