Files
integreat/src/cljs/auto_ap/views/pages/ledger/balance_sheet.cljs
2024-10-16 15:37:22 -07:00

312 lines
15 KiB
Clojure

(ns auto-ap.views.pages.ledger.balance-sheet
(:require
[auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.ledger.reports :as l-reports]
[auto-ap.status :as status]
[auto-ap.subs :as subs]
[auto-ap.views.components :as com]
[auto-ap.views.components.layouts
:refer [appearing-side-bar side-bar-layout]]
[auto-ap.views.components.modal :as modal]
[auto-ap.views.pages.data-page :as data-page]
[auto-ap.views.pages.ledger.report-table :as rtable]
[auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]]
[auto-ap.views.pages.ledger.table :as ledger-table]
[auto-ap.views.components.buttons :as buttons]
[auto-ap.views.utils
:refer [date-picker dispatch-event local-now with-user date->str standard]]
[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]))
(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)
:date-range (:date-range params)}))
(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 (:balance-sheet data)))))
(re-frame/reg-event-fx
::change
[with-user (forms/in-form ::form)]
(fn [{:keys [db]} [_ & event]]
{:db (dissoc db :report)
:dispatch-n [(into [::forms/change ::form] event)
[::ledger-list-closing]]}))
(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) (re-frame/inject-cofx ::inject/sub [::subs/client])]
(fn [{:keys [db user ::subs/client]} [_]]
{:db (dissoc db :report)
:graphql {:token user
:query-obj {:venia/queries [[:balance-sheet
(-> (:data db)
(dissoc :clients)
(assoc :client-ids (map (comp :id :client) (:clients (:data db)))))
[[:balance-sheet-accounts [:name :amount :account-type :id :numeric-code :client-id]]
[:comparable-balance-sheet-accounts [:name :amount :account-type :id :client-id :numeric-code]]]]]}
:owns-state {:single ::page}
:on-success [::received]}}))
(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/client])]
(fn [{:keys [::subs/client]} [_ result]]
{:dispatch [::modal/modal-requested {:title "Your report is ready"
:body [:div
[:div "Click "
[:a {:href (-> result :balance-sheet-pdf :url) :target "_new"} "here"] " to view it."]
(when (and (seq (:emails client)) @(re-frame/subscribe [::subs/is-admin?]))
[:div "Once you've confirmed you're happy with it, click "
[:a {:href (str "mailto:" (str/join ";" (map :email (:emails client))) "?body=" (email-body (-> result :balance-sheet-pdf :url))
"&subject=" (-> result :balance-sheet-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) ")"))
(:emails client))) "."])]}]}))
(re-frame/reg-event-fx
::export-pdf
[with-user (forms/in-form ::form) (re-frame/inject-cofx ::inject/sub [::subs/client])]
(fn [{:keys [db user ::subs/client]} [_]]
{:db (dissoc db :report)
:graphql {:token user
:query-obj {:venia/queries [[:balance-sheet-pdf
(-> (:data db)
(dissoc :clients)
(assoc :client-ids (map (comp :id :client) (:clients (:data db)))))
[:url :name]]]}
:owns-state {:single ::page}
:on-success [::received-pdf]}}))
(re-frame/reg-event-fx
::investigate-clicked
(fn [{:keys [db]} [_ {:keys [numeric-code date-range client-id]}]]
{:db (-> db (assoc ::ledger-list-active? true))
:dispatch [::data-page/additional-params-changed ::ledger {:client-id client-id
:numeric-code numeric-code
:date-range {:start "2000-01-01"
:end (date->str date-range standard)}}]}))
(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
: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
::unmounted-balance-sheet
(fn [{:keys [db]} _]
{
:db (dissoc db ::ledger-list-active?)
:dispatch [::data-page/dispose ::ledger]
::track/dispose {:id ::ledger-params}}))
(re-frame/reg-event-fx
::mounted-balance-sheet
(fn [{:keys [db]} _]
{:db (forms/start-form db ::form {:date (local-now)
:comparison-date (t/minus (local-now) (t/years 1))
:clients (mapv (fn [c] {:client c :id (random-uuid)})
[(some-> @(re-frame/subscribe [::subs/client]) (select-keys [:name :id]) )])
:include-comparison false})
::track/register {:id ::ledger-params
:subscription [::data-page/params ::ledger]
:event-fn (fn [params] [::ledger-params-change params])}}))
(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 report-form []
(let [!box (atom nil)
active (reagent/atom nil)]
(fn []
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])]
[form-builder/builder {:change-event [::change]
:submit-event [::report-requested]
:id ::form}
[:div
[:div.report-controls
[:div.level
[:div.level-left
[:div.level-item
[:div.mt-5
[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
[:div.control
[form-builder/field-v2 {:field :date}
"Date"
[date-picker {:output :cljs-date}]]]]
[:div.level-item
[form-builder/field-v2 {:field :include-comparison}
[:div.mt-5]
[com/switch-input {:id "include-comparison"
:label "Include comparison"}]]]
[:div.level-item
(when (boolean (:include-comparison data))
[form-builder/field-v2 {:field :comparison-date}
"Comparison Date"
[date-picker {:output :cljs-date}]])]]
[:div.level-right
[:div.buttons
[: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 balance-sheet-report [{:keys [args report-data]}]
(let [pnl-data (concat (->> (:balance-sheet-accounts report-data)
(map (fn [b]
(assoc b
:period (:date args)
:amount (js/parseFloat (:amount b))))))
(->> (:comparable-balance-sheet-accounts report-data)
(map (fn [b]
(assoc b
:period (:comparison-date args)
:amount (js/parseFloat (:amount b)))))))
client-names (->> @(re-frame/subscribe [::subs/clients-by-id])
(map (fn [[k v]]
[k (:name v)]))
(into {}))
pnl-data (l-reports/->PNLData args pnl-data client-names)
report (l-reports/summarize-balance-sheet pnl-data)
client-count (count (set (map :client-id (:data pnl-data))))]
[:<> [:h1.title "Balance Sheet - " (str/join ", " (map client-names (set (map :client-id (:data pnl-data)))))]
(when (:warning report)
[:div.notification.is-warning.is-light
(:warning report)])
[rtable/table {:widths (cond-> (into [30 ] (repeat 13 client-count))
(:include-comparison args) (into (repeat 13 (* 2 client-count))))
:click-event ::investigate-clicked
:table report}]]))
(defn balance-sheet-content []
(let [current-client @(re-frame/subscribe [::subs/client])
status @(re-frame/subscribe [::status/single ::page])
{params :data report :report} @(re-frame/subscribe [::forms/form ::form])]
[:div.is-inline
[status/status-notification {:statuses [[::status/single ::page]]}]
[report-form]
[status/big-loader status]
(when (and (not= :loading (:state status))
report)
[balance-sheet-report {:args (assoc params
:periods (filter identity (cond-> [(:date params)]
(:include-comparison params) (conj (:comparison-date params)))))
:report-data report}])]))
(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 balance-sheet-page []
(reagent/create-class
{:display-name "balance-sheet-page"
:component-did-mount #(re-frame/dispatch [::mounted-balance-sheet])
:component-will-unmount #(re-frame/dispatch [::unmounted-balance-sheet])
: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 [balance-sheet-content]
:right-side-bar [appearing-side-bar
{:visible? ledger-list-active?}
[ledger-list]]}]
[:div "Not authorized"])))}))