a good experience for dates.
This commit is contained in:
@@ -1,14 +1,24 @@
|
||||
(ns auto-ap.entities.shared
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[clojure.string :as str]))
|
||||
[clojure.string :as str]
|
||||
))
|
||||
|
||||
(def date-regex #"[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}")
|
||||
(def date-regex #"[2]{1}[0-9]{3}-[0-9]{1,2}-[0-9]{1,2}")
|
||||
(def money-regex #"\-?[0-9]+(\.[0-9]{2})?$")
|
||||
(def numeric-regex #"^[0-9]+$")
|
||||
(def only-upper-case #"^[A-Z]+$")
|
||||
|
||||
(s/def ::identifier (s/nilable string?))
|
||||
(s/def ::date (s/and string? #(re-matches date-regex %)))
|
||||
(s/def ::date
|
||||
#?(:cljs
|
||||
(s/or :dt? #(instance? goog.date.DateTime %)
|
||||
:d? #(instance? goog.date.Date %)
|
||||
:str? (s/and string? #(re-matches date-regex %)))
|
||||
:clj (s/or :dt? #(instance? org.joda.time.DateTime %)
|
||||
:ldt? #(instance? org.joda.time.LocalDateTime %)
|
||||
:ldt? #(instance? org.joda.time.LocalDate %)
|
||||
:str? (s/and string? #(re-matches date-regex %)))))
|
||||
|
||||
(s/def ::required-identifier (s/and string?
|
||||
#(not (str/blank? %))))
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[clojure.walk :as walk]
|
||||
[venia.core :as v]
|
||||
[auto-ap.history :as p]
|
||||
[auto-ap.views.utils :refer [date->str standard]]
|
||||
[auto-ap.status :as status]
|
||||
[pushy.core :as pushy]))
|
||||
|
||||
@@ -174,6 +175,12 @@
|
||||
(keyword? node)
|
||||
(snake node)
|
||||
|
||||
(instance? goog.date.DateTime node )
|
||||
(date->str node standard)
|
||||
|
||||
(instance? goog.date.Date node )
|
||||
(date->str node standard)
|
||||
|
||||
|
||||
:else
|
||||
node))
|
||||
|
||||
@@ -67,15 +67,18 @@
|
||||
(r/create-element FormScopeProvider #js {:value scope}
|
||||
(r/as-element (into [:<>]
|
||||
(r/children (r/current-component))))))
|
||||
(defn vertical-control [{:keys [is-small?]}]
|
||||
(defn vertical-control [{:keys [is-small? required?]}]
|
||||
(let [[label & children] (r/children (r/current-component))]
|
||||
[:> Consumer {}
|
||||
(fn [consume]
|
||||
(r/as-element
|
||||
[:div.field
|
||||
(when label (if (or (aget consume "fullwidth?")
|
||||
is-small?) [:p.help label]
|
||||
[:label.label label]))
|
||||
(if (aget consume "fullwidth?")
|
||||
[:p.help label]
|
||||
[:label.label
|
||||
(if required?
|
||||
[:span label [:span.has-text-danger " *"]]
|
||||
label)])
|
||||
(into [:div.control ] children)]))]))
|
||||
|
||||
(defn field []
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[auto-ap.entities.invoice :as invoice]
|
||||
[auto-ap.views.utils :refer [bind-field date-picker date->str local-now standard]]
|
||||
[auto-ap.views.utils :refer [bind-field date-picker-optional date->str local-now standard]]
|
||||
[cljs-time.core :as t]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
@@ -37,27 +37,21 @@
|
||||
[:div.field.has-addons
|
||||
[:div.control
|
||||
[bind-field
|
||||
[date-picker {:class-name "input is-fullwidth"
|
||||
:class "input"
|
||||
:format-week-number (fn [] "")
|
||||
:previous-month-button-label ""
|
||||
:placeholder-text "Start"
|
||||
:next-month-button-label ""
|
||||
:next-month-label ""
|
||||
:event on-change-event
|
||||
:type "date"
|
||||
:field [:start]
|
||||
:subscription value}]]]
|
||||
[date-picker-optional
|
||||
{:event on-change-event
|
||||
:type "date2"
|
||||
:placeholder "Start"
|
||||
:class "is-small"
|
||||
:field [:start]
|
||||
:subscription value
|
||||
:output :text}]]]
|
||||
[:div.control
|
||||
[bind-field
|
||||
[date-picker {:class-name "input is-fullwidth"
|
||||
:class "input"
|
||||
:format-week-number (fn [] "")
|
||||
:previous-month-button-label ""
|
||||
:placeholder-text "End"
|
||||
:next-month-button-label ""
|
||||
:event on-change-event
|
||||
:next-month-label ""
|
||||
:type "date"
|
||||
:field [:end]
|
||||
:subscription value}]]]]])
|
||||
[date-picker-optional
|
||||
{:event on-change-event
|
||||
:type "date2"
|
||||
:class "is-small"
|
||||
:placeholder "End"
|
||||
:field [:end]
|
||||
:subscription value
|
||||
:output :text}]]]]])
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
[auto-ap.time-utils :refer [next-dom]]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[auto-ap.views.components.expense-accounts-field
|
||||
:as expense-accounts-field
|
||||
:refer [recalculate-amounts]]
|
||||
:as eaf
|
||||
:refer [recalculate-amounts
|
||||
expense-accounts-field]]
|
||||
[auto-ap.views.components.layouts :as layouts]
|
||||
[auto-ap.views.components.level :refer [left-stack]]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.components.expense-accounts-field :refer [expense-accounts-field]]
|
||||
[auto-ap.views.components.dropdown :refer [drop-down]]
|
||||
[auto-ap.views.components.money-field :refer [money-field]]
|
||||
[auto-ap.views.components.switch-field :refer [switch-field]]
|
||||
@@ -23,11 +23,8 @@
|
||||
:refer [search-backed-typeahead]]
|
||||
[auto-ap.views.pages.invoices.common :refer [invoice-read]]
|
||||
[auto-ap.views.utils
|
||||
:refer [date->str
|
||||
date-picker-friendly
|
||||
:refer [date-picker-optional
|
||||
dispatch-event
|
||||
standard
|
||||
str->date
|
||||
with-user]]
|
||||
[cljs-time.core :as c]
|
||||
[clojure.spec.alpha :as s]
|
||||
@@ -58,11 +55,12 @@
|
||||
{:venia/operation {:operation/type :mutation
|
||||
:operation/name "AddInvoice"}
|
||||
:venia/queries [{:query/data [:add-invoice
|
||||
{:invoice {:date date
|
||||
:due due
|
||||
{:invoice {
|
||||
:vendor-id (:id vendor)
|
||||
:client-id (:id client)
|
||||
:date date
|
||||
:scheduled-payment scheduled-payment
|
||||
:due due
|
||||
:invoice-number (some-> invoice-number str/trim)
|
||||
:location location
|
||||
:total total
|
||||
@@ -79,6 +77,7 @@
|
||||
::edit-query
|
||||
:<- [::forms/form ::form]
|
||||
(fn [{{:keys [id invoice-number date due scheduled-payment total expense-accounts]} :data}]
|
||||
|
||||
{:venia/operation {:operation/type :mutation
|
||||
:operation/name "EditInvoice"}
|
||||
:venia/queries [{:query/data [:edit-invoice
|
||||
@@ -110,7 +109,7 @@
|
||||
(forms/stop-form ::form )
|
||||
(forms/start-form ::form {:client client
|
||||
:status :unpaid
|
||||
:date (date->str (c/now) standard)}))})))
|
||||
:date (c/now)}))})))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::adding
|
||||
@@ -119,7 +118,7 @@
|
||||
(fn [{:keys [db] ::subs/keys [locations-for-client]} [_ new]]
|
||||
{:db
|
||||
(-> db (forms/start-form ::form (assoc new :expense-accounts
|
||||
(expense-accounts-field/from-graphql (:expense-accounts new)
|
||||
(eaf/from-graphql (:expense-accounts new)
|
||||
0.0
|
||||
locations-for-client))))}))
|
||||
(re-frame/reg-event-fx
|
||||
@@ -127,10 +126,7 @@
|
||||
[(re-frame/inject-cofx ::inject/sub (fn [[_ which _]]
|
||||
[::subs/locations-for-client (:id (:client which))]))]
|
||||
(fn [{:keys [db] ::subs/keys [locations-for-client]} [_ which vendor-preferences]]
|
||||
(let [edit-invoice (update which :date #(date->str % standard))
|
||||
edit-invoice (update edit-invoice :due #(date->str % standard))
|
||||
edit-invoice (update edit-invoice :scheduled-payment #(date->str % standard))
|
||||
edit-invoice (assoc edit-invoice :original edit-invoice)]
|
||||
(let [edit-invoice which]
|
||||
{:db
|
||||
(-> db
|
||||
(forms/start-form ::form {:id (:id edit-invoice)
|
||||
@@ -143,10 +139,10 @@
|
||||
:invoice-number (:invoice-number edit-invoice)
|
||||
:total (cond-> (:total edit-invoice)
|
||||
(not (str/blank? (:total edit-invoice))) (js/parseFloat ))
|
||||
:original edit-invoice
|
||||
:original which
|
||||
:vendor (:vendor edit-invoice)
|
||||
:client (:client edit-invoice)
|
||||
:expense-accounts (expense-accounts-field/from-graphql (:expense-accounts which)
|
||||
:expense-accounts (eaf/from-graphql (:expense-accounts which)
|
||||
(:total which)
|
||||
locations-for-client)}))})))
|
||||
|
||||
@@ -160,8 +156,8 @@
|
||||
(cond
|
||||
(= [:vendor-preferences] field)
|
||||
(cond-> []
|
||||
(expense-accounts-field/can-replace-with-default? (:expense-accounts data))
|
||||
(into [[:expense-accounts] (expense-accounts-field/default-account (:expense-accounts data)
|
||||
(eaf/can-replace-with-default? (:expense-accounts data))
|
||||
(into [[:expense-accounts] (eaf/default-account (:expense-accounts data)
|
||||
(:default-account value)
|
||||
(:total data)
|
||||
(:locations (:client data)))])
|
||||
@@ -171,14 +167,14 @@
|
||||
[:schedule-when-due] true])
|
||||
|
||||
(:schedule-payment-dom value)
|
||||
(into [[:scheduled-payment] (date->str (next-dom (str->date (:date data) standard) (:schedule-payment-dom value)) standard)]))
|
||||
(into [[:scheduled-payment] (next-dom (:date data) (:schedule-payment-dom value))]))
|
||||
|
||||
(= [:total] field)
|
||||
[[:expense-accounts] (recalculate-amounts (:expense-accounts data) value)]
|
||||
|
||||
(and (= [:date] field)
|
||||
(:schedule-payment-dom (:vendor-preferences data)))
|
||||
[[:scheduled-payment] (date->str (next-dom (str->date value standard) (:schedule-payment-dom (:vendor-preferences data))) standard) ]
|
||||
[[:scheduled-payment] (next-dom value (:schedule-payment-dom (:vendor-preferences data))) ]
|
||||
|
||||
(and (= [:schedule-when-due] field) value)
|
||||
[[:scheduled-payment] (:due data)]
|
||||
@@ -293,11 +289,6 @@
|
||||
|
||||
;; VIEWS
|
||||
|
||||
(def invoice-form (forms/vertical-form {:can-submit [::can-submit]
|
||||
:change-event [::changed]
|
||||
:submit-event [::save-requested [::saving ]]
|
||||
:id ::form}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::mounted
|
||||
(fn []
|
||||
@@ -318,8 +309,7 @@
|
||||
|
||||
(defn form-content [params]
|
||||
[layouts/side-bar {:on-close (dispatch-event [::forms/form-closing ::form ])}
|
||||
(let [{:keys [data id]} @(re-frame/subscribe [::forms/form ::form])
|
||||
{:keys [form-inline field raw-field error-notification submit-button ]} invoice-form
|
||||
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])
|
||||
can-submit? (boolean @(re-frame/subscribe [::can-submit]))
|
||||
status @(re-frame/subscribe [::status/single ::form])
|
||||
active-client @(re-frame/subscribe [::subs/client])
|
||||
@@ -361,8 +351,7 @@
|
||||
:type "typeahead-v3"
|
||||
:auto-focus (if active-client false true)
|
||||
:field [:client]
|
||||
:disabled exists?
|
||||
:spec ::invoice/client}]])
|
||||
:disabled exists?}]])
|
||||
[form-builder/field {:required? true}
|
||||
"Vendor"
|
||||
[search-backed-typeahead {:disabled exists?
|
||||
@@ -373,25 +362,27 @@
|
||||
:type "typeahead-v3"
|
||||
:auto-focus (if active-client true false)
|
||||
:field [:vendor]}]]
|
||||
[form-builder/field {:required? true}
|
||||
[form-builder/vertical-control {:required? true}
|
||||
"Date"
|
||||
[date-picker-friendly {:type "date"
|
||||
:field [:date]
|
||||
:spec ::invoice/date}]]
|
||||
[:label
|
||||
[form-builder/raw-field
|
||||
[date-picker-optional {:type "date2"
|
||||
:field [:date]
|
||||
:output :cljs-date}]]]]
|
||||
|
||||
[form-builder/field
|
||||
"Due (optional)"
|
||||
[date-picker-friendly {:type "date"
|
||||
:field [:due]
|
||||
:spec ::invoice/due}]]
|
||||
[date-picker-optional {:type "date2"
|
||||
:field [:due]
|
||||
:output :cljs-date}]]
|
||||
[form-builder/vertical-control
|
||||
"Scheduled payment (optional)"
|
||||
[left-stack
|
||||
[:div.control
|
||||
[form-builder/raw-field
|
||||
[date-picker-friendly {:type "date"
|
||||
[date-picker-optional {:type "date2"
|
||||
:field [:scheduled-payment]
|
||||
:spec ::invoice/scheduled-payment}]]]
|
||||
:output :cljs-date}]]]
|
||||
[:div.control
|
||||
[form-builder/raw-field
|
||||
|
||||
@@ -402,8 +393,7 @@
|
||||
[form-builder/field {:required? true}
|
||||
"Invoice #"
|
||||
[:input.input {:type "text"
|
||||
:field [:invoice-number]
|
||||
:spec ::invoice/invoice-number}]]
|
||||
:field [:invoice-number]}]]
|
||||
[form-builder/field {:required? true}
|
||||
"Total"
|
||||
[money-field {:type "money"
|
||||
@@ -411,7 +401,6 @@
|
||||
:disabled (if can-change-amount? "" "disabled")
|
||||
:style {:max-width "8em"}
|
||||
:min min-total
|
||||
:spec ::invoice/total
|
||||
:step "0.01"}]]]
|
||||
[form-builder/raw-field
|
||||
[expense-accounts-field {:type "expense-accounts"
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
[react-transition-group :as react-transition-group]
|
||||
#_{:clj-kondo/ignore [:unused-namespace]}
|
||||
[react-datepicker :as react-datepicker]
|
||||
[reagent.core :as reagent])
|
||||
[reagent.core :as reagent]
|
||||
[reagent.core :as r]
|
||||
[react :as react]
|
||||
[auto-ap.entities.shared :as shared])
|
||||
(:import
|
||||
(goog.i18n NumberFormat)
|
||||
(goog.i18n.NumberFormat Format)))
|
||||
@@ -351,7 +354,7 @@
|
||||
(c/to-date selected)
|
||||
|
||||
:else
|
||||
selected )
|
||||
selected )
|
||||
keys (assoc keys
|
||||
:on-change (if (:cljs-date? keys)
|
||||
(dispatch-cljs-date-change (conj event field))
|
||||
@@ -363,6 +366,21 @@
|
||||
keys (dissoc keys :field :subscription :event :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
(defmethod do-bind "date2" [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)
|
||||
keys (assoc keys
|
||||
:on-change (fn [v]
|
||||
(re-frame/dispatch (-> event (conj field) (conj v))))
|
||||
|
||||
:value 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)
|
||||
@@ -475,8 +493,10 @@
|
||||
(reagent/adapt-react-class (.-default react-datepicker)))
|
||||
|
||||
(defn date-picker-friendly [params]
|
||||
[date-picker (assoc params
|
||||
[date-picker (assoc params
|
||||
:class-name "input"
|
||||
:disabled-keyboard-navigation true
|
||||
:start-open false
|
||||
:class "input"
|
||||
:format-week-number (fn [] "")
|
||||
:previous-month-button-label ""
|
||||
@@ -484,6 +504,71 @@
|
||||
:next-month-label ""
|
||||
:type "date")])
|
||||
|
||||
(defn coerce-date [d]
|
||||
(cond (and (string? d)
|
||||
(some->> (re-find #"^(\d{4})" d)
|
||||
second
|
||||
(js/parseInt)
|
||||
(#(> % 2000))))
|
||||
(try
|
||||
(c/to-date-time (t/to-default-time-zone (t/from-default-time-zone (str->date d standard))))
|
||||
(catch js/Error _
|
||||
nil))
|
||||
|
||||
(instance? goog.date.DateTime d)
|
||||
(c/to-date-time (t/to-default-time-zone (t/from-default-time-zone d)))
|
||||
|
||||
(instance? goog.date.Date d)
|
||||
(c/to-date-time d)
|
||||
|
||||
:else
|
||||
nil ))
|
||||
|
||||
(defn date-picker-optional-internal [params]
|
||||
|
||||
(let [[text set-text ] (react/useState (some-> params :value coerce-date (date->str standard)))
|
||||
[value set-value ] (react/useState (some-> params :value coerce-date))
|
||||
swap-external-value (fn [new-value]
|
||||
((:on-change params)
|
||||
(cond (= :text (:output params))
|
||||
(some-> new-value (date->str standard))
|
||||
|
||||
(= :cljs-date (:output params))
|
||||
new-value
|
||||
|
||||
:else
|
||||
(c/to-date new-value))))]
|
||||
(react/useEffect (fn []
|
||||
(let [prop-date (some-> params :value coerce-date)]
|
||||
(when (not (t/= prop-date
|
||||
value))
|
||||
(set-value prop-date)
|
||||
(if prop-date
|
||||
(set-text (date->str prop-date standard))
|
||||
(set-text ""))))))
|
||||
[:div.field.has-addons
|
||||
[:div.control
|
||||
[:input.input (assoc params
|
||||
:value text
|
||||
:on-change (fn [e]
|
||||
|
||||
(set-text (.. e -target -value))
|
||||
;; if it's a perfect match, change it on the spot
|
||||
;; especially important for calendar clicking, don't
|
||||
;; want to wait for blur
|
||||
(when (or (re-matches shared/date-regex (.. e -target -value))
|
||||
(nil? (.. e -target -value)))
|
||||
(swap-external-value (some-> (.. e -target -value) coerce-date))))
|
||||
|
||||
:on-blur (fn []
|
||||
(swap-external-value (some-> text coerce-date)))
|
||||
:type "date" :placeholder "12/1/2021")]
|
||||
]]))
|
||||
|
||||
(defn date-picker-optional []
|
||||
[:f> date-picker-optional-internal
|
||||
(r/props (r/current-component))])
|
||||
|
||||
(defn local-now []
|
||||
(t/to-default-time-zone (t/now)))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user