From e2b99961e48a4f3579e04e8f154a5df7926428bf Mon Sep 17 00:00:00 2001 From: BC Date: Mon, 18 Feb 2019 16:23:05 -0800 Subject: [PATCH] new approach to adding invoices. --- src/cljs/auto_ap/forms.cljs | 55 ++++ .../auto_ap/views/pages/admin/clients.cljs | 267 +++++++++--------- .../auto_ap/views/pages/unpaid_invoices.cljs | 182 ++++++++++-- 3 files changed, 334 insertions(+), 170 deletions(-) create mode 100644 src/cljs/auto_ap/forms.cljs diff --git a/src/cljs/auto_ap/forms.cljs b/src/cljs/auto_ap/forms.cljs new file mode 100644 index 00000000..ca03a032 --- /dev/null +++ b/src/cljs/auto_ap/forms.cljs @@ -0,0 +1,55 @@ +(ns auto-ap.forms + (:require [re-frame.core :as re-frame] + [auto-ap.views.utils :refer [dispatch-event]])) + + +(re-frame/reg-sub + ::form + (fn [db [_ x]] + (-> db ::forms x))) + +(re-frame/reg-sub + ::loading-class + (fn [db [_ x]] + (if (= (get-in db [::forms x :status]) "loading") + "is-loading" + ""))) + +(defn start-form [db form data] + (assoc-in db [::forms form] {:error nil + :active? true + :status nil + :data data})) +(defn stop-form [db form] + (update db ::forms dissoc form)) + +(re-frame/reg-event-db + ::form-closing + (fn [db [_ f]] + + (-> db + (stop-form f)))) + +(defn in-form + [form-name] + (re-frame/path [::forms form-name])) + +(re-frame/reg-event-db + ::change + (fn [db [_ form & path-pairs]] + (println path-pairs) + (reduce + (fn [db [path value]] + (assoc-in db (into [::forms form :data] path) value)) + db + (partition 2 path-pairs)))) + +(re-frame/reg-event-db + ::save-error + (fn [db [_ form result]] + (-> db + (assoc-in [::forms form :status] :error) + (assoc-in [::forms form :error] (:message (first result)))))) + +(defn side-bar-form [{:keys [form]} children] + [:div [:a.delete.is-pulled-right {:on-click (dispatch-event [::form-closing form])}] [:div children]]) diff --git a/src/cljs/auto_ap/views/pages/admin/clients.cljs b/src/cljs/auto_ap/views/pages/admin/clients.cljs index 67d6ce74..d3192efc 100644 --- a/src/cljs/auto_ap/views/pages/admin/clients.cljs +++ b/src/cljs/auto_ap/views/pages/admin/clients.cljs @@ -85,7 +85,7 @@ {:edit-client new-client-req} [:id :name :code :email :locations [:address [:street1 :street2 :city :state :zip]] [:bank-accounts [:id :number :check-number :name :code :bank-code :bank-name :routing :type :visible :sort-order]]]]}]} :on-success [::save-complete] - :on-error [::save-error]}} + :on-error [::forms/save-error ::new-client]}} {:db new-client-form})))) @@ -97,19 +97,9 @@ (assoc-in [:clients (:id (:edit-client client))] (update (:edit-client client) :bank-accounts (fn [bas] (->> bas (sort-by :sort-order) vec))))))) -(re-frame/reg-event-db - ::save-error - [(forms/in-form ::new-client)] - (fn [db [_ result]] - (-> db - (assoc :status :error) - (assoc :error (:message (first result)))))) -(re-frame/reg-event-db - ::change-new - [(forms/in-form ::new-client) (re-frame/path [:data])] - (fn [client [_ path value]] - (assoc-in client path value))) + + (re-frame/reg-event-db ::add-new-location @@ -200,138 +190,137 @@ [clients-table]])]) -(defn side-bar-form [_ children] - [:div [:a.delete.is-pulled-right {:on-click (dispatch-event [::forms/form-closing ::new-client])}] [:div children]]) + (defn bank-account-card [new-client {:keys [active? new? type visible code name number check-number id sort-order] :as bank-account} first? last?] - - - [:div.card {:style {:margin-bottom "1em"}} - [:header.card-header - [:p.card-header-title {:style {:text-overflow "ellipsis"}} - [:span.icon - (if ({:check ":check"} type) - [:span.icon-check-payment-sign] - [:span.icon-accounting-bill])] - code ": " name] - [:p {:style {:padding "0.75em 0.25em"}} - [:a.button.is-outlined {:on-click (dispatch-event [::toggle-visible sort-order])} [:span.icon (if visible - [:span.fa.fa-eye] - [:span.fa.fa-eye-slash] - )]]] - (when-not last? - [:p {:style {:padding "0.75em 0.25em"}} - [:a.button.is-primary.is-outlined {:on-click (dispatch-event [::sort-swapped sort-order (inc sort-order)])} [:span.icon [:span.fa.fa-sort-down]]]]) - (when-not first? - [:p {:style {:padding "0.75em 0.25em"}} - [:a.button.is-primary.is-outlined {:on-click (dispatch-event [::sort-swapped sort-order (dec sort-order)])} [:span.icon [:span.fa.fa-sort-up]]]]) - (if active? - [:a.card-header-icon - {:on-click (dispatch-event [::bank-account-deactivated sort-order])} + (let [change-event [::forms/change ::new-client]] + [:div.card {:style {:margin-bottom "1em"}} + [:header.card-header + [:p.card-header-title {:style {:text-overflow "ellipsis"}} [:span.icon - [:span.fa.fa-angle-up]]] - [:a.card-header-icon - {:on-click (dispatch-event [::bank-account-activated sort-order])} - [:span.icon - [:span.fa.fa-angle-down]]])] - (when active? - [:div.card-content - [:label.label "General"] - [horizontal-field - nil - [:div.control - [:p.help "Account Code"] - (if new? - [:div.field.has-addons.is-extended - [:p.control [:a.button.is-static (:code new-client) "-" ]] - [:p.control - [bind-field - [:input.input {:type "code" - :field [:bank-accounts sort-order :code] - :spec ::entity/code - :event ::change-new - :subscription new-client}]]]] - [:div.field [:p.control code]])] + (if ({:check ":check"} type) + [:span.icon-check-payment-sign] + [:span.icon-accounting-bill])] + code ": " name] + [:p {:style {:padding "0.75em 0.25em"}} + [:a.button.is-outlined {:on-click (dispatch-event [::toggle-visible sort-order])} [:span.icon (if visible + [:span.fa.fa-eye] + [:span.fa.fa-eye-slash] + )]]] + (when-not last? + [:p {:style {:padding "0.75em 0.25em"}} + [:a.button.is-primary.is-outlined {:on-click (dispatch-event [::sort-swapped sort-order (inc sort-order)])} [:span.icon [:span.fa.fa-sort-down]]]]) + (when-not first? + [:p {:style {:padding "0.75em 0.25em"}} + [:a.button.is-primary.is-outlined {:on-click (dispatch-event [::sort-swapped sort-order (dec sort-order)])} [:span.icon [:span.fa.fa-sort-up]]]]) + (if active? + [:a.card-header-icon + {:on-click (dispatch-event [::bank-account-deactivated sort-order])} + [:span.icon + [:span.fa.fa-angle-up]]] + [:a.card-header-icon + {:on-click (dispatch-event [::bank-account-activated sort-order])} + [:span.icon + [:span.fa.fa-angle-down]]])] + (when active? + [:div.card-content + [:label.label "General"] + [horizontal-field + nil + [:div.control + [:p.help "Account Code"] + (if new? + [:div.field.has-addons.is-extended + [:p.control [:a.button.is-static (:code new-client) "-" ]] + [:p.control + [bind-field + [:input.input {:type "code" + :field [:bank-accounts sort-order :code] + :spec ::entity/code + :event change-event + :subscription new-client}]]]] + [:div.field [:p.control code]])] - [:div.control - [:p.help "Nickname"] - [bind-field - [:input.input {:placeholder "BOA Checking #1" - :type "text" - :field [:bank-accounts sort-order :name] - :event ::change-new - :subscription new-client}]]]] - (when (#{:check ":check"} type ) - [:div + [:div.control + [:p.help "Nickname"] + [bind-field + [:input.input {:placeholder "BOA Checking #1" + :type "text" + :field [:bank-accounts sort-order :name] + :event change-event + :subscription new-client}]]]] + (when (#{:check ":check"} type ) + [:div - [:label.label "Bank"] - [horizontal-field - nil - [:div.control - [:p.help "Bank Name"] - [bind-field - [:input.input {:placeholder "Bank of America" - :type "text" - :field [:bank-accounts sort-order :bank-name] - :event ::change-new - :subscription new-client}]]] - [:div.control - [:p.help "Routing #"] - [bind-field - [:input.input {:placeholder "104819123" - :type "text" - :field [:bank-accounts sort-order :routing] - :event ::change-new - :subscription new-client}]]] - [:div.control - [:p.help "Bank code"] - [bind-field - [:input.input {:placeholder "12/10123" - :type "text" - :field [:bank-accounts sort-order :bank-code] - :event ::change-new - :subscription new-client}]]]] + [:label.label "Bank"] + [horizontal-field + nil + [:div.control + [:p.help "Bank Name"] + [bind-field + [:input.input {:placeholder "Bank of America" + :type "text" + :field [:bank-accounts sort-order :bank-name] + :event change-event + :subscription new-client}]]] + [:div.control + [:p.help "Routing #"] + [bind-field + [:input.input {:placeholder "104819123" + :type "text" + :field [:bank-accounts sort-order :routing] + :event change-event + :subscription new-client}]]] + [:div.control + [:p.help "Bank code"] + [bind-field + [:input.input {:placeholder "12/10123" + :type "text" + :field [:bank-accounts sort-order :bank-code] + :event change-event + :subscription new-client}]]]] - [:label.label "Checking account"] - [horizontal-field - nil - [:div.control - [:p.help "Account #"] - [bind-field - [:input.input {:placeholder "123456789" - :type "text" - :field [:bank-accounts sort-order :number] - :event ::change-new - :subscription new-client}]]] - [:div.control - [:p.help "Check Number"] - [bind-field - [:input.input {:placeholder "10000" - :type "text" - :field [:bank-accounts sort-order :check-number] - :event ::change-new - :subscription new-client}]]]] - [:div.field - [:label.label "Yodlee Account"] - [:div.control - [bind-field - [:input.input {:placeholder "Yodlee Account #" - :type "text" - :field [:bank-accounts :yodlee-account-id] - :event ::change-new - :subscription new-client}]]]]])]) + [:label.label "Checking account"] + [horizontal-field + nil + [:div.control + [:p.help "Account #"] + [bind-field + [:input.input {:placeholder "123456789" + :type "text" + :field [:bank-accounts sort-order :number] + :event change-event + :subscription new-client}]]] + [:div.control + [:p.help "Check Number"] + [bind-field + [:input.input {:placeholder "10000" + :type "text" + :field [:bank-accounts sort-order :check-number] + :event change-event + :subscription new-client}]]]] + [:div.field + [:label.label "Yodlee Account"] + [:div.control + [bind-field + [:input.input {:placeholder "Yodlee Account #" + :type "text" + :field [:bank-accounts :yodlee-account-id] + :event change-event + :subscription new-client}]]]]])]) - (when active? - [:footer.card-footer - [:a.card-footer-item {:href "#" :on-click (dispatch-event [::bank-account-deactivated sort-order])} "Done"] - (when new? - [:a.card-footer-item.is-warning {:href "#" :on-click (dispatch-event [::bank-account-removed sort-order])} "Remove"])])] + (when active? + [:footer.card-footer + [:a.card-footer-item {:href "#" :on-click (dispatch-event [::bank-account-deactivated sort-order])} "Done"] + (when new? + [:a.card-footer-item.is-warning {:href "#" :on-click (dispatch-event [::bank-account-removed sort-order])} "Remove"])])]) ) (defn new-client-form [] - (let [{error :error new-client :data } @(re-frame/subscribe [::forms/form ::new-client])] + (let [{error :error new-client :data } @(re-frame/subscribe [::forms/form ::new-client]) + change-event [::forms/change ::new-client]] - [side-bar-form {} + [forms/side-bar-form {:form ::new-client} [:form [:h1.title.is-2 "Add client"] [:div.field @@ -341,7 +330,7 @@ [:input.input {:type "text" :field :name :spec ::entity/name - :event ::change-new + :event change-event :subscription new-client}]]]] [:div.field @@ -355,7 +344,7 @@ [:input.input {:type "code" :field :code :spec ::entity/code - :event ::change-new + :event change-event :subscription new-client}]]])] [:div.field @@ -365,7 +354,7 @@ [:input.input {:type "email" :field :email :spec ::entity/email - :event ::change-new + :event change-event :subscription new-client}]]]] [:div.field @@ -376,7 +365,7 @@ [bind-field [:input.input {:type "text" :field :location - :event ::change-new + :event change-event :subscription new-client}]]] [:p.control [:button.button.is-primary {:on-click (dispatch-event [::add-new-location])} "Add"]]] [:ul @@ -386,7 +375,7 @@ [:div {:style {:padding-bottom "0.75em" :padding-top "0.75em"}} [:h2.subtitle "Address"] [address-field {:field [:address] - :event ::change-new + :event change-event :subscription new-client}]] [:h2.subtitle "Bank accounts"] diff --git a/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs b/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs index 40fa2f63..9fddfc9c 100644 --- a/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs +++ b/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs @@ -9,7 +9,8 @@ [auto-ap.entities.invoice :as invoice] [auto-ap.entities.vendors :as vendor] [bidi.bidi :as bidi] - [auto-ap.views.components.layouts :refer [side-bar-layout]] + [auto-ap.forms :as forms] + [auto-ap.views.components.layouts :refer [side-bar-layout appearing-side-bar]] [auto-ap.routes :as routes] [auto-ap.views.components.expense-accounts-dialog :as expense-accounts-dialog] [auto-ap.views.components.vendor-dialog :refer [vendor-dialog]] @@ -258,12 +259,12 @@ {:dispatch [::params-change @(re-frame/subscribe [::params])]})) (re-frame/reg-event-fx - ::new-invoice + ::new-invoice-clicked (fn [{:keys [db]} _] - {:dispatch [::events/modal-status ::new-invoice {:visible? true}] - :db (assoc-in db [::new-invoice] {:client-id (:id @(re-frame/subscribe [::subs/client])) - :date (date->str (c/now) standard) - :location (first (:locations @(re-frame/subscribe [::subs/client])))})})) + {:db (forms/start-form db ::new-invoice {:client-id (:id @(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 ::edit-invoice @@ -278,17 +279,23 @@ (re-frame/reg-event-fx ::create-invoice (fn [{:keys [db]} _] - (let [new-invoice @(re-frame/subscribe [::new-invoice])] + (let [{:keys [data]} @(re-frame/subscribe [::forms/form ::new-invoice])] {:graphql {:token (-> db :user) :query-obj {:venia/operation {:operation/type :mutation :operation/name "AddInvoice"} :venia/queries [{:query/data [:add-invoice - {:invoice new-invoice} + {:invoice {:date (:date data) + :vendor-id (:vendor-id data) + :client-id (:client-id data) + :invoice-number (:invoice-number data) + :location (:location data) + :total (:total data) + }} invoice-read]}]} :on-success [::invoice-created] - :on-error [::invoice-create-failed]}}))) + :on-error [::forms/save-error ::new-invoice]}}))) @@ -373,8 +380,8 @@ (re-frame/reg-event-fx ::invoice-created (fn [{:keys [db]} [_ {:keys [add-invoice]}]] - {:dispatch [::events/modal-completed ::new-invoice] - :db (-> db + {:db (-> db + (forms/stop-form ::new-invoice) (update-in [::invoice-page :invoices] (fn [is] (into [(assoc add-invoice :class "live-added")] @@ -431,9 +438,7 @@ {:dispatch [::events/modal-completed ::expense-accounts-dialog/change-expense-accounts] :db (-> db (update-in [::invoice-page :invoices] - (fn [is] - (replace-if #(= (:id %1) (:id %2)) updated is))) (dissoc ::change-expense-accounts))}))) @@ -552,9 +557,9 @@ (let [first-location (-> @(re-frame/subscribe [::subs/clients-by-id]) (get-in [value :locations]) first)] - {:dispatch [::events/change-form location field value] - :db (-> db - (assoc-in [::new-invoice :location] first-location))}))) + {:dispatch [::forms/change ::new-invoice + [:client-id] value + [:location] first-location]}))) (defn new-invoice-modal [] (let [data @(re-frame/subscribe [::new-invoice]) @@ -637,6 +642,119 @@ :step "0.01"}]]]]] ])) +(defn edit-invoice-form [{:keys [can-change-amount?]}] + [forms/side-bar-form {:form ::new-invoice } + (let [{:keys [data status active? error]} @(re-frame/subscribe [::forms/form ::new-invoice]) + can-change-amount? (= (:status data) :unpaid) + change-event [::forms/change ::new-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))] + [:form + [:h1.title.is-2 "New Invoice"] + (when-not @(re-frame/subscribe [::subs/client]) + [:div.field + [:p.help "Client"] + [:div.control + [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)]]]]) + [:div.field + [:p.help "Vendor"] + [:div.control + [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}]]]] + + [:div.field + [:p.help "Date"] + [:div.control + [bind-field + [:input.input {:type "date" + :field [:date] + :event change-event + :spec ::invoice/date + :subscription data}]]]] + + [:div.field + [:p.help "Invoice #"] + [:div.control + [bind-field + [:input.input {:type "text" + :field [:invoice-number] + :event change-event + :spec ::invoice/invoice-number + :subscription data}]]]] + + [:div.field + [:p.help "Total"] + [:div.control + [: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"}]]]]]] + + (when error + [:div.notification.is-warning.animated.fadeInUp + error]) + (println (s/explain-data ::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))) + "" + "disabled") + :on-click (dispatch-event [::create-invoice]) + :class (str @(re-frame/subscribe [::forms/loading-class ::new-invoice]) (when error " animated shake"))} "Save"] + ] + #_[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)))} + + + + + + + + ])] + ) + (defn edit-invoice-modal [{:keys [can-change-amount?]}] (let [data @(re-frame/subscribe [::edit-invoice]) change-event [::events/change-form [::edit-invoice]] @@ -734,7 +852,7 @@ [:div.is-pulled-right - [:button.button.is-danger {:on-click (dispatch-event [::new-invoice])} "New Invoice"] + [:button.button.is-success {:on-click (dispatch-event [::new-invoice-clicked])} "New Invoice"] (when current-client @@ -850,17 +968,19 @@ :component-will-mount #(re-frame/dispatch-sync [::params-change {:status status}]) })) (defn unpaid-invoices-page [{:keys [status]}] - [side-bar-layout {:side-bar [invoices-side-bar {} - ^{:key "extra-filter"} - [:div - [:p.menu-label "Vendor"] - [:div [vendor-filter {:on-change-event [::change-selected-vendor] - :value (:vendor-filter @(re-frame/subscribe [::invoice-page])) - :vendors @(re-frame/subscribe [::subs/vendors])}]] - [:p.menu-label "Invoice #"] - [:div - [invoice-number-filter]]]] - :main [unpaid-invoices-content {:status status}] - :bottom [vendor-dialog {:vendor @(re-frame/subscribe [::subs/user-editing-vendor]) - :save-event [::events/save-vendor] - :change-event [::events/change-nested-form-state [:user-editing-vendor]] :id :auto-ap.views.main/user-editing-vendor}]}]) + (let [{invoice-bar-active? :active?} @(re-frame/subscribe [::forms/form ::new-invoice])] + [side-bar-layout {:side-bar [invoices-side-bar {} + ^{:key "extra-filter"} + [:div + [:p.menu-label "Vendor"] + [:div [vendor-filter {:on-change-event [::change-selected-vendor] + :value (:vendor-filter @(re-frame/subscribe [::invoice-page])) + :vendors @(re-frame/subscribe [::subs/vendors])}]] + [:p.menu-label "Invoice #"] + [:div + [invoice-number-filter]]]] + :main [unpaid-invoices-content {:status status}] + :bottom [vendor-dialog {:vendor @(re-frame/subscribe [::subs/user-editing-vendor]) + :save-event [::events/save-vendor] + :change-event [::events/change-nested-form-state [:user-editing-vendor]] :id :auto-ap.views.main/user-editing-vendor}] + :right-side-bar [appearing-side-bar {:visible? invoice-bar-active?} [edit-invoice-form]]}]))