diff --git a/resources/public/css/main.css b/resources/public/css/main.css index 9200b922..8d5b1c6a 100644 --- a/resources/public/css/main.css +++ b/resources/public/css/main.css @@ -425,4 +425,23 @@ nav.navbar .navbar-item.is-active { background-color: #00d1b2; cursor: pointer; } + + +.buttons .dropdown:not(:last-child):not(.is-fullwidth) .button { + margin-right: 0.5em; +} + + +.dropdown.is-fullwidth { + width: 100%; + display: flex; +} + +.dropdown.is-fullwidth .dropdown-trigger { + width: 100%; +} + +.dropdown.is-fullwidth .dropdown-trigger * { + width: 100%; +} diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 9d3be83f..53f4b013 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -372,6 +372,12 @@ :args {:invoices {:type '(list :id)}} :resolve :mutation/approve-invoices} + :add_and_print_invoice {:type :check_result + :args {:invoice {:type :add_invoice} + :bank_account_id {:type :id} + :type {:type :payment_type}} + :resolve :mutation/add-and-print-invoice} + :print_checks {:type :check_result :args {:invoice_payments {:type '(list :invoice_payment_amount)} :bank_account_id {:type :id} @@ -567,6 +573,7 @@ :mutation/approve-invoices gq-invoices/approve-invoices :mutation/edit-user gq-users/edit-user :mutation/add-invoice gq-invoices/add-invoice + :mutation/add-and-print-invoice gq-invoices/add-and-print-invoice :mutation/edit-invoice gq-invoices/edit-invoice :mutation/edit-client gq-clients/edit-client :mutation/upsert-vendor gq-vendors/upsert-vendor diff --git a/src/clj/auto_ap/graphql/invoices.clj b/src/clj/auto_ap/graphql/invoices.clj index acdb4065..c07d87c0 100644 --- a/src/clj/auto_ap/graphql/invoices.clj +++ b/src/clj/auto_ap/graphql/invoices.clj @@ -2,8 +2,10 @@ (:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-can-see-client assert-admin]] [auto-ap.datomic.vendors :as d-vendors] + [auto-ap.datomic.clients :as d-clients] [auto-ap.datomic.invoices :as d-invoices] [auto-ap.expense-accounts :as expense-accounts] + [auto-ap.graphql.checks :as gq-checks] [auto-ap.time :refer [parse iso-date]] [datomic.api :as d] [auto-ap.datomic :refer [uri]] @@ -42,36 +44,53 @@ transaction-result @(d/transact (d/connect uri) transactions)] invoices)) -(defn add-invoice [context {{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in} :invoice} value] +(defn assert-no-conflicting [{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in}] (when (seq (d-invoices/find-conflicting {:invoice/invoice-number invoice_number :invoice/vendor vendor_id :invoice/client client_id})) - (throw (ex-info (str "Invoice '" invoice_number "' already exists.") {:invoice-number invoice_number}))) - (let [_ (assert-can-see-client (:id context) client_id) - vendor (d-vendors/get-by-id vendor_id) - + (throw (ex-info (str "Invoice '" invoice_number "' already exists.") {:invoice-number invoice_number :validation-error (str "Invoice '" invoice_number "' already exists.")})))) + +(defn add-invoice-transaction [{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in}] + (let [vendor (d-vendors/get-by-id vendor_id) expense-account-id (:vendor/default-expense-account vendor) _ (when-not expense-account-id - (throw (ex-info (str "Vendor '" (:vendor/name vendor) "' does not have a default expense acount.") {:vendor-id vendor_id} ))) - transaction [{:db/id "invoice" - :invoice/invoice-number invoice_number - :invoice/client client_id - :invoice/vendor vendor_id - :invoice/import-status :import-status/imported - :invoice/total total - :invoice/outstanding-balance total - :invoice/status :invoice-status/unpaid - :invoice/date (coerce/to-date date) - :invoice/expense-accounts [{:invoice-expense-account/expense-account-id expense-account-id - :invoice-expense-account/location (get-in expense-accounts/expense-accounts [expense-account-id :location] location) - :invoice-expense-account/amount total}]}] - transaction-result @(d/transact (d/connect uri) transaction) - ] - - + (throw (ex-info (str "Vendor '" (:vendor/name vendor) "' does not have a default expense acount.") {:vendor-id vendor_id} )))] + {:db/id "invoice" + :invoice/invoice-number invoice_number + :invoice/client client_id + :invoice/vendor vendor_id + :invoice/import-status :import-status/imported + :invoice/total total + :invoice/outstanding-balance total + :invoice/status :invoice-status/unpaid + :invoice/date (coerce/to-date date) + :invoice/expense-accounts [{:invoice-expense-account/expense-account-id expense-account-id + :invoice-expense-account/location (get-in expense-accounts/expense-accounts [expense-account-id :location] location) + :invoice-expense-account/amount total}]})) + +(defn add-invoice [context {{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in} :invoice} value] + (assert-no-conflicting in) + (assert-can-see-client (:id context) client_id) + (let [transaction-result @(d/transact (d/connect uri) [(add-invoice-transaction in)])] (-> (d-invoices/get-by-id (get-in transaction-result [:tempids "invoice"])) (->graphql)))) +(defn assert-bank-account-belongs [client-id bank-account-id] + (when-not ((set (map :db/id (:client/bank-accounts (d-clients/get-by-id client-id)))) bank-account-id) + (throw (ex-info (str "Bank account does not belong to client") {:validation-error "Bank account does not belong to client."} )))) + +(defn add-and-print-invoice [context {{:keys [total invoice_number location client_id vendor_id vendor_name date] :as in} :invoice bank-account-id :bank_account_id type :type} value] + (assert-no-conflicting in) + (assert-can-see-client (:id context) client_id) + (assert-bank-account-belongs client_id bank-account-id) + (let [transaction-result @(d/transact (d/connect uri) [(add-invoice-transaction in)])] + (-> (gq-checks/print-checks [{:invoice-id (get-in transaction-result [:tempids "invoice"]) + :amount total}] + client_id + bank-account-id + type) + ->graphql))) + (defn edit-invoice [context {{:keys [id invoice_number total vendor_id date client_id expense_accounts] :as in} :invoice} value] diff --git a/src/cljs/auto_ap/forms.cljs b/src/cljs/auto_ap/forms.cljs index f2e9dba8..c94b3495 100644 --- a/src/cljs/auto_ap/forms.cljs +++ b/src/cljs/auto_ap/forms.cljs @@ -8,10 +8,18 @@ (fn [db [_ x]] (-> db ::forms x))) + +(re-frame/reg-sub + ::is-loading? + (fn [db [_ x]] + (if (#{"loading" :loading} (get-in db [::forms x :status]) ) + true + false))) + (re-frame/reg-sub ::loading-class (fn [db [_ x]] - (if (= (get-in db [::forms x :status]) "loading") + (if (#{"loading" :loading} (get-in db [::forms x :status]) ) "is-loading" ""))) @@ -54,3 +62,8 @@ (defn side-bar-form [{:keys [form]} children] [:div [:a.delete.is-pulled-right {:on-click (dispatch-event [::form-closing form])}] [:div children]]) + +(defn loading [db id] + (-> db + (assoc-in [::forms id :status] :loading) + (assoc-in [::forms id :error] nil))) diff --git a/src/cljs/auto_ap/views/components/dropdown.cljs b/src/cljs/auto_ap/views/components/dropdown.cljs index 665044d8..1dfd80d1 100644 --- a/src/cljs/auto_ap/views/components/dropdown.cljs +++ b/src/cljs/auto_ap/views/components/dropdown.cljs @@ -13,14 +13,19 @@ (fn [children] children)}))) -(defn drop-down [{:keys [ header id]} child] +(defn drop-down [{:keys [ header id is-right? class]} child] (let [menu-active? (re-frame/subscribe [::subs/menu-active? id])] (r/create-class - {:reagent-render (fn [{:keys [header id]} child] + {:reagent-render (fn [{:keys [header id is-right? class] :or {:is-right? true}} child] (let [menu-active? @(re-frame/subscribe [::subs/menu-active? id])] - [:div.dropdown.is-right {:class (if menu-active? - "is-active" - "")} + [:div.dropdown {:class (str (if menu-active? + "is-active" + "") " " + (if is-right? + "is-right" + "") + " " + class)} [:div.dropdown-trigger header] [appearing {:visible? menu-active? :enter-class "appear" :exit-class "disappear" :timeout 200} @@ -28,6 +33,5 @@ [:div.dropdown-content (when menu-active? [drop-down-contents {:id id} - child])]]] - ]))}))) + child])]]]]))}))) diff --git a/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs b/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs index 52fee102..94dcbacd 100644 --- a/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs +++ b/src/cljs/auto_ap/views/pages/unpaid_invoices.cljs @@ -289,9 +289,7 @@ (fn [{:keys [db]} _] (when @(re-frame/subscribe [::can-submit-edit-invoice]) (let [{:keys [data]} @(re-frame/subscribe [::forms/form ::new-invoice])] - {:db (-> db - (assoc-in [::forms/forms ::new-invoice :status] :loading) - (assoc-in [::forms/forms ::new-invoice :error] nil)) + {:db (forms/loading db ::new-invoice) :graphql {:token (-> db :user) :query-obj {:venia/operation {:operation/type :mutation @@ -309,6 +307,31 @@ :on-success [::invoice-created] :on-error [::forms/save-error ::new-invoice]}})))) +(re-frame/reg-event-fx + ::save-and-print-invoice + (fn [{:keys [db]} [_ bank-account-id type ]] + (println bank-account-id type) + (when @(re-frame/subscribe [::can-submit-edit-invoice]) + (let [{:keys [data]} @(re-frame/subscribe [::forms/form ::new-invoice])] + {:db (forms/loading db ::new-invoice) + :graphql + {:token (-> db :user) + :query-obj {:venia/operation {:operation/type :mutation + :operation/name "AddAndPrintInvoice"} + + :venia/queries [{:query/data [:add-and-print-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)} + :bank-account-id bank-account-id + :type type} + [:pdf-url [:invoices invoice-read]]]}]} + :on-success [::invoice-created-and-printed] + :on-error [::forms/save-error ::new-invoice]}})))) + (re-frame/reg-event-fx @@ -407,6 +430,23 @@ (into [(assoc add-invoice :class "live-added")] is))))})) +(re-frame/reg-event-fx + ::invoice-created-and-printed + (fn [{:keys [db]} [_ {:keys [add-and-print-invoice]}]] + {:db (-> db + (forms/stop-form ::new-invoice) + (forms/start-form ::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])))}) + (update-in [::invoice-page :invoices] + (fn [is] + (into (vec (map #(assoc % :class "live-added") + (:invoices add-and-print-invoice))) + is))) + (assoc-in [::check-results :shown?] true) + (assoc-in [::check-results :pdf-url] (:pdf-url add-and-print-invoice)))})) + (re-frame/reg-event-fx ::invoice-edited (fn [{:keys [db]} [_ {:keys [edit-invoice]}]] @@ -516,11 +556,12 @@ (re-frame/reg-sub ::can-submit-edit-invoice :<- [::forms/form ::new-invoice] - (fn [{:keys [data]} _] + (fn [{:keys [data status]} _] (let [min-total (if (= (:total (:original data)) (:outstanding-balance (:original data))) nil (- (:total (:original data)) (:outstanding-balance (:original data))))] - (and (s/valid? ::invoice/invoice data) + (and (not= :loading status) + (s/valid? ::invoice/invoice data) (or (not min-total) (>= (:total data) min-total)) (or (not (:id data)) (< (.abs js/Math (- (js/parseFloat (:total data)) (reduce + 0 (map (fn [ea] (js/parseFloat (:amount ea))) (:expense-accounts data))))) 0.001)))))) @@ -592,6 +633,7 @@ [forms/side-bar-form {:form ::new-invoice } (let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::new-invoice]) exists? (:id data) + current-client @(re-frame/subscribe [::subs/client]) can-change-amount? (#{:unpaid ":unpaid"} (:status data)) change-event [::forms/change ::new-invoice] locations (get-in @(re-frame/subscribe [::subs/clients-by-id]) [(:client-id data) :locations]) @@ -605,7 +647,6 @@ chooseable-expense-accounts @(re-frame/subscribe [::subs/chooseable-expense-accounts])] ^{:key id} [:form { :on-submit (fn [e] - (println "x") (when (.-stopPropagation e) (.stopPropagation e) (.preventDefault e)) @@ -720,13 +761,43 @@ ^{:key error} [:div.notification.is-warning.animated.fadeInUp error]) + [:div.columns + (when-not exists? + [:div.column + [drop-down {:header [:button.button.is-info.is-outlined.is-medium.is-fullwidth {:aria-haspopup true + :type "button" + :on-click (dispatch-event [::events/toggle-menu ::save-and-print-invoice ]) + :disabled (if @(re-frame/subscribe [::can-submit-edit-invoice]) + "" + "disabled") + + :class (if false + "is-loading" + "")} + "Save & Pay " + [:span " "] + [:span.icon.is-small [:i.fa.fa-angle-down {:aria-hidden "true"}]]] + :class "is-fullwidth" + :id ::save-and-print-invoice} + [:div + (list + (for [{:keys [id number name type]} (->> (:bank-accounts current-client) (filter :visible) (sort-by :sort-order))] + (if (= :cash type) + ^{:key id} [:a.dropdown-item {:on-click (dispatch-event [::save-and-print-invoice id :cash])} "With cash"] + (list + ^{:key (str id "-check")} [:a.dropdown-item {:on-click (dispatch-event [::save-and-print-invoice id :check])} "Print checks from " name] + ^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event [::save-and-print-invoice id :debit])} "Debit from " name]))))]]]) + [:div.column + [:button.button.is-medium.is-primary.is-fullwidth {:disabled (if (doto @(re-frame/subscribe [::can-submit-edit-invoice]) println) + "" + "disabled") + :class (str @(re-frame/subscribe [::forms/loading-class ::new-invoice]) + (when error " animated shake"))} "Save"]] + + ] - [:button.button.is-large.is-primary {:disabled (if (doto @(re-frame/subscribe [::can-submit-edit-invoice]) println) - "" - "disabled") - :class (str @(re-frame/subscribe [::forms/loading-class ::new-invoice]) - (when error " animated shake"))} "Save"]])] + ])] ) @@ -817,14 +888,29 @@ (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))]] )) +(defn check-results-dialog [] + (let [{check-results-shown? :shown? pdf-url :pdf-url} @(re-frame/subscribe [::check-results])] + (when check-results-shown? + (if pdf-url + [modal + {:title "Your checks are ready!" + :hide-event [::close-check-results]} + [:div "Click " [:a {:href pdf-url :target "_new"} "here"] " to print them."] + [:div [:em "Remember to turn off all scaling and margins."]] + ] + [modal + {:title "Payment created!" + :hide-event [::close-check-results]} + [:div [:em "Your payment was created."]] + ])))) + (defn unpaid-invoices-content [{:keys [status]}] (r/create-class {:display-name "unpaid-invoices-content" :component-will-unmount (fn [this] (re-frame/dispatch [::unmount-invoices])) :reagent-render (fn [{:keys [status]}] (let [{:keys [checked print-checks-shown? print-checks-loading? advanced-print-shown? vendor-filter]} @(re-frame/subscribe [::invoice-page]) - current-client @(re-frame/subscribe [::subs/client]) - {check-results-shown? :shown? pdf-url :pdf-url} @(re-frame/subscribe [::check-results])] + current-client @(re-frame/subscribe [::subs/client])] [:div [:h1.title (str (str/capitalize status) " invoices")] @@ -855,19 +941,7 @@ [print-checks-modal] [handwrite-checks-modal] [change-expense-accounts-modal {:updated-event [::expense-accounts-updated]}] - (when check-results-shown? - (if pdf-url - [modal - {:title "Your checks are ready!" - :hide-event [::close-check-results]} - [:div "Click " [:a {:href pdf-url :target "_new"} "here"] " to print them."] - [:div [:em "Remember to turn off all scaling and margins."]] - ] - [modal - {:title "Payment created!" - :hide-event [::close-check-results]} - [:div [:em "Your payment was created."]] - ])) + ])) :component-will-mount #(re-frame/dispatch-sync [::params-change {:status status}]) })) @@ -885,7 +959,8 @@ [: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}] + :bottom [:div [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}] + [check-results-dialog]] :right-side-bar [appearing-side-bar {:visible? invoice-bar-active?} [edit-invoice-form]]}])) diff --git a/src/cljs/auto_ap/views/utils.cljs b/src/cljs/auto_ap/views/utils.cljs index 42ebad3a..abcdedc9 100644 --- a/src/cljs/auto_ap/views/utils.cljs +++ b/src/cljs/auto_ap/views/utils.cljs @@ -166,4 +166,4 @@ [css-transition-group {:in visible? :class-names {:exit exit-class :enter enter-class} :timeout timeout :onEnter (fn [] (reset! final-state true )) :onExited (fn [] (reset! final-state false))} (if (or @final-state visible?) (first children) - [:div])]))) + [:span])])))