(ns auto-ap.views.pages.unpaid-invoices (:require [auto-ap.entities.invoice :as invoice] [auto-ap.events :as events] [auto-ap.forms :as forms] [auto-ap.subs :as subs] [auto-ap.utils :refer [by replace-if replace-by]] [auto-ap.views.pages.invoices.form :as form] [auto-ap.views.components.dropdown :refer [drop-down]] [auto-ap.views.components.expense-accounts-dialog :as expense-accounts-dialog :refer [change-expense-accounts-modal]] [auto-ap.views.components.invoice-table :as invoice-table :refer [invoice-table]] [auto-ap.views.components.invoices.side-bar :refer [invoices-side-bar]] [auto-ap.views.components.layouts :refer [appearing-side-bar side-bar-layout]] [auto-ap.views.components.modal :refer [action-modal modal]] [auto-ap.views.components.typeahead :refer [typeahead]] [auto-ap.views.components.vendor-filter :refer [vendor-filter]] [auto-ap.views.components.date-range-filter :refer [date-range-filter]] [auto-ap.views.pages.invoices.common :refer [invoice-read]] [auto-ap.views.utils :refer [bind-field date->str date-picker dispatch-event horizontal-field standard]] [cljs-time.core :as c] [clojure.spec.alpha :as s] [clojure.string :as str :refer [blank?]] [goog.string :as gstring] [re-frame.core :as re-frame] [reagent.core :as r])) (defn does-amount-exceed-outstanding? [amount outstanding-balance] (let [amount (js/parseFloat amount) outstanding-balance (js/parseFloat outstanding-balance)] (or (and (> outstanding-balance 0) (> amount outstanding-balance)) (and (> outstanding-balance 0) (<= amount 0)) (and (< outstanding-balance 0) (< amount outstanding-balance)) (and (< outstanding-balance 0) (>= amount 0))))) (re-frame/reg-sub ::invoice-page (fn [db] (-> db ::invoice-page))) (re-frame/reg-sub ::invoice (fn [db id] (get (by :id (-> db ::invoice-page :invoices )) id))) (re-frame/reg-sub ::advanced-print-checks (fn [db] (-> db ::advanced-print-checks))) (re-frame/reg-sub ::handwrite-checks (fn [db] (-> db ::handwrite-checks))) (re-frame/reg-sub ::change-expense-accounts (fn [db] (-> db ::change-expense-accounts))) (re-frame/reg-sub ::check-results (fn [db] (-> db ::check-results))) (re-frame/reg-sub ::params (fn [db] (-> db (::params {})))) (re-frame/reg-event-db ::invoice-updated (fn [db [_ invoice]] (println "HERE") (update-in db [::invoice-page :invoices] replace-by :id (assoc invoice :class "live-added")))) (re-frame/reg-event-fx ::params-change (fn [cofx [_ params]] {:db (-> (:db cofx) (assoc-in [:status :loading] true) (assoc-in [::params] params)) :graphql {:token (-> cofx :db :user) :query-obj (invoice-table/query (-> params (assoc :import-status "imported") (dissoc :invoice-number-like-current)) ) :on-success [::received] :on-error [::events/page-failed]}})) (re-frame/reg-event-db ::unmount-invoices (fn [db [_ data]] (-> db (dissoc ::invoice-page )))) (re-frame/reg-event-db ::received (fn [db [_ data]] (-> db (update ::invoice-page merge (first (:invoice-page data))) (assoc-in [:status :loading] false)))) (re-frame/reg-event-db ::toggle-check (fn [db [_ id invoice]] (-> db (update-in [::invoice-page :checked] (fn [x] (let [x (or x {})] (if (x id) (dissoc x id) (assoc x id invoice)))))))) (re-frame/reg-event-db ::advanced-print-checks (fn [db _] (let [{:keys [checked invoices]} (get-in db [::invoice-page])] (-> db (forms/stop-form ::form/form) (update-in [::invoice-page :print-checks-shown?] #(not %) ) (assoc-in [::advanced-print-checks] {:shown? true :bank-account-id (:id (first @(re-frame/subscribe [::subs/bank-accounts]))) :invoices (->> checked vals (map #(assoc % :amount (:outstanding-balance %))))} ))))) (re-frame/reg-event-fx ::handwrite-checks (fn [{:keys [db]} _] (let [{:keys [checked invoices]} (get-in db [::invoice-page]) invoices (->> checked vals (map #(assoc % :amount (:outstanding-balance %))))] {:dispatch [::events/modal-status ::handwrite-checks {:visible? true}] :db (-> db (forms/stop-form ::form/form) (update-in [::invoice-page :print-checks-shown?] #(not %) ) (assoc-in [::handwrite-checks] {:bank-account-id (:id (first @(re-frame/subscribe [::subs/real-bank-accounts]))) :invoices invoices } ))}))) (re-frame/reg-event-db ::cancel-advanced-print (fn [db _] (assoc-in db [::advanced-print-checks :shown?] false ))) (re-frame/reg-event-db ::close-check-results (fn [db _] (assoc-in db [::check-results :shown?] false ))) (re-frame/reg-event-db ::edit-payment-bank-account (fn [db [_ f v]] (assoc-in db [::advanced-print-checks :bank-account-id] v))) (re-frame/reg-event-db ::edit-payment (fn [db [_ which f v]] (update-in db [::advanced-print-checks :invoices] (fn [is] (for [i is] (if (= which (:id i)) (assoc-in i f v) i)))))) (re-frame/reg-event-db ::edit-handwritten-payment (fn [db [_ which f v]] (update-in db [::handwrite-checks :invoices] (fn [is] (for [i is] (if (= which (:id i)) (assoc-in i f v) i)))))) (defn print-checks-query [invoice-payments bank-account-id type client-id] {:venia/operation {:operation/type :mutation :operation/name "PrintChecks"} :venia/queries [[:print-checks {:invoice_payments invoice-payments :type type :bank_account_id bank-account-id :client_id client-id} [[:invoices invoice-read] :pdf_url]]]}) (re-frame/reg-event-fx ::print-checks (fn [{:keys [db]} [_ bank-account-id type]] {:db (-> db (assoc-in [::invoice-page :print-checks-shown?] false ) (assoc-in [::invoice-page :print-checks-loading?] true )) :graphql {:token (-> db :user) :query-obj (print-checks-query (map (fn [[id invoice]] {:invoice-id id :amount (:outstanding-balance invoice)}) (get-in db [::invoice-page :checked])) bank-account-id type (:client db)) :on-success [::checks-created]}})) (re-frame/reg-event-fx ::advanced-print-checks-submitted (fn [{:keys [db]} [_ bank-account-id]] (let [invoice-amounts (by :id (comp js/parseFloat :amount) (get-in db [::advanced-print-checks :invoices])) bank-account-id (get-in db [::advanced-print-checks :bank-account-id]) type (->> @(re-frame/subscribe [::subs/client]) :bank-accounts (filter #(= bank-account-id (:id %))) first :type)] {:db (-> db (assoc-in [::advanced-print-checks :printing?] true )) :graphql {:token (-> db :user) :query-obj (print-checks-query (map (fn [x] {:invoice-id (:id x) :amount (invoice-amounts (:id x))}) (get-in db [::advanced-print-checks :invoices])) bank-account-id (cond (= type :check) :check (= type :cash) :cash :else :check) (:client db)) :on-success [::checks-created]}}))) (re-frame/reg-event-fx ::checks-created (fn [{:keys [db]} [_ data]] (let [{{:keys [pdf-url invoices]} :print-checks} data invoices-by-id (by :id invoices) ] { :db (-> db (update-in [::invoice-page :invoices] (fn [invoices] (map (fn [i] (merge i (invoices-by-id (:id i)))) invoices))) (assoc-in [::invoice-page :checked] nil) (assoc-in [::invoice-page :print-checks-loading?] false) (assoc-in [::advanced-print-checks :printing?] false) (assoc-in [::advanced-print-checks :shown?] false)) :dispatch [::checks-printed pdf-url]}))) (re-frame/reg-event-db ::checks-printed (fn [db [_ pdf-url :as g]] (-> db (assoc-in [::check-results :shown?] true) (assoc-in [::check-results :pdf-url] pdf-url)))) (re-frame/reg-event-fx ::invalidated (fn [cofx [_ params]] {:dispatch [::params-change @(re-frame/subscribe [::params])]})) (re-frame/reg-event-fx ::new-invoice-clicked (fn [{:keys [db]} _] {:dispatch [::form/adding {:client @(re-frame/subscribe [::subs/client]) :status :unpaid #_#_:date (date->str (c/now) standard) :location (first (:locations @(re-frame/subscribe [::subs/client])))}]})) (re-frame/reg-event-fx ::unvoid-invoice (fn [{:keys [db]} [_ {id :id}]] {:graphql {:token (-> db :user) :query-obj {:venia/operation {:operation/type :mutation :operation/name "UnvoidInvoice"} :venia/queries [{:query/data [:unvoid-invoice {:invoice-id id} invoice-read]}]} :on-success [::invoice-unvoided]}})) (re-frame/reg-event-fx ::void-invoice (fn [{:keys [db]} [_ {id :id}]] {:graphql {:token (-> db :user) :query-obj {:venia/operation {:operation/type :mutation :operation/name "VoidInvoice"} :venia/queries [{:query/data [:void-invoice {:invoice-id id} invoice-read]}]} :on-success [::invoice-voided]}})) (re-frame/reg-event-fx ::handwrite-checks-save (fn [{:keys [db]} _] (let [{:keys [date invoices check-number bank-account-id]} @(re-frame/subscribe [::handwrite-checks]) invoice-amounts (by :id (comp js/parseFloat :amount) invoices)] {:graphql {:token (-> db :user) :query-obj {:venia/operation {:operation/type :mutation :operation/name "AddHandwrittenCheck"} :venia/queries [{:query/data [:add-handwritten-check {:date date :invoice_payments (map (fn [x] {:invoice-id (:id x) :amount (invoice-amounts (:id x))}) invoices) :check-number check-number :bank-account-id bank-account-id } [[:invoices invoice-read]]]}]} :on-success [::handwrite-checks-succeeded] :on-error [::handwrite-checks-failed]}}))) (re-frame/reg-event-fx ::handwrite-checks-failed (fn [{:keys [db]} [_ result]] {:dispatch [::events/modal-failed ::handwrite-checks (:message (first result))]})) (re-frame/reg-event-fx ::handwrite-checks-succeeded (fn [{:keys [db]} [_ {:keys [add-handwritten-check] :as result}]] (let [invoices-by-id (by :id (:invoices add-handwritten-check))] {:dispatch [::events/modal-completed ::handwrite-checks] :db (-> db (update-in [::invoice-page :invoices] (fn [invoices] (map (fn [i] (merge i (invoices-by-id (:id i)))) invoices))) (dissoc ::handwrite-checks))}))) (re-frame/reg-event-fx ::invoice-unvoided (fn [{:keys [db]} [_ {:keys [unvoid-invoice]}]] {:db (-> db (update-in [::invoice-page :invoices] (fn [is] (mapv (fn [i] (if (= (:id i) (:id unvoid-invoice)) (assoc unvoid-invoice :class "live-removed") i)) is))))})) (re-frame/reg-event-fx ::invoice-voided (fn [{:keys [db]} [_ {:keys [void-invoice]}]] {:db (-> db (update-in [::invoice-page :invoices] (fn [is] (mapv (fn [i] (if (= (:id i) (:id void-invoice)) (assoc void-invoice :class "live-removed") i)) is))))})) (re-frame/reg-event-fx ::expense-accounts-updated (fn [{:keys [db]} [_ data]] (let [updated (:edit-expense-accounts data)] {:dispatch [::events/modal-completed ::expense-accounts-dialog/change-expense-accounts] :db (-> db (update-in [::invoice-page :invoices] (fn [is] (replace-if #(= (:id %1) (:id %2)) updated is))) (dissoc ::change-expense-accounts))}))) (defn print-checks-modal [] (let [{:keys [checked]} @(re-frame/subscribe [::invoice-page]) {:keys [shown? invoices printing?] :as advanced-print-checks} @(re-frame/subscribe [::advanced-print-checks]) current-client @(re-frame/subscribe [::subs/client])] (when shown? [modal {:title "Print Checks" :foot [:button.button.is-primary {:on-click (dispatch-event [::advanced-print-checks-submitted]) :disabled (cond printing? "disabled" (seq (filter (fn [{:keys [outstanding-balance amount]}] (does-amount-exceed-outstanding? amount outstanding-balance )) invoices)) "disabled" :else "") :class (if printing? "is-loading" "")} [:span "Print"]] :hide-event [::cancel-advanced-print]} "Print using" [:span.field [:span.select [bind-field [:select {:type "select" :field :bank-account-id :event ::edit-payment-bank-account :subscription advanced-print-checks} (for [{:keys [id number name]} (sort-by :sort-order (:bank-accounts current-client))] ^{:key id} [:option {:value id} name])]]]] [:table.table.is-fullwidth [:thead [:tr [:th "Vendor"] [:th "Invoice ID"] [:th {:style {"width" "10em"}} "Payment"]]] [:tbody (for [{:keys [vendor payment outstanding-balance invoice-number id] :as i} invoices] ^{:key id} [:tr [:td (:name vendor)] [:td invoice-number] [:td [:div.field.has-addons.is-extended [:p.control [:a.button.is-static "$"]] [:p.control [bind-field [:input.input {:type "number" :field :amount :event [::edit-payment id] :subscription i :value payment :max outstanding-balance :step "0.01"}]]]]]])]]]))) (defn handwrite-checks-modal [] (let [{:keys [checked]} @(re-frame/subscribe [::invoice-page]) {:keys [invoices] :as handwrite-checks} @(re-frame/subscribe [::handwrite-checks]) change-event [::events/change-form [::handwrite-checks]] current-client @(re-frame/subscribe [::subs/client])] [action-modal {:id ::handwrite-checks :title "Handwrite Check" :action-text "Save" :save-event [::handwrite-checks-save] :can-submit? (cond (seq (filter (fn [{:keys [outstanding-balance amount]}] (does-amount-exceed-outstanding? amount outstanding-balance )) invoices)) false :else (and (not (blank? (:check-number handwrite-checks))) (not (blank? (:date handwrite-checks))))) } [horizontal-field [:label.label "Pay using"] [:span.select [bind-field [:select {:type "select" :field :bank-account-id :event change-event :subscription handwrite-checks} (for [{:keys [id number name]} @(re-frame/subscribe [::subs/real-bank-accounts])] ^{:key id} [:option {:value id} name])]]]] [horizontal-field [:label.label "Date"] [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 :popper-props (clj->js {:placement "right"}) :subscription handwrite-checks}]]] [horizontal-field [:label.label "Check number"] [bind-field [:input.input {:type "number" :field [:check-number] :event change-event :subscription handwrite-checks}]]] [:table.table.is-fullwidth [:thead [:tr [:th "Invoice ID"] [:th {:style {"width" "10em"}} "Payment"]]] [:tbody (for [{:keys [payment outstanding-balance invoice-number id] :as i} invoices] ^{:key id} [:tr [:td invoice-number] [:td [:div.field.has-addons.is-extended [:p.control [:a.button.is-static "$"]] [:p.control [bind-field [:input.input {:type "number" :field :amount :event [::edit-handwritten-payment id] :subscription i :value payment #_#_:max outstanding-balance :step "0.01"}]]]]]])]]])) (re-frame/reg-event-db ::change-selected-vendor (fn [db [_ key value]] (let [[key] key updated (assoc-in db [::invoice-page :vendor-filter key] value)] (if (and (= key :vendor-id) (not= value (get-in db [::params :vendor-id]))) (do (re-frame/dispatch [::params-change (assoc (::params updated) :vendor-id value :start 0)]) (assoc-in updated [::params :vendor-id] value)) updated)))) (re-frame/reg-event-fx ::change-selected-date-range (fn [{:keys [db]} [_ key value]] (let [[key] key updated (-> db (assoc-in [::invoice-page :date-range-filter key] value) (assoc-in [::params :date-range key] value))] {:dispatch [::params-change (::params updated)] :db updated}))) (re-frame/reg-event-fx ::invoice-number-like-current-changed (fn [{:keys [db]} [_ params invoice-like]] {:db (assoc-in db [::params :invoice-number-like-current] invoice-like ) :dispatch-debounce {:event [::invoice-number-like-settled invoice-like] :time 500 :key ::invoice-number-like}})) (re-frame/reg-event-fx ::invoice-number-like-settled (fn [{:keys [db]} [_ invoice-like]] {:dispatch [::params-change (assoc (::params db) :invoice-number-like invoice-like :start 0) ]})) (defn invoice-number-filter [] (let [{:keys [invoice-number-like-current] :as params} @(re-frame/subscribe [::params])] [:div.field [:div.control [:input.input {:placeholder "AP-123" :value invoice-number-like-current :on-change (fn [x] (re-frame/dispatch [::invoice-number-like-current-changed params (.. x -target -value) ]))} ]]])) (defn pay-button [{:keys [print-checks-shown? checked-invoices print-checks-loading?]}] (let [current-client @(re-frame/subscribe [::subs/client])] [:div [:div.is-pulled-right [:div.buttons [:button.button.is-success {:on-click (dispatch-event [::new-invoice-clicked])} "New Invoice"] (when current-client [drop-down {:header [:button.button.is-success {:aria-haspopup true :on-click (dispatch-event [::events/toggle-menu ::print-checks ]) :disabled (if (and (seq checked-invoices) #_(->> checked-invoices vals (group-by #(get-in % [:vendor :id])) (reduce-kv (fn [negative? _ invoices] (or negative? (< (reduce + 0 (map (fn [x] (-> x :outstanding-balance js/parseFloat)) invoices) ) 0))) false) not)) "" "disabled") :class (if print-checks-loading? "is-loading" "")} "Pay " (when (> (count checked-invoices )) (str (count checked-invoices) " invoices " "(" (->> checked-invoices vals (map (comp js/parseFloat :outstanding-balance)) (reduce + 0) (gstring/format "$%.2f" )) ")")) [:span " "] [:span.icon.is-small [:i.fa.fa-angle-down {:aria-hidden "true"}]]] :id ::print-checks :is-right? true} [: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 [::print-checks id :cash])} "With cash"] (list ^{:key (str id "-check")} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :check])} "Print checks from " name] ^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :debit])} "Debit from " name]))) ^{:key "advanced-divider"} [:hr.dropdown-divider] (when (= 1 (count (set (map (comp :id :vendor) (vals checked-invoices))))) ^{:key "handwritten"} [:a.dropdown-item {:on-click (dispatch-event [::handwrite-checks])} "Handwritten Check..."]) ^{:key "advanced"} [:a.dropdown-item {:on-click (dispatch-event [::advanced-print-checks])} "Advanced..."])]])]] [:div.is-pulled-right {:style {:margin-right "0.5rem"}} (into [:div.tags ] (map (fn [[id invoice]] [:span.tag.is-medium (:invoice-number invoice) [:button.delete.is-small {:on-click (dispatch-event [::toggle-check id invoice])}]]) checked-invoices))]] )) (defn check-results-dialog [] (let [{check-results-shown? :shown? pdf-url :pdf-url} @(re-frame/subscribe [::check-results])] (when check-results-shown? (if pdf-url [modal {:title "Your checks are ready!" :hide-event [::close-check-results]} [:div "Click " [:a {:href pdf-url :target "_new"} "here"] " to print them."] [:div [:em "Remember to turn off all scaling and margins."]] ] [modal {:title "Payment created!" :hide-event [::close-check-results]} [:div [:em "Your payment was created."]] ])))) (defn unpaid-invoices-content [{:keys [status] :as params}] (r/create-class {:display-name "unpaid-invoices-content" :component-will-unmount (fn [this] (re-frame/dispatch [::unmount-invoices])) :reagent-render (fn [{:keys [status]}] (let [{:keys [checked print-checks-shown? print-checks-loading? advanced-print-shown? vendor-filter]} @(re-frame/subscribe [::invoice-page]) current-client @(re-frame/subscribe [::subs/client])] [:div [:h1.title (str (str/capitalize (or status "all")) " invoices")] (when (= status "unpaid") [pay-button {:print-checks-shown? print-checks-shown? :checked-invoices checked :print-checks-loading? print-checks-loading?}]) [invoice-table {:id :unpaid :params (re-frame/subscribe [::params]) :invoice-page (re-frame/subscribe [::invoice-page]) :status (re-frame/subscribe [::subs/status]) :on-edit-invoice (fn [which] (re-frame/dispatch [::form/editing which])) :on-unvoid-invoice (fn [which] (re-frame/dispatch [::unvoid-invoice which])) :on-void-invoice (fn [which] (re-frame/dispatch [::void-invoice which])) :on-params-change (fn [params] (re-frame/dispatch [::params-change params])) :check-boxes (= status "unpaid") :checked checked :on-check-changed (fn [which invoice] (re-frame/dispatch [::toggle-check which invoice])) :expense-event [::expense-accounts-dialog/change-expense-accounts]}] ])) :component-will-mount #(re-frame/dispatch-sync [::params-change params]) })) (defn unpaid-invoices-page [params] (let [{invoice-bar-active? :active?} @(re-frame/subscribe [::forms/form ::form/form])] [side-bar-layout {:side-bar [invoices-side-bar {} ^{:key "extra-filter"} [:div [:p.menu-label "Vendor"] [:div [vendor-filter {:on-change-event [::change-selected-vendor] :value (:vendor-filter @(re-frame/subscribe [::invoice-page])) :vendors @(re-frame/subscribe [::subs/vendors])}]] [:p.menu-label "Date Range"] [:div [date-range-filter {:on-change-event [::change-selected-date-range] :value (:date-range-filter @(re-frame/subscribe [::invoice-page]))}]] [:p.menu-label "Invoice #"] [:div [invoice-number-filter]]]] :main [unpaid-invoices-content params] :bottom [:div [check-results-dialog] [print-checks-modal] [handwrite-checks-modal] [change-expense-accounts-modal {:updated-event [::expense-accounts-updated]}]] :right-side-bar [appearing-side-bar {:visible? invoice-bar-active?} [form/form {:invoice-created [::invoice-updated] :invoice-printed [::checks-printed]}]]}]))