(ns auto-ap.views.components.expense-accounts-field (:require [auto-ap.forms.builder :as form-builder] [auto-ap.schema :as schema] [auto-ap.utils :refer [dollars-0?]] [auto-ap.views.components :as com] [auto-ap.views.components.button-radio :as button-radio] [auto-ap.views.components.level :refer [left-stack]] [auto-ap.views.components.money-field :refer [money-field]] [auto-ap.views.components.percentage-field :refer [percentage-field]] [auto-ap.views.components.typeahead.vendor :refer [search-backed-typeahead]] [auto-ap.views.utils :refer [->$ bind-field dispatch-event appearing-group]] [clojure.string :as str] [goog.string :as gstring] [malli.core :as m] [re-frame.core :as re-frame])) (defn can-replace-with-default? [accounts] (and (or (not (seq accounts)) (<= 1 (count accounts))) (not (get-in accounts [0 :account :id])))) (defn default-account [accounts default-account amount locations] [{:id (str "new-" (random-uuid)) :amount (Math/abs amount) :amount-percentage 100 :amount-mode "%" :location (or (:location default-account) (get-in accounts [0 :account :location]) (if (= 1 (count locations)) (first locations) nil)) :account default-account}]) (defn from-graphql [accounts total locations] (if (seq accounts) (vec (map (fn [a] (-> a (update :amount js/parseFloat) (assoc :amount-percentage (* 100 (/ (js/parseFloat (:amount a)) (Math/abs total)))) (assoc :amount-mode "$"))) accounts)) [{:id (str "new-" (random-uuid)) :amount-mode "$" :amount (Math/abs total) :amount-percentage 100 :location (if (= 1 (count locations)) (first locations) nil)}])) ;; EVENTS (re-frame/reg-event-fx ::add-expense-account (fn [_ [_ event expense-accounts locations]] {:dispatch (conj event (conj expense-accounts {:amount 0 :id (str "new-" (random-uuid)) :amount-mode "%" :amount-percentage 0 :location (if (= 1 (count locations)) (first locations) nil)}))})) (re-frame/reg-event-fx ::remove-expense-account (fn [_ [_ event expense-accounts id]] {:dispatch (conj event (transduce (filter (fn [ea] (not= (:id ea) id)) ) conj [] expense-accounts))})) (defn recalculate-amounts [expense-accounts total] (mapv (fn [ea] (assoc ea :amount (js/parseFloat (goog.string/format "%.2f" (* (/ (js/parseFloat (:amount-percentage ea)) 100.0) total))))) expense-accounts)) (re-frame/reg-event-fx ::spread-evenly (fn [_ [_ event expense-accounts max-value]] {:dispatch (into event [(recalculate-amounts (mapv (fn [ea] (assoc ea :amount-percentage (js/parseFloat (goog.string/format "%.2f" (* 100 (/ 1 (count expense-accounts))))))) expense-accounts) max-value)])})) (re-frame/reg-event-fx ::expense-account-changed (fn [_ [_ event expense-accounts max-value field value]] (let [updated-accounts (cond-> expense-accounts true (assoc-in field value) (= (list :account) (drop 1 field)) (assoc-in [(first field) :location] nil) (= (list :amount-percentage) (drop 1 field)) (assoc-in [(first field) :amount] (js/parseFloat (goog.string/format "%.2f" (* (/ (cond-> value (not (float? value)) (js/parseFloat )) 100.0) (cond-> max-value (not (float? max-value)) (js/parseFloat))))))) updated-accounts (if-let [location (get-in updated-accounts [(first field) :account :location])] (assoc-in updated-accounts [(first field) :location] location) updated-accounts)] {:dispatch (into event [updated-accounts])}))) ;; VIEWS (defn expense-accounts-field [{expense-accounts :value client :client max-value :max locations :locations event :event descriptor :descriptor disabled :disabled percentage-only? :percentage-only? :or {percentage-only? false}}] [:div [:div.columns [:div.column [:h1.subtitle.is-4.is-inline (str/capitalize descriptor) "s"] (when-not percentage-only? [:p.help "Remaining" (->$ (- max-value (reduce + 0 (map (comp js/parseFloat :amount) expense-accounts))))])] [:div.column.is-narrow (when-not disabled [:p.buttons [:a.button {:on-click (dispatch-event [::spread-evenly event expense-accounts max-value])} "Spread evenly"] [:a.button {:on-click (dispatch-event [::add-expense-account event expense-accounts locations])} "Add"]])]] (for [[index {:keys [account id location amount amount-percentage amount-mode] :as expense-account}] (map vector (range) expense-accounts)] ^{:key id} [:div.box [:div.columns [:div.column [:h1.subtitle.is-6 (cond (and account (not percentage-only?)) (str (:name account) " - " location ": " (gstring/format "$%.2f" (or amount 0) )) account (str (:name account) " - " location ": %" amount-percentage) :else [:i "New " descriptor])]] [:div.column.is-narrow (when-not disabled [:a.delete {:on-click (dispatch-event [::remove-expense-account event expense-accounts id])}])]] [:div.field [:div.columns [:div.column [:p.help "Account"] [:div.control.is-fullwidth [bind-field ^{:key (:id client)} [search-backed-typeahead {:search-query (fn [i] [:search_account {:query i :client-id (:id client)} [:name :id :location]]) :type "typeahead-v3" :field [index :account] :disabled disabled :event [::expense-account-changed event expense-accounts max-value] :subscription expense-accounts}]]]] [:div.column.is-narrow [:p.help "Location"] [:div.control (if-let [forced-location (:location account)] [:div.select [:select {:disabled "disabled" :style {:width "5em"} :value forced-location} [:option {:value forced-location} forced-location]]] [:div.select [bind-field [:select {:type "select" :disabled (boolean (or (:location account) disabled)) :style {:width "5em"} :field [index :location] :allow-nil? true :spec (set locations) :event [::expense-account-changed event expense-accounts max-value] :subscription expense-accounts} (map (fn [l] ^{:key l} [:option {:value l} l]) locations)]]])]]]] [:div.field [:p.help "Amount"] [:div.control [:div.field.has-addons.is-extended [:p.control [:span.select [bind-field [:select {:type "select" :disabled (or disabled percentage-only?) :field [index :amount-mode] :allow-nil? false :event [::expense-account-changed event expense-accounts max-value] :subscription expense-accounts} [:option "$"] [:option "%"]]]]] [:p.control (if (= "$" amount-mode) [bind-field [:input.input {:type "number" :field [index :amount] :style {:text-align "right" :width "7em"} :event [::expense-account-changed event expense-accounts max-value] :disabled disabled :subscription expense-accounts :precision 2 :value (get-in expense-account [:amount]) :max max-value :step "0.01"}]] [bind-field [:input.input {:type "number" :field [index :amount-percentage] :style {:text-align "right" :width "7em"} :disabled disabled :event [::expense-account-changed event expense-accounts max-value] :precision 2 :subscription expense-accounts :value (get-in expense-account [:amount-percentage]) :max "100" :step "0.01"}]])]]]]])]) (def schema (m/schema [:sequential [:map [:id :string] [:account schema/reference] [:location schema/not-empty-string] [:amount schema/money]]])) (defn expense-accounts-field-v2 [{value :value on-change :on-change expense-accounts :value client :client max-value :max locations :locations disabled :disabled percentage-only? :percentage-only? :or {percentage-only? false}}] [form-builder/virtual-builder {:value value :schema schema :on-change (fn [expense-accounts original-expense-accounts] (let [updated-expense-accounts (for [[before-account after-account] (map vector original-expense-accounts expense-accounts)] (cond-> after-account (not= (:id (:account before-account)) (:id (:account after-account))) (assoc :location nil) (not= (:amount-percentage before-account) (:amount-percentage after-account)) (assoc :amount (* (/ (:amount-percentage after-account) 100.0) max-value)) (:location (:account after-account)) (assoc :location (:location (:account after-account)))))] (on-change (into [] updated-expense-accounts))))} [:div [:div.tags (when max-value [:div.tag "To Allocate: " (->$ max-value)]) (when-not percentage-only? (let [total (reduce + 0 (map (or :amount 0.0) expense-accounts))] [:<> [:div.tag "Total: " (->$ total) ] [:div.tag {:class (if (dollars-0? (- max-value total)) ["is-primary" "is-light"] ["is-danger" "is-light"])} "Remaining: " (->$ (- max-value total))]]))] (into [appearing-group] (for [[index {:keys [account id amount amount-mode]}] (map vector (range) expense-accounts)] ^{:key id} [:div.card {:style {:margin-bottom "2em"}} [:div.card-header [:p.card-header-title "Expense Account"] (when-not disabled [:div.card-header-icon {:on-click (fn [] (on-change (into [] (filter #(not= id (:id %)) expense-accounts))))} [:a.delete ]])] [:div.card-content [:div.field [:div.columns [:div.column [:div.control.is-fullwidth [form-builder/field-v2 {:required? true :field [index :account]} "Account" [search-backed-typeahead {:search-query (fn [i] [:search_account {:query i :client-id (:id client)} [:name :id :location]]) :disabled disabled}]]]] [:div.column.is-narrow [form-builder/field-v2 {:required? true :field [index :location]} "Location" [com/select-field {:options (if (:location account) [[(:location account) (:location account)]] (map (fn [l] [l l]) locations)) :disabled (boolean (:location account)) :allow-nil? true}]]]]] [left-stack [:div.field.has-addons [form-builder/raw-field-v2 {:field [index :amount-mode]} [button-radio/button-radio {:options [["$" "Amount"] ["%" "Percent"]]}]] (if (= "$" amount-mode) [form-builder/raw-field-v2 {:field [index :amount]} [money-field {}] ] [form-builder/raw-field-v2 {:field [index :amount-percentage]} [percentage-field {}]])] (when (= "%" amount-mode) [:div.tag.is-primary.is-light (gstring/format "$%.2f" (or amount 0) )])]]])) (when-not disabled [:p.buttons [:a.button {:on-click (fn [] (on-change (recalculate-amounts (mapv (fn [ea] (assoc ea :amount-percentage (* 100.0 (/ 1 (count expense-accounts))))) expense-accounts) max-value)) )} "Spread evenly"] [:a.button {:on-click (fn [] (on-change (conj value {:id (str "new-" (random-uuid)) :amount-mode "%" :location (if (= 1 (count locations)) (first locations) nil)})))} "Add"]])]])