Files
integreat/src/cljs/auto_ap/views/components/invoice_table.cljs
Bryce d73a3153bb payments ssr
voiding

supports bulk void.

exact match id linking

voidnig payments works.

minor tweak.
2024-03-09 11:59:17 -08:00

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