Files
integreat/src/cljs/auto_ap/views/utils.cljs
2021-05-13 16:59:35 -07:00

487 lines
20 KiB
Clojure

(ns auto-ap.views.utils
(:require [re-frame.core :as re-frame]
[reagent.core :as reagent]
[react-transition-group :as react-transition-group]
[react-datepicker]
[clojure.spec.alpha :as s]
[cljs-time.coerce :as c]
[cljs-time.core :as time]
[auto-ap.events :as events]
[auto-ap.subs :as subs]
[cljs-time.format :as format]
[goog.i18n.NumberFormat.Format]
[cljs-time.core :as t]
[clojure.string :as str]
[goog.crypt.base64 :as base64]
[reagent.core :as r])
(:import
(goog.i18n NumberFormat)
(goog.i18n.NumberFormat Format)))
(def nff
(NumberFormat. Format/CURRENCY))
(defn nf
[num]
(.format nff (str num)))
(defn ->$ [x]
(nf x))
(defn- nf%
[num]
(.format (doto
(NumberFormat. Format/PERCENT)
(.setMaximumFractionDigits 1)
(.setMinimumFractionDigits 1))
(str num)))
(defn ->% [x]
(nf% x))
(defn active-when= [active-page candidate]
(when (= active-page candidate) " is-active"))
(defn active-when [active-page f & rest]
(when (apply f (into [active-page] rest)) " is-active"))
(def login-url
(let [client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com"
redirect-uri (js/encodeURI (str (.-origin (.-location js/window)) "/api/oauth"))]
(str "https://accounts.google.com/o/oauth2/auth?access_type=online&client_id=" client-id "&redirect_uri=" redirect-uri "&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile")))
(defn dispatch-value-change [event]
(fn [e]
(.preventDefault e)
(re-frame/dispatch (conj event (.. e -target -value)))))
(defn delayed-dispatch [e]
(fn [x]
(js/setTimeout #(re-frame/dispatch e) 151)
false))
(defn dispatch-event [event]
(fn [e]
(when (.-stopPropagation e)
(.stopPropagation e)
(.preventDefault e))
(re-frame/dispatch-sync event)))
(defn dispatch-event-with-propagation [event]
(fn [e]
(re-frame/dispatch-sync event)))
(def pretty-long (format/formatter "MM/dd/yyyy HH:mm:ss"))
(def pretty (format/formatter "MM/dd/yyyy"))
(def standard (format/formatter "yyyy-MM-dd"))
(defn date->str
([d] (date->str d pretty))
([d format]
(when d
(format/unparse format d))))
(defn date-time->str [d]
(when d
(format/unparse pretty-long d)))
(defn str->date [d f]
(when d
(format/parse f d)))
(defn dispatch-date-change [event]
(fn [e g]
(re-frame/dispatch (conj event
(if (str/blank? e)
e
(date->str (time/from-default-time-zone (c/from-date e)) standard))))))
(defmulti do-bind (fn [a {:keys [type] :as x}]
type))
(defn with-keys [children]
(map-indexed (fn [i c] ^{:key i} c) children))
(def css-transition-group
(reagent/adapt-react-class react-transition-group/CSSTransition))
(defn appearing [{:keys [visible? enter-class exit-class timeout]} & children ]
(let [final-state (reagent/atom visible?)]
(fn [{:keys [visible?]} & children]
[css-transition-group {:in visible? :class-names {:exit exit-class :enter enter-class} :timeout timeout :onEnter (fn [] (reset! final-state true )) :onExited (fn [] (reset! final-state false))}
(if (or @final-state visible?)
(first children)
[:span])])))
(defn multi-field [{:keys [change-event data value template on-change allow-change?]} ]
(let [value-repr (r/atom (mapv
(fn [x]
(assoc x :key (random-uuid) :new? false))
value))]
(fn [{:keys [change-event data value template on-change allow-change?]} ]
(let [value @value-repr
already-has-new-row? (= [:key :new?] (keys (last value)))
value (if already-has-new-row?
value
(conj value {:key (random-uuid)
:new? true}))]
[:div
(for [[i override] (map vector (range) value)
:let [is-disabled? (if (= false allow-change?)
(not (boolean (:new? override)))
nil)]]
^{:key (:key override)}
[:div.level
[:div.level-left
[:div.level-item
(if (:new? override)
[:div.icon.is-medium {:class (if (not= i (dec (count value)))
"has-text-info")}
[:i.fa.fa-plus]]
[:div.icon.is-medium])]
[:<> (for [[idx template] (map vector (range ) template)]
^{:key idx}
[:div.level-item
(update template 1 assoc
:value (get-in override (get-in template [1 :field]))
:disabled is-disabled?
:on-change (fn [e]
(reset! value-repr
(into []
(filter (fn [r]
(not= [:key :new?] (keys r)))
(assoc-in value (into [i] (get-in template [1 :field]))
(if (and e (.. e -target))
(.. e -target -value )
e) ))))
(on-change (mapv
(fn [v]
(dissoc v :new? :key))
@value-repr))))])
]
[:div.level-item
[:a.button.level-item
{:disabled is-disabled?
:on-click (fn []
(when-not is-disabled?
(reset! value-repr (into []
(filter (fn [{:keys [key ] :as v}]
(not= key (:key override)))
(filter (fn [r]
(not= [:key :new?] (keys r)))
value))))
(on-change (mapv
(fn [v]
(dissoc v :new? :key))
@value-repr))))}
[:span.icon [:span.icon-remove]]]]
]])]))))
(defmethod do-bind "select" [dom {:keys [field allow-nil? subscription event class value spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (dispatch-value-change (conj event field))
:value (or (get-in subscription field) "")
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)
options (if allow-nil?
(with-keys (conj rest [:option {:value nil}]))
(with-keys rest))]
(into [dom (dissoc keys :allow-nil?)] options)))
(defmethod do-bind "radio" [dom {:keys [field subscription event class value spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (dispatch-value-change (conj event field))
:checked (= (get-in subscription field) value)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field ))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "checkbox" [dom {:keys [field subscription event class value spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (dispatch-event (-> event
(conj field)
(conj (not (get-in subscription field)))))
:checked (boolean (get-in subscription field))
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field ))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "typeahead" [dom {:keys [field text-field event text-event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (fn [selected text-description text-value]
(re-frame/dispatch (conj (conj event field) selected))
(when text-field
(re-frame/dispatch (conj (conj (or text-event event) text-field) text-value))))
:value (get-in subscription field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "multi-field" [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (fn [value]
(re-frame/dispatch (conj (conj event field) value)))
:value (get-in subscription field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "typeahead-entity" [dom {:keys [field event text-event subscription class spec match->text] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (fn [selected]
(re-frame/dispatch (conj (conj event field) selected))
#_(when text-field
(re-frame/dispatch (conj (conj (or text-event event) text-field) text-value))))
:value (get-in subscription field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "date" [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
selected (get-in subscription field)
selected (cond (string? selected)
(c/to-date (time/to-default-time-zone (time/from-default-time-zone (str->date selected standard))))
(instance? goog.date.DateTime selected)
(c/to-date (time/to-default-time-zone (time/from-default-time-zone selected)))
(instance? goog.date.Date selected)
(c/to-date selected)
:else
selected )
keys (assoc keys
:on-change (dispatch-date-change (conj event field))
:selected selected
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "expense-accounts" [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:value (get-in subscription field)
:event (conj event field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "button-radio" [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:value (get-in subscription field)
:on-change (fn [v]
(re-frame/dispatch (-> event (conj field) (conj v))))
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :event :subscription :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "number" [dom {:keys [field precision event subscription class spec] :as keys :or {precision 2}} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (fn [e]
(.preventDefault e)
(re-frame/dispatch (-> event
(conj field)
(conj (let [val (.. e -target -value)]
(cond (and val
(re-matches #"[\-]?(\d+)(\.\d{2})?" val))
(js/parseFloat val)
(str/blank? val )
nil
:else
val))))))
:value (get-in subscription field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "textarea->table" [dom {:keys [field event subscription class spec] :as keys :or {precision 2}} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (fn [x]
(re-frame/dispatch (-> event
(conj field)
(conj x))))
:value (get-in subscription field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind "money" [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (fn [x]
(re-frame/dispatch (-> event
(conj field)
(conj x))))
:value (get-in subscription field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defmethod do-bind :default [dom {:keys [field event subscription class spec] :as keys} & rest]
(let [field (if (keyword? field) [field] field)
event (if (keyword? event) [event] event)
keys (assoc keys
:on-change (dispatch-value-change (conj event field))
:value (get-in subscription field)
:class (str class
(when (and spec (not (s/valid? spec (get-in subscription field))))
" is-danger")))
keys (dissoc keys :field :subscription :event :spec)]
(into [dom keys] (with-keys rest))))
(defn bind-field [all]
(apply do-bind all))
(defn horizontal-field [label & controls]
[:div.field.is-horizontal
(when label
[:div.field-label
label
])
(into
[:div.field-body]
(with-keys (map (fn [x] [:div.field x]) controls)))])
(def date-picker
(do
(reagent/adapt-react-class (.-default react-datepicker))))
(defn local-now []
(t/to-default-time-zone (t/now)))
(defn local-today []
(t/at-midnight (t/to-default-time-zone (t/now))))
(def with-user
(re-frame/->interceptor
:id :with-user
:before (fn [context]
(-> context
(assoc-in [:coeffects :user] (get-in context [:coeffects :db :user]))))))
(def with-is-admin?
(re-frame/->interceptor
:id :with-is-admin?
:before (fn [context]
(-> context
(assoc-in [:coeffects :is-admin?] (= "admin"
(-> (get-in context [:coeffects :db :user])
(str/split #"\.")
second
(base64/decodeString )
(#(.parse js/JSON % ))
(js->clj :keywordize-keys true)
:user/role)))))))
(defn query-params []
(reduce-kv
(fn [result k v]
(assoc result (keyword k) (cljs.tools.reader.edn/read-string v)))
{}
(:query (cemerick.url/url (.-location js/window)))))
(defn loading [db]
(-> db
(assoc-in [:status] :loading)
(assoc-in [:error] nil)))
(defn triggers-loading [form]
(re-frame/enrich
(fn [db event]
(loading db))))
(defn action-cell-width [cnt]
(str (inc (* cnt 51)) "px"))
(defn days-until [d]
(let [today (t/at-midnight (t/now))
d (t/at-midnight d)
in (if (t/after? today d)
(- (t/in-days (t/interval (t/minus d (t/days 1)) today)))
(t/in-days (t/interval today d )))]
in))
(defn copy-to-clipboard [text]
(let [el (js/document.createElement "textarea")]
(set! (.-value el) text)
(.appendChild js/document.body el)
(.select el)
(js/document.execCommand "copy")
(.removeChild js/document.body el)))
(defn account->match-text [x]
(str (:numeric-code x) " - " (:name x)))