diff --git a/project.clj b/project.clj index 418796a8..722a3e6d 100644 --- a/project.clj +++ b/project.clj @@ -18,6 +18,7 @@ [dk.ative/docjure "1.12.0"] [org.clojure/java.jdbc "0.7.3"] [cljsjs/dropzone "4.3.0-0"] + [cljsjs/recharts "1.4.2-0"] [clj-fuzzy "0.4.1"] [honeysql "0.9.2"] [com.walmartlabs/lacinia "0.25.0"] @@ -47,6 +48,7 @@ [com.amazonaws/aws-java-sdk-sqs "1.11.282"] [com.amazonaws/aws-java-sdk-s3 "1.11.282"] [org.clojure/data.json "0.2.6"] + [org.clojure/data.csv "0.1.4"] [cider/cider-nrepl "0.16.0"] [io.rkn/conformity "0.5.1"] [hiccup "1.0.5"]] @@ -74,7 +76,6 @@ [javax.servlet/servlet-api "2.5"] [figwheel-sidecar "0.5.13"] [com.cemerick/piggieback "0.2.2"] - [org.clojure/data.csv "0.1.4"] ] :plugins [[lein-figwheel "0.5.13"] diff --git a/src/clj/auto_ap/datomic.clj b/src/clj/auto_ap/datomic.clj index cfea0651..21e9c5c2 100644 --- a/src/clj/auto_ap/datomic.clj +++ b/src/clj/auto_ap/datomic.clj @@ -749,3 +749,15 @@ (defn migrate-users [conn] [(load-users (users/get-all))]) + +(defn merge-query [query-part-1 query-part-2] + (-> query-part-1 + (update-in [:query :find] into (get-in query-part-2 [:query :find])) + (update-in [:query :in] into (get-in query-part-2 [:query :in])) + (update-in [:query :where] into (get-in query-part-2 [:query :where])) + (update-in [:args] into (get-in query-part-2 [:args])))) + + + + + diff --git a/src/clj/auto_ap/datomic/migrate.clj b/src/clj/auto_ap/datomic/migrate.clj index f2ca3ea1..62f946d1 100644 --- a/src/clj/auto_ap/datomic/migrate.clj +++ b/src/clj/auto_ap/datomic/migrate.clj @@ -69,3 +69,4 @@ (d/release conn) (println "Done"))) #_(-main) + diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index fd928c80..7e3e480a 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -8,6 +8,9 @@ [buddy.auth :refer [throw-unauthorized]] [auto-ap.utils :refer [by]] [auto-ap.graphql.utils :refer [assert-admin can-see-client? assert-can-see-client]] + [auto-ap.datomic :refer [uri merge-query]] + [datomic.api :as d] + [auto-ap.expense-accounts :as e-expense-accounts] [auto-ap.datomic.clients :as d-clients] [auto-ap.datomic.checks :as d-checks] [auto-ap.datomic.users :as d-users] @@ -199,12 +202,31 @@ :start {:type 'Int} :end {:type 'Int}}} :check_result {:fields {:invoices {:type '(list :invoice)} - :pdf_url {:type 'String}}}} + :pdf_url {:type 'String}}} + + :expense_account_stat {:fields {:expense_account_id {:type 'Int} + :expense_account_name {:type 'String} + :total {:type 'String}}} + + :invoice_stat {:fields {:name {:type 'String} + :paid {:type 'String} + :unpaid {:type 'String}}} + } + + :queries - {:invoice_page {:type '(list :invoice_page) + {:expense_account_stats {:type '(list :expense_account_stat) + :args {:client_id {:type :id}} + :resolve :get-expense-account-stats} + + :invoice_stats {:type '(list :invoice_stat) + :args {:client_id {:type :id}} + :resolve :get-invoice-stats} + + :invoice_page {:type '(list :invoice_page) :args {:import_status {:type 'String} :status {:type 'String} :client_id {:type :id} @@ -465,6 +487,61 @@ (:bank_account_id args) (:type args)))) +(defn get-expense-account-stats [context {:keys [client_id] } value] + (let [result (cond-> {:query {:find ['?expense-account-id '(sum ?amount)] + :in ['$] + :where []} + :args [(d/db (d/connect uri)) client_id]} + client_id (merge-query {:query {:in ['?c]} + + :args [client_id]}) + (not client_id) (merge-query {:query {:where ['[?c :client/name]]}}) + + true (merge-query {:query {:where ['[?i :invoice/client ?c] + '[?i :invoice/expense-accounts ?expense-account] + '[?expense-account :invoice-expense-account/expense-account-id ?expense-account-id] + '[?expense-account :invoice-expense-account/amount ?amount]]}}) + true (doto println) + + true (d/query ))] + (for [[expense-account-id total] result] + {:expense_account_id expense-account-id :total total :expense_account_name (-> expense-account-id e-expense-accounts/expense-accounts :name)}))) + +(defn categorize [x] + (cond (<= x 0) :due + (<= x 30 ) :due-30 + (<= x 60 ) :due-60 + :else :due-later)) + +(defn get-invoice-stats [context {:keys [client_id] } value] + (let [result (cond-> {:query {:find ['?name '(sum ?outstanding-balance) '(sum ?total)] + :in ['$] + :where []} + :args [(d/db (d/connect uri)) client_id]} + client_id (merge-query {:query {:in ['?c]} + :args [client_id]}) + (not client_id) (merge-query {:query {:where ['[?c :client/name]]}}) + + true (merge-query {:query {:where ['[?i :invoice/client ?c] + '[?i :invoice/outstanding-balance ?outstanding-balance] + '[?i :invoice/total ?total] + '[?i :invoice/date ?date] + '[(.toInstant ^java.util.Date ?date) ?d2] + '[(.between java.time.temporal.ChronoUnit/DAYS (java.time.Instant/now) ?d2 ) ?d3] + '[(+ 30 ?d3) ?d4] + '[(auto-ap.graphql/categorize ?d4) ?name]]}}) + + true (d/query )) + result (group-by first result)] + + (for [[id name] [[:due "Due"] [:due-30 "0-30 days"] [:due-60 "31-60 days"] [:due-later ">60 days"] ] + :let [[[_ outstanding-balance total] ] (id result nil) + outstanding-balance (or outstanding-balance 0) + total (or total 0)]] + {:name name :unpaid outstanding-balance :paid (if (= :due id) + 0 + (- total outstanding-balance))}))) + (def schema @@ -474,6 +551,8 @@ :get-all-payments get-all-payments :get-payment-page gq-checks/get-payment-page :get-transaction-page gq-transactions/get-transaction-page + :get-expense-account-stats get-expense-account-stats + :get-invoice-stats get-invoice-stats :get-client gq-clients/get-client :get-user get-user diff --git a/src/cljs/auto_ap/core.cljs b/src/cljs/auto_ap/core.cljs index 9d7036b8..2fe65a2a 100644 --- a/src/cljs/auto_ap/core.cljs +++ b/src/cljs/auto_ap/core.cljs @@ -8,7 +8,8 @@ [auto-ap.effects :as effects] [pushy.core :as pushy] [auto-ap.history :as p] - [bidi.bidi :as bidi])) + [bidi.bidi :as bidi] + [cljsjs.recharts])) (defn dev-setup [] (when true diff --git a/src/cljs/auto_ap/events.cljs b/src/cljs/auto_ap/events.cljs index e90f98fb..8e8e6a43 100644 --- a/src/cljs/auto_ap/events.cljs +++ b/src/cljs/auto_ap/events.cljs @@ -18,6 +18,7 @@ ::initialize-db (fn [{:keys [db]} [_ token]] (let [handler (:handler (bidi/match-route routes/routes (.. js/window -location -pathname)))] + (prn (and token (jwt->data token))) (cond (and (not= :login handler) (not token)) {:redirect "/login" diff --git a/src/cljs/auto_ap/views/main.cljs b/src/cljs/auto_ap/views/main.cljs index bb702846..0395432e 100644 --- a/src/cljs/auto_ap/views/main.cljs +++ b/src/cljs/auto_ap/views/main.cljs @@ -14,6 +14,7 @@ [auto-ap.views.pages.login :refer [login-page]] [auto-ap.views.pages.checks :refer [checks-page]] [auto-ap.views.pages.admin :refer [admin-page]] + [auto-ap.views.pages.home :refer [home-page]] [auto-ap.views.pages.admin.clients :refer [admin-clients-page]] [auto-ap.views.pages.admin.vendors :refer [admin-vendors-page]] [auto-ap.views.pages.admin.excel-import :refer [admin-excel-import-page]] @@ -59,8 +60,8 @@ (admin-yodlee-page)) (defmethod page :index [_] - [side-bar-layout {:side-bar [:div] - :main [:h1 "Home"]}]) + (home-page) + ) (defmethod page :login [_] [login-page]) diff --git a/src/cljs/auto_ap/views/pages/home.cljs b/src/cljs/auto_ap/views/pages/home.cljs new file mode 100644 index 00000000..2889cc63 --- /dev/null +++ b/src/cljs/auto_ap/views/pages/home.cljs @@ -0,0 +1,110 @@ +(ns auto-ap.views.pages.home + (:require [auto-ap.views.components.layouts :refer [side-bar-layout]] + [re-frame.core :as re-frame] + [auto-ap.subs :as subs] + [reagent.core :as r])) + + +(def pie-chart (r/adapt-react-class js/Recharts.PieChart)) +(def pie (r/adapt-react-class js/Recharts.Pie)) +(def bar-chart (r/adapt-react-class js/Recharts.BarChart)) +(def x-axis (r/adapt-react-class js/Recharts.XAxis)) +(def y-axis (r/adapt-react-class js/Recharts.YAxis)) +(def bar (r/adapt-react-class js/Recharts.Bar)) +(def legend (r/adapt-react-class js/Recharts.Legend)) +(def cell (r/adapt-react-class js/Recharts.Cell)) +(def tool-tip (r/adapt-react-class js/Recharts.Tooltip)) + +(def colors ["hsl(171, 100%, 41%)" "hsl(217, 71%, 53%)" "hsl(141, 71%, 48%)" "hsl(48, 100%, 67%)" "hsl(348, 100%, 61%)" "hsl(217, 71%, 53%)"]) +(def light-colors ["hsl(171, 60%, 80%)" "hsl(217, 71%, 53%)" "hsl(141, 71%, 48%)" "hsl(48, 100%, 67%)" "hsl(348, 100%, 61%)" "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]] + ) + +(re-frame/reg-event-db + ::received + (fn [db [_ {:keys [expense-account-stats invoice-stats]}]] + (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 {:expense-account-id 0 :expense-account-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) + + + ) + ))) + +(re-frame/reg-sub + ::invoice-stats + (fn [db] + (println (::invoice-stats db)) + (::invoice-stats db))) + +(re-frame/reg-sub + ::top-expense-categories + (fn [db] + (::top-expense-categories db))) + +(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]))} + [:expense-account-id :total :expense-account-name]] + [:invoice_stats + {:client-id (:id @(re-frame/subscribe [::subs/client]))} + [:name :paid :unpaid]]]} + :on-success [::received]}})) + +(defn home-content [] + (let [client-id (-> @(re-frame/subscribe [::subs/client]) :id)] + ^{: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 (:expense-account-name 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]))})]}])) + + +(defn home-page [] + (let [client-id (-> @(re-frame/subscribe [::subs/client]) :id)] + (re-frame/dispatch [::mounted]) + ^{:key client-id} [home-content]))