making main form better.

This commit is contained in:
2022-07-16 15:53:27 -07:00
parent 16a1d243e8
commit 2830004092
9 changed files with 284 additions and 227 deletions

View File

@@ -10,7 +10,6 @@
(s/def ::due (s/nilable ::shared/date)) (s/def ::due (s/nilable ::shared/date))
(s/def ::scheduled-payment (s/nilable ::shared/date)) (s/def ::scheduled-payment (s/nilable ::shared/date))
(s/def ::total ::shared/money) (s/def ::total ::shared/money)
(s/def ::vendor-id ::shared/identifier)
(s/def ::invoice (s/keys :req-un [::client (s/def ::invoice (s/keys :req-un [::client
::invoice-number ::invoice-number

View File

@@ -3,7 +3,7 @@
[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 #"[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}")
(def money-regex #"\-?[0-9]+(\.[0-9]{1,2})?$") (def money-regex #"\-?[0-9]+(\.[0-9]{2})?$")
(def numeric-regex #"^[0-9]+$") (def numeric-regex #"^[0-9]+$")
(def only-upper-case #"^[A-Z]+$") (def only-upper-case #"^[A-Z]+$")
@@ -14,4 +14,5 @@
(s/def ::money (s/or :string (s/and string? (s/def ::money (s/or :string (s/and string?
#(re-matches money-regex %)) #(re-matches money-regex %))
:float float?)) :float float?
:int int?))

View File

@@ -18,7 +18,7 @@
(defn builder [{:keys [can-submit data-sub change-event submit-event id fullwidth?] :as z}] (defn builder [{:keys [can-submit data-sub change-event submit-event id fullwidth?] :as z}]
(let [data-sub (or data-sub [::forms/form id]) (let [data-sub (or data-sub [::forms/form id])
change-event (or change-event [::forms/change id]) change-event (or change-event [::forms/change id])
{:keys [data error]} @(re-frame/subscribe data-sub) {:keys [data error] form-key :id} @(re-frame/subscribe data-sub)
status @(re-frame/subscribe [::status/single id])] status @(re-frame/subscribe [::status/single id])]
(r/create-element Provider #js {:value #js {:can-submit @(re-frame/subscribe can-submit) (r/create-element Provider #js {:value #js {:can-submit @(re-frame/subscribe can-submit)
:change-event change-event :change-event change-event
@@ -29,6 +29,7 @@
:data data :data data
:fullwidth? fullwidth?}} :fullwidth? fullwidth?}}
(r/as-element (r/as-element
^{:key form-key}
[:form {:on-submit (fn [e] [:form {:on-submit (fn [e]
(when (.-stopPropagation e) (when (.-stopPropagation e)
(.stopPropagation e) (.stopPropagation e)
@@ -78,13 +79,19 @@
(into [:div.control ] children)]))])) (into [:div.control ] children)]))]))
(defn field [] (defn field []
(let [[label child] (r/children (r/current-component))] (let [props (r/props (r/current-component))
[label child] (r/children (r/current-component))]
[:> Consumer {} [:> Consumer {}
(fn [consume] (fn [consume]
(r/as-element (r/as-element
[:div.field [:div.field
(when label (if (aget consume "fullwidth?") [:p.help label] (when label
[:label.label label])) (if (aget consume "fullwidth?")
[:p.help label]
[:label.label
(if (:required? props)
[:span label [:span.has-text-danger " *"]]
label)]))
[:div.control [raw-field {} child]]]))])) [:div.control [raw-field {} child]]]))]))
(defn horizontal-control [] (defn horizontal-control []
@@ -111,7 +118,7 @@
(into [:div {:style {:margin-bottom "5em"}}] (into [:div {:style {:margin-bottom "5em"}}]
(r/children (r/current-component)))]) (r/children (r/current-component)))])
(defn submit-button [] (defn submit-button [{:keys [class]}]
(let [[child] (r/children (r/current-component))] (let [[child] (r/children (r/current-component))]
[:> Consumer {} [:> Consumer {}
(fn [consume] (fn [consume]
@@ -121,7 +128,8 @@
(r/as-element (r/as-element
[:button.button.is-medium.is-primary {:disabled (or (status/disabled-for status) [:button.button.is-medium.is-primary {:disabled (or (status/disabled-for status)
(not can-submit)) (not can-submit))
:class (cond-> (status/class-for status) :class (cond-> (or class [])
(status/class-for status) (conj (status/class-for status))
fullwidth? (conj "is-fullwidth")) } fullwidth? (conj "is-fullwidth")) }
child])))])) child])))]))

View File

@@ -103,7 +103,6 @@
updated-accounts (if-let [location (get-in updated-accounts [(first field) :account :location])] updated-accounts (if-let [location (get-in updated-accounts [(first field) :account :location])]
(assoc-in updated-accounts [(first field) :location] location) (assoc-in updated-accounts [(first field) :location] location)
updated-accounts)] updated-accounts)]
(println updated-accounts)
{:dispatch (into event [updated-accounts])}))) {:dispatch (into event [updated-accounts])})))
@@ -163,7 +162,6 @@
[:div.column.is-narrow [:div.column.is-narrow
[:p.help "Location"] [:p.help "Location"]
[:div.control [:div.control
(println account)
(if-let [forced-location (:location account)] (if-let [forced-location (:location account)]
[:div.select [:div.select
[:select {:disabled "disabled" :style {:width "5em"} :value forced-location} [:option {:value forced-location} forced-location]]] [:select {:disabled "disabled" :style {:width "5em"} :value forced-location} [:option {:value forced-location} forced-location]]]

View File

@@ -1,23 +1,71 @@
(ns auto-ap.views.components.money-field (ns auto-ap.views.components.money-field
(:require [reagent.core :as r] (:require [reagent.core :as r]
[clojure.string :as str])) [auto-ap.views.utils :refer [->short$]]
[clojure.string :as str]
[react :as react]))
(def good-$ #"^\-?[0-9]+(\.[0-9][0-9])?$")
(defn money-field [{:keys [min max disabled on-change value]}] (defn -money-field [{:keys [min max disabled on-change value class style]}]
(let [parsed-amount (r/atom {:parsed value (let [[ parsed-amount set-parsed-amount] (react/useState {:parsed value
:raw (str value)})] :raw (cond
(fn [{:keys [min max disabled on-change value]}] (str/blank? value)
[:input.input {:type "number" ""
(js/Number.isNaN (js/parseFloat value))
""
:else
(->short$ (js/parseFloat value)))})]
(react/useEffect (fn []
;; allow the controlling field to change the raw representation
;; when the raw amount is a valid representation, so that 33.
;; doesn't get unset
(when (or
(and (:raw parsed-amount)
(re-find good-$ (:raw parsed-amount)))
(str/blank? (:raw parsed-amount)))
(set-parsed-amount
(assoc parsed-amount
:parsed value
:raw (cond
(str/blank? value)
""
(js/Number.isNaN (js/parseFloat value))
""
:else
(->short$ (js/parseFloat value))))))
nil))
[:div.control.has-icons-left
[:input.input {:type "text"
:disabled disabled :disabled disabled
:class class
:on-change (fn [e] :on-change (fn [e]
(let [raw (.. e -target -value) (let [raw (.. e -target -value)
new-value (when (and raw (not (str/blank? raw))) new-value (when (and raw
(not (str/blank? raw))
(re-find good-$ raw))
(js/parseFloat raw))] (js/parseFloat raw))]
(swap! parsed-amount assoc (set-parsed-amount {:raw raw
:raw raw :parsed new-value})
:parsed new-value)
(when (not= value new-value) (when (not= value new-value)
(on-change new-value)))) (on-change new-value))))
:value (:raw @parsed-amount) :value (or (:raw parsed-amount)
"")
:on-blur (fn []
(when-not (re-find good-$ (:raw parsed-amount))
(set-parsed-amount {:raw ""
:parsed nil})
(on-change nil)))
:min min :min min
:max max :max max
:step "0.01"}]))) :step "0.01"
:style (or style
{:width "8em"})}]
[:span.icon.is-left
[:i.fa.fa-usd]]]))
(defn money-field []
[:f> -money-field (r/props (r/current-component))])

View File

@@ -13,7 +13,7 @@
::search-completed ::search-completed
(fn [_ [_ set-items set-loading-status result]] (fn [_ [_ set-items set-loading-status result]]
(set-loading-status nil) (set-loading-status nil)
(set-items (:search-results result)) (set-items (into-array (:search-results result)))
{})) {}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::search-failed ::search-failed
@@ -54,20 +54,27 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::input-value-changed ::input-value-changed
(fn [_ [_ input-value search-query set-items set-loading-status]] (fn [_ [_ input-value search-query set-items set-loading-status]]
(set-items []) (set-items #js [])
(when (> (count input-value) 2) (when (> (count input-value) 2)
(set-loading-status :loading) (set-loading-status :loading)
{:dispatch-debounce {:event [::input-value-settled input-value search-query set-items set-loading-status] {:dispatch-debounce {:event [::input-value-settled input-value search-query set-items set-loading-status]
:time 250 :time 250
:key ::input-value-settled}}))) :key ::input-value-settled}})))
(defn typeahead-v3-internal [{:keys [class entity->text entities on-input-change style ^js on-change disabled value name auto-focus] :or {disabled false} :as i}] (defn typeahead-v3-internal [{:keys [class entity->text entities on-input-change style ^js on-change disabled value name auto-focus] :or {disabled false}
(let [[items set-items] (react/useState (or (clj->js entities) prop-value :value
:as i}]
(let [[items set-items] (react/useState (or entities
[])) []))
[focused set-focus] (react/useState (boolean auto-focus)) [focused set-focus] (react/useState (boolean auto-focus))
[loading-status set-loading-status] (react/useState false) [loading-status set-loading-status] (react/useState false)
[value set-value] (react/useState value)
;; resets internal representation of value when props change
_ (react/useEffect (fn []
(set-value prop-value)))
[getMenuProps getComboboxProps getInputProps getItemProps isOpen highlightedIndex selectItem selectedItem setInputValue] [getMenuProps getComboboxProps getInputProps getItemProps isOpen highlightedIndex selectItem selectedItem setInputValue]
(as-> (useCombobox (clj->js {:items items (as-> (useCombobox #js {:items (into-array items)
:defaultHighlightedIndex 0 :defaultHighlightedIndex 0
:defaultSelectedItem value :defaultSelectedItem value
:itemToString (fn [] :itemToString (fn []
@@ -76,9 +83,11 @@
:onInputValueChange (fn [input] :onInputValueChange (fn [input]
(on-input-change input set-items set-loading-status)) (on-input-change input set-items set-loading-status))
:stateReducer state-reducer :stateReducer state-reducer
:selectedItem value
:onSelectedItemChange (fn [z] :onSelectedItemChange (fn [z]
(set-value (aget z "selectedItem"))
(when on-change (when on-change
(on-change (js->clj (aget z "selectedItem") :keywordize-keys true))))})) $ (on-change (aget z "selectedItem"))))}) $
(map #(aget $ %) ["getMenuProps" "getComboboxProps" "getInputProps" "getItemProps" "isOpen" "highlightedIndex" "selectItem" "selectedItem" "setInputValue"]))] (map #(aget $ %) ["getMenuProps" "getComboboxProps" "getInputProps" "getItemProps" "isOpen" "highlightedIndex" "selectItem" "selectedItem" "setInputValue"]))]
[:<> [:<>
[:div.typeahead (assoc (js->clj (getComboboxProps)) [:div.typeahead (assoc (js->clj (getComboboxProps))
@@ -110,7 +119,7 @@
[:div.level-item [:div.level-item
[:div.control [:div.control
[:div.tags.has-addons [:div.tags.has-addons
[:span.tag (:name (js->clj selectedItem :keywordize-keys true))] [:span.tag (:name selectedItem)]
(when name (when name
[:input {:type "hidden" :name name :value (:id (js->clj selectedItem :keywordize-keys true))}]) [:input {:type "hidden" :name name :value (:id (js->clj selectedItem :keywordize-keys true))}])
(when-not disabled (when-not disabled
@@ -166,11 +175,12 @@
(fn [input set-items] (fn [input set-items]
(if entities-by-id (if entities-by-id
(do (do
(set-items
(into-array
(->> (.search entity-index (or (aget input "inputValue") "") #js {:fuzzy 0.2} ) (->> (.search entity-index (or (aget input "inputValue") "") #js {:fuzzy 0.2} )
clj->js (take 10)))))
(take 10)
(set-items)))
(set-items (map clj->js (take 10 (filter (fn [x] (str/includes? (or (some-> (entity->text x) str/lower-case) "") (set-items (into-array
(take 10 (filter (fn [x] (str/includes? (or (some-> (entity->text x) str/lower-case) "")
(or (some-> (aget input "inputValue") str/lower-case) ""))) (or (some-> (aget input "inputValue") str/lower-case) "")))
entities)))))))]]) entities)))))))]])

View File

@@ -45,7 +45,7 @@
:name name :name name
:print-as print-as :print-as print-as
:terms (or (str->int terms) :terms (or (str->int terms)
0) nil)
:default-account-id (:id default-account) :default-account-id (:id default-account)
:address address :address address
:primary-contact primary-contact :primary-contact primary-contact

View File

@@ -1,34 +1,41 @@
(ns auto-ap.views.pages.invoices.form (ns auto-ap.views.pages.invoices.form
(:require [auto-ap.entities.invoice :as invoice] (:require
[auto-ap.entities.invoice :as invoice]
[auto-ap.events :as events] [auto-ap.events :as events]
[auto-ap.forms :as forms] [auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.status :as status] [auto-ap.status :as status]
[auto-ap.subs :as subs] [auto-ap.subs :as subs]
[auto-ap.time-utils :refer [next-dom]] [auto-ap.time-utils :refer [next-dom]]
[auto-ap.utils :refer [dollars=]] [auto-ap.utils :refer [dollars=]]
[auto-ap.views.components.dropdown :refer [drop-down]]
[auto-ap.views.components.expense-accounts-field [auto-ap.views.components.expense-accounts-field
:as :as expense-accounts-field
expense-accounts-field :refer [recalculate-amounts]]
:refer
[expense-accounts-field recalculate-amounts]]
[auto-ap.views.components.layouts :as layouts] [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.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.money-field :refer [money-field]]
[auto-ap.views.components.switch-field :refer [switch-field]] [auto-ap.views.components.switch-field :refer [switch-field]]
[auto-ap.views.components.typeahead :refer [typeahead-v3]] [auto-ap.views.components.typeahead :refer [typeahead-v3]]
[auto-ap.views.components.typeahead.vendor :refer [search-backed-typeahead]] [auto-ap.views.components.typeahead.vendor
:refer [search-backed-typeahead]]
[auto-ap.views.pages.invoices.common :refer [invoice-read]] [auto-ap.views.pages.invoices.common :refer [invoice-read]]
[auto-ap.views.utils [auto-ap.views.utils
:refer :refer [date->str
[date->str date-picker dispatch-event standard str->date with-user]] date-picker-friendly
dispatch-event
standard
str->date
with-user]]
[cljs-time.core :as c] [cljs-time.core :as c]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.string :as str] [clojure.string :as str]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[reagent.core :as r] [reagent.core :as r]
[vimsical.re-frame.fx.track :as track] [vimsical.re-frame.cofx.inject :as inject]
[vimsical.re-frame.cofx.inject :as inject])) [vimsical.re-frame.fx.track :as track]))
;; SUBS ;; SUBS
(re-frame/reg-sub (re-frame/reg-sub
@@ -134,7 +141,8 @@
:vendor-preferences vendor-preferences :vendor-preferences vendor-preferences
:scheduled-payment (:scheduled-payment edit-invoice) :scheduled-payment (:scheduled-payment edit-invoice)
:invoice-number (:invoice-number edit-invoice) :invoice-number (:invoice-number edit-invoice)
:total (:total edit-invoice) :total (cond-> (:total edit-invoice)
(not (str/blank? (:total edit-invoice))) (js/parseFloat ))
:original edit-invoice :original edit-invoice
:vendor (:vendor edit-invoice) :vendor (:vendor edit-invoice)
:client (:client edit-invoice) :client (:client edit-invoice)
@@ -320,8 +328,12 @@
min-total (if (= (:total (:original data)) (:outstanding-balance (:original data))) min-total (if (= (:total (:original data)) (:outstanding-balance (:original data)))
nil nil
(- (:total (:original data)) (:outstanding-balance (:original data))))] (- (:total (:original data)) (:outstanding-balance (:original data))))]
(with-meta [form-builder/builder {:can-submit [::can-submit]
(form-inline (assoc params :title [:div "New Invoice " :change-event [::changed]
:submit-event [::save-requested [::saving ]]
:id ::form}
[form-builder/section {:title [:div "New Invoice "
(cond (cond
(#{:unpaid ":unpaid"} (:status data)) (#{:unpaid ":unpaid"} (:status data))
nil nil
@@ -339,20 +351,20 @@
[:div.tag.is-info.is-light "Paid"]) [:div.tag.is-info.is-light "Paid"])
:else :else
nil)]) nil)]}
[:<>
(when-not active-client (when-not active-client
(field [:span "Client" [form-builder/field {:required? true}
[:span.has-text-danger " *"]] "Client"
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients]) [typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients])
:entity->text :name :entity->text :name
:type "typeahead-v3" :type "typeahead-v3"
:auto-focus (if active-client false true) :auto-focus (if active-client false true)
:field [:client] :field [:client]
:disabled exists? :disabled exists?
:spec ::invoice/client}])) :spec ::invoice/client}]])
(field [:span "Vendor" [form-builder/field {:required? true}
[:span.has-text-danger " *"]] "Vendor"
[search-backed-typeahead {:disabled exists? [search-backed-typeahead {:disabled exists?
:search-query (fn [i] :search-query (fn [i]
[:search_vendor [:search_vendor
@@ -360,87 +372,56 @@
[:name :id]]) [:name :id]])
:type "typeahead-v3" :type "typeahead-v3"
:auto-focus (if active-client true false) :auto-focus (if active-client true false)
:field [:vendor]}]) :field [:vendor]}]]
[form-builder/field {:required? true}
(field [:span "Date" "Date"
[:span.has-text-danger " *"]] [date-picker-friendly {:type "date"
[date-picker {:class-name "input"
:class "input"
:format-week-number (fn [] "")
:previous-month-button-label ""
:placeholder "mm/dd/yyyy"
:disable-keyboard-navigation true
:next-month-button-label ""
:next-month-label ""
:type "date"
:field [:date] :field [:date]
:spec ::invoice/date}]) :spec ::invoice/date}]]
(field "Due (optional)" [form-builder/field
[date-picker {:class-name "input" "Due (optional)"
:class "input" [date-picker-friendly {:type "date"
:format-week-number (fn [] "")
:previous-month-button-label ""
:placeholder "mm/dd/yyyy"
:disable-keyboard-navigation true
:next-month-button-label ""
:next-month-label ""
:type "date"
:field [:due] :field [:due]
:spec ::invoice/due}]) :spec ::invoice/due}]]
[form-builder/vertical-control
[:p.help "Scheduled payment (optional)"] "Scheduled payment (optional)"
[:div.level [left-stack
[:div.level-left
[:div.level-item
[:div.control [:div.control
(raw-field [form-builder/raw-field
[date-picker {:class-name "input" [date-picker-friendly {:type "date"
:class "input"
:disabled (boolean (:schedule-when-due data))
:format-week-number (fn [] "")
:previous-month-button-label ""
:placeholder "mm/dd/yyyy"
:next-month-button-label ""
:next-month-label ""
:type "date"
:field [:scheduled-payment] :field [:scheduled-payment]
:spec ::invoice/scheduled-payment}])]] :spec ::invoice/scheduled-payment}]]]
[:div.level-item [:div.control [:div.control
(raw-field [form-builder/raw-field
[switch-field {:id "schedule-when-due" [switch-field {:id "schedule-when-due"
:field [:schedule-when-due] :field [:schedule-when-due]
:label "Same as due date" :label "Same as due date"
:type "checkbox"}])]]]] :type "checkbox"}]]]]]
[form-builder/field {:required? true}
(field [:span "Invoice #" "Invoice #"
[:span.has-text-danger " *"]]
[:input.input {:type "text" [:input.input {:type "text"
:field [:invoice-number] :field [:invoice-number]
:spec ::invoice/invoice-number}]) :spec ::invoice/invoice-number}]]
[form-builder/field {:required? true}
"Total"
(field [:span "Total"
[:span.has-text-danger " *"]]
[money-field {:type "money" [money-field {:type "money"
:field [:total] :field [:total]
:disabled (if can-change-amount? "" "disabled") :disabled (if can-change-amount? "" "disabled")
:style {:max-width "8em"}
:min min-total :min min-total
:spec ::invoice/total :spec ::invoice/total
:step "0.01"}]) :step "0.01"}]]]
[form-builder/raw-field
(with-meta
(field nil
[expense-accounts-field {:type "expense-accounts" [expense-accounts-field {:type "expense-accounts"
:descriptor "expense account" :descriptor "expense account"
:locations (:locations (:client data)) :locations (:locations (:client data))
:max (:total data) :max (:total data)
:client (or (:client data) active-client) :client (or (:client data) active-client)
:field [:expense-accounts]}]) :field [:expense-accounts]}]]
{:key (str (:id (:vendor data) "none") "-" (:id (:client data) "none") )}) [form-builder/error-notification]
[:div {:style {:margin-bottom "1em"}}]
(error-notification)
[:div.columns [:div.columns
(when-not exists? (when-not exists?
[:div.column [:div.column
@@ -464,9 +445,8 @@
^{:key (str id "-check")} [:a.dropdown-item {:on-click (dispatch-event [::save-requested [::add-and-print id :check]])} "Print checks from " name] ^{:key (str id "-check")} [:a.dropdown-item {:on-click (dispatch-event [::save-requested [::add-and-print id :check]])} "Print checks from " name]
^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event [::save-requested [::add-and-print id :debit]])} "Debit from " name]))))]]]) ^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event [::save-requested [::add-and-print id :debit]])} "Debit from " name]))))]]])
[:div.column [:div.column
[form-builder/submit-button {:class ["is-fullwidth"]}
(submit-button "Save")]]]) "Save"]]]])])
{:key id}))])
(defn form [_] (defn form [_]

View File

@@ -38,6 +38,19 @@
(defn ->% [x] (defn ->% [x]
(nf% x)) (nf% x))
(defn ->short$ [x]
(cond
(nil? x)
nil
(int? x)
(str x)
(float? x)
(.toFixed x 2)
))
(defn active-when= [active-page candidate] (defn active-when= [active-page candidate]
(when (= active-page candidate) " is-active")) (when (= active-page candidate) " is-active"))