(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.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] [cljs.tools.reader.edn :as edn] [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)))))) ;; TODO inline on-changes causes each field to be rerendered each time. When we fix this ;; let's make sure that we find away not to trigger a re-render for every component any time any form field ;; changes (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 "typeahead-v3" [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) (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)))