348 lines
15 KiB
Clojure
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]))
|