now you can create and edit invoices from one form.

This commit is contained in:
BC
2019-02-18 17:04:51 -08:00
parent e2b99961e4
commit 5b622cd4cc
2 changed files with 175 additions and 311 deletions

View File

@@ -13,7 +13,7 @@
valid-matches)) valid-matches))
(defn typeahead [{:keys [matches on-change field text-field value class not-found-description (defn typeahead [{:keys [matches on-change field text-field value class not-found-description
not-found-value auto-focus]}] disabled not-found-value auto-focus]}]
(let [text (r/atom (or (second (first (filter #(= (first %) value) matches))) "")) (let [text (r/atom (or (second (first (filter #(= (first %) value) matches))) ""))
highlighted (r/atom nil) highlighted (r/atom nil)
selected (r/atom (first (first (filter #(= (first %) value) matches)))) selected (r/atom (first (first (filter #(= (first %) value) matches))))
@@ -25,68 +25,75 @@
(on-change nil text-description text-value) (on-change nil text-description text-value)
(on-change id text-description (or text-value text-description)))))] (on-change id text-description (or text-value text-description)))))]
(r/create-class (r/create-class
{:reagent-render (fn [{:keys [matches on-change field text-field value class not-found-description]}] {:reagent-render (fn [{:keys [matches on-change disabled field text-field value class not-found-description]}]
(let [text @text (let [text @text
valid-matches (get-valid-matches matches not-found-description not-found-value text)] valid-matches (get-valid-matches matches not-found-description not-found-value text)]
[:div.typeahead [:div.typeahead
(if @selected (if disabled
^{:key "typeahead"} [:div.input {:class class
:tab-index "0"
:on-key-up (fn [e]
(if (= 8 (.-keyCode e))
(do
(select [nil "" nil])
true)
false))}
[:div.control
[:div.tags.has-addons
[:span.tag text]
[:a.tag.is-delete {:on-click (fn [] (select [nil "" nil]))}]]]]
^{:key "typeahead"} [:input.input {:type "text"
:class class
:value text
:auto-focus auto-focus
:on-blur (fn [e]
(cond @selected
nil
(#{"" nil} text) ^{:key (str "typeahead" text) } [:input.input {:disabled "disabled" :value text} ]
nil
@highlighted (if @selected
(do (select @highlighted) ^{:key "typeahead"} [:div.input {:class class
true) :tab-index "0"
:on-key-up (fn [e]
(if (= 8 (.-keyCode e))
(do
(select [nil "" nil])
true)
false))}
[:div.control
[:div.tags.has-addons
[:span.tag text]
[:a.tag.is-delete {:on-click (fn [] (select [nil "" nil]))}]]]]
^{:key "typeahead"} [:input.input {:type "text"
:class class
:else :value text
(do (select [nil ""]) :auto-focus auto-focus
true))) :on-blur (fn [e]
:on-key-down (fn [e] (cond @selected
(condp = (.-keyCode e) nil
; up
38 (do (#{"" nil} text)
(when-let [new-match (->> valid-matches nil
(take-while #(not= % @highlighted))
(last))] @highlighted
(reset! highlighted new-match)) (do (select @highlighted)
true) true)
;; dwon
40 (do :else
(when-let [new-match (->> valid-matches (do (select [nil ""])
(drop-while #(not= % @highlighted)) true)))
(drop 1) :on-key-down (fn [e]
(first))] (condp = (.-keyCode e)
(reset! highlighted new-match)) ; up
true) 38 (do
13 (do (.preventDefault e) (when-let [new-match (->> valid-matches
(when @highlighted (take-while #(not= % @highlighted))
(last))]
(select @highlighted) (reset! highlighted new-match))
false)) true)
true)) ;; dwon
:on-change (fn [e] 40 (do
(let [new-matches (get-valid-matches matches not-found-description not-found-value (.. e -target -value))] (when-let [new-match (->> valid-matches
(reset! highlighted (first new-matches))) (drop-while #(not= % @highlighted))
(select [nil (.. e -target -value)]))}]) (drop 1)
(first))]
(reset! highlighted new-match))
true)
13 (do (.preventDefault e)
(when @highlighted
(select @highlighted)
false))
true))
:on-change (fn [e]
(let [new-matches (get-valid-matches matches not-found-description not-found-value (.. e -target -value))]
(reset! highlighted (first new-matches)))
(select [nil (.. e -target -value)]))}]))
(cond (cond
(and (seq text) (and (seq text)
(not @selected) (not @selected)

View File

@@ -64,16 +64,6 @@
(fn [db] (fn [db]
(-> db ::check-results))) (-> db ::check-results)))
(re-frame/reg-sub
::new-invoice
(fn [db]
(-> db ::new-invoice)))
(re-frame/reg-sub
::edit-invoice
(fn [db]
(-> db ::edit-invoice)))
(re-frame/reg-sub (re-frame/reg-sub
::params ::params
(fn [db] (fn [db]
@@ -261,18 +251,31 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::new-invoice-clicked ::new-invoice-clicked
(fn [{:keys [db]} _] (fn [{:keys [db]} _]
{:db (forms/start-form db ::new-invoice {:client-id (:id @(re-frame/subscribe [::subs/client])) {:db
:status :unpaid (-> db
:date (date->str (c/now) standard) (forms/start-form ::new-invoice {:client-id (:id @(re-frame/subscribe [::subs/client]))
:location (first (:locations @(re-frame/subscribe [::subs/client])))})})) :status :unpaid
:date (date->str (c/now) standard)
:location (first (:locations @(re-frame/subscribe [::subs/client])))}))}))
(re-frame/reg-event-fx (re-frame/reg-event-db
::edit-invoice ::edit-invoice
(fn [{:keys [db]} [_ which]] (fn [db [_ which]]
(let [edit-invoice (update which :date #(date->str % standard)) (let [edit-invoice (update which :date #(date->str % standard))
edit-invoice (assoc edit-invoice :original edit-invoice)] edit-invoice (assoc edit-invoice :original edit-invoice)]
{:dispatch [::events/modal-status ::edit-invoice {:visible? true}]
:db (assoc-in db [::edit-invoice] edit-invoice)}))) (-> db
(forms/start-form ::new-invoice {:id (:id edit-invoice)
:status (:status edit-invoice)
:date (:date edit-invoice) #_(date->str (:date edit-invoice) standard)
:invoice-number (:invoice-number edit-invoice)
:total (:total edit-invoice)
:original edit-invoice
:vendor-id (:id (:vendor edit-invoice))
:vendor-name (:name (:vendor edit-invoice))
:client-id (:id (:client edit-invoice))
:client-name (:name (:client edit-invoice))})))))
@@ -300,9 +303,9 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::edit-invoice-save ::edit-invoice-saving
(fn [{:keys [db]} _] (fn [{:keys [db]} _]
(let [{:keys [date total invoice-number id]} @(re-frame/subscribe [::edit-invoice])] (let [{{:keys [date total invoice-number id]} :data} @(re-frame/subscribe [::forms/form ::new-invoice])]
{:graphql {:graphql
{:token (-> db :user) {:token (-> db :user)
:query-obj {:venia/operation {:operation/type :mutation :query-obj {:venia/operation {:operation/type :mutation
@@ -312,7 +315,7 @@
{:invoice {:id id :invoice-number invoice-number :date date :total total}} {:invoice {:id id :invoice-number invoice-number :date date :total total}}
invoice-read]}]} invoice-read]}]}
:on-success [::invoice-edited] :on-success [::invoice-edited]
:on-error [::invoice-edit-failed]}}))) :on-error [::forms/save-error ::new-invoice]}})))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::unvoid-invoice ::unvoid-invoice
@@ -372,10 +375,7 @@
invoices))) invoices)))
(dissoc ::handwrite-checks))}))) (dissoc ::handwrite-checks))})))
(re-frame/reg-event-fx
::invoice-create-failed
(fn [{:keys [db]} [_ data]]
{:dispatch [::events/modal-failed ::new-invoice (:message data)]}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::invoice-created ::invoice-created
@@ -385,27 +385,22 @@
(update-in [::invoice-page :invoices] (update-in [::invoice-page :invoices]
(fn [is] (fn [is]
(into [(assoc add-invoice :class "live-added")] (into [(assoc add-invoice :class "live-added")]
is))) is))))}))
(dissoc ::new-invoice))}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::invoice-edited ::invoice-edited
(fn [{:keys [db]} [_ {:keys [edit-invoice]}]] (fn [{:keys [db]} [_ {:keys [edit-invoice]}]]
{:dispatch [::events/modal-completed ::edit-invoice] {:db (-> db
:db (-> db (forms/stop-form ::new-invoice)
(update-in [::invoice-page :invoices] (update-in [::invoice-page :invoices]
(fn [is] (fn [is]
(mapv (fn [i] (mapv (fn [i]
(if (= (:id i) (:id edit-invoice)) (if (= (:id i) (:id edit-invoice))
(assoc edit-invoice :class "live-added") (assoc edit-invoice :class "live-added")
i)) is))) i)) is))))}))
(dissoc ::edit-invoice))}))
(re-frame/reg-event-fx
::invoice-edit-failed
(fn [{:keys [db]} [_ data]]
{:dispatch [::events/modal-failed ::edit-invoice "That invoice already exists."]}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::invoice-unvoided ::invoice-unvoided
@@ -561,91 +556,12 @@
[:client-id] value [:client-id] value
[:location] first-location]}))) [:location] first-location]})))
(defn new-invoice-modal []
(let [data @(re-frame/subscribe [::new-invoice])
change-event [::events/change-form [::new-invoice]]
locations (get-in @(re-frame/subscribe [::subs/clients-by-id]) [(:client-id data) :locations])
should-select-location? (and locations
(> (count locations) 1))]
[action-modal {:id ::new-invoice
:title "New Invoice"
:action-text "Create"
:save-event [::create-invoice]
:can-submit? (s/valid? ::invoice/invoice data)}
(when-not @(re-frame/subscribe [::subs/client])
[horizontal-field
[:label.label "Client"]
[bind-field
[typeahead {:matches (map (fn [x] [(:id x) (:name x)]) @(re-frame/subscribe [::subs/clients]))
:type "typeahead"
:field [:client-id]
:event [::change-new-invoice-client [::new-invoice]]
:spec ::invoice/client-id
:subscription data}]]])
(when should-select-location?
[horizontal-field
[:label.label "Location"]
[:div.select
[bind-field
[:select {:type "select"
:field [:location]
:spec (set locations)
:event change-event
:subscription data}
(map (fn [l] [:option {:value l} l]) locations)]]]])
[horizontal-field
[:label.label "Vendor"]
[bind-field
[typeahead {:matches (map (fn [x] [(:id x) (:name x)]) @(re-frame/subscribe [::subs/vendors]))
:type "typeahead"
:auto-focus true
:field [:vendor-id]
:text-field [:vendor-name]
:event change-event
:spec (s/nilable ::invoice/vendor-id)
:subscription data}]]]
[horizontal-field
[:label.label "Date"]
[bind-field
[:input.input {:type "date"
:field [:date]
:event change-event
:spec ::invoice/date
:subscription data}]]]
[horizontal-field
[:label.label "Invoice #"]
[bind-field
[:input.input {:type "text"
:field [:invoice-number]
:event change-event
:spec ::invoice/invoice-number
:subscription data}]]]
[horizontal-field
[:label.label "Total"]
[:div.field.has-addons.is-extended
[:p.control [:a.button.is-static "$"]]
[:p.control
[bind-field
[:input.input {:type "number"
:field [:total]
:event change-event
:subscription data
:spec ::invoice/total
:step "0.01"}]]]]]
]))
(defn edit-invoice-form [{:keys [can-change-amount?]}] (defn edit-invoice-form [{:keys [can-change-amount?]}]
[forms/side-bar-form {:form ::new-invoice } [forms/side-bar-form {:form ::new-invoice }
(let [{:keys [data status active? error]} @(re-frame/subscribe [::forms/form ::new-invoice]) (let [{:keys [data active? error]} @(re-frame/subscribe [::forms/form ::new-invoice])
can-change-amount? (= (:status data) :unpaid) _ (println data)
exists? (:id data)
can-change-amount? (#{:unpaid ":unpaid"} (:status data))
change-event [::forms/change ::new-invoice] change-event [::forms/change ::new-invoice]
locations (get-in @(re-frame/subscribe [::subs/clients-by-id]) [(:client-id data) :locations]) locations (get-in @(re-frame/subscribe [::subs/clients-by-id]) [(:client-id data) :locations])
min-total (if (= (:total (:original data)) (:outstanding-balance (:original data))) min-total (if (= (:total (:original data)) (:outstanding-balance (:original data)))
@@ -653,6 +569,7 @@
(- (:total (:original data)) (:outstanding-balance (:original data)))) (- (:total (:original data)) (:outstanding-balance (:original data))))
should-select-location? (and locations should-select-location? (and locations
(> (count locations) 1))] (> (count locations) 1))]
^{:key (or (:id data) "new")}
[:form [:form
[:h1.title.is-2 "New Invoice"] [:h1.title.is-2 "New Invoice"]
(when-not @(re-frame/subscribe [::subs/client]) (when-not @(re-frame/subscribe [::subs/client])
@@ -663,27 +580,30 @@
[typeahead {:matches (map (fn [x] [(:id x) (:name x)]) @(re-frame/subscribe [::subs/clients])) [typeahead {:matches (map (fn [x] [(:id x) (:name x)]) @(re-frame/subscribe [::subs/clients]))
:type "typeahead" :type "typeahead"
:field [:client-id] :field [:client-id]
:disabled exists?
:event [::change-new-invoice-client [::new-invoice]] :event [::change-new-invoice-client [::new-invoice]]
:spec ::invoice/client-id :spec ::invoice/client-id
:subscription data}]]]]) :subscription data}]]]])
(when should-select-location? (when (and should-select-location? (not exists?))
[horizontal-field [:div.field
[:label.label "Location"] [:p.help "Location"]
[:div.select [:div.control
[bind-field [:div.select
[:select {:type "select" [bind-field
:field [:location] [:select {:type "select"
:spec (set locations) :field [:location]
:event change-event :spec (set locations)
:subscription data} :event change-event
(map (fn [l] [:option {:value l} l]) locations)]]]]) :subscription data}
(map (fn [l] [:option {:value l} l]) locations)]]]]])
[:div.field [:div.field
[:p.help "Vendor"] [:p.help "Vendor"]
[:div.control [:div.control
[bind-field [bind-field
[typeahead {:matches (map (fn [x] [(:id x) (:name x)]) @(re-frame/subscribe [::subs/vendors])) [typeahead {:matches (map (fn [x] [(:id x) (:name x)]) @(re-frame/subscribe [::subs/vendors]))
:type "typeahead" :type "typeahead"
:disabled exists?
:auto-focus true :auto-focus true
:field [:vendor-id] :field [:vendor-id]
:text-field [:vendor-name] :text-field [:vendor-name]
@@ -730,85 +650,23 @@
(when error (when error
[:div.notification.is-warning.animated.fadeInUp [:div.notification.is-warning.animated.fadeInUp
error]) error])
(println (s/explain-data ::invoice/invoice data))
[:submit.button.is-large.is-primary {:disabled (if (and (s/valid? ::invoice/invoice data) [:submit.button.is-large.is-primary {:disabled (if (and (s/valid? ::invoice/invoice data)
(or (not min-total) (>= (:total data) min-total))) (or (not min-total) (>= (:total data) min-total)))
"" ""
"disabled") "disabled")
:on-click (dispatch-event [::create-invoice]) :on-click (fn [e]
:class (str @(re-frame/subscribe [::forms/loading-class ::new-invoice]) (when error " animated shake"))} "Save"] #_(when (.-stopPropagation e)
] (.stopPropagation e)
#_[action-modal {:id ::edit-invoice (.preventDefault e))
:title "Update Invoice" (if exists?
:action-text "Save" (re-frame/dispatch-sync [::edit-invoice-saving])
:save-event [::edit-invoice-save] (re-frame/dispatch-sync [::create-invoice])))
:can-submit? (and #_(s/valid? ::invoice/invoice data) :class (str @(re-frame/subscribe [::forms/loading-class ::new-invoice])
(or (not min-total) (>= (:total data) min-total)))} (when error " animated shake"))} "Save"]])]
])]
) )
(defn edit-invoice-modal [{:keys [can-change-amount?]}]
(let [data @(re-frame/subscribe [::edit-invoice])
change-event [::events/change-form [::edit-invoice]]
locations (get-in @(re-frame/subscribe [::subs/clients-by-id]) [(:client-id data) :locations])
min-total (if (= (:total (:original data)) (:outstanding-balance (:original data)))
nil
(- (:total (:original data)) (:outstanding-balance (:original data))))
should-select-location? (and locations
(> (count locations) 1))]
[action-modal {:id ::edit-invoice
:title "Update Invoice"
:action-text "Save"
:save-event [::edit-invoice-save]
:can-submit? (and #_(s/valid? ::invoice/invoice data)
(or (not min-total) (>= (:total data) min-total)))}
[horizontal-field
[:label.label "Date"]
[bind-field
[:input.input {:type "date"
:field [:date]
:event change-event
:spec ::invoice/date
:subscription data}]]]
[horizontal-field
[:label.label "Invoice #"]
[bind-field
[:input.input {:type "text"
:field [:invoice-number]
:event change-event
:spec ::invoice/invoice-number
:subscription data}]]]
[horizontal-field
[:label.label "Total"]
[:div.field.has-addons.is-extended
[:p.control [:a.button.is-static "$"]]
[:p.control
[bind-field
[:input.input {:type "number"
:field [:total]
:disabled (if can-change-amount? "" "disabled")
:event change-event
:min min-total
:subscription data
:spec ::invoice/total
:step "0.01"}]]]]]]))
(re-frame/reg-event-db (re-frame/reg-event-db
::change-selected-vendor ::change-selected-vendor
@@ -852,59 +710,60 @@
[:div.is-pulled-right [:div.is-pulled-right
[:button.button.is-success {:on-click (dispatch-event [::new-invoice-clicked])} "New Invoice"] [:div.buttons
[:button.button.is-success {:on-click (dispatch-event [::new-invoice-clicked])} "New Invoice"]
(when current-client
[:div.dropdown.is-right {:class (if print-checks-shown? (when current-client
"is-active" [:div.dropdown.is-right {:class (if print-checks-shown?
"")} "is-active"
[:div.dropdown-trigger "")}
[:button.button.is-success {:aria-haspopup true [:div.dropdown-trigger
:on-click (dispatch-event [::print-checks-clicked ]) [:button.button.is-success {:aria-haspopup true
:disabled (if (and (seq checked-invoices) :on-click (dispatch-event [::print-checks-clicked ])
(->> checked-invoices :disabled (if (and (seq checked-invoices)
vals (->> checked-invoices
(group-by #(get-in % [:vendor :id])) vals
(reduce-kv (fn [negative? _ invoices] (group-by #(get-in % [:vendor :id]))
(or negative? (< (reduce + 0 (map (fn [x] (-> x :outstanding-balance js/parseFloat)) invoices) ) 0))) (reduce-kv (fn [negative? _ invoices]
false) (or negative? (< (reduce + 0 (map (fn [x] (-> x :outstanding-balance js/parseFloat)) invoices) ) 0)))
not)) false)
"" not))
"disabled") ""
"disabled")
:class (if print-checks-loading? :class (if print-checks-loading?
"is-loading" "is-loading"
"")} "")}
"Pay " "Pay "
(when (> (count checked-invoices )) (when (> (count checked-invoices ))
(str (str
(count checked-invoices) (count checked-invoices)
" invoices " " invoices "
"(" (->> checked-invoices "(" (->> checked-invoices
vals vals
(map (comp js/parseFloat :outstanding-balance)) (map (comp js/parseFloat :outstanding-balance))
(reduce + 0) (reduce + 0)
(gstring/format "$%.2f" )) (gstring/format "$%.2f" ))
")")) ")"))
[:span " "] [:span " "]
[:span.icon.is-small [:i.fa.fa-angle-down {:aria-hidden "true"}]]]] [:span.icon.is-small [:i.fa.fa-angle-down {:aria-hidden "true"}]]]]
[:div.dropdown-menu {:role "menu"} [:div.dropdown-menu {:role "menu"}
[:div.dropdown-content [:div.dropdown-content
(list (list
(for [{:keys [id number name type]} (->> (:bank-accounts current-client) (filter :visible) (sort-by :sort-order))] (for [{:keys [id number name type]} (->> (:bank-accounts current-client) (filter :visible) (sort-by :sort-order))]
(if (= :cash type) (if (= :cash type)
^{:key id} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :cash])} "With cash"] ^{:key id} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :cash])} "With cash"]
(list (list
^{:key (str id "-check")} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :check])} "Print checks from " name] ^{:key (str id "-check")} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :check])} "Print checks from " name]
^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :debit])} "Debit from " name]))) ^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event [::print-checks id :debit])} "Debit from " name])))
^{:key "advanced-divider"} [:hr.dropdown-divider] ^{:key "advanced-divider"} [:hr.dropdown-divider]
(when (= 1 (count checked-invoices)) (when (= 1 (count checked-invoices))
^{:key "handwritten"} [:a.dropdown-item {:on-click (dispatch-event [::handwrite-checks])} "Handwritten Check..."]) ^{:key "handwritten"} [:a.dropdown-item {:on-click (dispatch-event [::handwrite-checks])} "Handwritten Check..."])
^{:key "advanced"} [:a.dropdown-item {:on-click (dispatch-event [::advanced-print-checks])} "Advanced..."])]]])] ^{:key "advanced"} [:a.dropdown-item {:on-click (dispatch-event [::advanced-print-checks])} "Advanced..."])]]])]]
[:div.is-pulled-right [:div.is-pulled-right
(into [:div.tags {:style {:margin-right ".5 rem;"}}] (map (fn [[id invoice]] [:span.tag.is-medium (:invoice-number invoice) [:button.delete.is-small {:on-click (dispatch-event [::toggle-check id invoice])}]]) checked-invoices))]] (into [:div.tags {:style {:margin-right ".5 rem;"}}] (map (fn [[id invoice]] [:span.tag.is-medium (:invoice-number invoice) [:button.delete.is-small {:on-click (dispatch-event [::toggle-check id invoice])}]]) checked-invoices))]]
)) ))
@@ -946,8 +805,6 @@
:expense-event [::expense-accounts-dialog/change-expense-accounts]}] :expense-event [::expense-accounts-dialog/change-expense-accounts]}]
[print-checks-modal] [print-checks-modal]
[new-invoice-modal]
[edit-invoice-modal {:can-change-amount? (= status "unpaid")}]
[handwrite-checks-modal] [handwrite-checks-modal]
[change-expense-accounts-modal {:updated-event [::expense-accounts-updated]}] [change-expense-accounts-modal {:updated-event [::expense-accounts-updated]}]
(when check-results-shown? (when check-results-shown?