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