diff --git a/resources/ngzo.jpg b/resources/ngzo.jpg new file mode 100644 index 00000000..4a641948 Binary files /dev/null and b/resources/ngzo.jpg differ diff --git a/resources/ngzo.png b/resources/ngzo.png new file mode 100644 index 00000000..ffb76a18 Binary files /dev/null and b/resources/ngzo.png differ diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 291ef4c3..1a358861 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -11,6 +11,7 @@ [auto-ap.datomic :refer [uri merge-query]] [datomic.api :as d] [clj-time.coerce :as coerce] + [clj-time.core :as t] [auto-ap.datomic.clients :as d-clients] [auto-ap.datomic.checks :as d-checks] [auto-ap.datomic.users :as d-users] @@ -343,6 +344,10 @@ :existing {:type '(list :import_ledger_entry_result)} :errors {:type '(list :import_ledger_entry_result)} }} + + :cash_flow_result {:fields {:beginning_balance {:type :money} + :invoices_due_soon {:type '(list :invoice)} + :outstanding_payments {:type :money}}} } @@ -364,6 +369,9 @@ :args {:client_id {:type :id}} :resolve :get-invoice-stats} + :cash_flow {:type :cash_flow_result + :args {:client_id {:type :id}} + :resolve :get-cash-flow} :potential_payment_matches {:type '(list :payment) :args {:transaction_id {:type :id}} :resolve :get-potential-payments} @@ -880,7 +888,47 @@ 0 (- total outstanding-balance))}))) - +(defn get-cash-flow [context {:keys [client_id]} value] + (let [total-cash (reduce + (fn [total [credit debit]] + (- (+ total credit) + debit)) + 0.0 + (d/query {:query {:find '[?debit ?credit] + :in '[$ ?client] + :where ['[?j :journal-entry/client ?client] + '[?j :journal-entry/line-items ?je] + '[?je :journal-entry-line/account ?ba] + '[?ba :bank-account/type :bank-account-type/check] + '[(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] + :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]]} + :args [(d/db (d/connect uri)) client_id (coerce/to-date (t/plus (time/local-now) (t/days 7)))]}) + outstanding-checks (reduce + + + 0.0 + (map first (d/query {:query {:find '[?amount] + :in '[$ ?client ?due-before] + :where ['[?p :payment/client ?client] + '[?p :payment/status :payment-status/pending] + '[?p :payment/amount ?amount] + '(or + [?p :payment/type :payment-type/debit] + [?p :payment/type :payment-type/check])]} + :args [(d/db (d/connect uri)) client_id (coerce/to-date (t/plus (auto-ap.time/local-now) (t/days 7)))]})))] + {:beginning_balance total-cash + :outstanding_payments outstanding-checks + :invoices_due_soon (mapv (fn [[due outstanding]] + {:due (coerce/to-date-time due) + :outstanding_balance outstanding}) + bills-due-soon)})) (def schema (-> integreat-schema @@ -898,6 +946,7 @@ :get-transaction-rule-matches gq-transaction-rules/get-transaction-rule-matches :get-expense-account-stats get-expense-account-stats :get-invoice-stats get-invoice-stats + :get-cash-flow get-cash-flow :get-yodlee-merchants ym/get-yodlee-merchants :get-client gq-clients/get-client :get-user get-user diff --git a/src/clj/user.clj b/src/clj/user.clj index 4eb5f0ed..e4d2a000 100644 --- a/src/clj/user.clj +++ b/src/clj/user.clj @@ -227,7 +227,47 @@ @(d/transact conn txes))) (defn cash-flow-simple [] - (->> (d/query {:query {:find '[?account-type-ident ?date ?debit ?credit] + (let [total-cash (reduce + (fn [total [credit debit]] + (- (+ total credit) + debit)) + 0.0 + (d/query {:query {:find '[?debit ?credit] + :in '[$ ?client] + :where ['[?j :journal-entry/client ?c] + '[?c :client/code ?client] + '[?j :journal-entry/line-items ?je] + '[?je :journal-entry-line/account ?ba] + '[?ba :bank-account/type :bank-account-type/check] + '[(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)) "CBC"]})) + bills-due-soon (d/query {:query {:find '[?due ?outstanding] + :in '[$ ?client ?due-before] + :where ['[?i :invoice/client ?c] + '[?c :client/code ?client] + '[?i :invoice/status :invoice-status/unpaid] + '[?i :invoice/due ?due] + '[(<= ?due ?due-before)] + '[?i :invoice/outstanding-balance ?outstanding]]} + :args [(d/db (d/connect uri)) "CBC" (c/to-date (t/plus (auto-ap.time/local-now) (t/days 7)))]}) + outstanding-checks (reduce + + + 0.0 + (map first (d/query {:query {:find '[?amount] + :in '[$ ?client ?due-before] + :where ['[?p :payment/client ?c] + '[?c :client/code ?client] + '[?p :payment/status :payment-status/pending] + '[?p :payment/amount ?amount] + '(or + [?p :payment/type :payment-type/debit] + [?p :payment/type :payment-type/check])]} + :args [(d/db (d/connect uri)) "CBC" (c/to-date (t/plus (auto-ap.time/local-now) (t/days 7)))]})))] + [total-cash bills-due-soon outstanding-checks]) + + + #_(->> (d/query {:query {:find '[?account-type-ident ?date ?debit ?credit] :in '[$ ?client] :where ['[?j :journal-entry/line-items ?je] '[?j :journal-entry/date ?date] @@ -253,3 +293,9 @@ credit))) (update-in [year-month account-type :count] #(inc (or % 0))))))) {}))) + +(defn attach-signature [client-code filename] + @(d/transact (d/connect uri) + [{:db/id [:client/code client-code] + :client/signature-file (str "https://s3.amazonaws.com/integreat-signature-images/" filename)}])) + diff --git a/src/cljs/auto_ap/views/components/invoice_table.cljs b/src/cljs/auto_ap/views/components/invoice_table.cljs index 09ca5d4d..011f18dc 100644 --- a/src/cljs/auto_ap/views/components/invoice_table.cljs +++ b/src/cljs/auto_ap/views/components/invoice_table.cljs @@ -165,8 +165,9 @@ visible-expense-accounts @(re-frame/subscribe [::visible-expense-accounts]) selected-client @(re-frame/subscribe [::subs/client]) percentage-size (if selected-client "%50%" "33%") - is-sorted-by-vendor? (seq (filter #(= "vendor" (:sort-key %)) sort)) - _ (println is-sorted-by-vendor? sort) + is-loading? (:loading @status) + is-sorted-by-vendor? (and (= "vendor" (:sort-key (first sort))) + (not is-loading?)) [invoice-groups] (if is-sorted-by-vendor? (reduce (fn [[acc last-vendor] invoice] @@ -192,8 +193,8 @@ (for [invoices invoice-groups] ^{:key (:id (first invoices))} [:table.table.is-fullwidth - [:thead - [:tr + [:thead + [:tr (when check-boxes [:th {:style {"width" "20px"}}]) (when-not selected-client @@ -208,7 +209,9 @@ :sort-name "Vendor" :sort-key "vendor" :sort sort} - "Vendor"] + (if is-sorted-by-vendor? + (:name (:vendor (first invoices))) + "Vendor")] [sorted-column {:on-sort opc :style {:width percentage-size :cursor "pointer"} :sort-name "Invoice Number" @@ -248,20 +251,13 @@ :class "has-text-right" :sort sort} "Outstanding"] - - [:th { - :style {:width "20rem" :cursor "pointer"} - - - } + [:th {:style {:width "20rem" :cursor "pointer"}} ""]]] [:tbody - - (if (:loading @status) - [:tr + (if is-loading? + [:tr [:td {:col-span 5} [:i.fa.fa-spin.fa-spinner]]] - (for [{:keys [client payments expense-accounts invoice-number date due total outstanding-balance id vendor] :as i} invoices] ^{:key id} [row {:invoice i diff --git a/src/cljs/auto_ap/views/pages/home.cljs b/src/cljs/auto_ap/views/pages/home.cljs index c6f0537d..dbc4f6d3 100644 --- a/src/cljs/auto_ap/views/pages/home.cljs +++ b/src/cljs/auto_ap/views/pages/home.cljs @@ -2,6 +2,8 @@ (:require [auto-ap.views.components.layouts :refer [side-bar-layout]] [re-frame.core :as re-frame] [auto-ap.subs :as subs] + [cljs-time.core :as t] + [auto-ap.views.utils :refer [local-now date->str]] [reagent.core :as r])) @@ -44,9 +46,22 @@ [legend]] ) +(defn make-cash-flow-chart [{:keys [width height data] }] + (println data) + [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"}] + [bar {:dataKey "outstanding-payments" :fill (get colors 0) :stackId "a" :name "Outstanding Payments"}] + [bar {:dataKey "invoices" :fill (get colors 3) :stackId "a" :name "Invoices"}] + + [x-axis {:dataKey "name"}] + [y-axis] + [legend]] + ) + (re-frame/reg-event-db ::received - (fn [db [_ {:keys [expense-account-stats invoice-stats]}]] + (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) @@ -60,12 +75,14 @@ (assoc ::top-expense-categories (conj top-5 other)) (seq invoice-stats) - (assoc ::invoice-stats invoice-stats))))) + (assoc ::invoice-stats invoice-stats) + + (seq cash-flow) + (assoc ::cash-flow cash-flow))))) (re-frame/reg-sub ::invoice-stats (fn [db] - (println (::invoice-stats db)) (::invoice-stats db))) (re-frame/reg-sub @@ -73,6 +90,39 @@ (fn [db] (::top-expense-categories db))) +(re-frame/reg-sub + ::cash-flow + (fn [db] + (let [{:keys [outstanding-payments beginning-balance invoices-due-soon]} (::cash-flow db) + invoices-due-soon (reduce + (fn [result invoice] + (let [due (if (t/before? (:due invoice) (local-now)) + (local-now) + (:due invoice))] + (println due) + + (update result (date->str due) + (fn [r] (+ (or r 0.0) (:outstanding-balance invoice)))))) + {} + invoices-due-soon) + 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] } :as acc] day] + (let [invoices-due-today (invoices-due-soon (date->str (t/plus start-date (t/days day))) 0.0)] + (conj acc + {:name (date->str (t/plus start-date (t/days day))) + :effective-balance (- effective-balance invoices-due-today) + :invoices (- invoices-due-today)}))) + (list {:name (date->str start-date) + :effective-balance effective-balance + :invoices (- (invoices-due-soon (date->str start-date) 0.0)) + :outstanding-payments (- outstanding-payments)}) + (range 1 7)))))) + (re-frame/reg-event-fx ::mounted (fn [{:keys [db]} _] @@ -83,7 +133,10 @@ [[:account [:id :name]] :total]] [:invoice_stats {:client-id (:id @(re-frame/subscribe [::subs/client]))} - [:name :paid :unpaid]]]} + [:name :paid :unpaid]] + [:cash-flow + {:client-id (:id @(re-frame/subscribe [::subs/client]))} + [:beginning-balance :outstanding-payments [:invoices-due-soon [:due :outstanding-balance]]]]]} :on-success [::received]}})) (defn home-content [] @@ -97,7 +150,11 @@ (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]))})]}])) + @(re-frame/subscribe [::invoice-stats]))}) + + [:h1.title.is-4 "Cash Flow"] + (make-cash-flow-chart {:width 800 :height 500 + :data (clj->js @(re-frame/subscribe [::cash-flow]))})]}])) (defn home-page []