voiding supports bulk void. exact match id linking voidnig payments works. minor tweak.
303 lines
14 KiB
Clojure
303 lines
14 KiB
Clojure
(ns auto-ap.views.components.invoice-table
|
|
(:require [auto-ap.events :as events]
|
|
[auto-ap.routes :as routes]
|
|
[auto-ap.subs :as subs]
|
|
[auto-ap.routes.payments :as payment-routes]
|
|
[auto-ap.status :as status]
|
|
[auto-ap.views.components.buttons :as buttons]
|
|
[auto-ap.views.components.dropdown
|
|
:refer
|
|
[drop-down drop-down-contents]]
|
|
[auto-ap.views.components.grid :as grid]
|
|
[auto-ap.views.pages.invoices.form :as form]
|
|
[auto-ap.views.pages.invoices.common :refer [invoice-read]]
|
|
[auto-ap.views.utils :refer [date->str dispatch-event dispatch-event-with-propagation nf days-until]]
|
|
[bidi.bidi :as bidi]
|
|
[cemerick.url :as url]
|
|
[clojure.string :as str]
|
|
[goog.string :as gstring]
|
|
[re-frame.core :as re-frame]
|
|
[auto-ap.views.components.expense-accounts-dialog :as expense-accounts-dialog]
|
|
[auto-ap.views.pages.data-page :as data-page]
|
|
[auto-ap.ssr-routes :as ssr-routes]))
|
|
|
|
(defn data-params->query-params [params]
|
|
(if (:exact-match-id params)
|
|
{:exact-match-id (some-> params :exact-match-id str)}
|
|
{:exact-match-id (some-> params :exact-match-id str)
|
|
:start (:start params 0)
|
|
:sort (:sort params)
|
|
:per-page (:per-page params)
|
|
|
|
:vendor-id (:id (:vendor params))
|
|
:account-id (:id (:account params))
|
|
:date-range (:date-range params)
|
|
:due-range (:due-range params)
|
|
:amount-gte (:amount-gte (:amount-range params))
|
|
:amount-lte (:amount-lte (:amount-range params))
|
|
:location (:location params)
|
|
:unresolved (:unresolved params)
|
|
:scheduled-payments (:scheduled-payments params)
|
|
:invoice-number-like (:invoice-number-like params)
|
|
:import-status (:import-status params)
|
|
:status (condp = @(re-frame/subscribe [::subs/active-page])
|
|
:invoices nil
|
|
:import-invoices nil
|
|
:unpaid-invoices :unpaid
|
|
:paid-invoices :paid
|
|
:voided-invoices :voided)}))
|
|
|
|
(defn query [params]
|
|
{:venia/queries [[:invoice_page
|
|
{:filters (data-params->query-params params)}
|
|
|
|
[[:invoices [:id :total :outstanding-balance :invoice-number :date :due :status :client-identifier :scheduled-payment :source-url :similarity
|
|
[:vendor [:name :id]]
|
|
[:expense_accounts [:amount :id :location
|
|
[:account [:id :name :location]]]]
|
|
[:client [:name :id :locations]]
|
|
[:payments [:amount :id [:payment [:id :status :amount :s3_url :check_number
|
|
[:transaction [:post_date]]]]]]]]
|
|
:outstanding
|
|
:total_amount
|
|
:total
|
|
:start
|
|
:end]]]})
|
|
|
|
|
|
|
|
(re-frame/reg-event-fx
|
|
::void-invoice
|
|
(fn [{:keys [db]} [_ {id :id}]]
|
|
{:graphql
|
|
{:token (-> db :user)
|
|
:owns-state {:multi ::void
|
|
:which id}
|
|
:query-obj {:venia/operation {:operation/type :mutation
|
|
:operation/name "VoidInvoice"}
|
|
:venia/queries [{:query/data [:void-invoice
|
|
{:invoice-id id}
|
|
invoice-read]}]}
|
|
:on-success (fn [result]
|
|
[::invoice-updated (assoc (:void-invoice result)
|
|
:class "live-removed")])}}))
|
|
|
|
(re-frame/reg-event-fx
|
|
::unvoid-invoice
|
|
(fn [{:keys [db]} [_ {id :id}]]
|
|
{:graphql
|
|
{:token (-> db :user)
|
|
:owns-state {:multi ::unvoid
|
|
:which id}
|
|
:query-obj {:venia/operation {:operation/type :mutation
|
|
:operation/name "UnvoidInvoice"}
|
|
|
|
:venia/queries [{:query/data [:unvoid-invoice
|
|
{:invoice-id id}
|
|
invoice-read]}]}
|
|
:on-success (fn [result]
|
|
[::invoice-updated (:unvoid-invoice result)])}}))
|
|
(re-frame/reg-event-fx
|
|
::unautopay
|
|
(fn [{:keys [db]} [_ {id :id}]]
|
|
{:graphql
|
|
{:token (-> db :user)
|
|
:owns-state {:multi ::unautopay
|
|
:which id}
|
|
:query-obj {:venia/operation {:operation/type :mutation
|
|
:operation/name "UnautopayInvoice"}
|
|
|
|
:venia/queries [{:query/data [:unautopay-invoice
|
|
{:invoice-id id}
|
|
invoice-read]}]}
|
|
:on-success (fn [result]
|
|
[::invoice-updated (:unautopay-invoice result)])}}))
|
|
|
|
(re-frame/reg-event-fx
|
|
::invoice-updated
|
|
(fn [{:keys [db]} [_ _]]
|
|
{:db db}))
|
|
|
|
(defn row [{:keys [invoice selected-client overrides checkable? actions]}]
|
|
(let [{:keys [client status payments expense-accounts invoice-number date due total outstanding-balance id vendor source-url] :as i} invoice
|
|
unautopay-states @(re-frame/subscribe [::status/multi ::unautopay])
|
|
editing-states @(re-frame/subscribe [::status/multi ::edits])]
|
|
[grid/row {:class (:class i) :id id :checkable? checkable? :entity invoice}
|
|
(when-not selected-client
|
|
[grid/cell {}
|
|
(if-let [client-override (:client overrides)]
|
|
(client-override i)
|
|
(:name client))])
|
|
[grid/cell {} (:name vendor)]
|
|
[grid/cell {} invoice-number]
|
|
[grid/cell {:class "is-hidden-mobile"} (date->str date) ]
|
|
[grid/cell {}
|
|
(when due
|
|
(if (#{":paid" :paid ":voided" :voided} status)
|
|
nil
|
|
(let [due-in (days-until due)]
|
|
(if (> due-in 0)
|
|
[:span.has-text-success due-in " days"]
|
|
[:span.has-text-danger due-in " days"])
|
|
)))]
|
|
[grid/cell {} (str/join ", " (set (map :location expense-accounts)))]
|
|
|
|
[grid/cell {:class "has-text-right"} (nf total )]
|
|
[grid/cell {:class "has-text-right"}
|
|
(when (:scheduled-payment i)
|
|
[:<> [:div.tag.is-info.is-light "Autopay"] " "])
|
|
(nf outstanding-balance )]
|
|
[grid/button-cell {}
|
|
[:div.buttons
|
|
(when (seq expense-accounts)
|
|
[drop-down {:id [::expense-accounts id ]
|
|
:is-right? true
|
|
:header [buttons/sl-icon {:class "badge"
|
|
:event [::events/toggle-menu [::expense-accounts id]]
|
|
:data-badge (str (clojure.core/count expense-accounts))
|
|
:icon "icon-navigation-menu"}]}
|
|
[drop-down-contents
|
|
[:div
|
|
(for [e expense-accounts]
|
|
^{:key (:id e)}
|
|
[:span.dropdown-item (:name (:account e)) " " (gstring/format "$%.2f" (:amount e) ) ])
|
|
|
|
(when (get actions :expense-accounts)
|
|
[:<>
|
|
|
|
[:hr.dropdown-divider]
|
|
|
|
[:a.dropdown-item.is-primary {:on-click (dispatch-event [::expense-accounts-dialog/show i])} "Change"]])]]])
|
|
[:span {:style {:margin-left "1em"}}]
|
|
(when (or (seq payments)
|
|
source-url
|
|
)
|
|
[:<>
|
|
[drop-down {:id [::payments id]
|
|
:is-right? true
|
|
:header [buttons/fa-icon {:class "badge"
|
|
:on-click (dispatch-event-with-propagation [::events/toggle-menu [::payments id]])
|
|
:data-badge (str (cond-> (clojure.core/count payments)
|
|
source-url inc))
|
|
:icon "fa-paperclip"}]}
|
|
[drop-down-contents
|
|
[:div.dropdown-item
|
|
[:table.table.grid.compact
|
|
[:tbody
|
|
(for [invoice-payment payments]
|
|
^{:key (:id invoice-payment)}
|
|
[:tr
|
|
[:td
|
|
"Payment"]
|
|
[:td (gstring/format "$%.2f" (:amount invoice-payment) )]
|
|
[:td
|
|
(when (= :cleared (:status (:payment invoice-payment)))
|
|
(str "cleared")
|
|
)]
|
|
[:td (:post-date (:transaction (:payment invoice-payment)))]
|
|
[:td
|
|
[buttons/fa-icon {:icon "fa-external-link"
|
|
:href (str (bidi/path-for ssr-routes/only-routes ::payment-routes/page )
|
|
"?"
|
|
(url/map->query {:exact-match-id (:id (:payment invoice-payment))}))}]]])
|
|
(when source-url
|
|
[:tr
|
|
[:td
|
|
"File"]
|
|
[:td {:colspan 4}
|
|
[buttons/fa-icon {:icon "fa-external-link"
|
|
:target "_new"
|
|
:href source-url}]]])]]]]]
|
|
[:span {:style {:margin-right "1em"}}]])
|
|
|
|
|
|
(when (and (get actions :edit)
|
|
(not= ":voided" (:status i)))
|
|
[buttons/fa-icon {:icon "fa-pencil"
|
|
:class (status/class-for (get editing-states id))
|
|
:event
|
|
[::events/vendor-preferences-requested {:client-id (:id client)
|
|
:vendor-id (:id vendor)
|
|
:on-success [::form/editing i]
|
|
:on-failure []
|
|
:owns-state {:multi ::edits
|
|
:which (:id i)}}]}])
|
|
(when (and (get actions :void)
|
|
(= (:outstanding-balance i) (:total i)) (not= ":voided" (:status i)))
|
|
[buttons/sl-icon {:icon "icon-bin-2"
|
|
:event [::void-invoice i]}])
|
|
(when (and (get actions :void)
|
|
(= ":voided" (:status i)))
|
|
[buttons/fa-icon {:icon "fa-undo"
|
|
:event [::unvoid-invoice i]}])
|
|
(when (and (get actions :void)
|
|
(= ":paid" (:status i))
|
|
(:scheduled-payment i)
|
|
(not (seq (:payments i))))
|
|
[buttons/fa-icon {:icon "fa-undo"
|
|
:class (status/class-for (get unautopay-states (:id i)))
|
|
:event [::unautopay i]}])]]]))
|
|
|
|
(defn invoice-table [{:keys [check-boxes overrides actions data-page checkable-fn action-buttons]}]
|
|
(let [{:keys [data status params table-params]} @(re-frame/subscribe [::data-page/page data-page])
|
|
selected-client @(re-frame/subscribe [::subs/client])
|
|
x @(re-frame/subscribe [::subs/selected-clients])
|
|
is-loading? (= :loading (:state status))
|
|
is-sorted-by-vendor? (and (= "vendor" (:sort-key (first (:sort table-params))))
|
|
(not is-loading?)
|
|
(or (apply <= (map (comp :name :vendor) (:data data)))
|
|
(apply >= (map (comp :name :vendor) (:data data)))))
|
|
[invoice-groups] (if is-sorted-by-vendor?
|
|
(reduce
|
|
(fn [[acc last-vendor] invoice]
|
|
(if (not= (:id (:vendor invoice))
|
|
last-vendor)
|
|
[(update-in acc [(clojure.core/count acc)] #(conj (or % []) invoice))
|
|
(:id (:vendor invoice))]
|
|
[(update-in acc [(dec (clojure.core/count acc))] #(conj (or % []) invoice))
|
|
(:id (:vendor invoice))]))
|
|
[[] nil]
|
|
(:data data))
|
|
[[(:data data)]])]
|
|
[grid/grid {:data-page data-page
|
|
:check-boxes? check-boxes
|
|
:column-count (if selected-client 8 9)}
|
|
[grid/controls (assoc data :action-buttons action-buttons)
|
|
[:div.level-item
|
|
[:div.tags
|
|
[:div.tag.is-info.is-light "Outstanding " (nf (:outstanding data))]
|
|
[:div.tag.is-info.is-light " Total " (nf (:total-amount data))]]]]
|
|
(for [invoices invoice-groups]
|
|
^{:key (or (:id (first invoices)) "init")}
|
|
[grid/table {:fullwidth true}
|
|
[grid/header {}
|
|
[grid/row {:id "header"
|
|
:entity params}
|
|
(when-not selected-client
|
|
[grid/sortable-header-cell {:sort-key "client" :sort-name "Client"} "Client"])
|
|
[grid/sortable-header-cell {:sort-key "vendor" :sort-name "Vendor"}
|
|
(if is-sorted-by-vendor?
|
|
(:name (:vendor (first invoices)))
|
|
"Vendor")]
|
|
|
|
[grid/sortable-header-cell {:sort-key "invoice-number" :sort-name "Invoice Number"} "Invoice #"]
|
|
[grid/sortable-header-cell {:sort-key "date" :sort-name "Date" :style {:width "8em"}} "Date"]
|
|
[grid/sortable-header-cell {:sort-key "due" :sort-name "Due" :style {:width "8em"} :class "is-hidden-mobile"} "Due"]
|
|
[grid/sortable-header-cell {:sort-key "location" :sort-name "Location" :style {:width "5em"}} "Loc"]
|
|
[grid/sortable-header-cell {:sort-key "total" :sort-name "Total" :style {:width "8em"} :class "has-text-right"} "Total"]
|
|
|
|
[grid/sortable-header-cell {:sort-key "outstanding-balance" :sort-name "Outstanding" :style {:width "10em"} :class "has-text-right"} "Outstanding"]
|
|
[grid/header-cell {:style {:width "14rem"}}]]]
|
|
|
|
[grid/body
|
|
(for [{:keys [id] :as i} invoices]
|
|
^{:key id}
|
|
[row {:invoice i
|
|
:selected-client selected-client
|
|
:checkable? (if checkable-fn
|
|
(checkable-fn i)
|
|
true)
|
|
:actions actions
|
|
:overrides overrides}])]])
|
|
[grid/bottom-paginator data]]))
|