Files
integreat/src/cljs/auto_ap/views/pages/home.cljs
2020-12-21 08:18:30 -08:00

348 lines
15 KiB
Clojure

(ns auto-ap.views.pages.home
(: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]
[pushy.core :as pushy]
[re-frame.core :as re-frame]
[recharts]
[reagent.core :as r]))
(def pie-chart (r/adapt-react-class recharts/PieChart))
(def pie (r/adapt-react-class recharts/Pie))
(def bar-chart (r/adapt-react-class recharts/BarChart))
(def x-axis (r/adapt-react-class recharts/XAxis))
(def y-axis (r/adapt-react-class recharts/YAxis))
(def bar (r/adapt-react-class recharts/Bar))
(def legend (r/adapt-react-class recharts/Legend))
(def cell (r/adapt-react-class recharts/Cell))
(def tool-tip (r/adapt-react-class recharts/Tooltip))
(def colors ["#79b52e" "#009cea" "#209b1c" "#f48017" " #ff0303" "hsl(217, 71%, 53%)" "hsl(141, 53%, 53%)"])
(def light-colors ["#a6d869" "#8ad8ff" "#2cd327" "#fac899" "#ff6b6b" "hsl(217, 71%, 53%)"])
(defn make-pie-chart
[{:keys [width height data]}]
[pie-chart {:width width
:height height}
[pie {:fill "#82ca9d"
:data data
:dataKey "value"
:inner-radius 20}
(map (fn [x y]
^{:key y}
[cell {:key y :fill (colors y)}]) data (range))
]
[tool-tip]
[legend]])
(defn make-bar-chart [{:keys [width height data]}]
[bar-chart {:width width :height height :data data :fill "#FFFFFF"}
[tool-tip]
[bar {:dataKey "paid" :fill (get colors 0) :stackId "a" :name "Paid"}]
[bar {:dataKey "unpaid" :fill (get light-colors 0) :stackId "a" :name "Unpaid"}]
[x-axis {:dataKey "name"}]
[y-axis]
[legend]])
(defn make-cash-flow-chart [{:keys [width height data] }]
(let [redirect-fn (fn [x]
(pushy/set-token! auto-ap.history/history (str (bidi/path-for routes/routes :unpaid-invoices) "?" (get (js->clj x) "query-params")))
)]
[bar-chart {:width width :height height :data data :fill "#FFFFFF" :stackOffset "sign"}
[tool-tip]
[bar {:dataKey "effective-balance" :fill (get colors 1) :stackId "a" :name "Effective Balance"
:on-click redirect-fn}]
[bar {:dataKey "outstanding-payments" :fill (get colors 0) :stackId "a" :name "Outstanding Payments"
:on-click redirect-fn}]
[bar {:dataKey "invoices" :fill (get colors 3) :stackId "a" :name "Invoices"
:on-click redirect-fn}]
[bar {:dataKey "credits" :fill (get colors 2) :stackId "a" :name "Upcoming Credits"
:on-click redirect-fn}]
[bar {:dataKey "debits" :fill (get colors 4) :stackId "a" :name "Upcoming Debits"
:on-click redirect-fn}]
[x-axis {:dataKey "name"}]
[y-axis]
[legend]])
)
(re-frame/reg-event-db
::received
(fn [db [_ {:keys [expense-account-stats invoice-stats cash-flow]}]]
(let [expense-account-stats (->> expense-account-stats
(map #(update % :total (fn [t] (js/parseFloat t))))
(sort-by :total)
(reverse)
(take 5))
top-5 (vec (take 5 expense-account-stats))
rest (drop 5 expense-account-stats)
other {:account {:id 0 :name "Other"} :total (reduce + 0 (map :total rest))}]
(cond-> db
(seq top-5)
(assoc ::top-expense-categories (conj top-5 other))
(seq invoice-stats)
(assoc ::invoice-stats invoice-stats)
(seq cash-flow)
(assoc ::cash-flow cash-flow)))))
(re-frame/reg-event-db
::select-cash-flow-range
[(re-frame/path ::chart-options)]
(fn [chart-options [_ which]]
(assoc chart-options :cash-flow-range which)))
(re-frame/reg-sub
::invoice-stats
(fn [db]
(::invoice-stats db)))
(re-frame/reg-sub
::chart-options
(fn [db]
(merge {:cash-flow-range :seven-days} (::chart-options db))))
(re-frame/reg-sub
::top-expense-categories
(fn [db]
(::top-expense-categories db)))
(defn sum-by-date [pairs]
(reduce
(fn [result [date amount]]
(let [due (if (t/before? date (local-now))
(local-now)
date)]
(update result (date->str due)
(fn [r] (+ (or r 0.0) (js/parseFloat amount))))))
{}
pairs))
(re-frame/reg-sub
::cash-flow-data
(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]
:<- [::cash-flow-data]
(fn [[chart-options cash-flow-data]]
(let [{:keys [outstanding-payments beginning-balance invoices-due-soon upcoming-credits upcoming-debits]} cash-flow-data
invoices-due-soon (sum-by-date (map (fn [i] [(:due i) (:outstanding-balance i)]) invoices-due-soon))
upcoming-credits (sum-by-date (map (fn [i] [(:date i) (:amount i)]) upcoming-credits))
upcoming-debits (sum-by-date (map (fn [i] [(:date i) (:amount i)]) upcoming-debits))
start-date (local-now)
effective-balance (- beginning-balance outstanding-payments (invoices-due-soon (date->str start-date) 0.0))]
(reverse
(reduce
(fn [[{:keys [effective-balance credits-yesterday] } :as acc] day]
(let [invoices-due-today (invoices-due-soon (date->str (t/plus start-date (t/days day))) 0.0)
credits-due-today (upcoming-credits (date->str (t/plus start-date (t/days day))) 0.0)
debits-due-today (upcoming-debits (date->str (t/plus start-date (t/days day))) 0.0)]
(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)
:credits-yesterday credits-due-today
:credits credits-due-today
:debits debits-due-today
:invoices (- invoices-due-today)
: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)
:credits-yesterday (upcoming-credits (date->str start-date) 0.0)
:debits (upcoming-debits (date->str start-date) 0.0)
:outstanding-payments (- outstanding-payments)
:query-params (cemerick.url/map->query {:due-range {:end (date->str start-date standard)}})})
(condp = (:cash-flow-range chart-options)
:seven-days
(range 1 7)
:thirty-days
(range 1 31)
:sixty-days
(range 1 61)
:ninety-days
(range 1 91)
:one-hundred-twenty-days
(range 1 121)
:one-hundred-fifty-days
(range 1 151)
: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 ) (:per-page params) rows)
:data (grid/virtual-paginate (:start params)
(:per-page params)
(sort-by (comp coerce/to-date :date) rows))))))
(re-frame/reg-event-fx
::mounted
(fn [{:keys [db]} _]
{:db (assoc db ::top-expense-categories nil)
:graphql {:token (-> db :user)
:query-obj {:venia/queries [[:expense_account_stats
{:client-id (:id @(re-frame/subscribe [::subs/client]))}
[[:account [:id :name]] :total]]
[:invoice_stats
{:client-id (:id @(re-frame/subscribe [::subs/client]))}
[:name :paid :unpaid]]
[:cash-flow
{:client-id (:id @(re-frame/subscribe [::subs/client]))}
[:beginning-balance
:outstanding-payments
[: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]}]
[:a.button {:class (when (= value (:cash-flow-range chart-options))
["is-info" "is-selected"])
: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 [[i {:keys [date days-until type name amount] }] (map vector (range) (:data page))]
^{:key i}
[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])]
^{:key client-id}
[side-bar-layout {:side-bar [:div]
:main [:div [:h1.title "Home"]
[:h1.title.is-4 "Top expense categories"]
(let [expense-categories @(re-frame/subscribe [::top-expense-categories])]
(make-pie-chart {:width 800 :height 500 :data (clj->js
(map (fn [x] {:name (:name (:account x)) :value (:total x)}) expense-categories))}))
[:h1.title.is-4 "Upcoming Bills"]
(make-bar-chart {:width 800 :height 500 :data (clj->js
@(re-frame/subscribe [::invoice-stats]))})
[:h1.title.is-4 "Cash Flow"]
[:div.buttons.has-addons
[cash-flow-range-button {:name "7 days"
:value :seven-days
:chart-options chart-options}]
[cash-flow-range-button {:name "30 days"
:value :thirty-days
:chart-options chart-options}]
[cash-flow-range-button {:name "60 days"
:value :sixty-days
:chart-options chart-options}]
[cash-flow-range-button {:name "90 days"
:value :ninety-days
:chart-options chart-options}]
[cash-flow-range-button {:name "120 days"
:value :one-hundred-twenty-days
:chart-options chart-options}]
[cash-flow-range-button {:name "150 days"
:value :one-hundred-fifty-days
:chart-options chart-options}]
[cash-flow-range-button {:name "180 days"
:value :one-hundred-eighty-days
:chart-options chart-options}]]
(make-cash-flow-chart {:width 800 :height 500
:data (clj->js @(re-frame/subscribe [::cash-flow]))})
[cash-flow-grid]]}]))
(defn home-page []
(let [client-id (-> @(re-frame/subscribe [::subs/client]) :id)]
(re-frame/dispatch [::mounted])
^{:key client-id} [home-content]))