diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 0f52157a..f8025f6c 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -554,6 +554,7 @@ :profit_and_loss {:type :profit_and_loss_report :args {:client_id {:type :id} + :client_ids {:type '(list :id)} :periods {:type '(list :date_range)}} :resolve :get-profit-and-loss} diff --git a/src/clj/auto_ap/graphql/ledger.clj b/src/clj/auto_ap/graphql/ledger.clj index e48a6da6..12a4e2ad 100644 --- a/src/clj/auto_ap/graphql/ledger.clj +++ b/src/clj/auto_ap/graphql/ledger.clj @@ -183,9 +183,14 @@ (defn get-profit-and-loss [context args value] (let [client-id (:client_id args) - _ (assert-can-see-client (:id context) client-id) - all-ledger-entries (full-ledger-for-client client-id) - lookup-account (build-account-lookup client-id)] + client-ids (or (some-> client-id vector) + (:client_ids args)) + _ (when (not (seq client-ids)) + (throw (ex-info "Please select a client." {:validation-error "Please select a client."}))) + _ (doseq [client-id client-ids] + (assert-can-see-client (:id context) client-id)) + all-ledger-entries (full-ledger-for-client (first client-ids)) + lookup-account (build-account-lookup (first client-ids))] (->graphql {:periods (reduce (fn [acc {:keys [start end]}] (conj acc 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 999f780a..79e8610f 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 @@ -7,8 +7,9 @@ [auto-ap.utils :refer [dollars-0? by ]] [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.modal :as modal] - [auto-ap.views.utils :refer [date->str date-picker bind-field standard pretty dispatch-event local-today ->% ->$ str->date with-user dispatch-value-change query-params]] + [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] [auto-ap.status :as status] @@ -44,9 +45,12 @@ ;; SUBS (re-frame/reg-sub ::locations + + :<- [::forms/form ::form] + (fn [db] (->> db - ::report + :report :periods (mapcat :accounts) (filter (comp in-range? :numeric-code)) @@ -63,11 +67,6 @@ "ZZZZZZ" x)))))) -(re-frame/reg-sub - ::report - (fn [db] - (-> db ::report))) - (re-frame/reg-sub ::error (fn [db] @@ -81,9 +80,10 @@ (re-frame/reg-sub ::period-accounts + :<- [::forms/form ::form] (fn [db [_ which type only-location]] - (->> (get-in db [::report :periods which :accounts]) + (->> (get-in db [:report :periods which :accounts]) (map #(update % :amount js/parseFloat)) (filter (fn [{:keys [account-type location numeric-code]}] (and (or (nil? only-location) @@ -93,9 +93,10 @@ (re-frame/reg-sub ::uncategorized-accounts + :<- [::forms/form ::form] (fn [db [_ ]] - (->> (get-in db [::report :periods ]) + (->> (get-in db [:report :periods ]) (mapcat :accounts) (map #(update % :amount js/parseFloat)) (filter (fn [{:keys [account-type location numeric-code]}] @@ -121,6 +122,7 @@ (re-frame/reg-sub ::all-accounts + :<- [::forms/form ::form] (fn [db [_ which type only-location]] (transduce (comp @@ -129,12 +131,13 @@ conj [] - (get-in db [::report :periods])))) + (get-in db [:report :periods])))) (re-frame/reg-event-db ::received + [(forms/in-form ::form)] (fn [db [_ data]] - (-> db (assoc ::report (:profit-and-loss data))))) + (-> db (assoc :report (:profit-and-loss data))))) (re-frame/reg-sub ::params @@ -159,68 +162,53 @@ (assoc db ::ledger-list-active? false))) (re-frame/reg-event-fx - ::mounted - (fn [{:keys [db]}] - (let [qp (query-params)] - (if (:periods qp) - (let [periods (mapv (fn [[start end title]] - [ - (str->date start standard) - (str->date end standard) - title]) - (:periods qp))] - {:dispatch [::range-selected periods (:include-deltas qp) nil]}) - {:dispatch [::range-selected (and-last-year [(t/minus (local-today) (t/period :years 1)) (local-today)]) true nil]})))) + ::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 :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)})))) + + + + (re-frame/reg-event-db - ::period-inputs-change - (fn [db [_ field value]] - (assoc-in db (into [::period-inputs ] field) value))) + ::change + (forms/change-handler ::form + (fn [data field value] + (cond + (= [:periods] field) + [field (mapv (fn [period] + (mapv + (fn [dt] + (str->date dt standard)) + period)) + value)] -(re-frame/reg-event-fx - ::params-change - (fn [cofx [_ params]] - (let [c @(re-frame/subscribe [::subs/client])] - (cond-> {:db (-> (:db cofx) - (assoc-in [::params] params) - (dissoc ::report)) - :set-uri-params (update params - :periods - (fn [p] - (mapv (fn [[start end title]] - [(date->str start standard) - (date->str end standard) - title] - ) p)))} - c (assoc :graphql (when @(re-frame/subscribe [::subs/client]) - {:token (-> cofx :db :user) - :owns-state {:single ::page} - :query-obj {:venia/queries [[:profit-and-loss - {:client-id (:id c) - :periods (mapv (fn [[start end] ] {:start (date->str start standard) :end (date->str end standard)} ) - (:periods params))} - [[:periods [[:accounts [:name :amount :account-type :id :count :numeric-code :location]]]]]]]} - :on-success [::received]})))))) + (and (= :periods (first field)) + (= 2 (count field))) + [field value] ;;already serialized + (= :periods (first field)) [field (str->date value standard) ] - - - -(re-frame/reg-event-fx - ::date-picked - (fn [cofx [_ [_ period which] date]] - {:dispatch [::range-selected (assoc-in @(re-frame/subscribe [::periods]) [period which] (str->date date standard)) nil nil]})) - -(re-frame/reg-event-fx - ::range-selected - (fn [{:keys [db]} [_ periods include-deltas selected]] - {:dispatch [::params-change (cond-> @(re-frame/subscribe [::params]) - true (assoc - :periods periods - :include-deltas include-deltas - :selected selected) - (not (nil? include-deltas)) (assoc :include-deltas include-deltas))] - :db (assoc db ::periods periods)})) + :else nil) + ))) (defn data-params->query-params [params] @@ -288,6 +276,7 @@ :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] @@ -319,23 +308,23 @@ }] )]))) (re-frame/reg-event-fx ::period-change-submitted - (fn [{:keys [db]} [_ idx which]] - { :db (-> db (forms/stop-form ::period-form) - (assoc-in [::periods idx] which)) + (fn [{:keys [db]} [_ idx [start end] :as z]] + { :db (-> db (forms/stop-form ::period-form)) :dispatch-n [[::modal/modal-closed ] - [::range-selected (assoc (::periods db) idx which) nil nil]]})) + [::change [:periods idx ] [start end]]]})) (re-frame/reg-event-fx ::period-removed (fn [{:keys [db]} [_ idx which]] { :db (-> db (forms/stop-form ::period-form) - (update ::periods (fn [ps] - (->> ps - (map vector (range) ) - (filter (fn [[i _]] - (not= i idx))) - (map second) - (into []))))) + (update-in [::forms/forms ::form :data :periods] + (fn [ps] + (->> ps + (map vector (range) ) + (filter (fn [[i _]] + (not= i idx))) + (map second) + (into []))))) :dispatch [::modal/modal-closed ]})) (defn change-period-foot [idx which] @@ -609,249 +598,223 @@ ])) +(re-frame/reg-sub + ::can-submit + (fn [db] + true)) +(def pnl-form (forms/vertical-form {:can-submit [::can-submit] + :change-event [::change] + :submit-event [::params-change] + :id ::form})) -(def profit-and-loss-content - (with-meta - (fn [] - (let [current-client @(re-frame/subscribe [::subs/client]) - user @(re-frame/subscribe [::subs/user]) - status @(re-frame/subscribe [::status/single ::page]) - params @(re-frame/subscribe [::params]) - period-inputs @(re-frame/subscribe [::period-inputs]) - periods @(re-frame/subscribe [::periods]) - unresolved-accounts @(re-frame/subscribe [::uncategorized-accounts]) ] - - (if-not current-client - [:div - [:h1.title "Profit and Loss " ] - [:h2.title.is-4 "Please choose a client first"]] - - [:div - [:h1.title "Profit and Loss - " (:name current-client)] - [status/status-notification {:statuses [[::status/single ::page]]}] - [:div.report-controls - [:h2.title.is-4 "Range"] - [:div - [:div.field.is-grouped - [:div.control - [:div.field.has-addons - [:div.control - [bind-field - [date-picker {:class-name "input" - :class "input" - :format-week-number (fn [] "") - :previous-month-button-label "" - :placeholder "End date" - :next-month-button-label "" - :next-month-label "" - :type "date" - :field [:thirteen-periods-end] - :subscription period-inputs - :event [::period-inputs-change]}]]] - [:div.control - [:a.button - {:class (when (= (:selected params) "13 periods") "is-active") - :on-click (dispatch-event - [::range-selected - (let [today (or (some-> (:thirteen-periods-end period-inputs) (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)))]))) - - false - "13 periods"])} - "13 periods"]]]] +(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]) + 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"]]]] -[:div.control - [:div.field.has-addons - [:div.control - [bind-field - [date-picker {:class-name "input" - :class "input" - :format-week-number (fn [] "") - :previous-month-button-label "" - :placeholder "End date" - :next-month-button-label "" - :next-month-label "" - :type "date" - :field [:twelve-periods-end] - :subscription period-inputs - :event [::period-inputs-change]}]]] - [:div.control - [:a.button - {:class (when (= (:selected params) "13 periods") "is-active") - :on-click (dispatch-event - [::range-selected - (let [end-date (or (some-> (:twelve-periods-end period-inputs) (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))]))) - - false - "12 months"] - )} - "12 months"]]]] + [: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.control - [:a.button - {:class (when (= (:selected params) "Last week") "is-active") - :on-click (dispatch-event - (let [last-sunday (loop [current (local-today)] - (if (= 7 (t/day-of-week current)) - current - (recur (t/minus current (t/period :days 1)))))] - [::range-selected - (and-last-year [(t/minus last-sunday (t/period :days 6)) last-sunday]) - true - "Last week"]))} - "Last week"]] - [:div.control - [:a.button - {:class (when (= (:selected params) "Week to date") "is-active") - :on-click (dispatch-event [::range-selected - (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)]) - true - "Week to date"])} - "Week to date"]] - [:div.control - [:a.button - {:class (when (= (:selected params) "Last Month") "is-active") - :on-click (dispatch-event [::range-selected - (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))]) - true - "Last Month"])} - "Last Month"]] - [:div.control - [:a.button - {:class (when (= (:selected params) "Month to date") "is-active") - :on-click (dispatch-event [::range-selected - (and-last-year [(t/local-date (t/year (local-today)) - (t/month (local-today)) - 1) - (local-today)]) - true - "Month to date"])} - "Month to date"]] - [:div.control - [:a.button - {:class (when (= (:selected params) "Year to date") "is-active") - :on-click (dispatch-event [::range-selected - (and-last-year [(t/local-date (t/year (local-today)) 1 1) - (local-today)]) - true - "Year to date"])} - "Year to date"]] - [:div.control - [:a.button - {:class (when (= (:selected params) "Full year") "is-active") - :on-click (dispatch-event [::range-selected - (and-last-year [(t/plus (t/minus (local-today) (t/period :years 1)) - (t/period :days 1)) - (local-today)]) - true - "Full year"])} - "Full year"]]]] - [:div - [:div.field - [:label.checkbox - [bind-field - [:input {:type "checkbox" - :field [:show-advanced?] - :event [::period-inputs-change] - :subscription period-inputs}]] - " Show Advanced"]]] - (when (:show-advanced? period-inputs) - (for [[_ i] (map vector periods (range))] - ^{:key i} - [:div.field.is-grouped - [:div.control - [:p.help "From"] - [bind-field - [date-picker {:class-name "input" - :class "input" - :format-week-number (fn [] "") - :previous-month-button-label "" - :placeholder "mm/dd/yyyy" - :next-month-button-label "" - :next-month-label "" - :type "date" - :field [:periods i 0] - :event [::date-picked] - :popper-props (clj->js {:placement "right"}) - :subscription params}]]] + [: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 - [:div.control - [:p.help "To"] - [bind-field - [date-picker {:class-name "input" - :class "input" - :format-week-number (fn [] "") - :previous-month-button-label "" - :placeholder "mm/dd/yyyy" - :next-month-button-label "" - :next-month-label "" - :type "date" - :field [:periods i 1] - :event [::date-picked] - :popper-props (clj->js {:placement "right"}) - :subscription params}]]]]))] - [status/big-loader status] - (when (not= :loading (:state status)) - [: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))]) - [:<> - (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] - )]]]])]))) - {:component-will-mount #(re-frame/dispatch [::mounted]) })) + [::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))]) + [:<> + (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])]]]])])))) (re-frame/reg-event-fx ::unmounted-pnl @@ -862,9 +825,18 @@ (re-frame/reg-event-fx ::mounted-pnl (fn [{:keys [db]} _] - {::track/register {:id ::ledger-params - :subscription [::data-page/params ::ledger] - :event-fn (fn [params] [::ledger-params-change params])}})) + (let [qp (query-params)] + {:db (forms/start-form db ::form {:periods (->> qp + :periods + (mapv (fn [period] + (mapv + (fn [dt] + (str->date dt standard)) + period)))) + :client (:client qp)}) + ::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])}] diff --git a/src/cljs/auto_ap/views/utils.cljs b/src/cljs/auto_ap/views/utils.cljs index 6f5f83dc..744d950e 100644 --- a/src/cljs/auto_ap/views/utils.cljs +++ b/src/cljs/auto_ap/views/utils.cljs @@ -435,6 +435,17 @@ (do (reagent/adapt-react-class (.-default react-datepicker)))) +(defn date-picker-friendly [params] + [date-picker (assoc params + :class-name "input" + :class "input" + :format-week-number (fn [] "") + :previous-month-button-label "" + :next-month-button-label "" + :next-month-label "" + :type "date")]) + + (defn local-now [] (t/to-default-time-zone (t/now)))