Files
integreat/src/cljs/auto_ap/views/components/expense_accounts_field.cljs

337 lines
17 KiB
Clojure

(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"]])]])