490 lines
25 KiB
Clojure
490 lines
25 KiB
Clojure
(ns auto-ap.views.pages.ledger.cash-flows
|
|
(:require
|
|
[auto-ap.forms :as forms]
|
|
[auto-ap.ledger.reports :as l-reports]
|
|
[auto-ap.status :as status]
|
|
[auto-ap.subs :as subs]
|
|
[auto-ap.views.pages.ledger.report-table :as rtable]
|
|
[auto-ap.views.components.buttons :as buttons]
|
|
[auto-ap.views.components.layouts
|
|
:refer [appearing-side-bar side-bar-layout]]
|
|
[auto-ap.views.components.modal :as modal]
|
|
[auto-ap.views.components.switch-field :refer [switch-field]]
|
|
[auto-ap.views.pages.data-page :as data-page]
|
|
[auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]]
|
|
[auto-ap.views.pages.ledger.table :as ledger-table]
|
|
[auto-ap.views.utils
|
|
:refer [date->str
|
|
date-picker
|
|
dispatch-event
|
|
local-today
|
|
query-params
|
|
standard
|
|
str->date
|
|
with-user]]
|
|
[cljs-time.core :as t]
|
|
[clojure.set :as set]
|
|
[clojure.string :as str]
|
|
[re-frame.core :as re-frame]
|
|
[react-dom :as react-dom]
|
|
[reagent.core :as reagent]
|
|
[vimsical.re-frame.cofx.inject :as inject]
|
|
[vimsical.re-frame.fx.track :as track]
|
|
[auto-ap.forms.builder :as form-builder]
|
|
[auto-ap.views.components :as com]))
|
|
|
|
|
|
|
|
(defn and-last-year [{:keys [start end] :as p1}]
|
|
[p1
|
|
{:start (t/minus start (t/years 1))
|
|
:end (t/minus end (t/years 1))}])
|
|
|
|
(defn encode-period [p]
|
|
{:start (date->str (:start p) standard)
|
|
:end (date->str (:end p) standard)})
|
|
|
|
|
|
;; SUBS
|
|
|
|
(re-frame/reg-sub
|
|
::ledger-list-active?
|
|
(fn [db]
|
|
(-> db ::ledger-list-active?)))
|
|
|
|
(re-frame/reg-event-db
|
|
::received
|
|
[(forms/in-form ::form)]
|
|
(fn [db [_ data]]
|
|
(-> db (assoc :report (:profit-and-loss data)))))
|
|
|
|
;; EVENTS
|
|
|
|
(re-frame/reg-event-db
|
|
::ledger-list-closing
|
|
(fn [db]
|
|
(assoc db ::ledger-list-active? false)))
|
|
|
|
(re-frame/reg-event-fx
|
|
::report-requested
|
|
[with-user (forms/in-form ::form)]
|
|
(fn [{:keys [db user]}]
|
|
(cond-> {:graphql {:token user
|
|
:owns-state {:single ::page}
|
|
:query-obj {:venia/queries [[:profit-and-loss
|
|
{:client-ids (map (comp :id :client) (:clients (:data db)))
|
|
:periods (mapv #(select-keys % #{:start :end} ) (:periods (:data db)))
|
|
:include-deltas (:include-deltas (:data db))
|
|
:column-per-location (:column-per-location (:data db))}
|
|
[[:periods [[:accounts [:name :amount :debits :credits :client_id :account-type :id :count :numeric-code :location]]]]]]]}
|
|
:on-success [::received]}
|
|
:set-uri-params {:periods (mapv
|
|
encode-period
|
|
(:periods (:data db)))
|
|
:clients (mapv #(select-keys (:client %) [:name :id]) (:clients (:data db))) }
|
|
:db (-> db
|
|
(dissoc :report)
|
|
(update-in [:data :clients] #(into [] (filter seq %))))})))
|
|
|
|
(defn email-body [report-url]
|
|
(js/encodeURIComponent
|
|
(str
|
|
"Hello,
|
|
Click here (" report-url ") to download your financial reports. We have not finished reviewing and reconciling these numbers with you. Please review and let us know if anything seems missing or in need of correction.
|
|
Click here (http://app.integreatconsult.com/) to login to the Financials app to review the details here.
|
|
Click here (https://share.vidyard.com/watch/MHTo5PyXPxXUpVH93RWFM9?) for a video on how to run a P&L on your own.
|
|
To see a history of past financial reports, click here: https://app.integreatconsult.com/reports/
|
|
|
|
NOTE: Please review the transactions we may have question for you here: https://app.integreatconsult.com/transactions/requires-feedback. You can either edit the transaction to what expense account it should be or email back what it should be.")))
|
|
|
|
(re-frame/reg-event-fx
|
|
::received-pdf
|
|
[(re-frame/inject-cofx ::inject/sub [::subs/clients-by-id])]
|
|
(fn [{:keys [db ::subs/clients-by-id]} [_ result]]
|
|
(let [selected-clients (-> db ::forms/forms ::form :data :clients)
|
|
single-client? (= (count selected-clients)
|
|
1)
|
|
client-emails (when single-client?
|
|
(-> clients-by-id
|
|
(get (:id (:client (first selected-clients))))
|
|
:emails))]
|
|
{:dispatch [::modal/modal-requested {:title "Your report is ready"
|
|
:body [:div
|
|
[:div "Click "
|
|
[:a {:href (-> result :profit-and-loss-pdf :url) :target "_new"} "here"] " to view it."]
|
|
(when (and single-client? (seq client-emails))
|
|
[:div "Once you've confirmed you're happy with it, click "
|
|
[:a {:href (str "mailto:" (str/join ";" (map :email client-emails)) "?body=" (email-body (-> result :profit-and-loss-pdf :url))
|
|
"&subject=" (-> result :profit-and-loss-pdf :name) " is ready")}
|
|
"here"] " to open your email client and to send it to " (str/join "," (map (fn [e]
|
|
(str (:email e) " (" (:description e) ")"))
|
|
client-emails)) "."])]}]})))
|
|
(re-frame/reg-event-fx
|
|
::export-pdf
|
|
[with-user (forms/in-form ::form)]
|
|
(fn [{:keys [db user]}]
|
|
(cond-> {:graphql {:token user
|
|
:owns-state {:single ::page}
|
|
:query-obj {:venia/queries [[:profit-and-loss-pdf
|
|
{:client-ids (map (comp :id :client) (:clients (:data db)))
|
|
:include-deltas (:include-deltas (:data db))
|
|
:column-per-location (:column-per-location (:data db))
|
|
:periods (mapv #(select-keys % #{:start :end}) (:periods (:data db)))}
|
|
[:url :name]]]}
|
|
:on-success [::received-pdf]}
|
|
:set-uri-params {:periods (mapv encode-period (:periods (:data db)))
|
|
:clients (mapv #(select-keys (:client %) [:name :id]) (:clients (:data db))) }
|
|
:db (dissoc db :report)})))
|
|
|
|
|
|
(re-frame/reg-event-db
|
|
::change-internal
|
|
(forms/change-handler ::form
|
|
(fn [_ field value]
|
|
(cond
|
|
(= [:periods] field)
|
|
[field value]
|
|
|
|
(and (= :periods (first field))
|
|
(= 2 (count field)))
|
|
[field value] ;;already serialized
|
|
|
|
(= :periods (first field)) [field value ]
|
|
|
|
:else nil)
|
|
)))
|
|
|
|
(re-frame/reg-event-fx
|
|
::change
|
|
[with-user (forms/in-form ::form)]
|
|
(fn [{:keys [db]} [_ & event]]
|
|
{:db (dissoc db :report)
|
|
:dispatch-n [(into [::change-internal] event)
|
|
[::ledger-list-closing]]}))
|
|
|
|
|
|
(defn data-params->query-params [params]
|
|
(when params
|
|
{:start (:start params 0)
|
|
:sort (:sort params)
|
|
:per-page (:per-page params)
|
|
:vendor-id (:id (:vendor params))
|
|
:client-id (:client-id params)
|
|
:numeric-code (:numeric-code params)
|
|
:location (:location params)
|
|
:date-range (:date-range params)}))
|
|
|
|
(re-frame/reg-event-fx
|
|
::ledger-params-change
|
|
[with-user]
|
|
(fn [{:keys [user]} [_ ledger-params]]
|
|
(when (seq ledger-params)
|
|
{:graphql {:token user
|
|
:owns-state {:single [::data-page/page ::ledger]}
|
|
:query-obj {:venia/queries [[:ledger-page
|
|
{:filters (data-params->query-params ledger-params)}
|
|
[[:journal-entries [:id
|
|
:source
|
|
:original-entity
|
|
:note
|
|
:amount
|
|
:alternate-description
|
|
[:vendor
|
|
[:name :id]]
|
|
[:client
|
|
[:name :id]]
|
|
[:line-items
|
|
[:id :debit :credit :location :running-balance
|
|
[:account [:id :name]]]]
|
|
:date]]
|
|
:total
|
|
:start
|
|
:end]]]}
|
|
:on-success (fn [result]
|
|
[::data-page/received ::ledger (set/rename-keys (:ledger-page result)
|
|
{:journal-entries :data})])}})))
|
|
|
|
(re-frame/reg-event-fx
|
|
::investigate-clicked
|
|
(fn [{:keys [db]} [_ {:keys [location numeric-code client-id]
|
|
{:keys [start end]} :date-range}]]
|
|
{:db (-> db (assoc ::ledger-list-active? true))
|
|
:dispatch [::data-page/additional-params-changed ::ledger {:client-id client-id
|
|
:numeric-code numeric-code
|
|
:location location
|
|
:date-range {:start (date->str start standard)
|
|
:end (date->str end standard)}}]}))
|
|
|
|
|
|
(re-frame/reg-sub
|
|
::can-submit
|
|
(fn [_]
|
|
true))
|
|
|
|
|
|
(defn report-control-detail [{:keys [active box which]} children]
|
|
(when (and @box
|
|
(= which @active))
|
|
(react-dom/createPortal (reagent/as-element
|
|
[:div.notification.is-light
|
|
[:a.delete {:on-click (fn [] (reset! active nil))}]
|
|
children
|
|
])
|
|
@box)))
|
|
|
|
(defn period-preset-button [{:keys [title periods]}]
|
|
(let [{{:keys [selected-preset]} :data} @(re-frame/subscribe [::forms/form ::form])]
|
|
[:div.control
|
|
[:a.button
|
|
{:class (when (= selected-preset title) "is-active")
|
|
:on-click (fn []
|
|
(re-frame/dispatch-sync [::change
|
|
[:periods]
|
|
periods
|
|
[:selected-preset] title])
|
|
(re-frame/dispatch-sync [::change
|
|
[:show-advanced?]
|
|
false]))}
|
|
title]]))
|
|
|
|
(defn report-controls []
|
|
(let [!box (atom nil)
|
|
active (reagent/atom nil)]
|
|
(fn []
|
|
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])
|
|
{:keys [selected-preset include-deltas column-per-location]} data]
|
|
[form-builder/builder {:can-submit [::can-submit]
|
|
:change-event [::change]
|
|
:submit-event [::report-requested]
|
|
:id ::form}
|
|
[:div.report-controls
|
|
[:div.level.mb-2
|
|
[:div.level-left
|
|
[:div.level-item
|
|
[buttons/dropdown {:on-click (fn [] (reset! active :clients))}
|
|
[:span (str "Companies"
|
|
(when-let [clients (:clients data)]
|
|
(str " (" (str/join ", " (map (comp :name :client) clients)) ")")))]]
|
|
[report-control-detail {:active active :box !box :which :clients}
|
|
[:div {:style {:width "20em"}}
|
|
[:h4.subtitle "Companies"]
|
|
[form-builder/raw-field-v2 {:field :clients}
|
|
[com/multi-field-v2 {:new-text "Add another company"
|
|
:template [[form-builder/raw-field-v2 {:field :client}
|
|
[com/entity-typeahead {:entities @(re-frame/subscribe [::subs/clients])
|
|
:style {:width "18em"}
|
|
:entity->text :name}]]]
|
|
:key-fn :id}]]]]]
|
|
[:div.level-item
|
|
[buttons/dropdown {:on-click (fn [] (reset! active :range))}
|
|
[:span (str "Range"
|
|
(when selected-preset
|
|
(str " (" selected-preset ")")))]]
|
|
[report-control-detail {:active active :box !box :which :range}
|
|
[:div
|
|
[:h4.subtitle "Range"]
|
|
[:div.field.is-grouped
|
|
[:div.control
|
|
[:div.field.has-addons
|
|
[:div.control
|
|
[form-builder/raw-field-v2 {:field :thirteen-periods-end}
|
|
[date-picker {:placeholder "End date"
|
|
:output :cljs-date}]]]
|
|
[period-preset-button {:title "13 periods"
|
|
:periods (let [today (or (some-> (:thirteen-periods-end data))
|
|
(local-today))]
|
|
(into
|
|
[{:start (t/plus (t/minus today (t/weeks (* 13 4)))
|
|
(t/days 1))
|
|
:end today
|
|
:title "Total"}]
|
|
(for [i (range 13)]
|
|
{:start (t/plus (t/minus today (t/weeks (* (inc i) 4)))
|
|
(t/days 1))
|
|
:end (t/minus today (t/weeks (* i 4)))})))}]]]
|
|
|
|
[:div.control
|
|
[:div.field.has-addons
|
|
[:div.control
|
|
[form-builder/raw-field-v2 {:field :twelve-periods-end}
|
|
[date-picker {:placeholder "End date"
|
|
:output :cljs-date}]]]
|
|
[period-preset-button {:title "12 months"
|
|
:periods (let [end-date (or (some-> (:twelve-periods-end data))
|
|
(local-today))
|
|
this-month (t/local-date (t/year end-date)
|
|
(t/month end-date)
|
|
1)]
|
|
(into
|
|
[{:start (t/minus this-month (t/months 11))
|
|
:end (t/minus (t/plus this-month (t/months 1))
|
|
(t/days 1))
|
|
:title "Total"}]
|
|
(for [i (range 12)]
|
|
{:start (t/minus this-month (t/months (- 11 i)))
|
|
:end (t/minus (t/minus this-month (t/months (- 10 i)))
|
|
(t/days 1))})))}]]]
|
|
|
|
[period-preset-button {:periods (let [last-sunday (loop [current (local-today)]
|
|
(if (= 7 (t/day-of-week current))
|
|
current
|
|
(recur (t/minus current (t/period :days 1)))))]
|
|
(and-last-year {:start (t/minus last-sunday (t/period :days 6))
|
|
:end last-sunday}))
|
|
:title "Last week"}]
|
|
|
|
[period-preset-button {:periods (and-last-year {:start (loop [current (local-today)]
|
|
(if (= 1 (t/day-of-week current))
|
|
current
|
|
(recur (t/minus current (t/period :days 1)))))
|
|
:end (local-today)})
|
|
:title "Week to date"}]
|
|
|
|
[period-preset-button {:periods (and-last-year {:start (t/minus (t/local-date (t/year (local-today))
|
|
(t/month (local-today))
|
|
1)
|
|
(t/period :months 1))
|
|
:end (t/minus (t/local-date (t/year (local-today))
|
|
(t/month (local-today))
|
|
1)
|
|
(t/period :days 1))})
|
|
:title "Last month"}]
|
|
|
|
[period-preset-button {:periods (and-last-year {:start (t/local-date (t/year (local-today))
|
|
(t/month (local-today))
|
|
1)
|
|
:end (local-today)})
|
|
:title "Month to date"}]
|
|
|
|
[period-preset-button {:periods (and-last-year {:start (t/local-date (t/year (local-today)) 1 1)
|
|
:end
|
|
(local-today)})
|
|
:title "Year to date"}]
|
|
|
|
[period-preset-button {:periods [{:start (t/local-date (dec (t/year (local-today))) 1 1)
|
|
:end (t/local-date (dec (t/year (local-today))) 12 31)}]
|
|
:title "Last calendar year"}]
|
|
|
|
[period-preset-button {:periods (and-last-year {:start (t/plus (t/minus (local-today) (t/period :years 1))
|
|
(t/period :days 1))
|
|
:end (local-today)})
|
|
:title "Full year"}]]
|
|
[:div
|
|
[form-builder/raw-field-v2 {:field :show-advanced?}
|
|
[com/checkbox {:label "Show Advanced"}]]]
|
|
(when (:show-advanced? data)
|
|
[form-builder/raw-field-v2 {:field :periods}
|
|
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :start}
|
|
[date-picker {:output :cljs-date}]]
|
|
[form-builder/raw-field-v2 {:field :end}
|
|
[date-picker {:output :cljs-date}]]]}]])]]]
|
|
|
|
|
|
]
|
|
[:div.level-right
|
|
[:div.buttons
|
|
|
|
(when @(re-frame/subscribe [::subs/is-admin?])
|
|
[:button.button.is-secondary {:on-click (dispatch-event [::export-pdf])} "Export"])
|
|
[:button.button.is-primary "Run"]]
|
|
|
|
]]
|
|
[:div.report-control-detail {:ref (fn [el]
|
|
(when (not= @!box el)
|
|
(reset! !box el)))}]]]))))
|
|
|
|
|
|
|
|
(defn pnl-report [{:keys [args report-data]}]
|
|
(let [pnl-data (->> report-data
|
|
:periods
|
|
(mapcat (fn [p1 p2]
|
|
(map
|
|
(fn [a]
|
|
(assoc a :period p1
|
|
:amount (js/parseFloat (:amount a))
|
|
:debits (js/parseFloat (:debits a))
|
|
:credits (js/parseFloat (:credits a)))
|
|
)
|
|
(:accounts p2)))
|
|
(:periods args)))
|
|
client-codes (->> @(re-frame/subscribe [::subs/clients-by-id])
|
|
(map (fn [[k v]]
|
|
[k (:code v)]))
|
|
(into {}))
|
|
pnl-data (l-reports/->PNLData args pnl-data client-codes)
|
|
report (l-reports/summarize-cash-flows pnl-data)
|
|
table (rtable/concat-tables (:details report))]
|
|
[:div
|
|
[:h1.title "Profit and Loss - " (str/join ", " (map (comp :name :client) (:clients args)))]
|
|
(when (:warning report)
|
|
[:div.notification.is-warning.is-light
|
|
(:warning report)])
|
|
[rtable/table {:widths (into [20] (take (dec (rtable/cell-count table))
|
|
(mapcat identity
|
|
(repeat
|
|
[13 6]))))
|
|
:click-event ::investigate-clicked
|
|
:table table}]]))
|
|
|
|
|
|
(defn profit-and-loss-content []
|
|
(let [status @(re-frame/subscribe [::status/single ::page])
|
|
{:keys [data report]} @(re-frame/subscribe [::forms/form ::form])]
|
|
[:div
|
|
[:div
|
|
[status/status-notification {:statuses [[::status/single ::page]]}]
|
|
[report-controls]]
|
|
[status/big-loader status]
|
|
(when (and (not= :loading (:state status))
|
|
report)
|
|
[pnl-report {:report-data report
|
|
:args data}])]))
|
|
|
|
(re-frame/reg-event-fx
|
|
::unmounted-pnl
|
|
(fn [{:keys [_]} _]
|
|
{::track/dispose {:id ::ledger-params}}))
|
|
|
|
(re-frame/reg-event-fx
|
|
::mounted-pnl
|
|
(fn [{:keys [db]} _]
|
|
(let [qp (query-params)]
|
|
{:db (forms/start-form db ::form {:periods (->> qp
|
|
:periods
|
|
(mapv (fn [period]
|
|
{:start (str->date (:start period) standard)
|
|
:end (str->date (:end period) standard)})))
|
|
:clients (mapv (fn [c] {:client c :id (random-uuid)})
|
|
(or (:clients qp)
|
|
[(some-> @(re-frame/subscribe [::subs/client]) (select-keys [:name :id]) )]))
|
|
:show-advanced? false})
|
|
::track/register {:id ::ledger-params
|
|
:subscription [::data-page/params ::ledger]
|
|
:event-fn (fn [params] [::ledger-params-change params])}})))
|
|
|
|
(defn ledger-list [_ ]
|
|
[:div [:a.delete.is-pulled-right {:on-click (dispatch-event [::ledger-list-closing])}]
|
|
[:div
|
|
[:h1.title "Ledger entries"]
|
|
[ledger-table/table {:id :ledger
|
|
:data-page ::ledger}]]])
|
|
|
|
(defn cash-flows-page []
|
|
(reagent/create-class
|
|
{:display-name "profit-and-loss-page"
|
|
:component-did-mount #(re-frame/dispatch [::mounted-pnl])
|
|
:component-will-unmount #(re-frame/dispatch [::unmounted-pnl])
|
|
:reagent-render
|
|
(fn []
|
|
(let [ledger-list-active? @(re-frame/subscribe [::ledger-list-active?])
|
|
user (re-frame/subscribe [::subs/user])]
|
|
(if (not= "manager" (:user/role @user))
|
|
[side-bar-layout
|
|
{:side-bar [ledger-side-bar]
|
|
:main [profit-and-loss-content]
|
|
:right-side-bar [appearing-side-bar
|
|
{:visible? ledger-list-active?}
|
|
[ledger-list]]}]
|
|
[:div "Not authorized"])))}))
|