From edbc9f85c4e9f3b06016ebb755617bad9e63193b Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Sun, 23 Aug 2020 07:42:12 -0700 Subject: [PATCH] You can now see cash flow details --- src/clj/auto_ap/graphql.clj | 10 +- .../auto_ap/views/components/buttons.cljs | 15 ++- src/cljs/auto_ap/views/components/grid.cljs | 5 +- .../views/components/invoice_table.cljs | 8 +- .../auto_ap/views/components/layouts.cljs | 9 +- .../auto_ap/views/pages/admin/accounts.cljs | 10 +- src/cljs/auto_ap/views/pages/home.cljs | 106 ++++++++++++++++-- .../auto_ap/views/pages/unpaid_invoices.cljs | 8 +- src/cljs/auto_ap/views/utils.cljs | 8 ++ 9 files changed, 144 insertions(+), 35 deletions(-) diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 2afc00c4..ed8a2667 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -1020,13 +1020,15 @@ '[(get-else $ ?je :journal-entry-line/debit 0.0) ?debit] '[(get-else $ ?je :journal-entry-line/credit 0.0) ?credit]]} :args [(d/db (d/connect uri)) client_id]})) - bills-due-soon (d/query {:query {:find '[?due ?outstanding] + bills-due-soon (d/query {:query {:find '[?due ?outstanding ?invoice-number ?vendor-id] :in '[$ ?client ?due-before] :where ['[?i :invoice/client ?client] '[?i :invoice/status :invoice-status/unpaid] '[?i :invoice/due ?due] '[(<= ?due ?due-before)] - '[?i :invoice/outstanding-balance ?outstanding]]} + '[?i :invoice/outstanding-balance ?outstanding] + '[?i :invoice/invoice-number ?invoice-number] + '[?i :invoice/vendor ?vendor-id]]} :args [(d/db (d/connect uri)) client_id (coerce/to-date (t/plus (time/local-now) (t/days 180)))]}) outstanding-checks (reduce + @@ -1063,8 +1065,10 @@ {:beginning_balance total-cash :outstanding_payments outstanding-checks - :invoices_due_soon (mapv (fn [[due outstanding]] + :invoices_due_soon (mapv (fn [[due outstanding invoice-number vendor-id]] {:due (coerce/to-date-time due) + :invoice_number invoice-number + :vendor {:id vendor-id} :outstanding_balance outstanding}) bills-due-soon) :upcoming_credits (into (mapv diff --git a/src/cljs/auto_ap/views/components/buttons.cljs b/src/cljs/auto_ap/views/components/buttons.cljs index 240839fc..c736a9c5 100644 --- a/src/cljs/auto_ap/views/components/buttons.cljs +++ b/src/cljs/auto_ap/views/components/buttons.cljs @@ -1,12 +1,23 @@ (ns auto-ap.views.components.buttons - (:require [auto-ap.views.utils :refer [dispatch-event]])) + (:require [auto-ap.views.utils :refer [dispatch-event]] + [reagent.core :as r])) (defn fa-icon [{:keys [event icon class]}] [:a.button {:class class - :on-click (dispatch-event event)} [:span.icon [:i.fa {:class icon}]]]) + :on-click (dispatch-event event)} (into + [:<> + [:span.icon [:i.fa {:class icon}]] + ] + (r/children (r/current-component)))]) (defn sl-icon [{:keys [event icon class] :as params}] [:a.button (-> params (dissoc :event :icon) (assoc :on-click (dispatch-event event))) [:span.icon [:span {:class icon :style {:font-weight "400"}}]]]) + +(defn new-button [{:keys [event name class ]}] + [:a.button.is-outlined {:class class + :on-click (dispatch-event event)} + [:span.icon [:i.fa.fa-plus]] + [:span name]]) diff --git a/src/cljs/auto_ap/views/components/grid.cljs b/src/cljs/auto_ap/views/components/grid.cljs index 1ad598f7..6c42db99 100644 --- a/src/cljs/auto_ap/views/components/grid.cljs +++ b/src/cljs/auto_ap/views/components/grid.cljs @@ -121,12 +121,13 @@ (mapv (fn [c] [:div.level-item c]) children))]]))))])) -(defn table [{:keys [fullwidth class]}] +(defn table [{:keys [fullwidth class style]}] (into [:table.table.compact.grid {:class (cond-> [] fullwidth (conj "is-fullwidth") - class (into class))}] + class (into class)) + :style style}] (r/children (r/current-component)))) (defn header [] diff --git a/src/cljs/auto_ap/views/components/invoice_table.cljs b/src/cljs/auto_ap/views/components/invoice_table.cljs index f6ea731c..08b09fce 100644 --- a/src/cljs/auto_ap/views/components/invoice_table.cljs +++ b/src/cljs/auto_ap/views/components/invoice_table.cljs @@ -9,7 +9,7 @@ [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 nf]] + [auto-ap.views.utils :refer [date->str dispatch-event nf days-until]] [bidi.bidi :as bidi] [cemerick.url :as url] [cljs-time.core :as t] @@ -106,11 +106,7 @@ [grid/cell {} (date->str date) ] [grid/cell {} (when due - (let [today (t/at-midnight (t/now)) - due (t/at-midnight due) - due-in (if (t/after? today due) - (- (t/in-days (t/interval (t/minus due (t/days 1)) today))) - (t/in-days (t/interval today due )))] + (let [due-in (days-until due)] (if (> due-in 0) [:span.has-text-success due-in " days"] [:span.has-text-danger due-in " days"]) diff --git a/src/cljs/auto_ap/views/components/layouts.cljs b/src/cljs/auto_ap/views/components/layouts.cljs index 1bba0b2c..d76e7ba2 100644 --- a/src/cljs/auto_ap/views/components/layouts.cljs +++ b/src/cljs/auto_ap/views/components/layouts.cljs @@ -12,7 +12,8 @@ [auto-ap.entities.vendors :as vendor] [clojure.string :as str] [reagent.core :as r] - [auto-ap.views.components.vendor-dialog :as vendor-dialog])) + [auto-ap.views.components.vendor-dialog :as vendor-dialog] + [auto-ap.views.components.buttons :as buttons])) (defn navbar-drop-down-contents [{:keys [id]} children ] @@ -128,9 +129,9 @@ (when-not is-initial-loading [:div.navbar-end [:div.navbar-item - [:a.button.is-primary.is-outlined - {:on-click (dispatch-event [::vendor-dialog/started {}])} - [:span.icon [:i.fa.fa-plus] ] [:span "Vendor"]]] + [buttons/new-button {:event [::vendor-dialog/started {}] + :name "Vendor" + :class "is-primary"}]] (when (> (count @clients) 1) diff --git a/src/cljs/auto_ap/views/pages/admin/accounts.cljs b/src/cljs/auto_ap/views/pages/admin/accounts.cljs index ef642857..b15c1e25 100644 --- a/src/cljs/auto_ap/views/pages/admin/accounts.cljs +++ b/src/cljs/auto_ap/views/pages/admin/accounts.cljs @@ -110,10 +110,12 @@ [:div [:h1.title "Accounts"] [:div.is-pulled-right - [:a.button.is-success {:on-click (dispatch-event [::account-form/editing - {:type :asset - :account-set "default"} - [::edit-completed]])} "New Account"]] + [buttons/new-button {:name "Account" + :class "is-primary" + :event [::account-form/editing + {:type :asset + :account-set "default"} + [::edit-completed]]}]] [accounts-table {:accounts @(re-frame/subscribe [::account-page])}]]) (defn admin-accounts-page [] diff --git a/src/cljs/auto_ap/views/pages/home.cljs b/src/cljs/auto_ap/views/pages/home.cljs index df7747ca..e5caa329 100644 --- a/src/cljs/auto_ap/views/pages/home.cljs +++ b/src/cljs/auto_ap/views/pages/home.cljs @@ -1,15 +1,17 @@ (ns auto-ap.views.pages.home - (:require [auto-ap.views.components.layouts :refer [side-bar-layout]] - [re-frame.core :as re-frame] - [bidi.bidi :as bidi] - [auto-ap.routes :as routes] + (:require [auto-ap.routes :as routes] [auto-ap.subs :as subs] + [auto-ap.views.components.grid :as grid] + [auto-ap.views.components.layouts :refer [side-bar-layout]] + [auto-ap.views.utils + :refer + [->$ date->str days-until dispatch-event local-now standard]] + [bidi.bidi :as bidi] + [cljs-time.coerce :as coerce] [cljs-time.core :as t] - [auto-ap.views.utils :refer [local-now date->str standard dispatch-event]] - [reagent.core :as r] [pushy.core :as pushy] - )) - + [re-frame.core :as re-frame] + [reagent.core :as r])) (def pie-chart (r/adapt-react-class js/Recharts.PieChart)) (def pie (r/adapt-react-class js/Recharts.Pie)) @@ -129,6 +131,16 @@ (fn [db] (::cash-flow db))) +(re-frame/reg-sub + ::cash-flow-table-params + (fn [db] + (::cash-flow-table-params db))) + +(re-frame/reg-event-db + ::cash-flow-table-params-changed + (fn [db [_ new]] + (assoc db ::cash-flow-table-params new))) + (re-frame/reg-sub ::cash-flow :<- [::chart-options] @@ -150,6 +162,7 @@ (let [today (t/plus start-date (t/days day))] (conj acc {:name (date->str today) + :date today :effective-balance (+ (- effective-balance invoices-due-today ) debits-due-today credits-yesterday) @@ -160,6 +173,7 @@ :query-params (cemerick.url/map->query {:due-range {:start (date->str today standard) :end (date->str today standard)}})})))) (list {:name (date->str start-date) + :date start-date :effective-balance effective-balance :invoices (- (invoices-due-soon (date->str start-date) 0.0)) :credits (upcoming-credits (date->str start-date) 0.0) @@ -188,6 +202,38 @@ :one-hundred-eighty-days (range 1 181))))))) + +(re-frame/reg-sub + ::cash-flow-page + :<- [::cash-flow-table-params] + :<- [::cash-flow-data] + :<- [::subs/vendors-by-id] + (fn [[params cash-flow-data vendors-by-id]] + (let [ {:keys [outstanding-payments invoices-due-soon upcoming-credits upcoming-debits]} cash-flow-data + rows (concat (map (fn [c] + {:date (:date c) + :days-until (days-until (:date c)) + :name (or (:identifier c) "Bi-weekly Average") + :amount (:amount c) + :type "Debit"}) upcoming-debits) + (map (fn [c] + {:date (:date c) + :days-until (days-until (:date c)) + :amount (:amount c) + :name (or (:identifier c) "Bi-weekly Average") + :type "Credit"}) + upcoming-credits) + (map (fn [c] + {:date (:due c) + :days-until (days-until (:due c)) + :amount (:outstanding-balance c) + :name (str (:name (get vendors-by-id (:id (:vendor c)))) " (" (:invoice-number c) ")") + :type "Invoice"}) + invoices-due-soon))] + (assoc (grid/virtual-paginate-controls (:start params ) rows) + :data (grid/virtual-paginate (:start params) + (sort-by (comp coerce/to-date :date) rows)))))) + (re-frame/reg-event-fx ::mounted (fn [{:keys [db]} _] @@ -203,9 +249,9 @@ {:client-id (:id @(re-frame/subscribe [::subs/client]))} [:beginning-balance :outstanding-payments - [:invoices-due-soon [:due :outstanding-balance]] - [:upcoming-credits [:date :amount]] - [:upcoming-debits [:date :amount]]]]]} + [:invoices-due-soon [:due :outstanding-balance [:vendor [:id]] :invoice-number]] + [:upcoming-credits [:date :amount :identifier]] + [:upcoming-debits [:date :amount :identifier]]]]]} :on-success [::received]}})) (defn cash-flow-range-button [{:keys [name value chart-options]}] @@ -214,6 +260,40 @@ :on-click (dispatch-event [::select-cash-flow-range value])} name]) +(defn cash-flow-grid [] + (let [page @(re-frame/subscribe [::cash-flow-page]) + opc (fn [p] + (re-frame/dispatch [::cash-flow-table-params-changed p])) + params @(re-frame/subscribe [::cash-flow-table-params])] + [grid/grid {:status {:state :complete} + :on-params-change opc + :params params + :column-count 4} + [grid/controls page] + [grid/table {:style {:width "800px"}} + [grid/header + [grid/row {} + [grid/header-cell {} "Date"] + [grid/header-cell {} "Type"] + [grid/header-cell {} "Name"] + [grid/header-cell {:class "has-text-right"} "Amount"]]] + [grid/body + (for [{:keys [date days-until type name amount] } (:data page)] + ^{:key date} + [grid/row {} + [grid/cell {} + (if (> days-until 0) + [:span.has-text-success days-until " days"] + [:span.has-text-danger days-until " days"]) + [:i.is-size-7 " (" (date->str date) ")"] ] + [grid/cell {} (if (> date 0) + "Upcoming " + "Due ") + type] + [grid/cell {} name] + [grid/cell {:class "has-text-right"} (->$ amount)] + ])]]])) + (defn home-content [] (let [client-id (-> @(re-frame/subscribe [::subs/client]) :id) chart-options @(re-frame/subscribe [::chart-options])] @@ -256,7 +336,9 @@ (make-cash-flow-chart {:width 800 :height 500 - :data (clj->js @(re-frame/subscribe [::cash-flow]))})]}])) + :data (clj->js @(re-frame/subscribe [::cash-flow]))}) + + [cash-flow-grid]]}])) (defn home-page [] diff --git a/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs b/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs index d7ca50a5..cf89652a 100644 --- a/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs +++ b/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs @@ -31,7 +31,8 @@ [goog.string :as gstring] [re-frame.core :as re-frame] [reagent.core :as r] - [vimsical.re-frame.fx.track :as track])) + [vimsical.re-frame.fx.track :as track] + [auto-ap.views.components.buttons :as buttons])) (re-frame/reg-event-fx ::params-change @@ -135,7 +136,10 @@ [:div [:div.is-pulled-right [:div.buttons - [:button.button.is-outlined.is-primary {:on-click (dispatch-event [::new-invoice-clicked])} "New Invoice"] + [buttons/new-button {:event [::new-invoice-clicked] + :name "Invoice" + :class "is-primary"}] + (when current-client [drop-down {:header [:button.button.is-primary {:aria-haspopup true :on-click (dispatch-event [::events/toggle-menu ::print-checks ]) diff --git a/src/cljs/auto_ap/views/utils.cljs b/src/cljs/auto_ap/views/utils.cljs index 00a40ed8..2947ba2d 100644 --- a/src/cljs/auto_ap/views/utils.cljs +++ b/src/cljs/auto_ap/views/utils.cljs @@ -456,3 +456,11 @@ (defn action-cell-width [cnt] (str (inc (* cnt 51)) "px")) + +(defn days-until [d] + (let [today (t/at-midnight (t/now)) + d (t/at-midnight d) + in (if (t/after? today d) + (- (t/in-days (t/interval (t/minus d (t/days 1)) today))) + (t/in-days (t/interval today d )))] + in))