(ns auto-ap.views.pages.invoices.form (:require [auto-ap.entities.invoice :as invoice] [auto-ap.utils :refer [by dollars=]] [auto-ap.events :as events] [auto-ap.forms :as forms] [auto-ap.subs :as subs] [auto-ap.views.components.dropdown :refer [drop-down]] [auto-ap.views.components.typeahead :refer [typeahead]] [auto-ap.views.components.expense-accounts-field :refer [expense-accounts-field recalculate-amounts]] [auto-ap.views.pages.invoices.common :refer [invoice-read]] [auto-ap.views.utils :refer [bind-field date->str date-picker dispatch-event standard]] [cljs-time.core :as c] [clojure.spec.alpha :as s] [re-frame.core :as re-frame] [clojure.string :as str] [goog.string :as gstring])) ;; SUBS (re-frame/reg-sub ::can-submit-edit-invoice :<- [::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 (not= :loading status) (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))))))))) (defmulti submit-query (fn [_ [_ command]] command)) (defmethod submit-query :create [db] (let [{:keys [data] {:keys [id invoice-number date location total expense-accounts vendor-id client-id]} :data} @(re-frame/subscribe [::forms/form ::form])] {:venia/operation {:operation/type :mutation :operation/name "AddInvoice"} :venia/queries [{:query/data [:add-invoice {:invoice {:date date :vendor-id vendor-id :client-id client-id :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]}]})) (defmethod submit-query :edit [db] (let [{:keys [data] {:keys [id invoice-number date location total expense-accounts vendor-id client-id]} :data} @(re-frame/subscribe [::forms/form ::form])] {:venia/operation {:operation/type :mutation :operation/name "EditInvoice"} :venia/queries [{:query/data [:edit-invoice {:invoice {:id id :invoice-number invoice-number :date date :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]}]})) (defmethod submit-query :add-and-print [db [_ _ bank-account-id type]] (let [{:keys [data] {:keys [id invoice-number date location total expense-accounts vendor-id client-id]} :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 vendor-id :client-id client-id :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]]]}]})) (re-frame/reg-sub ::submit-query submit-query) ;; EVENTS (re-frame/reg-event-db ::adding (fn [db [_ new]] (-> db (forms/start-form ::form (assoc new :expense-accounts [{:amount 0 :id (str "new-" (random-uuid)) :amount-percentage 100 :amount-mode "%"}]))))) (re-frame/reg-event-db ::editing (fn [db [_ which]] (let [edit-invoice (update which :date #(date->str % standard)) edit-invoice (assoc edit-invoice :original edit-invoice)] (-> db (forms/start-form ::form {:id (:id edit-invoice) :status (:status edit-invoice) :date (:date edit-invoice) :invoice-number (:invoice-number edit-invoice) :total (:total edit-invoice) :original edit-invoice :vendor-id (:id (:vendor edit-invoice)) :vendor-name (:name (:vendor edit-invoice)) :client-id (:id (:client edit-invoice)) :expense-accounts (if (seq (:expense-accounts which)) (vec (map (fn [a] (-> a (update :amount #(js/parseFloat %)) (assoc :amount-percentage (* 100 (/ (js/parseFloat (:amount a)) (Math/abs (js/parseFloat (:total which)))))) (assoc :amount-mode "%"))) (:expense-accounts edit-invoice))) [{:id (str "new-" (random-uuid)) :amount-mode "$" :amount (Math/abs (:total edit-invoice)) :amount-percentage 100}]) :client-name (:name (:client edit-invoice))}))))) (re-frame/reg-event-fx ::change-new-invoice-client (fn [{:keys [db ]} [_ location field value]] (let [first-location (-> @(re-frame/subscribe [::subs/clients-by-id]) (get-in [value :locations]) first)] {:dispatch [::forms/change ::form [:client-id] value [:location] first-location]}))) (re-frame/reg-event-fx ::change-amount [(forms/in-form ::form)] (fn [{{:keys [data]} :db} [_ field value]] (print field value (:expense-accounts data)) {:dispatch [::forms/change ::form field value [:expense-accounts] (recalculate-amounts (:expense-accounts data) value)]})) (re-frame/reg-event-fx ::change-vendor [(forms/in-form ::form)] (fn [{{:keys [data]} :db} [_ location field value]] (let [has-only-one-expense-account? (and value (or (not (seq (:expense-accounts data))) (<= 1 (count (:expense-accounts data)))) (not (get-in data [:expense-accounts 0 :account :id])))] (if has-only-one-expense-account? {:dispatch [::forms/change ::form field value [:expense-accounts] [{:id (str "new-" (random-uuid)) :amount (:total data) :amount-percentage 100 :amount-mode "%" :account @(re-frame/subscribe [::subs/vendor-default-account value])}]]} {:dispatch [::forms/change ::form field value]})))) (re-frame/reg-event-fx ::submitted (fn [{:keys [db]} [_ params command bank-account-id type]] (when @(re-frame/subscribe [::can-submit-edit-invoice]) (let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])] {:db (forms/loading db ::form) :graphql {:token (-> db :user) :query-obj @(re-frame/subscribe [::submit-query command bank-account-id type]) :on-success [::succeeded params command] :on-error [::forms/save-error ::form]}})))) (re-frame/reg-event-fx ::succeeded (fn [{:keys [db]} [_ {:keys [invoice-created invoice-printed]} command result]] (let [invoice (condp = command :edit (:edit-invoice result) :create (:add-invoice result) :add-and-print (first (:invoices (:add-and-print-invoice result))))] {:db (cond-> db true (forms/stop-form ::form) (#{:create :add-and-print} command) (forms/start-form ::form {:client-id (:id @(re-frame/subscribe [::subs/client])) :status :unpaid :date (date->str (c/now) standard) :location (first (:locations @(re-frame/subscribe [::subs/client])))})) :dispatch-n (cond-> [(conj invoice-created invoice)] (= :add-and-print command) (conj (conj invoice-printed (:pdf-url (:add-and-print-invoice result)))))}))) ;; VIEWS (defn form [{:keys [can-change-amount?] :as params}] [forms/side-bar-form {:form ::form } (let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form]) exists? (:id data) current-client @(re-frame/subscribe [::subs/client]) can-change-amount? (#{:unpaid ":unpaid"} (:status data)) change-event [::forms/change ::form] locations (get-in @(re-frame/subscribe [::subs/clients-by-id]) [(:client-id data) :locations]) multi-location? (> (count locations) 1) min-total (if (= (:total (:original data)) (:outstanding-balance (:original data))) nil (- (:total (:original data)) (:outstanding-balance (:original data)))) should-select-location? (and locations (> (count locations) 1)) chooseable-expense-accounts @(re-frame/subscribe [::subs/chooseable-expense-accounts]) accounts-by-id @(re-frame/subscribe [::subs/accounts-for-client-by-id])] ^{:key id} [:form { :on-submit (fn [e] (when (.-stopPropagation e) (.stopPropagation e) (.preventDefault e)) (if exists? (re-frame/dispatch-sync [::submitted params :edit]) (re-frame/dispatch-sync [::submitted params :create])))} [:h1.title.is-2 "New Invoice"] (when-not @(re-frame/subscribe [::subs/client]) [:div.field [:p.help "Client"] [:div.control [bind-field [typeahead {:matches (map (fn [x] [(:id x) (:name x)]) @(re-frame/subscribe [::subs/clients])) :type "typeahead" :auto-focus (if @(re-frame/subscribe [::subs/client]) false true) :field [:client-id] :disabled exists? :event [::change-new-invoice-client [::form]] :spec ::invoice/client-id :subscription data}]]]]) [:div.field [:p.help "Vendor"] [:div.control [bind-field [typeahead {:matches (map (fn [x] [(:id x) (:name x)]) @(re-frame/subscribe [::subs/vendors])) :type "typeahead" :disabled exists? :auto-focus (if @(re-frame/subscribe [::subs/client]) true false) :field [:vendor-id] :text-field [:vendor-name] :text-event change-event :event [::change-vendor [::form]] :spec (s/nilable ::invoice/vendor-id) :subscription data}]]]] [:div.field [:p.help "Date"] [:div.control [bind-field [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] :event change-event :spec ::invoice/date :subscription data}]]]] [:div.field [:p.help "Invoice #"] [:div.control [bind-field [:input.input {:type "text" :field [:invoice-number] :event change-event :spec ::invoice/invoice-number :subscription data}]]]] [:div.field [:p.help "Total"] [:div.control [:div.field.has-addons.is-extended [:p.control [:a.button.is-static "$"]] [:p.control [bind-field [:input.input {:type "number" :field [:total] :disabled (if can-change-amount? "" "disabled") :event [::change-amount] :min min-total :subscription data :spec ::invoice/total :step "0.01"}]]]]]] [:div.field [bind-field [expense-accounts-field {:subscription data :type "expense-accounts" :descriptor "expense account" :event change-event :locations locations :max (:total data) :field [:expense-accounts]}]]] (when error ^{:key error} [:div.notification.is-warning.animated.fadeInUp error]) [:div.columns (when-not exists? [:div.column [drop-down {:header [:button.button.is-info.is-outlined.is-medium.is-fullwidth {:aria-haspopup true :type "button" :on-click (dispatch-event [::events/toggle-menu ::add-and-print-invoice ]) :disabled (if @(re-frame/subscribe [::can-submit-edit-invoice]) "" "disabled") :class (if false "is-loading" "")} "Save & Pay " [:span " "] [:span.icon.is-small [: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 current-client) (filter :visible) (sort-by :sort-order))] (if (= :cash type) ^{:key id} [:a.dropdown-item {:on-click (dispatch-event [::submitted params id :cash])} "With cash"] (list ^{:key (str id "-check")} [:a.dropdown-item {:on-click (dispatch-event [::submitted params :add-and-print id :check])} "Print checks from " name] ^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event [::submitted params :add-and-print id :debit])} "Debit from " name]))))]]]) [:div.column [:button.button.is-medium.is-primary.is-fullwidth {:disabled (if @(re-frame/subscribe [::can-submit-edit-invoice]) "" "disabled") :class (str @(re-frame/subscribe [::forms/loading-class ::form]) (when error " animated shake"))} "Save"]] ]])])