From 8c50085fccbb2d7f40a9f08a917fa3c83d5a4de7 Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Tue, 7 May 2019 18:18:57 -0700 Subject: [PATCH] implemented crud --- src/clj/auto_ap/datomic.clj | 2 +- src/clj/auto_ap/datomic/transaction_rules.clj | 7 +- src/clj/auto_ap/graphql.clj | 23 ++- src/clj/auto_ap/graphql/transaction_rules.clj | 20 +++ .../auto_ap/entities/transaction_rule.cljc | 11 ++ src/cljs/auto_ap/subs.cljs | 5 + src/cljs/auto_ap/views/pages/admin/rules.cljs | 38 ++++- .../views/pages/admin/rules/common.cljs | 10 ++ .../auto_ap/views/pages/admin/rules/form.cljs | 149 ++++++++++++++++++ .../views/pages/admin/rules/table.cljs | 77 ++++++++- test/clj/auto_ap/graphql.clj | 35 +++- 11 files changed, 355 insertions(+), 22 deletions(-) create mode 100644 src/cljc/auto_ap/entities/transaction_rule.cljc create mode 100644 src/cljs/auto_ap/views/pages/admin/rules/common.cljs create mode 100644 src/cljs/auto_ap/views/pages/admin/rules/form.cljs diff --git a/src/clj/auto_ap/datomic.clj b/src/clj/auto_ap/datomic.clj index 185a1718..427b8763 100644 --- a/src/clj/auto_ap/datomic.clj +++ b/src/clj/auto_ap/datomic.clj @@ -12,7 +12,7 @@ [clj-time.core :as time] [clj-time.coerce :as coerce])) - (def uri "datomic:sql://invoices-backup?jdbc:postgresql://database:5432/datomic?user=datomic&password=datomic") + (def uri "datomic:sql://invoices?jdbc:postgresql://database:5432/datomic?user=datomic&password=datomic") (defn create-database [] (d/create-database uri)) diff --git a/src/clj/auto_ap/datomic/transaction_rules.clj b/src/clj/auto_ap/datomic/transaction_rules.clj index 4fe10384..ac6f84df 100644 --- a/src/clj/auto_ap/datomic/transaction_rules.clj +++ b/src/clj/auto_ap/datomic/transaction_rules.clj @@ -20,12 +20,15 @@ :args [db]} (:sort-by args) (add-sorter-field {"client" ['[?e :transaction-rule/client ?c] '[?c :client/name ?sorter]] + "yodlee-merchant" ['[?e :transaction-rule/yodlee-merchant ?c] '[?c :yodlee-merchant/name ?sorter]] "bank-account" ['[?e :transaction-rule/bank-account ?c] '[?c :bank-account/name ?sorter]] - "amount_lte" ['[?e :transaction-rule/amount-lte ?sorter]] - "amount_gte" ['[?e :transaction-rule/amount-gte ?sorter]] + "description" ['[?e :transaction-rule/description ?sorter]] + "note" ['[?e :transaction-rule/note ?sorter]] + "amount-lte" ['[?e :transaction-rule/amount-lte ?sorter]] + "amount-gte" ['[?e :transaction-rule/amount-gte ?sorter]] } args) diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 18d488ac..77bd75a4 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -39,7 +39,14 @@ :ident {:parse (schema/as-conformer (fn [x] {:db/ident x})) :serialize (schema/as-conformer #(or (:ident %) (:db/ident %) %))} :iso_date {:parse (schema/as-conformer #(time/parse % time/iso-date)) - :serialize (schema/as-conformer #(time/unparse % time/iso-date))}} + :serialize (schema/as-conformer #(time/unparse % time/iso-date))} + :money {:parse (schema/as-conformer #(if (string? %) + (Double/parseDouble %) + %)) + :serialize (schema/as-conformer #(if (double? %) + (str %) + %)) + }} :objects { :client @@ -493,6 +500,15 @@ :vendor_id {:type :id} :accounts {:type '(list :edit_expense_account)}}} + :edit_transaction_rule + {:fields {:id {:type :id} + :description {:type 'String} + :note {:type 'String} + :bank_account_id {:type :id} + :client_id {:type :id} + :amount_lte {:type :money} + :amount_gte {:type :money}}} + :edit_account {:fields {:id {:type :id} :type {:type :account_type} @@ -552,6 +568,10 @@ :upsert_vendor {:type :vendor :args {:vendor {:type :add_vendor}} :resolve :mutation/upsert-vendor} + + :upsert_transaction_rule {:type :transaction_rule + :args {:transaction_rule {:type :edit_transaction_rule}} + :resolve :mutation/upsert-transaction-rule} :add_invoice {:type :invoice :args {:invoice {:type :add_invoice}} :resolve :mutation/add-invoice} @@ -739,6 +759,7 @@ :mutation/add-and-print-invoice gq-invoices/add-and-print-invoice :mutation/edit-invoice gq-invoices/edit-invoice :mutation/edit-transaction gq-transactions/edit-transaction + :mutation/upsert-transaction-rule gq-transaction-rules/upsert-transaction-rule :mutation/match-transaction gq-transactions/match-transaction :mutation/edit-client gq-clients/edit-client :mutation/upsert-vendor gq-vendors/upsert-vendor diff --git a/src/clj/auto_ap/graphql/transaction_rules.clj b/src/clj/auto_ap/graphql/transaction_rules.clj index 5aea3675..b0c38195 100644 --- a/src/clj/auto_ap/graphql/transaction_rules.clj +++ b/src/clj/auto_ap/graphql/transaction_rules.clj @@ -1,8 +1,28 @@ (ns auto-ap.graphql.transaction-rules (:require [auto-ap.datomic.transaction-rules :as tr] + [datomic.api :as d] + [auto-ap.datomic :refer [remove-nils uri]] [auto-ap.graphql.utils :refer [->graphql <-graphql limited-clients assert-admin result->page]])) (defn get-transaction-rule-page [context args value] (let [args (assoc args :id (:id context)) [journal-entries journal-entries-count] (tr/get-graphql (<-graphql args))] (result->page journal-entries journal-entries-count :transaction_rules args))) + +(defn upsert-transaction-rule [context {{:keys [id description note client_id bank_account_id amount_lte amount_gte ]} :transaction_rule :as z} value] + (let [transaction [(remove-nils #:transaction-rule {:db/id (if id + id + "transaction-rule") + :description description + :note note + :client client_id + :bank-account bank_account_id + :amount-lte amount_lte + :amount-gte amount_gte})] + _ (println transaction) + transaction-result @(d/transact (d/connect uri) transaction)] + (println "HI" (or (-> transaction-result ) + id)) + (-> (tr/get-by-id (or (-> transaction-result :tempids (get "transaction-rule")) + id)) + (->graphql)))) diff --git a/src/cljc/auto_ap/entities/transaction_rule.cljc b/src/cljc/auto_ap/entities/transaction_rule.cljc new file mode 100644 index 00000000..4d1ccdbf --- /dev/null +++ b/src/cljc/auto_ap/entities/transaction_rule.cljc @@ -0,0 +1,11 @@ +(ns auto-ap.entities.transaction-rule + (:require [clojure.spec.alpha :as s])) + +(s/def ::client (s/nilable map?)) +(s/def ::description (s/nilable string?)) +(s/def ::amount-gte (s/nilable double?)) +(s/def ::amount-lte (s/nilable double?)) +(s/def ::note (s/nilable string?)) +(s/def ::bank-account (s/nilable map?)) + +(s/def ::transaction-rule (s/keys :req-un [::client ::description ::amount-gte ::amount-lte ::note ::bank-account])) diff --git a/src/cljs/auto_ap/subs.cljs b/src/cljs/auto_ap/subs.cljs index 71b88eae..802bb9fa 100644 --- a/src/cljs/auto_ap/subs.cljs +++ b/src/cljs/auto_ap/subs.cljs @@ -63,6 +63,11 @@ (fn [client] (->> client :bank-accounts (filter #(= (:type %) :check)) (sort-by :sort-order)))) +(re-frame/reg-sub + ::real-bank-accounts-for-client + (fn [db [_ {:keys [id]}]] + (sort-by :sort-order (-> db :clients (get id) :bank-accounts )))) + (re-frame/reg-sub ::locations-for-client diff --git a/src/cljs/auto_ap/views/pages/admin/rules.cljs b/src/cljs/auto_ap/views/pages/admin/rules.cljs index 4a1857b4..8c8384ca 100644 --- a/src/cljs/auto_ap/views/pages/admin/rules.cljs +++ b/src/cljs/auto_ap/views/pages/admin/rules.cljs @@ -4,9 +4,14 @@ [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] [auto-ap.views.components.layouts :refer [appearing-side-bar side-bar-layout]] [auto-ap.views.pages.admin.rules.table :as table] + [auto-ap.views.pages.admin.rules.form :as form] + [auto-ap.views.pages.admin.rules.common :refer [default-read]] [auto-ap.views.utils :refer [dispatch-event with-user]] + [auto-ap.utils :refer [replace-by]] [re-frame.core :as re-frame])) +;; SUBS + (re-frame/reg-sub ::notification (fn [db] @@ -22,13 +27,22 @@ (fn [db] (-> db (::params {})))) -(def rule-read [:description-matches]) +(re-frame/reg-event-db + ::edit-completed + (fn [db [_ edit-transaction-rule]] + (println edit-transaction-rule) + (-> db + (update-in [::page :transaction-rules] + replace-by :id (assoc edit-transaction-rule :class "live-added"))))) + +;; EVENTS (re-frame/reg-event-db ::received (fn [db [_ data]] + (println data) (-> db - (update ::page merge (first (:rule-page data))) + (update ::page merge (:transaction-rule-page data)) (assoc-in [:status :loading] false)))) (re-frame/reg-event-fx @@ -39,14 +53,22 @@ (assoc-in [:status :loading] true) (assoc-in [::params] params)) :graphql {:token user - :query-obj {:venia/queries [[:rule_page + :query-obj {:venia/queries [[:transaction_rule_page (assoc params :client-id (:id @(re-frame/subscribe [::subs/client]))) - [[:rules rule-read] + [[:transaction-rules default-read] :total :start :end]]]} :on-success [::received]}})) + +(re-frame/reg-event-fx + ::new-rule-clicked + (fn [{:keys [db]} _] + {:dispatch [::form/adding {:client @(re-frame/subscribe [::subs/client])}]})) + +;; VIEWS + (def rules-content (with-meta (fn [] @@ -57,17 +79,19 @@ [:h1.title "Transaction Rules"] (when (= "admin" (:user/role user)) [:div.is-pulled-right - [:button.button.is-outlined.is-primary {:on-click (dispatch-event [::create-rule-clicked])} "New Rule"]]) + [:button.button.is-outlined.is-primary {:on-click (dispatch-event [::new-rule-clicked])} "New Rule"]]) [table/table {:id :transactions :params (re-frame/subscribe [::params]) :rule-page (re-frame/subscribe [::page]) + :status (re-frame/subscribe [::subs/status]) :on-params-change (fn [params] (re-frame/dispatch [::params-change params]))}] ])) {:component-will-mount #(re-frame/dispatch-sync [::params-change {}]) })) (defn admin-rules-page [] - (let [{:keys [active?]} @(re-frame/subscribe [::forms/form ::new-client])] + (let [{:keys [active?]} @(re-frame/subscribe [::forms/form ::form/form])] [side-bar-layout {:side-bar [admin-side-bar {}] :main [rules-content] - :right-side-bar [appearing-side-bar {:visible? active?} [:div]] }])) + :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/common.cljs b/src/cljs/auto_ap/views/pages/admin/rules/common.cljs new file mode 100644 index 00000000..2f84a3ee --- /dev/null +++ b/src/cljs/auto_ap/views/pages/admin/rules/common.cljs @@ -0,0 +1,10 @@ +(ns auto-ap.views.pages.admin.rules.common) + +(def default-read [:id + :description + :note + :amount-gte + :amount-lte + [:client [:name :id]] + [:bank-account [:name :id]] + ]) diff --git a/src/cljs/auto_ap/views/pages/admin/rules/form.cljs b/src/cljs/auto_ap/views/pages/admin/rules/form.cljs new file mode 100644 index 00000000..8d86c1c1 --- /dev/null +++ b/src/cljs/auto_ap/views/pages/admin/rules/form.cljs @@ -0,0 +1,149 @@ +(ns auto-ap.views.pages.admin.rules.form + (:require [auto-ap.entities.transaction-rule :as entity] + [auto-ap.events :as events] + [auto-ap.forms :as forms] + [auto-ap.subs :as subs] + [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.typeahead :refer [typeahead-entity]] + [auto-ap.views.pages.admin.rules.common :refer [default-read]] + [auto-ap.views.utils :refer [date->str date-picker dispatch-event standard with-user]] + [cljs-time.core :as c] + [clojure.spec.alpha :as s] + [clojure.string :as str] + [re-frame.core :as re-frame])) + +;; SUBS + + +(re-frame/reg-sub + ::can-submit + :<- [::forms/form ::form] + (fn [{:keys [data status]} _] + (s/valid? ::entity/transaction-rule data))) + +(re-frame/reg-sub + ::query + :<- [::forms/form ::form] + (fn [{:keys [data] }] + {:venia/operation {:operation/type :mutation + :operation/name "UpsertTransactionRule"} + :venia/queries [{:query/data [:upsert-transaction-rule + {:transaction-rule (-> data + (select-keys [:id + :description + :amount-lte + :amount-gte + :note]) + (assoc :client-id (:id (:client data))) + (assoc :bank-account-id (:id (:bank-account data))))} + default-read]}]})) + +;; EVENTS + +(re-frame/reg-event-db + ::adding + (fn [db [_ new]] + (-> db (forms/start-form ::form (assoc new + :description nil))))) + +(re-frame/reg-event-db + ::editing + (fn [db [_ which]] + (-> db (forms/start-form ::form {})))) + + +(re-frame/reg-event-db + ::changed + (forms/change-handler ::form + (fn [data field value] + []))) + +(re-frame/reg-event-fx + ::saving + [with-user (forms/triggers-loading ::form) (forms/in-form ::form)] + (fn [{:keys [user] {:keys [data]} :db} [_ params]] + {:graphql + {:token user + :query-obj @(re-frame/subscribe [::query]) + :on-success [::succeeded params] + :on-error [::forms/save-error ::form]}})) + +(re-frame/reg-event-fx + ::succeeded + [(forms/triggers-stop ::form)] + (fn [{:keys [db]} [_ {:keys [rule-saved]} result]] + {:db (forms/start-form db ::form {:client @(re-frame/subscribe [::subs/client])}) + :dispatch (conj rule-saved (:upsert-transaction-rule result))})) + + + +;; VIEWS + +(def rule-form (forms/vertical-form {:can-submit [::can-submit] + :change-event [::changed] + :submit-event [::saving ] + :id ::form})) + +(defn form [{:keys [can-change-amount?] :as params}] + [layouts/side-bar {:on-close (dispatch-event [::forms/form-closing ::form ])} + (let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form]) + {:keys [form field raw-field error-notification submit-button ]} rule-form + exists? (:id data) + chooseable-expense-accounts @(re-frame/subscribe [::subs/chooseable-expense-accounts]) + accounts-by-id @(re-frame/subscribe [::subs/accounts-for-client-by-id])] + ^{:key id} + [form (assoc params :title "New Invoice") + (when-not @(re-frame/subscribe [::subs/client]) + [field "Client" + [typeahead-entity {:matches @(re-frame/subscribe [::subs/clients]) + :match->text :name + :type "typeahead-entity" + :auto-focus (if @(re-frame/subscribe [::subs/client]) false true) + :field [:client] + :spec ::entity/client}]]) + + [field "Bank account" + [typeahead-entity {:matches @(re-frame/subscribe [::subs/real-bank-accounts-for-client (:client data)]) + :match->text :name + :auto-focus (if @(re-frame/subscribe [::subs/client]) true false) + :type "typeahead-entity" + :field [:bank-account] + :spec ::entity/bank-account}]] + + [field "Description" + [:input.input {:type "text" + :field [:description] + :spec ::entity/description}]] + + [:div.field + [:p.help "Amount"] + [:div.control + [:div.columns + [:div.column + [raw-field + [:input.input {:type "number" + :placeholder ">=" + :field [:amount-gte] + :spec ::entity/amount-gte + :step "0.01"}]]] + [:div.column + [raw-field + [:input.input {:type "number" + :placeholder "<=" + :field [:amount-lte] + :spec ::entity/amount-lte + :step "0.01"}]]]]]] + + [field "Note" + [:input.input {:type "text" + :field [:note] + :spec ::entity/note}]] + + + + [error-notification] + + [submit-button "Save"]])]) 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 cef556c7..cdd7e452 100644 --- a/src/cljs/auto_ap/views/pages/admin/rules/table.cljs +++ b/src/cljs/auto_ap/views/pages/admin/rules/table.cljs @@ -1,5 +1,76 @@ -(ns auto-ap.views.pages.admin.rules.table) +(ns auto-ap.views.pages.admin.rules.table + (:require [auto-ap.subs :as subs] + [auto-ap.views.components.paginator :refer [paginator]] + [auto-ap.views.components.sorter :refer [sorted-column]] + [re-frame.core :as re-frame])) +(defn table [{:keys [id rule-page on-params-change params status]}] + (let [opc (fn [p] + (on-params-change (merge @params p )))] + (fn [{:keys [id rule-page on-params-change params status]}] + (let [{:keys [sort-by asc]} @params + {:keys [transaction-rules start end count total]} @rule-page + selected-client @(re-frame/subscribe [::subs/client])] + [:div + [paginator {:start start :end end :count count :total total + :on-change (fn [p ] + (on-params-change (merge @params p)))}] + [:table.table.is-fullwidth.compact + [:thead + [:tr + [sorted-column {:on-sort opc + :style {:width "25%" :cursor "pointer"} + :sort-key "client" + :sort-by sort-by + :asc asc} + "Client"] -(defn table [x] - [:div]) + [sorted-column {:on-sort opc + :style {:width "25%" :cursor "pointer"} + :sort-key "bank-account" + :sort-by sort-by + :asc asc} + "Bank Account"] + + [sorted-column {:on-sort opc + :style {:width "25%" :cursor "pointer"} + :sort-key "description" + :sort-by sort-by + :asc asc} + "Description"] + + [sorted-column {:on-sort opc + :style {:width "8em" :cursor "pointer"} + :class "has-text-right" + :sort-key "amount-gte" + :sort-by sort-by + :asc asc} + ">="] + [sorted-column {:on-sort opc + :class "has-text-right" + :style {:width "8em" :cursor "pointer"} + :sort-key "amount-lte" + :sort-by sort-by + :asc asc} + "<="] + + [sorted-column {:on-sort opc + :style {:width "25%" :cursor "pointer"} + :sort-key "note" + :sort-by sort-by + :asc asc} + "Note"]]] + [:tbody + (if (:loading @status) + [:tr + [:td {:col-span 5} + [:i.fa.fa-spin.fa-spinner]]] + (for [{:keys [client bank-account description amount-lte amount-gte note] :as r} transaction-rules] + ^{:key id} + [:tr {:class (:class r)} + [:td (:name client)] + [:td (:name bank-account)] + [:td description] + [:td.has-text-right amount-gte ] + [:td.has-text-right amount-lte] + [:td note]]))]]])))) diff --git a/test/clj/auto_ap/graphql.clj b/test/clj/auto_ap/graphql.clj index 8d7c1cf3..76df58da 100644 --- a/test/clj/auto_ap/graphql.clj +++ b/test/clj/auto_ap/graphql.clj @@ -1,15 +1,34 @@ (ns test.auto-ap.graphql (:require [auto-ap.graphql :as sut] + [venia.core :as v] [clojure.test :as t :refer [deftest is testing]])) (deftest query - (testing "it should find rules" - (let [result (:transaction-rule-page (:data (sut/query nil "{ transaction_rule_page(client_id: null) { count, start, transaction_rules { id } }}")))] - (is (int? (:count result))) - (is (int? (:start result))) - (is (seqable? (:transaction-rules result))))) + (testing "ledger" + (testing "it should find ledger entries" + (let [result (:ledger-page (:data (sut/query nil "{ ledger_page(client_id: null) { count, start, journal_entries { id } }}")))] + (is (int? (:count result))) + (is (int? (:start result))) + (is (seqable? (:journal-entries result)))))) - (testing "it should find ledger entries" - (is (= {:data {:ledger-page {:count 100}}} - (sut/query nil "{ ledger_page(client_id: null) { count }}"))))) + (testing "transaction-rules" + (testing "it should find rules" + (let [result (-> (sut/query nil "{ transaction_rule_page(client_id: null) { count, start, transaction_rules { id } }}") + :data + :transaction-rule-page)] + (is (int? (:count result))) + (is (int? (:start result))) + (is (seqable? (:transaction-rules result))))) + + (testing "it should add rules" + (let [q (v/graphql-query {:venia/operation {:operation/type :mutation + :operation/name "UpsertTransactionRule"} + :venia/queries [{:query/data (sut/->graphql [:upsert-transaction-rule + {:transaction-rule {:description "123"}} + [:id :description]])}]}) + result (-> (sut/query nil q) + :data + :upsert-transaction-rule)] + (is (= "123" (:description result))) + (is (:id result))))))