diff --git a/project.clj b/project.clj index 933f3340..561fa334 100644 --- a/project.clj +++ b/project.clj @@ -15,7 +15,7 @@ [ring/ring-defaults "0.2.1"] [mount "0.1.16"] [tolitius/yang "0.1.10"] - + [day8.re-frame/forward-events-fx "0.0.6"] [ring "1.6.3" :exclusions [commons-codec commons-io clj-time diff --git a/resources/public/css/main.css b/resources/public/css/main.css index 1552af87..a7b48b70 100644 --- a/resources/public/css/main.css +++ b/resources/public/css/main.css @@ -45,7 +45,7 @@ @keyframes flashWarning { from { - background-color: #00d1b2; + background-color: hsl(348, 100%, 61%); } to { diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index dd324d82..3a9428ee 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -742,7 +742,9 @@ :approval_status {:type :transaction_approval_status}} :resolve :mutation/unapprove-transactions} - + :delete_transaction_rule {:type :id + :args {:transaction_rule_id {:type :id}} + :resolve :mutation/delete-transaction-rule} :merge_vendors {:type :id :args {:from {:type :id} :to {:type :id}} @@ -1060,6 +1062,7 @@ :get-client gq-clients/get-client :get-user get-user :mutation/add-handwritten-check gq-checks/add-handwritten-check + :mutation/delete-transaction-rule gq-transaction-rules/delete-transaction-rule :mutation/print-checks print-checks :mutation/reject-invoices gq-invoices/reject-invoices :mutation/approve-invoices gq-invoices/approve-invoices diff --git a/src/clj/auto_ap/graphql/transaction_rules.clj b/src/clj/auto_ap/graphql/transaction_rules.clj index b2a723eb..d057f2c4 100644 --- a/src/clj/auto_ap/graphql/transaction_rules.clj +++ b/src/clj/auto_ap/graphql/transaction_rules.clj @@ -45,6 +45,15 @@ :account account_id :location location})) +(defn delete-transaction-rule [context {:keys [transaction_rule_id ]} value] + (assert-admin (:id context)) + (let [existing-transaction-rule (tr/get-by-id transaction_rule_id)] + (when-not (:transaction-rule/description existing-transaction-rule) + (throw (ex-info "Transaction rule not found" {:validation-error "Transaction Rule not found"}))) + + @(d/transact (d/connect uri) [[:db/retractEntity transaction_rule_id]]) + transaction_rule_id)) + (defn upsert-transaction-rule [context {{:keys [id description yodlee_merchant_id note client_id bank_account_id amount_lte amount_gte vendor_id accounts transaction_approval_status dom_gte dom_lte]} :transaction_rule :as z} value] (assert-admin (:id context)) (let [existing-transaction (tr/get-by-id id) diff --git a/src/cljc/auto_ap/utils.cljc b/src/cljc/auto_ap/utils.cljc index 57484026..1a66c13b 100644 --- a/src/cljc/auto_ap/utils.cljc +++ b/src/cljc/auto_ap/utils.cljc @@ -31,6 +31,19 @@ replaced (into [x] replaced)))) +(defn merge-by [xs f x] + (let [found? (atom false) + replaced (mapv + (fn [t] + (if (= (f t) (f x)) + (do (reset! found? true) + (merge t x)) + t)) + xs)] + (if @found? + replaced + (into [x] replaced)))) + (defn dollars-0? [amt] (< -0.001 amt 0.001)) diff --git a/src/cljs/auto_ap/views/components/buttons.cljs b/src/cljs/auto_ap/views/components/buttons.cljs index 2b98b428..f56fe31a 100644 --- a/src/cljs/auto_ap/views/components/buttons.cljs +++ b/src/cljs/auto_ap/views/components/buttons.cljs @@ -4,3 +4,8 @@ (defn fa-icon [{:keys [event icon class]}] [:a.button {:class class :on-click (dispatch-event event)} [:span.icon [:i.fa {:class icon}]]]) + +(defn sl-icon [{:keys [event icon class]}] + [:a.button {:class class + :on-click (dispatch-event event)} + [:span.icon [:span {:class icon :style {:font-weight "400"}}]]]) diff --git a/src/cljs/auto_ap/views/components/layouts.cljs b/src/cljs/auto_ap/views/components/layouts.cljs index 86128d69..954caaad 100644 --- a/src/cljs/auto_ap/views/components/layouts.cljs +++ b/src/cljs/auto_ap/views/components/layouts.cljs @@ -11,6 +11,7 @@ [auto-ap.views.components.vendor-dialog :refer [vendor-dialog]] [auto-ap.views.components.vendor-dialog :refer [vendor-dialog]] + [auto-ap.views.components.modal :as modal] [auto-ap.entities.vendors :as vendor] [auto-ap.views.components.vendor-dialog :as vendor-dialog] [clojure.string :as str])) @@ -177,6 +178,7 @@ client @(re-frame/subscribe [::subs/client]) is-initial-loading @(re-frame/subscribe [::subs/is-initial-loading?])] [:div + [modal/global-modal] [navbar ap] [:div {:class "columns has-shadow", :style {:margin-bottom "0px" :height "calc(100vh - 46px)" } :id "mail-app" } [:aside {:class "column aside menu is-2 " } diff --git a/src/cljs/auto_ap/views/components/modal.cljs b/src/cljs/auto_ap/views/components/modal.cljs index 5b3dfab0..7fb52cdf 100644 --- a/src/cljs/auto_ap/views/components/modal.cljs +++ b/src/cljs/auto_ap/views/components/modal.cljs @@ -3,7 +3,8 @@ [reagent.core :as r] [auto-ap.events :as events] [auto-ap.subs :as subs] - [auto-ap.views.utils :refer [with-keys appearing]])) + [auto-ap.status :as status] + [auto-ap.views.utils :refer [with-keys appearing dispatch-event]])) (defn modal [{:keys [title foot hide-event class]} & body] [:div.modal.is-active (cond-> {} @@ -67,3 +68,61 @@ (into (r/children (r/current-component))) (into [(when saving? [:div.is-overlay {:style {"backgroundColor" "rgba(150,150,150, 0.5)"}}])]))))) +(re-frame/reg-sub + ::modal-state + (fn [db] + (::state db))) + +(re-frame/reg-event-db + ::modal-requested + (fn [db [_ state]] + (assoc db ::state (assoc state :visible? true)))) + +(re-frame/reg-event-fx + ::modal-closed + (fn [{:keys [db]} [_ state]] + (let [[_ status-id] (some-> db ::state :confirm :status-from )] + (cond-> {:db (dissoc db ::state)} + status-id (assoc :dispatch [::status/completed status-id]))))) + + +(defn global-modal [] + (let [state (re-frame/subscribe [::modal-state])] + (fn [] + (if (:visible? @state) + (let [{:keys [title body foot class cancel? confirm]} @state] + [:div.modal.is-active (cond-> {} + class (assoc :class class)) + [:div.modal-background {:on-click (dispatch-event [::modal-closed])}] + + [:div.modal-card + [:header.modal-card-head + [:p.modal-card-title + title] + [:button.delete {:on-click (dispatch-event [::modal-closed])}]] + [:section.modal-card-body + body] + (let [status (some-> confirm :status-from re-frame/subscribe deref )] + (if foot + [:footer.modal-card-foot + [appearing {:visible? (= :error (:state status)) + :timeout 200 + :enter-class "appear" + :exit-class "disappear"} + [:div.notification.is-warning (:message (first (:error status)))]] + foot] + [:footer.modal-card-foot + [:div + [appearing {:visible? (= :error (:state status)) + :timeout 200 + :enter-class "appear" + :exit-class "disappear"} + [:div.notification.is-warning (:message (first (:error status)))]] + + [:div.buttons + (when confirm + [:button.button.is-danger {:class (status/class-for status) + :on-click (:on-click confirm)} + (:value confirm)]) + (when cancel? + [:button.button {:on-click (dispatch-event [::modal-closed] )} "Cancel"])]]]))]]))))) diff --git a/src/cljs/auto_ap/views/pages/admin/rules.cljs b/src/cljs/auto_ap/views/pages/admin/rules.cljs index e935c0eb..3370eeeb 100644 --- a/src/cljs/auto_ap/views/pages/admin/rules.cljs +++ b/src/cljs/auto_ap/views/pages/admin/rules.cljs @@ -10,17 +10,15 @@ [auto-ap.views.pages.admin.rules.common :refer [default-read]] [auto-ap.views.utils :refer [dispatch-event with-user]] [vimsical.re-frame.cofx.inject :as inject] + [vimsical.re-frame.fx.track :as track] + [day8.re-frame.forward-events-fx] [auto-ap.events :as events] - [auto-ap.utils :refer [replace-by]] + [auto-ap.utils :refer [replace-by merge-by]] [re-frame.core :as re-frame] [auto-ap.status :as status])) ;; SUBS -(re-frame/reg-sub - ::notification - (fn [db] - (-> db ::notification))) (re-frame/reg-sub ::page @@ -57,34 +55,22 @@ (update :transaction-rules (fn [rules] (mapv ungraphql-transaction-rule rules)))))))) -(re-frame/reg-sub - ::last-params - (fn [db] - (-> db ::last-params))) - (re-frame/reg-sub ::params - :<- [::last-params] :<- [::subs/client] :<- [::side-bar/filter-params] :<- [::table/table-params] - (fn [[last-params client filter-params table-params]] - (let [params (cond-> {} - client (assoc :client-id (:id client)) - (seq filter-params) (merge filter-params) - (seq table-params) (merge table-params))] - (when (not= params last-params) - (re-frame/dispatch [::params-change])) - params))) + (fn [[client filter-params table-params]] + (cond-> {} + client (assoc :client-id (:id client)) + (seq filter-params) (merge filter-params) + (seq table-params) (merge table-params)))) (re-frame/reg-event-fx ::params-change - [with-user (re-frame/inject-cofx ::inject/sub [::params])] - (fn [{:keys [db user] ::keys [params] :as cofx} _] - {:db (-> db - (assoc-in [::last-params] params)) - - :graphql {:token user + [with-user ] + (fn [{:keys [db user] :as cofx} [_ params]] + {:graphql {:token user :owns-state {:single ::page} :query-obj {:venia/queries [[:transaction_rule_page (or params {}) @@ -102,13 +88,34 @@ (fn [{:keys [db]} _] {:dispatch [::form/adding {:client @(re-frame/subscribe [::subs/client])}]})) -;; VIEWS +(re-frame/reg-event-db + ::deleted-transaction-rule + [(re-frame/path [::page :transaction-rules])] + (fn [transaction-rules [_ [_ {id :delete-transaction-rule}]]] + (merge-by transaction-rules :id {:id id :class "live-removed"}))) +(re-frame/reg-event-fx + ::mounted + (fn [{:keys [db]}] + {:dispatch-n [[::events/yodlee-merchants-needed]] + :forward-events {:register ::page + :events #{::table/deleted-transaction-rule} + :dispatch-to [::deleted-transaction-rule]} + ::track/register {:id ::params + :subscription [::params] + :event-fn (fn [params] [::params-change params])}})) + +(re-frame/reg-event-fx + ::unmounted + (fn [{:keys [db]}] + {:forward-events {:unregister ::page} + ::track/dispose {:id ::params}})) + +;; VIEWS (def rules-content (with-meta (fn [] - (let [notification (re-frame/subscribe [::notification]) - current-client @(re-frame/subscribe [::subs/client]) + (let [current-client @(re-frame/subscribe [::subs/client]) user @(re-frame/subscribe [::subs/user])] [:div [:h1.title "Transaction Rules"] @@ -123,16 +130,14 @@ (println "CHANGING PARAMS TO" params) (re-frame/dispatch [::params-change params]))}] ])) - {:component-will-mount #(do (re-frame/dispatch-sync [::params-change {}]) - (re-frame/dispatch [::events/yodlee-merchants-needed])) })) + {:component-did-mount (dispatch-event [::mounted ]) + :component-will-unmount #(re-frame/dispatch-sync [::unmounted])})) (defn admin-rules-page [] - (let [{:keys [active?]} @(re-frame/subscribe [::forms/form ::form/form]) - params @(re-frame/subscribe [::params])] + (let [{:keys [active?]} @(re-frame/subscribe [::forms/form ::form/form])] [side-bar-layout {:side-bar [admin-side-bar {} [:<> - [side-bar/rule-side-bar] - ]] + [side-bar/rule-side-bar]]] :main [rules-content] :right-side-bar [appearing-side-bar {:visible? active?} [form/form {:rule-saved [::edit-completed]}]] diff --git a/src/cljs/auto_ap/views/pages/admin/rules/table.cljs b/src/cljs/auto_ap/views/pages/admin/rules/table.cljs index 697bf334..d4cc0d58 100644 --- a/src/cljs/auto_ap/views/pages/admin/rules/table.cljs +++ b/src/cljs/auto_ap/views/pages/admin/rules/table.cljs @@ -8,10 +8,13 @@ [auto-ap.views.components.sorter :refer [sorted-column toggle-sort-by sort-icon]] [auto-ap.views.components.buttons :as buttons] [auto-ap.views.components.grid :as grid] + [auto-ap.views.components.modal :refer [simple-modal]] + [auto-ap.events :as events] [auto-ap.status :as status] [re-frame.core :as re-frame] [reagent.core :as reagent] - [reagent.core :as r])) + [reagent.core :as r] + [auto-ap.views.components.modal :as modal])) (re-frame/reg-event-fx ::run-clicked @@ -63,6 +66,39 @@ {:db (merge table-params params)})) +(re-frame/reg-event-fx + ::deleted-transaction-rule + (fn [] + {:dispatch [::modal/modal-closed]})) + +(re-frame/reg-event-fx + ::delete-transaction-rule + [with-user] + (fn [{:keys [db user]} [_ id]] + {:graphql + {:token user + :owns-state {:single ::delete-transaction-rule} + :query-obj {:venia/operation {:operation/type :mutation + :operation/name "DeleteTransactionRule"} + :venia/queries [{:query/data [:delete-transaction-rule + {:transaction-rule-id id}]}]} + :on-success [::deleted-transaction-rule]}})) + + +;; TODO count how many transactions +(re-frame/reg-event-fx + ::request-delete + (fn [_ [_ which]] + {:dispatch [::modal/modal-requested {:title "Confirmation" + :body [:div "Are you sure you want to delete transaction rule '" (:description which) "'? Any previously transactions will remain updated, but the rule association will be lost."] + :cancel? true + :confirm {:value "Delete Transaction Rule" + :status-from [::status/single ::delete-transaction-rule] + :on-click (dispatch-event [::delete-transaction-rule (:id which)] )} + :close-event [::status/completed ::delete-transaction-rule]}]})) + + + (defn table* [{:keys [id rule-page on-params-change params status]}] (let [{:keys [sort asc]} @params {:keys [transaction-rules start end count total]} @rule-page @@ -70,33 +106,34 @@ opc (fn [p] (re-frame/dispatch [::params-changed p])) states @(re-frame/subscribe [::status/multi ::run])] - [grid/grid {:on-params-change opc - :params @(re-frame/subscribe [::table-params]) - :status status - :column-count 6} - [grid/controls {:start start :end end :count count :total total}] - [grid/table {:fullwidth true } + [:div + [grid/grid {:on-params-change opc + :params @(re-frame/subscribe [::table-params]) + :status status + :column-count 6} + [grid/controls {:start start :end end :count count :total total}] + [grid/table {:fullwidth true } [grid/header [grid/row {} [grid/sortable-header-cell {:sort-key "client" - :sort-name "Client"} + :sort-name "Client"} "Client"] [grid/sortable-header-cell {:sort-key "bank-account" - :sort-name "Bank Account"} + :sort-name "Bank Account"} "Bank Account"] [grid/sortable-header-cell {:sort-key "description" - :sort-name "Description"} + :sort-name "Description"} "Description"] [grid/header-cell {:style {:width "12em"}} "Amount"] [grid/sortable-header-cell {:sort-key "note" - :sort-name "Note"} + :sort-name "Note"} "Note"] - [grid/header-cell {:style {:width (str (inc (* 2 44)) "px")}}]]] + [grid/header-cell {:style {:width (str (inc (* 3 44)) "px")}}]]] [grid/body (for [{:keys [client bank-account description amount-lte amount-gte note id] :as r} transaction-rules] ^{:key id} @@ -120,7 +157,8 @@ [grid/cell {} [:div.buttons [buttons/fa-icon {:event [::run-clicked r] :icon :fa-play :class (status/class-for (get states (:id r)))}] - [buttons/fa-icon {:event [::form/editing r] :icon :fa-pencil}]]]])]]])) + [buttons/sl-icon {:event [::request-delete r] :icon :icon-bin-2}] + [buttons/fa-icon {:event [::form/editing r] :icon :fa-pencil}]]]])]]]])) (defn table [params] (r/create-class {:component-will-unmount (dispatch-event [::unmounted])