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 ::scheduled-payment (s/nilable ::shared/date))
(s/def ::total ::shared/money)
(s/def ::vendor-id ::shared/identifier)
(s/def ::invoice (s/keys :req-un [::client
::invoice-number

View File

@@ -3,7 +3,7 @@
[clojure.string :as str]))
(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 only-upper-case #"^[A-Z]+$")
@@ -14,4 +14,5 @@
(s/def ::money (s/or :string (s/and string?
#(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}]
(let [data-sub (or data-sub [::forms/form 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])]
(r/create-element Provider #js {:value #js {:can-submit @(re-frame/subscribe can-submit)
:change-event change-event
@@ -29,6 +29,7 @@
:data data
:fullwidth? fullwidth?}}
(r/as-element
^{:key form-key}
[:form {:on-submit (fn [e]
(when (.-stopPropagation e)
(.stopPropagation e)
@@ -78,13 +79,19 @@
(into [:div.control ] children)]))]))
(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 {}
(fn [consume]
(r/as-element
[:div.field
(when label (if (aget consume "fullwidth?") [:p.help label]
[:label.label label]))
(when 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]]]))]))
(defn horizontal-control []
@@ -111,7 +118,7 @@
(into [:div {:style {:margin-bottom "5em"}}]
(r/children (r/current-component)))])
(defn submit-button []
(defn submit-button [{:keys [class]}]
(let [[child] (r/children (r/current-component))]
[:> Consumer {}
(fn [consume]
@@ -121,8 +128,9 @@
(r/as-element
[:button.button.is-medium.is-primary {:disabled (or (status/disabled-for status)
(not can-submit))
:class (cond-> (status/class-for status)
fullwidth? (conj "is-fullwidth")) }
:class (cond-> (or class [])
(status/class-for status) (conj (status/class-for status))
fullwidth? (conj "is-fullwidth")) }
child])))]))
(defn hidden-submit-button []

View File

@@ -103,7 +103,6 @@
updated-accounts (if-let [location (get-in updated-accounts [(first field) :account :location])]
(assoc-in updated-accounts [(first field) :location] location)
updated-accounts)]
(println updated-accounts)
{:dispatch (into event [updated-accounts])})))
@@ -163,7 +162,6 @@
[:div.column.is-narrow
[:p.help "Location"]
[:div.control
(println account)
(if-let [forced-location (:location account)]
[:div.select
[: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
(: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]}]
(let [parsed-amount (r/atom {:parsed value
:raw (str value)})]
(fn [{:keys [min max disabled on-change value]}]
[:input.input {:type "number"
:disabled disabled
:on-change (fn [e]
(let [raw (.. e -target -value)
new-value (when (and raw (not (str/blank? raw)))
(js/parseFloat raw))]
(swap! parsed-amount assoc
:raw raw
:parsed new-value)
(when (not= value new-value)
(on-change new-value))))
:value (:raw @parsed-amount)
:min min
:max max
:step "0.01"}])))
(defn -money-field [{:keys [min max disabled on-change value class style]}]
(let [[ parsed-amount set-parsed-amount] (react/useState {:parsed value
:raw (cond
(str/blank? value)
""
(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
:class class
:on-change (fn [e]
(let [raw (.. e -target -value)
new-value (when (and raw
(not (str/blank? raw))
(re-find good-$ raw))
(js/parseFloat raw))]
(set-parsed-amount {:raw raw
:parsed new-value})
(when (not= value new-value)
(on-change new-value))))
: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
:max max
: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
(fn [_ [_ set-items set-loading-status result]]
(set-loading-status nil)
(set-items (:search-results result))
(set-items (into-array (:search-results result)))
{}))
(re-frame/reg-event-fx
::search-failed
@@ -54,31 +54,40 @@
(re-frame/reg-event-fx
::input-value-changed
(fn [_ [_ input-value search-query set-items set-loading-status]]
(set-items [])
(set-items #js [])
(when (> (count input-value) 2)
(set-loading-status :loading)
{:dispatch-debounce {:event [::input-value-settled input-value search-query set-items set-loading-status]
:time 250
: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}]
(let [[items set-items] (react/useState (or (clj->js entities)
(defn typeahead-v3-internal [{:keys [class entity->text entities on-input-change style ^js on-change disabled value name auto-focus] :or {disabled false}
prop-value :value
:as i}]
(let [[items set-items] (react/useState (or entities
[]))
[focused set-focus] (react/useState (boolean auto-focus))
[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]
(as-> (useCombobox (clj->js {:items items
:defaultHighlightedIndex 0
:defaultSelectedItem value
:itemToString (fn []
;; once an item is selected, you just use empty text
"")
:onInputValueChange (fn [input]
(on-input-change input set-items set-loading-status))
:stateReducer state-reducer
:onSelectedItemChange (fn [z]
(when on-change
(on-change (js->clj (aget z "selectedItem") :keywordize-keys true))))})) $
(as-> (useCombobox #js {:items (into-array items)
:defaultHighlightedIndex 0
:defaultSelectedItem value
:itemToString (fn []
;; once an item is selected, you just use empty text
"")
:onInputValueChange (fn [input]
(on-input-change input set-items set-loading-status))
:stateReducer state-reducer
:selectedItem value
:onSelectedItemChange (fn [z]
(set-value (aget z "selectedItem"))
(when on-change
(on-change (aget z "selectedItem"))))}) $
(map #(aget $ %) ["getMenuProps" "getComboboxProps" "getInputProps" "getItemProps" "isOpen" "highlightedIndex" "selectItem" "selectedItem" "setInputValue"]))]
[:<>
[:div.typeahead (assoc (js->clj (getComboboxProps))
@@ -110,7 +119,7 @@
[:div.level-item
[:div.control
[:div.tags.has-addons
[:span.tag (:name (js->clj selectedItem :keywordize-keys true))]
[:span.tag (:name selectedItem)]
(when name
[:input {:type "hidden" :name name :value (:id (js->clj selectedItem :keywordize-keys true))}])
(when-not disabled
@@ -166,11 +175,12 @@
(fn [input set-items]
(if entities-by-id
(do
(->> (.search entity-index (or (aget input "inputValue") "") #js {:fuzzy 0.2} )
clj->js
(take 10)
(set-items)))
(set-items
(into-array
(->> (.search entity-index (or (aget input "inputValue") "") #js {:fuzzy 0.2} )
(take 10)))))
(set-items (map clj->js (take 10 (filter (fn [x] (str/includes? (or (some-> (entity->text x) str/lower-case) "")
(or (some-> (aget input "inputValue") str/lower-case) "")))
entities)))))))]])
(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) "")))
entities)))))))]])

View File

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

View File

@@ -1,34 +1,41 @@
(ns auto-ap.views.pages.invoices.form
(:require [auto-ap.entities.invoice :as invoice]
[auto-ap.events :as events]
[auto-ap.forms :as forms]
[auto-ap.status :as status]
[auto-ap.subs :as subs]
[auto-ap.time-utils :refer [next-dom]]
[auto-ap.utils :refer [dollars=]]
[auto-ap.views.components.dropdown :refer [drop-down]]
[auto-ap.views.components.expense-accounts-field
:as
expense-accounts-field
:refer
[expense-accounts-field recalculate-amounts]]
[auto-ap.views.components.layouts :as layouts]
[auto-ap.views.components.modal :as modal]
[auto-ap.views.components.money-field :refer [money-field]]
[auto-ap.views.components.switch-field :refer [switch-field]]
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
[auto-ap.views.components.typeahead.vendor :refer [search-backed-typeahead]]
[auto-ap.views.pages.invoices.common :refer [invoice-read]]
[auto-ap.views.utils
:refer
[date->str date-picker dispatch-event standard str->date with-user]]
[cljs-time.core :as c]
[clojure.spec.alpha :as s]
[clojure.string :as str]
[re-frame.core :as re-frame]
[reagent.core :as r]
[vimsical.re-frame.fx.track :as track]
[vimsical.re-frame.cofx.inject :as inject]))
(:require
[auto-ap.entities.invoice :as invoice]
[auto-ap.events :as events]
[auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.status :as status]
[auto-ap.subs :as subs]
[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]]
[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]]
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
[auto-ap.views.components.typeahead.vendor
:refer [search-backed-typeahead]]
[auto-ap.views.pages.invoices.common :refer [invoice-read]]
[auto-ap.views.utils
:refer [date->str
date-picker-friendly
dispatch-event
standard
str->date
with-user]]
[cljs-time.core :as c]
[clojure.spec.alpha :as s]
[clojure.string :as str]
[re-frame.core :as re-frame]
[reagent.core :as r]
[vimsical.re-frame.cofx.inject :as inject]
[vimsical.re-frame.fx.track :as track]))
;; SUBS
(re-frame/reg-sub
@@ -134,7 +141,8 @@
:vendor-preferences vendor-preferences
:scheduled-payment (:scheduled-payment 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
:vendor (:vendor edit-invoice)
:client (:client edit-invoice)
@@ -320,153 +328,125 @@
min-total (if (= (:total (:original data)) (:outstanding-balance (:original data)))
nil
(- (:total (:original data)) (:outstanding-balance (:original data))))]
(with-meta
(form-inline (assoc params :title [:div "New Invoice "
(cond
(#{:unpaid ":unpaid"} (:status data))
nil
[form-builder/builder {:can-submit [::can-submit]
:change-event [::changed]
:submit-event [::save-requested [::saving ]]
:id ::form}
(#{:voided ":voided"} (:status data))
[:div.tag.is-info.is-light "Voided"]
[form-builder/section {:title [:div "New Invoice "
(cond
(#{:unpaid ":unpaid"} (:status data))
nil
(and (#{:paid ":paid"} (:status data))
(not (seq (:payments data))))
[:div.tag.is-info.is-light "Automatically paid"]
(#{:voided ":voided"} (:status data))
[:div.tag.is-info.is-light "Voided"]
(#{:paid ":paid"} (:status data))
(if-let [check-number (:check-number (:payment (first (:payments data))))]
[:div.tag.is-info.is-light "Paid by check #" check-number ]
[:div.tag.is-info.is-light "Paid"])
(and (#{:paid ":paid"} (:status data))
(not (seq (:payments data))))
[:div.tag.is-info.is-light "Automatically paid"]
:else
nil)])
[:<>
(when-not active-client
(field [:span "Client"
[:span.has-text-danger " *"]]
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients])
:entity->text :name
:type "typeahead-v3"
:auto-focus (if active-client false true)
:field [:client]
:disabled exists?
:spec ::invoice/client}]))
(field [:span "Vendor"
[:span.has-text-danger " *"]]
[search-backed-typeahead {:disabled exists?
:search-query (fn [i]
[:search_vendor
{:query i}
[:name :id]])
:type "typeahead-v3"
:auto-focus (if active-client true false)
:field [:vendor]}])
(#{:paid ":paid"} (:status data))
(if-let [check-number (:check-number (:payment (first (:payments data))))]
[:div.tag.is-info.is-light "Paid by check #" check-number ]
[:div.tag.is-info.is-light "Paid"])
(field [:span "Date"
[:span.has-text-danger " *"]]
[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]
:spec ::invoice/date}])
:else
nil)]}
(field "Due (optional)"
[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 [:due]
:spec ::invoice/due}])
(when-not active-client
[form-builder/field {:required? true}
"Client"
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients])
:entity->text :name
:type "typeahead-v3"
:auto-focus (if active-client false true)
:field [:client]
:disabled exists?
:spec ::invoice/client}]])
[form-builder/field {:required? true}
"Vendor"
[search-backed-typeahead {:disabled exists?
:search-query (fn [i]
[:search_vendor
{:query i}
[:name :id]])
:type "typeahead-v3"
:auto-focus (if active-client true false)
:field [:vendor]}]]
[form-builder/field {:required? true}
"Date"
[date-picker-friendly {:type "date"
:field [:date]
:spec ::invoice/date}]]
[:p.help "Scheduled payment (optional)"]
[:div.level
[:div.level-left
[:div.level-item
[:div.control
(raw-field
[date-picker {:class-name "input"
: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]
:spec ::invoice/scheduled-payment}])]]
[:div.level-item [:div.control
(raw-field
[switch-field {:id "schedule-when-due"
:field [:schedule-when-due]
:label "Same as due date"
:type "checkbox"}])]]]]
[form-builder/field
"Due (optional)"
[date-picker-friendly {:type "date"
:field [:due]
:spec ::invoice/due}]]
[form-builder/vertical-control
"Scheduled payment (optional)"
[left-stack
[:div.control
[form-builder/raw-field
[date-picker-friendly {:type "date"
:field [:scheduled-payment]
:spec ::invoice/scheduled-payment}]]]
[:div.control
[form-builder/raw-field
(field [:span "Invoice #"
[:span.has-text-danger " *"]]
[:input.input {:type "text"
:field [:invoice-number]
:spec ::invoice/invoice-number}])
(field [:span "Total"
[:span.has-text-danger " *"]]
[money-field {:type "money"
:field [:total]
:disabled (if can-change-amount? "" "disabled")
:min min-total
:spec ::invoice/total
:step "0.01"}])
(with-meta
(field nil
[expense-accounts-field {:type "expense-accounts"
:descriptor "expense account"
:locations (:locations (:client data))
:max (:total data)
:client (or (:client data) active-client)
:field [:expense-accounts]}])
{:key (str (:id (:vendor data) "none") "-" (:id (:client data) "none") )})
(error-notification)
[:div.columns
(when-not exists?
[:div.column
[drop-down {:header [:button.button.is-primary-two.is-medium.is-fullwidth {:aria-haspopup true
:type "button"
:on-click (dispatch-event [::events/toggle-menu ::add-and-print-invoice ])
:disabled (or (status/disabled-for status)
(not can-submit?))
:class (status/class-for status)}
"Pay "
[:span " "]
[:span.icon [:i.fa.fa-angle-down {:aria-hidden "true"}]]]
:class "is-fullwidth"
:id ::add-and-print-invoice}
[:div
(list
(for [{:keys [id name type]} (->> (:bank-accounts (:client data)) (filter :visible) (sort-by :sort-order))]
(if (= :cash type)
^{:key id} [:a.dropdown-item {:on-click (dispatch-event [::save-requested [::add-and-print id :cash]])} "With cash"]
(list
^{: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]))))]]])
[:div.column
(submit-button "Save")]]])
{:key id}))])
[switch-field {:id "schedule-when-due"
:field [:schedule-when-due]
:label "Same as due date"
:type "checkbox"}]]]]]
[form-builder/field {:required? true}
"Invoice #"
[:input.input {:type "text"
:field [:invoice-number]
:spec ::invoice/invoice-number}]]
[form-builder/field {:required? true}
"Total"
[money-field {:type "money"
:field [:total]
: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"
:descriptor "expense account"
:locations (:locations (:client data))
:max (:total data)
:client (or (:client data) active-client)
:field [:expense-accounts]}]]
[form-builder/error-notification]
[:div {:style {:margin-bottom "1em"}}]
[:div.columns
(when-not exists?
[:div.column
[drop-down {:header [:button.button.is-primary-two.is-medium.is-fullwidth {:aria-haspopup true
:type "button"
:on-click (dispatch-event [::events/toggle-menu ::add-and-print-invoice ])
:disabled (or (status/disabled-for status)
(not can-submit?))
:class (status/class-for status)}
"Pay "
[:span " "]
[:span.icon [:i.fa.fa-angle-down {:aria-hidden "true"}]]]
:class "is-fullwidth"
:id ::add-and-print-invoice}
[:div
(list
(for [{:keys [id name type]} (->> (:bank-accounts (:client data)) (filter :visible) (sort-by :sort-order))]
(if (= :cash type)
^{:key id} [:a.dropdown-item {:on-click (dispatch-event [::save-requested [::add-and-print id :cash]])} "With cash"]
(list
^{: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]))))]]])
[:div.column
[form-builder/submit-button {:class ["is-fullwidth"]}
"Save"]]]])])
(defn form [_]

View File

@@ -38,6 +38,19 @@
(defn ->% [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]
(when (= active-page candidate) " is-active"))