(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.history :refer [history]] [cemerick.url :as url] [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] [react])) (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 [_ 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! 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) 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 (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 (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] (fn [[params cash-flow-data]] (let [ {:keys [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 (: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 :name]] :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]))