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