diff --git a/src/cljs/auto_ap/views/components/buttons.cljs b/src/cljs/auto_ap/views/components/buttons.cljs index 96f05a50..1fe1b55b 100644 --- a/src/cljs/auto_ap/views/components/buttons.cljs +++ b/src/cljs/auto_ap/views/components/buttons.cljs @@ -13,6 +13,8 @@ [:span.icon [:i.fa {:class icon}]]] (r/children (r/current-component)))]) + + (defn sl-icon [{:keys [event icon class on-click] :as params}] [:a.button (cond-> params true (dissoc :event :icon) @@ -25,3 +27,14 @@ :on-click (dispatch-event event)} [:span.icon [:i.fa.fa-plus]] [:span name]]) + +(defn dropdown [{:keys [event icon class on-click] :as params}] + [:a.button (cond-> params + true (dissoc :event :icon) + (and (not on-click) + event) + (assoc :on-click (dispatch-event event))) + (conj (into + [:<>] + (r/children (r/current-component))) + [:span.icon [:i.fa.fa-chevron-down]])]) diff --git a/src/cljs/auto_ap/views/components/switch_field.cljs b/src/cljs/auto_ap/views/components/switch_field.cljs index 039742db..6d2f6eab 100644 --- a/src/cljs/auto_ap/views/components/switch_field.cljs +++ b/src/cljs/auto_ap/views/components/switch_field.cljs @@ -1,6 +1,6 @@ (ns auto-ap.views.components.switch-field) -(defn switch-field [{:keys [id label on-change checked]}] +(defn switch-field [{:keys [id label on-change checked class]}] [:<> - [:input.switch {:type "checkbox" :id id :on-change on-change :checked checked}] + [:input.switch {:type "checkbox" :id id :on-change on-change :checked checked :class class}] [:label {:for id} label]]) diff --git a/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs b/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs index 23b53ae3..d64e2890 100644 --- a/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs +++ b/src/cljs/auto_ap/views/pages/ledger/profit_and_loss.cljs @@ -8,10 +8,13 @@ [auto-ap.forms :as forms] [auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]] [auto-ap.views.components.typeahead :refer [typeahead-v3]] + [auto-ap.views.components.buttons :as buttons] + [auto-ap.views.components.switch-field :refer [switch-field]] [auto-ap.views.components.modal :as modal] [auto-ap.views.utils :refer [date->str date-picker date-picker-friendly bind-field standard pretty dispatch-event local-today ->% ->$ str->date with-user dispatch-value-change query-params]] [cljs-time.core :as t] [re-frame.core :as re-frame] + [react-dom :as react-dom] [auto-ap.status :as status] [auto-ap.views.pages.data-page :as data-page] [vimsical.re-frame.fx.track :as track] @@ -139,10 +142,6 @@ (fn [db [_ data]] (-> db (assoc :report (:profit-and-loss data))))) -(re-frame/reg-sub - ::params - (fn [db] - (-> db ::params))) (re-frame/reg-sub ::period-inputs @@ -152,7 +151,12 @@ (re-frame/reg-sub ::periods (fn [db] - (::periods db))) + (-> db ::forms/forms ::form :data :periods))) + +(re-frame/reg-sub + ::include-deltas + (fn [db] + (-> db ::forms/forms ::form :data :include-deltas))) ;; EVENTS @@ -165,24 +169,22 @@ ::params-change [with-user (forms/in-form ::form)] (fn [{:keys [db user] :as cofx}] - (let [c @(re-frame/subscribe [::subs/client])] - (cond-> {:graphql {:token user - :owns-state {:single ::page} - :query-obj {:venia/queries [[:profit-and-loss - {:client-ids (or (some-> (:id c) vector) - (some-> (:id (:client (:data db))) - vector)) - :periods (mapv (fn [[start end] ] {:start (date->str start standard) :end (date->str end standard)} ) - (:periods (:data db)))} - [[:periods [[:accounts [:name :amount :client_id :account-type :id :count :numeric-code :location]]]]]]]} - :on-success [::received]} - :set-uri-params {:periods (mapv (fn [[start end title]] - [(date->str start standard) - (date->str end standard)]) - (:periods (:data db))) - :client (select-keys (:client (:data db)) - [:name :id])} - :db (dissoc db :report)})))) + (cond-> {:graphql {:token user + :owns-state {:single ::page} + :query-obj {:venia/queries [[:profit-and-loss + {:client-ids (some-> (:id (:client (:data db))) + vector) + :periods (mapv (fn [[start end] ] {:start (date->str start standard) :end (date->str end standard)} ) + (:periods (:data db)))} + [[:periods [[:accounts [:name :amount :client_id :account-type :id :count :numeric-code :location]]]]]]]} + :on-success [::received]} + :set-uri-params {:periods (mapv (fn [[start end title]] + [(date->str start standard) + (date->str end standard)]) + (:periods (:data db))) + :client (select-keys (:client (:data db)) + [:name :id])} + :db (dissoc db :report)}))) @@ -276,7 +278,6 @@ :can-submit [::can-submit-period-form] :id ::period-form})) -;; 877 w fremont ave suite i4 (defn change-period-body [idx which] (let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::period-form]) {:keys [form-inline horizontal-field field raw-field error-notification submit-button]} change-period-form] @@ -424,11 +425,12 @@ {:key (str "between-" i)}))])) -;; reimplement include-deltas. Are numbers even correct? support actually using more than 1 client. +;; TODO Allow choosing more than one client +;; TODO allow "sandwhiching" clients data +;; TODO do we need squashing locations? (defn grouping [{:keys [header type groupings location periods all-accounts]}] - - (let [params @(re-frame/subscribe [::params])] + (let [include-deltas @(re-frame/subscribe [::include-deltas])] [:<> (doall (for [[grouping-name from to] groupings @@ -445,7 +447,7 @@ (fn [i] [:td]) periods - (:include-deltas params))] + include-deltas)] [:<> (for [{:keys [numeric-code name]} account-codes] ^{:key numeric-code} @@ -462,7 +464,7 @@ [:td.has-text-right (->$ (- (get-in all-accounts [i [numeric-code location] :amount] 0.0) (get-in all-accounts [(dec i) [numeric-code location] :amount] 0.0)))]) periods - (:include-deltas params))])] + include-deltas)])] [:tr [:th] @@ -477,7 +479,7 @@ [:th.has-text-right.total (->$ (- (aggregate-accounts (filter-accounts all-accounts i [from to] location)) (aggregate-accounts (filter-accounts all-accounts (dec i) [from to] location))))]) periods - (:include-deltas params))]]))])) + include-deltas)]]))])) @@ -485,7 +487,7 @@ (let [all-accounts @(re-frame/subscribe [::all-accounts]) periods @(re-frame/subscribe [::periods]) [min-numeric-code max-numeric-code] (ranges type) - params @(re-frame/subscribe [::params])] + include-deltas @(re-frame/subscribe [::include-deltas])] [:<> [:tr [:th.is-size-5 title]] @@ -509,12 +511,12 @@ [:th.has-text-right (->$ (- (aggregate-accounts (filter-accounts all-accounts i [min-numeric-code max-numeric-code] location)) (aggregate-accounts (filter-accounts all-accounts (dec i) [min-numeric-code max-numeric-code] location))))]) periods - (:include-deltas params))]])) + include-deltas)]])) (defn subtotal [types negs title location] (let [all-accounts @(re-frame/subscribe [::all-accounts]) periods @(re-frame/subscribe [::periods]) - params @(re-frame/subscribe [::params])] + include-deltas @(re-frame/subscribe [::include-deltas])] [:tr [:th.is-size-5 title] (map-periods @@ -536,7 +538,7 @@ (negs t) (map #(update % :amount -)))) types))))]) periods - (:include-deltas params))])) + include-deltas)])) (defn period-header [{:keys [include-deltas periods]}] [:<> @@ -582,13 +584,13 @@ [subtotal [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} (str location " Net Income") location] [subtotal [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} "Net Income" nil]]) -(defn location-summary [location params] +(defn location-summary [location include-deltas] (let [periods @(re-frame/subscribe [::periods])] [:div [:h2.title.is-4.mb-4 location " Summary"] [:table.table.compact.balance-sheet.mb-6 [:tbody - [period-header {:include-deltas (:include-deltas params) + [period-header {:include-deltas include-deltas :periods periods}] @@ -597,9 +599,7 @@ [subtotal [:payroll ]#{} "Payroll" location] [subtotal [:sales :payroll :cogs] #{:payroll :cogs} "Gross Profits" location] [subtotal [:controllable :fixed-overhead :ownership-controllable] #{} "Overhead" location] - [subtotal [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} "Net Income" location]]] - - ])) + [subtotal [:sales :cogs :payroll :controllable :fixed-overhead :ownership-controllable] #{:cogs :payroll :controllable :fixed-overhead :ownership-controllable} "Net Income" location]]]])) (re-frame/reg-sub ::can-submit @@ -611,213 +611,265 @@ :submit-event [::params-change] :id ::form})) +(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-controls [pnl-form] + (let [!box (reagent/atom nil) + active (reagent/atom nil)] + (fn [pnl-form] + (let [{:keys [form-inline field raw-field error-notification submit-button ]} pnl-form + {:keys [data report active? error id]} @(re-frame/subscribe [::forms/form ::form]) + {:keys [periods selected-period include-deltas]} data] + [: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 (:client data) + (str " (" (:name (:client data)) ")")))]] + [report-control-detail {:active active :box !box :which :clients} + [:div {:style {:width "20em"}} + [:h4.subtitle "Companies"] + (raw-field + [typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients]) + :entity->text :name + :type "typeahead-v3" + :field [:client]}])]]] + [:div.level-item + [buttons/dropdown {:on-click (fn [] (reset! active :range))} + [:span (str "Range" + (when selected-period + (str " (" selected-period ")")))]] + [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 + (raw-field + [date-picker-friendly {:placeholder "End date" + :type "date" + :field [:thirteen-periods-end]}])] + [:div.control + [:a.button + {:class (when (= selected-period "13 periods") "is-active") + :on-click (dispatch-event + [::forms/change ::form + [:periods] + (let [today (or (some-> (:thirteen-periods-end data) (str->date standard)) + (local-today))] + (into + [[(t/plus (t/minus today (t/weeks (* 13 4))) + (t/days 1)) + today + "Total"]] + (for [i (range 13)] + [(t/plus (t/minus today (t/weeks (* (inc i) 4))) + (t/days 1)) + (t/minus today (t/weeks (* i 4)))]))) + [:selected-period] "13 periods"])} + "13 periods"]]]] + + [:div.control + [:div.field.has-addons + [:div.control + (raw-field + [date-picker-friendly {:placeholder "End date" + :type "date" + :field [:twelve-periods-end]}]) + ] + [:div.control + [:a.button + {:class (when (= selected-period "12 months") "is-active") + :on-click (dispatch-event + [::forms/change ::form + [:periods] + (let [end-date (or (some-> (:twelve-periods-end data) (str->date standard)) + (local-today)) + this-month (t/local-date (t/year end-date) + (t/month end-date) + 1) + ] + (into + [[(t/minus this-month (t/months 11)) + (t/minus (t/plus this-month (t/months 1)) + (t/days 1)) + "Total"]] + (for [i (range 12)] + [(t/minus this-month (t/months (- 11 i))) + (t/minus (t/minus this-month (t/months (- 10 i))) + (t/days 1))]))) + [:selected-period] "12 months"])} + "12 months"]]]] + + [:div.control + [:a.button + {:class (when (= selected-period "Last week") "is-active") + :on-click (dispatch-event + [::forms/change ::form + [: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 [(t/minus last-sunday (t/period :days 6)) last-sunday])) + [:selected-period] "Last week"])} + "Last week"]] + [:div.control + [:a.button + {:class (when (= selected-period "Week to date") "is-active") + :on-click (dispatch-event + [::forms/change ::form + [:periods] + (and-last-year [(loop [current (local-today)] + (if (= 1 (t/day-of-week current)) + current + (recur (t/minus current (t/period :days 1))))) + (local-today)]) + [:selected-period] "Week to date"])} + "Week to date"]] + [:div.control + [:a.button + {:class (when (= selected-period "Last Month") "is-active") + :on-click (dispatch-event + + [::forms/change ::form + [:periods] + (and-last-year [(t/minus (t/local-date (t/year (local-today)) + (t/month (local-today)) + 1) + (t/period :months 1)) + (t/minus (t/local-date (t/year (local-today)) + (t/month (local-today)) + 1) + (t/period :days 1))]) + [:selected-period] "Last Month"])} + "Last Month"]] + [:div.control + [:a.button + {:class (when (= selected-period "Month to date") "is-active") + :on-click (dispatch-event + [::forms/change ::form + [:periods] + (and-last-year [(t/local-date (t/year (local-today)) + (t/month (local-today)) + 1) + (local-today)]) + [:selected-period] "Month to date"] + )} + "Month to date"]] + [:div.control + [:a.button + {:class (when (= selected-period "Year to date") "is-active") + :on-click (dispatch-event + [::forms/change ::form + [:periods] + (and-last-year [(t/local-date (t/year (local-today)) 1 1) + (local-today)]) + [:selected-period] "Year to date"])} + "Year to date"]] + [:div.control + [:a.button + {:class (when (= selected-period "Full year") "is-active") + :on-click (dispatch-event + [::forms/change ::form + [:periods] + (and-last-year [(t/plus (t/minus (local-today) (t/period :years 1)) + (t/period :days 1)) + (local-today)]) + [:selected-period] "Full year"])} + "Full year"]]] + [:div + [:div.field + [:label.checkbox + (raw-field + [:input {:type "checkbox" + :field [:show-advanced?]}]) + " Show Advanced"]]] + (when (:show-advanced? data) + (doall + (for [[_ i] (map vector periods (range))] + ^{:key i} + [:div.field.is-grouped + [:div.control + [:p.help "From"] + (raw-field + [date-picker-friendly {:type "date" + :field [:periods i 0]}])] + + [:div.control + [:p.help "To"] + (raw-field + [date-picker-friendly {:type "date" + :field [:periods i 1]}])]])))]]] + + [:div.level-item + [:div + [switch-field {:id "include-deltas" + :checked (boolean include-deltas) + :on-change (fn [e] + (re-frame/dispatch [::forms/change ::form + [:include-deltas] (.-checked (.-target e))])) + :label "Include deltas" + :type "checkbox"}]]]] + [:div.level-right + [:button.button.is-primary "Run"]]] + [:div.report-control-detail {:ref (fn [el] + (when-not @!box + (reset! !box el)))}]])))) + (defn profit-and-loss-content [] - (let [current-client @(re-frame/subscribe [::subs/client]) - clients @(re-frame/subscribe [::subs/clients]) - user @(re-frame/subscribe [::subs/user]) + (let [user @(re-frame/subscribe [::subs/user]) status @(re-frame/subscribe [::status/single ::page]) - params @(re-frame/subscribe [::params]) unresolved-accounts @(re-frame/subscribe [::uncategorized-accounts]) {:keys [data report active? error id]} @(re-frame/subscribe [::forms/form ::form]) {:keys [form-inline field raw-field error-notification submit-button ]} pnl-form periods (:periods data)] - (when data - (form-inline {} - [:div - [:h1.title "Profit and Loss - " (:name current-client)] - [status/status-notification {:statuses [[::status/single ::page]]}] - [:div.report-controls - (when-not current-client - [:<> - [:h2.title.is-4 "Entities"] - (raw-field - [typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients]) - :entity->text :name - :type "typeahead-v3" - :field [:client]}])]) - [:h2.title.is-4 "Range"] - [:div - [:div.field.is-grouped - [:div.control - [:div.field.has-addons - [:div.control - (raw-field - [date-picker-friendly {:placeholder "End date" - :type "date" - :field [:thirteen-periods-end]}])] - [:div.control - [:a.button - {:class (when (= (:selected params) "13 periods") "is-active") - :on-click (dispatch-event - [::forms/change ::form - [:periods] - (let [today (or (some-> (:thirteen-periods-end data) (str->date standard)) - (local-today))] - (into - [[(t/plus (t/minus today (t/weeks (* 13 4))) - (t/days 1)) - today - "Total"]] - (for [i (range 13)] - [(t/plus (t/minus today (t/weeks (* (inc i) 4))) - (t/days 1)) - (t/minus today (t/weeks (* i 4)))])))])} - "13 periods"]]]] + (form-inline {} + [:div + [status/status-notification {:statuses [[::status/single ::page]]}] + [report-controls pnl-form] + + [status/big-loader status] + (when (and (not= :loading (:state status)) + report) - [:div.control - [:div.field.has-addons - [:div.control - (raw-field - [date-picker-friendly {:placeholder "End date" - :type "date" - :field [:twelve-periods-end]}]) - ] - [:div.control - [:a.button - {:class (when (= (:selected params) "13 periods") "is-active") - :on-click (dispatch-event - [::forms/change ::form - [:periods] - (let [end-date (or (some-> (:twelve-periods-end data) (str->date standard)) - (local-today)) - this-month (t/local-date (t/year end-date) - (t/month end-date) - 1) - ] - (into - [[(t/minus this-month (t/months 11)) - (t/minus (t/plus this-month (t/months 1)) - (t/days 1)) - "Total"]] - (for [i (range 12)] - [(t/minus this-month (t/months (- 11 i))) - (t/minus (t/minus this-month (t/months (- 10 i))) - (t/days 1))])))])} - "12 months"]]]] + [:div - [:div.control - [:a.button - {:class (when (= (:selected params) "Last week") "is-active") - :on-click (dispatch-event - [::forms/change ::form - [: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 [(t/minus last-sunday (t/period :days 6)) last-sunday]))])} - "Last week"]] - [:div.control - [:a.button - {:class (when (= (:selected params) "Week to date") "is-active") - :on-click (dispatch-event - [::forms/change ::form - [:periods] - (and-last-year [(loop [current (local-today)] - (if (= 1 (t/day-of-week current)) - current - (recur (t/minus current (t/period :days 1))))) - (local-today)])])} - "Week to date"]] - [:div.control - [:a.button - {:class (when (= (:selected params) "Last Month") "is-active") - :on-click (dispatch-event - - [::forms/change ::form - [:periods] - (and-last-year [(t/minus (t/local-date (t/year (local-today)) - (t/month (local-today)) - 1) - (t/period :months 1)) - (t/minus (t/local-date (t/year (local-today)) - (t/month (local-today)) - 1) - (t/period :days 1))])])} - "Last Month"]] - [:div.control - [:a.button - {:class (when (= (:selected params) "Month to date") "is-active") - :on-click (dispatch-event - [::forms/change ::form - [:periods] - (and-last-year [(t/local-date (t/year (local-today)) - (t/month (local-today)) - 1) - (local-today)])] - )} - "Month to date"]] - [:div.control - [:a.button - {:class (when (= (:selected params) "Year to date") "is-active") - :on-click (dispatch-event - [::forms/change ::form - [:periods] - (and-last-year [(t/local-date (t/year (local-today)) 1 1) - (local-today)])])} - "Year to date"]] - [:div.control - [:a.button - {:class (when (= (:selected params) "Full year") "is-active") - :on-click (dispatch-event - [::forms/change ::form - [:periods] - (and-last-year [(t/plus (t/minus (local-today) (t/period :years 1)) - (t/period :days 1)) - (local-today)])])} - "Full year"]]]] - [:div - [:div.field - [:label.checkbox - (raw-field - [:input {:type "checkbox" - :field [:show-advanced?]}]) - " Show Advanced"]]] - (when (:show-advanced? data) - (doall - (for [[_ i] (map vector periods (range))] - ^{:key i} - [:div.field.is-grouped - [:div.control - [:p.help "From"] - (raw-field - [date-picker-friendly {:type "date" - :field [:periods i 0]}])] - - [:div.control - [:p.help "To"] - (raw-field - [date-picker-friendly {:type "date" - :field [:periods i 1]}])]]))) - (submit-button "Run")] - [status/big-loader status] - (when (and (not= :loading (:state status)) - report) - - [:div - (when (seq unresolved-accounts) - [:div.notification.is-warning.is-light - "This report does not include " (str/join ", " - (map #(str (:count %) " unresolved ledger entries for " (if (str/blank? (:location %)) - " all locations" - (:location %))) - unresolved-accounts))]) + [:h1.title "Profit and Loss - " (:name (:client data))] + (when (seq unresolved-accounts) + [:div.notification.is-warning.is-light + "This report does not include " (str/join ", " + (map #(str (:count %) " unresolved ledger entries for " (if (str/blank? (:location %)) + " all locations" + (:location %))) + unresolved-accounts))]) + [:<> + (for [location @(re-frame/subscribe [::locations])] + ^{:key (str location "-summary")} + [location-summary location (:include-deltas data)] + )] + [:h2.title.is-4 {:style {:margin-bottom "1rem"}} "Detail"] + [:table.table.compact.balance-sheet + [:tbody + [period-header {:include-deltas (:include-deltas data) + :periods periods}] + [:<> (for [location @(re-frame/subscribe [::locations])] - ^{:key (str location "-summary")} - [location-summary location params] - )] - [:h2.title.is-4 {:style {:margin-bottom "1rem"}} "Detail"] - [:table.table.compact.balance-sheet - [:tbody - [period-header {:include-deltas (:include-deltas params) - :periods periods}] - - [:<> - (for [location @(re-frame/subscribe [::locations])] - ^{:key location} - [location-rows location])]]]])])))) + ^{:key location} + [location-rows location])]]]])]))) (re-frame/reg-event-fx ::unmounted-pnl @@ -836,7 +888,8 @@ (fn [dt] (str->date dt standard)) period)))) - :client (:client qp) + :client (or (:client qp) + (some-> @(re-frame/subscribe [::subs/client]) (select-keys [:name :id]))) :include-deltas true}) ::track/register {:id ::ledger-params :subscription [::data-page/params ::ledger]