diff --git a/src/clj/auto_ap/datomic/migrate.clj b/src/clj/auto_ap/datomic/migrate.clj index 1a95b099..ddfe5f3c 100644 --- a/src/clj/auto_ap/datomic/migrate.clj +++ b/src/clj/auto_ap/datomic/migrate.clj @@ -5,6 +5,8 @@ [auto-ap.datomic.migrate.add-bank-account-codes :refer [add-bank-account-codes]] [auto-ap.datomic.migrate.invoice-converter :refer [add-import-status-existing-invoices]] [auto-ap.datomic.migrate.add-general-ledger :as add-general-ledger] + [auto-ap.datomic.migrate.sales :as sales] + [clojure.java.io :as io] [io.rkn.conformity :as c]) (:import [datomic Util]) @@ -313,6 +315,7 @@ ] (println "Conforming database...") (c/ensure-conforms conn norms-map) + (c/ensure-conforms conn sales/norms-map) (when (not (seq args)) (d/release conn)) (println "Done"))) diff --git a/src/clj/auto_ap/datomic/migrate/sales.clj b/src/clj/auto_ap/datomic/migrate/sales.clj new file mode 100644 index 00000000..e5d8db5c --- /dev/null +++ b/src/clj/auto_ap/datomic/migrate/sales.clj @@ -0,0 +1,99 @@ +(ns auto-ap.datomic.migrate.sales + (:require [datomic.api :as d] + [auto-ap.datomic :refer [uri]])) + +(def norms-map {:add-orders {:txes [[{:db/ident :sales-order/external-id + :db/doc "The client for the sale" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/unique :db.unique/identity} + + {:db/ident :sales-order/client + :db/doc "The client for the sale" + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one} + + {:db/ident :sales-order/location + :db/doc "The location of the sale" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} + + {:db/ident :sales-order/date + :db/doc "The date the order was placed" + :db/valueType :db.type/instant + :db/cardinality :db.cardinality/one} + + {:db/ident :sales-order/total + :db/doc "The total amount on the sale" + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + + {:db/ident :sales-order/tax + :db/doc "The tax on the sale" + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + + {:db/ident :sales-order/tip + :db/doc "The tip on the sale" + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + + {:db/ident :sales-order/charges + :db/doc "How the sale was paid" + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/many + :db/isComponent true} + + {:db/ident :sales-order/line-items + :db/doc "What the person ordered" + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/many + :db/isComponent true} + + + {:db/ident :charge/type-name + :db/doc "The type of the charge" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} + + {:db/ident :charge/total + :db/doc "The total amount on the charge" + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + + {:db/ident :charge/tax + :db/doc "The tax on the charge" + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + + {:db/ident :charge/tip + :db/doc "The tip on the charge" + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + + {:db/ident :order-line-item/item-name + :db/doc "The type of the item" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} + + {:db/ident :order-line-item/total + :db/doc "The total cost on the item" + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + + {:db/ident :order-line-item/tax + :db/doc "The tax on the item" + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + + {:db/ident :order-line-item/tip + :db/doc "The tip on the item" + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one} + + ]]}}) + + + + + diff --git a/src/clj/auto_ap/datomic/sales_orders.clj b/src/clj/auto_ap/datomic/sales_orders.clj new file mode 100644 index 00000000..59123fc1 --- /dev/null +++ b/src/clj/auto_ap/datomic/sales_orders.clj @@ -0,0 +1,104 @@ +(ns auto-ap.datomic.sales-orders + (:require [auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-3 merge-query uri]] + [auto-ap.graphql.utils :refer [limited-clients]] + [auto-ap.utils :refer [dollars=]] + [clj-time.coerce :as c] + [datomic.api :as d])) + +(defn <-datomic [result] + (-> result + (update :sales-order/date c/from-date))) + +(def default-read '[* + {:sales-order/client [:client/name :db/id :client/code]}]) + +(defn raw-graphql-ids [db args] + (let [query (cond-> {:query {:find [] + :in ['$] + :where []} + :args [db]} + (:sort args) (add-sorter-fields {"client" ['[?e :sales-order/client ?c] + '[?c :client/name ?sort-client]] + "location" ['[?e :sales-order/location ?sort-location]] + "date" ['[?e :sales-order/date ?sort-date]] + "total" ['[?e :sales-order/total ?sort-total]] + "tax" ['[?e :sales-order/tax ?sort-tax]] + "tip" ['[?e :sales-order/tip ?sort-tip]]} + args) + + (limited-clients (:id args)) + (merge-query {:query {:in ['[?xx ...]] + :where ['[?e :sales-order/client ?xx]]} + :args [(set (map :db/id (limited-clients (:id args))))]}) + + (:client-id args) + (merge-query {:query {:in ['?client-id] + :where ['[?e :sales-order/client ?client-id]]} + :args [(:client-id args)]}) + (:client-code args) + (merge-query {:query {:in ['?client-code] + :where ['[?e :sales-order/client ?client-id] + '[?client-id :client/code ?client-code]]} + :args [(:client-code args)]}) + + (:total-gte args) + (merge-query {:query {:in ['?total-gte] + :where ['[?e :sales-order/total ?a] + '[(>= ?a ?total-gte)]]} + :args [(:total-gte args)]}) + + (:total-lte args) + (merge-query {:query {:in ['?total-lte] + :where ['[?e :sales-order/total ?a] + '[(<= ?a ?total-lte)]]} + :args [(:total-lte args)]}) + + (:total args) + (merge-query {:query {:in ['?total] + :where ['[?e :sales-order/total ?sales-order-total] + '[(auto-ap.utils/dollars= ?sales-order-total ?total)]]} + :args [(:total args)]}) + + + (:start (:date-range args)) + (merge-query {:query {:in '[?start-date] + :where ['[?e :sales-order/date ?date] + '[(>= ?date ?start-date)]]} + :args [(c/to-date (:start (:date-range args)))]}) + + (:end (:date-range args)) + (merge-query {:query {:in '[?end-date] + :where ['[?e :sales-order/date ?date] + '[(<= ?date ?end-date)]]} + :args [(c/to-date (:end (:date-range args)))]}) + + true + (merge-query {:query {:find ['?base-date '?e] + :where ['[?e :sales-order/date ?base-date]]}}))] + + + (cond->> query + true (d/query) + true (apply-sort-3 args) + true (apply-pagination args)))) + +(defn graphql-results [ids db args] + (let [results (->> (d/pull-many db default-read ids) + (group-by :db/id)) + payments (->> ids + (map results) + (map first) + (mapv <-datomic))] + payments)) + +(defn get-graphql [args] + (let [db (d/db (d/connect uri)) + {ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args)] + + [(->> (graphql-results ids-to-retrieve db args)) + matching-count])) + +(defn get-by-id [id] + (->> + (d/pull (d/db (d/connect uri)) default-read id) + (<-datomic))) diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 398800cc..5e5fd9c7 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -21,6 +21,7 @@ [auto-ap.graphql.users :as gq-users] [auto-ap.graphql.yodlee-merchants :as ym] [auto-ap.graphql.ledger :as gq-ledger] + [auto-ap.graphql.sales-orders :as gq-sales-orders] [auto-ap.graphql.accounts :as gq-accounts] [auto-ap.graphql.clients :as gq-clients] [auto-ap.graphql.vendors :as gq-vendors] @@ -189,6 +190,29 @@ :line_items {:type '(list :journal_entry_line)}}} + :order_line_item + {:fields {:id {:type :id} + :item_name {:type 'String} + :total {:type :money} + :tax {:type :money}}} + :charge + {:fields {:id {:type :id} + :type_name {:type 'String} + :total {:type :money} + :tip {:type :money}}} + + :sales_order + {:fields {:id {:type :id} + :location {:type 'String} + :external_id {:type 'String} + :total {:type :money} + :tip {:type :money} + :tax {:type :money} + :client {:type :client} + :date {:type 'String} + :charges {:type '(list :charge)} + :line_items {:type '(list :order_line_item)}}} + :check {:fields {:id {:type :id} :type {:type 'String} :amount {:type 'String} @@ -348,6 +372,12 @@ :start {:type 'Int} :end {:type 'Int}}} + :sales_order_page {:fields {:sales_orders {:type '(list :sales_order)} + :count {:type 'Int} + :total {:type 'Int} + :start {:type 'Int} + :end {:type 'Int}}} + :reminder_page {:fields {:reminders {:type '(list :reminder)} :count {:type 'Int} :total {:type 'Int} @@ -497,6 +527,16 @@ :resolve :get-ledger-page} + :sales_order_page {:type :sales_order_page + :args {:client_id {:type :id} + :date_range {:type :date_range} + :total_lte {:type :money} + :total_gte {:type :money} + :start {:type 'Int} + :sort {:type '(list :sort_item)}} + + :resolve :get-sales-order-page} + :payment_page {:type '(list :payment_page) :args {:client_id {:type :id} :vendor_id {:type :id} @@ -1053,6 +1093,7 @@ :get-accounts gq-accounts/get-accounts :get-transaction-page gq-transactions/get-transaction-page :get-ledger-page gq-ledger/get-ledger-page + :get-sales-order-page gq-sales-orders/get-sales-orders-page :get-balance-sheet gq-ledger/get-balance-sheet :get-profit-and-loss gq-ledger/get-profit-and-loss :get-transaction-rule-page gq-transaction-rules/get-transaction-rule-page diff --git a/src/clj/auto_ap/graphql/sales_orders.clj b/src/clj/auto_ap/graphql/sales_orders.clj new file mode 100644 index 00000000..4aa7be9a --- /dev/null +++ b/src/clj/auto_ap/graphql/sales_orders.clj @@ -0,0 +1,8 @@ +(ns auto-ap.graphql.sales-orders + (:require [auto-ap.datomic.sales-orders :as d-sales-orders2] + [auto-ap.graphql.utils :refer [->graphql <-graphql result->page]])) + +(defn get-sales-orders-page [context args value] + (let [args (assoc args :id (:id context)) + [sales-orders sales-orders-count] (d-sales-orders2/get-graphql (<-graphql args))] + (result->page sales-orders sales-orders-count :sales_orders args ))) diff --git a/src/clj/auto_ap/server.clj b/src/clj/auto_ap/server.clj index 12bec0db..ded51caf 100644 --- a/src/clj/auto_ap/server.clj +++ b/src/clj/auto_ap/server.clj @@ -4,6 +4,7 @@ [auto-ap.ledger] [auto-ap.yodlee.core] [auto-ap.background.invoices] + [auto-ap.square.core] [nrepl.server :refer [start-server stop-server]] [config.core :refer [env]] [ring.adapter.jetty :refer [run-jetty]] diff --git a/src/clj/auto_ap/square/core.clj b/src/clj/auto_ap/square/core.clj new file mode 100644 index 00000000..c2efd87d --- /dev/null +++ b/src/clj/auto_ap/square/core.clj @@ -0,0 +1,118 @@ +(ns auto-ap.square.core + (:require [auto-ap.datomic :refer [conn remove-nils]] + [clj-http.client :as client] + [clj-time.coerce :as coerce] + [clj-time.core :as time] + [clojure.data.json :as json] + [datomic.api :as d] + [unilog.context :as lc] + [clojure.tools.logging :as log] + [mount.core :as mount] + [yang.scheduler :as scheduler])) + +(defn locations [] + (->> (client/get "https://connect.squareup.com/v2/locations" + {:headers {"Square-Version" "2020-08-12" + "Authorization" "Bearer EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7" + "Content-Type" "application/json"} + :as :json}) + :body + :locations)) + +(defn search [l] + (log/info "Searching for" l) + (->> (client/post "https://connect.squareup.com/v2/orders/search" + {:headers {"Square-Version" "2020-08-12" + "Authorization" "Bearer EAAAEO2xSqesDutZz71hz3eulKmrlKTiEqG3uZ4j25x5GYlOluQ2cj2JxNUXqXD7" + "Content-Type" "application/json"} + :body (json/write-str {"location_ids" [l] "limit" 4000}) + :as :json}) + :body + :orders)) + + +(defn amount->money [amt] + (* 0.01 (or (:amount amt) 0.0))) + +(defn location_id->client-location [location] + ({"2RVBYER6QSV7W" ["NGAK" "MH"] + "8JT71V8XGYAT3" ["NGKG" "NB"] + "SCX0Y8CTGM1S0" ["NGE1" "UC"] + "FNH5VRT890WK8" ["NGMJ" "SC"] + "AMQ0NPA8FGDEF" ["NGPG" "SZ"] + "4X8T65741AEPS" ["NGVZ" "NP"] + "KMVFQ9CRCXJ10" ["NGZO" "VT"]} location)) + +(defn daily-results [] + (->> (locations) + (map :id) + (filter location_id->client-location) + (mapcat search) + (map (fn [order] + (let [[client loc] (location_id->client-location (:location_id order))] + (remove-nils + #:sales-order + {:date (coerce/to-date (time/to-time-zone (coerce/to-date-time (:created_at order)) (time/time-zone-for-id "America/Los_Angeles"))) + :client [:client/code client] + :location loc + :external-id (str "square-" client "-" loc "-" (:id order)) + :total (-> order :net_amounts :total_money amount->money) + :tax (-> order :net_amounts :tax_money amount->money) + :tip (-> order :net_amounts :tip_money amount->money) + :charges (->> (:tenders order) + (map (fn [t] + (remove-nils + #:charge + {:type-name (:type t) + :total (amount->money (:amount_money t)) + :tip (amount->money (:tip_money t))})))) + :line-items (->> (:line_items order) + (map (fn [li] + (remove-nils + #:order-line-item + {:item-name (:name li) + :total (amount->money (:total_money li)) + :tax (amount->money (:total_tax_money li))}))))})))))) + +#_(daily-results) + + + +(defn upsert [] + (lc/with-context {:source "Square loading"} + (try + (let [existing (->> (d/query {:query {:find ['?external-id] + :in ['$] + :where ['[_ :sales-order/external-id ?external-id]]} + :args [(d/db conn)]}) + (map first) + set) + _ (log/info (count existing) "Sales orders already exist") + to-create (filter #(not (existing (:sales-order/external-id %))) + (daily-results))] + (doseq [x (partition-all 20 to-create)] + (log/info "Loading " (count x)) + @(d/transact conn x))) + (catch Exception e + (log/error e))))) + +(defn reset [] + (->> + (d/query {:query {:find ['?e] + :in ['$] + :where ['[?e :sales-order/date]]} + :args [(d/db conn)]}) + (map first) + (map (fn [x] [:db/retractEntity x])))) + +(mount/defstate square-loader + :start (scheduler/every (* 15 60 1000) upsert) + :stop (scheduler/stop square-loader)) + +(comment + (daily-results) + + (do (upsert) nil) + + (do @(d/transact conn (reset)) nil)) + diff --git a/src/cljs/auto_ap/routes.cljs b/src/cljs/auto_ap/routes.cljs index 92b39745..7d510145 100644 --- a/src/cljs/auto_ap/routes.cljs +++ b/src/cljs/auto_ap/routes.cljs @@ -21,6 +21,7 @@ "paid" :paid-invoices "voided" :voided-invoices "new" :new-invoice} + "sales-orders/" {"" :sales-orders} "transactions/" {"" :transactions "unapproved" :unapproved-transactions "approved" :approved-transactions diff --git a/src/cljs/auto_ap/views/components/layouts.cljs b/src/cljs/auto_ap/views/components/layouts.cljs index 79483b61..cb9b0070 100644 --- a/src/cljs/auto_ap/views/components/layouts.cljs +++ b/src/cljs/auto_ap/views/components/layouts.cljs @@ -111,6 +111,10 @@ [:a.navbar-item {:class [(active-when ap = :payments)] :href (bidi/path-for routes/routes :payments)} "Payments" ] + (when (= "admin" (:user/role @user)) + [:a.navbar-item {:class [(active-when ap = :sales-orders)] + :href (bidi/path-for routes/routes :sales-orders)} + "Sales Orders" ]) [:a.navbar-item {:class [(active-when ap = :transactions)] :href (bidi/path-for routes/routes :transactions)} "Transactions" ] diff --git a/src/cljs/auto_ap/views/main.cljs b/src/cljs/auto_ap/views/main.cljs index 67e4f3c8..c29f5cb7 100644 --- a/src/cljs/auto_ap/views/main.cljs +++ b/src/cljs/auto_ap/views/main.cljs @@ -17,6 +17,7 @@ [auto-ap.views.pages.ledger.profit-and-loss :refer [profit-and-loss-page]] [auto-ap.views.pages.login :refer [login-page]] [auto-ap.views.pages.payments :refer [payments-page]] + [auto-ap.views.pages.sales-orders :refer [sales-orders-page]] [auto-ap.views.pages.admin :refer [admin-page]] [auto-ap.views.pages.home :refer [home-page]] [auto-ap.views.pages.admin.clients :refer [admin-clients-page]] @@ -54,6 +55,9 @@ (defmethod page :payments [_] [payments-page]) +(defmethod page :sales-orders [_] + [sales-orders-page]) + (defmethod page :transactions [_] (transactions-page {})) diff --git a/src/cljs/auto_ap/views/pages/sales_orders.cljs b/src/cljs/auto_ap/views/pages/sales_orders.cljs new file mode 100644 index 00000000..ca04b007 --- /dev/null +++ b/src/cljs/auto_ap/views/pages/sales_orders.cljs @@ -0,0 +1,74 @@ +(ns auto-ap.views.pages.sales-orders + (:require [auto-ap.forms :as forms] + [auto-ap.subs :as subs] + [auto-ap.views.components.layouts :refer [side-bar-layout appearing-side-bar]] + [auto-ap.views.pages.data-page :as data-page] + [auto-ap.views.pages.sales-orders.form :as form] + [auto-ap.views.pages.sales-orders.side-bar :as side-bar] + [auto-ap.views.pages.sales-orders.table :as table] + [auto-ap.views.utils :refer [with-user]] + [clojure.set :as set] + [re-frame.core :as re-frame] + [reagent.core :as reagent] + [vimsical.re-frame.fx.track :as track])) + +(re-frame/reg-event-fx + ::params-change + [with-user] + (fn [{:keys [user db ]}[_ params]] + {:graphql {:token user + :owns-state {:single [::data-page/page ::page]} + :query-obj {:venia/queries [[:sales_order_page + {:start (:start params 0) + :sort (:sort params) + :total-gte (:amount-gte (:total-range params)) + :total-lte (:amount-lte (:total-range params)) + :date-range (:date-range params) + :client-id (:id @(re-frame/subscribe [::subs/client]))} + [[:sales-orders [:id :total :tax :tip :date + [:charges [:type-name :total]] + [:line-items [:item-name :total]] + [:client [:name :id]]]] + :total + :start + :end]]]} + :on-success (fn [result] + (let [result (set/rename-keys (:sales-order-page result) + {:sales-orders :data})] + [::data-page/received ::page result]))}})) + + +(re-frame/reg-event-fx + ::unmounted + (fn [{:keys [db]} _] + {:dispatch [::data-page/dispose ::page] + ::track/dispose {:id ::params}})) + +(re-frame/reg-event-fx + ::mounted + (fn [{:keys [db]} _] + {::track/register {:id ::params + :subscription [::data-page/params ::page] + :event-fn (fn [params] + [::params-change params])}})) + +(defn content [] + [:div + [:h1.title "Sales Orders"] + [table/table {:id :sales-orders + :data-page ::page}]]) + +(defn sales-orders-page [] + (reagent/create-class + {:display-name "sales-orders-page" + :component-will-unmount #(re-frame/dispatch-sync [::unmounted]) + :component-did-mount #(re-frame/dispatch [::mounted]) + :reagent-render + (fn [] + (let [{form-active? :active?} @(re-frame/subscribe [::forms/form ::form/form])] + (println form-active?) + [side-bar-layout {:side-bar [side-bar/side-bar {:data-page ::page}] + :main [content] + :right-side-bar [appearing-side-bar {:visible? form-active?} + [form/form {}]]}]))})) + diff --git a/src/cljs/auto_ap/views/pages/sales_orders/form.cljs b/src/cljs/auto_ap/views/pages/sales_orders/form.cljs new file mode 100644 index 00000000..e7ca430e --- /dev/null +++ b/src/cljs/auto_ap/views/pages/sales_orders/form.cljs @@ -0,0 +1,81 @@ +(ns auto-ap.views.pages.sales-orders.form + (:require + [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.money-field :refer [money-field]] + [auto-ap.views.components.typeahead :refer [typeahead-entity]] + [auto-ap.status :as status] + [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])) +(re-frame/reg-sub + ::can-submit + :<- [::forms/form ::form] + (fn [{:keys [data status]} _] + false)) + +(re-frame/reg-event-db + ::editing + (fn [db [_ which]] + (let [which (update which :date #(date->str % standard))] + (forms/start-form db ::form which)))) + +(def sales-order-form (forms/vertical-form {:can-submit [::can-submit] + :change-event [::forms/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-inline field raw-field error-notification submit-button ]} sales-order-form] + (with-meta + (form-inline (assoc params :title "Sales order") + [:<> + (when-not @(re-frame/subscribe [::subs/client]) + (field [:span "Client" + [:span.has-text-danger " *"]] + [typeahead-entity {:matches @(re-frame/subscribe [::subs/clients]) + :match->text :name + :type "typeahead" + :field [:client] + :disabled true}])) + + + (field "Date" + [date-picker {:class-name "input" + :class "input" + :type "date" + :disabled true + :field [:date]}]) + (field "Total" + [money-field {:type "money" + :field [:total] + :disabled true}]) + (field "Tax" + [money-field {:type "money" + :field [:tax] + :disabled true}]) + (field "Tip" + [money-field {:type "money" + :field [:tip] + :disabled true}]) + + [:h1.subtitle.is-4 "Charges"] + [:ul + (for [charge (:charges data)] + [:li (:type-name charge) ": " (:total charge)])] + + [:h1.subtitle.is-4 "Line Items"] + [:ul + (for [line-item (:line-items data)] + [:li (:item-name line-item) ": " (:total line-item)])]]) + {:key (:id data)}))]) + diff --git a/src/cljs/auto_ap/views/pages/sales_orders/side_bar.cljs b/src/cljs/auto_ap/views/pages/sales_orders/side_bar.cljs new file mode 100644 index 00000000..a51303c8 --- /dev/null +++ b/src/cljs/auto_ap/views/pages/sales_orders/side_bar.cljs @@ -0,0 +1,31 @@ +(ns auto-ap.views.pages.sales-orders.side-bar + (:require [auto-ap.routes :as routes] + [auto-ap.subs :as subs] + [auto-ap.views.utils :refer [active-when dispatch-value-change]] + [auto-ap.views.components.vendor-filter :refer [vendor-filter]] + [auto-ap.views.components.date-range-filter :refer [date-range-filter]] + [auto-ap.views.components.number-filter :refer [number-filter]] + [auto-ap.views.components.bank-account-filter :refer [bank-account-filter]] + [auto-ap.views.components.typeahead :refer [typeahead-entity]] + [bidi.bidi :as bidi] + [re-frame.core :as re-frame] + [auto-ap.views.pages.data-page :as data-page])) + +(defn side-bar [{:keys [data-page]}] + (let [ap @(re-frame/subscribe [::subs/active-page]) + user @(re-frame/subscribe [::subs/user])] + [:div + [:div + + + [:p.menu-label "Date Range"] + [:div + [date-range-filter + {:on-change-event [::data-page/filter-changed data-page :date-range] + :value @(re-frame/subscribe [::data-page/filter data-page :date-range])}]] + + [:p.menu-label "Total"] + [:div + [number-filter + {:on-change-event [::data-page/filter-changed data-page :total-range] + :value @(re-frame/subscribe [::data-page/filter data-page :total-range])}]]]])) diff --git a/src/cljs/auto_ap/views/pages/sales_orders/table.cljs b/src/cljs/auto_ap/views/pages/sales_orders/table.cljs new file mode 100644 index 00000000..5241c67f --- /dev/null +++ b/src/cljs/auto_ap/views/pages/sales_orders/table.cljs @@ -0,0 +1,49 @@ +(ns auto-ap.views.pages.sales-orders.table + (:require [auto-ap.subs :as subs] + [auto-ap.views.components.buttons :as buttons] + [auto-ap.views.components.grid :as grid] + [auto-ap.views.pages.data-page :as data-page] + [auto-ap.views.pages.sales-orders.form :as form] + [auto-ap.views.utils :refer [date->str nf]] + [clojure.string :as str] + [re-frame.core :as re-frame])) + +(defn row [{sales-order :sales-order + selected-client :selected-client}] + (let [{:keys [client date total tax tip charges line-items]} sales-order] + [grid/row {:class (:class sales-order)} + (when-not selected-client + [grid/cell {} (:name client)]) + [grid/cell {} (date->str date) ] + [grid/cell {:class "has-text-right"} (nf total )] + [grid/cell {:class "has-text-right"} (nf tax )] + [grid/cell {:class "has-text-right"} (nf tip )] + [grid/cell {} (str/join ", " (map :type-name charges))] + [grid/cell {} (str/join ", " (map :item-name line-items))] +[grid/button-cell {} + [:div.buttons + [buttons/fa-icon {:event [::form/editing sales-order] :icon "fa-pencil"}]]]])) + +(defn table [{:keys [data-page]}] + (let [selected-client @(re-frame/subscribe [::subs/client]) + {:keys [data status]} @(re-frame/subscribe [::data-page/page data-page])] + [grid/grid {:data-page data-page + :column-count (if selected-client 7 8)} + [grid/controls data] + [grid/table {:fullwidth true} + [grid/header {} + [grid/row {} + (when-not selected-client + [grid/sortable-header-cell {:sort-key "client" :sort-name "Client"} "Client"]) + [grid/sortable-header-cell {:sort-key "date" :sort-name "Date" :style {:width "8em"}} "Date"] + [grid/sortable-header-cell {:sort-key "total" :sort-name "Total" :class "has-text-right" :style {:width "8em"}} "Total"] + [grid/sortable-header-cell {:sort-key "tax" :sort-name "Tax" :class "has-text-right" :style {:width "7em"}} "Tax"] + [grid/sortable-header-cell {:sort-key "tip" :sort-name "Tip" :class "has-text-right" :style {:width "7em"}} "Tip"] + [grid/header-cell {} "Payment Methods"] + [grid/header-cell {} "Line Items"] + [grid/header-cell {:style {:width "4em"}}]]] + [grid/body + (for [sales-order (:data data)] + ^{:key (:id sales-order)} + [row {:sales-order sales-order + :selected-client selected-client}])]]])) diff --git a/src/cljs/auto_ap/views/pages/transactions.cljs b/src/cljs/auto_ap/views/pages/transactions.cljs index 69e6cadc..34e4f958 100644 --- a/src/cljs/auto_ap/views/pages/transactions.cljs +++ b/src/cljs/auto_ap/views/pages/transactions.cljs @@ -1,81 +1,66 @@ (ns auto-ap.views.pages.transactions - (:require [auto-ap.events :as events] + (:require [auto-ap.effects.forward :as forward] [auto-ap.forms :as forms] [auto-ap.subs :as subs] - [bidi.bidi :as bidi] - [auto-ap.routes :as routes] - [vimsical.re-frame.cofx.inject :as inject] - [auto-ap.views.components.bank-account-filter :refer [bank-account-filter]] - [auto-ap.views.components.date-range-filter :refer [date-range-filter]] - [auto-ap.views.components.number-filter :refer [number-filter]] - [auto-ap.views.components.vendor-filter :refer [vendor-filter]] - [auto-ap.views.components.typeahead :refer [typeahead-entity]] - [auto-ap.views.components.layouts :refer [appearing-side-bar side-bar-layout]] - [auto-ap.views.components.modal :refer [action-modal]] - [auto-ap.views.components.paginator :refer [paginator]] - [auto-ap.views.components.sort-by-list :refer [sort-by-list]] - [auto-ap.views.components.sorter :refer [sorted-column]] - [auto-ap.views.pages.transactions.form :as edit] - [auto-ap.views.pages.transactions.table :as table] - [auto-ap.views.pages.transactions.common :refer [transaction-read]] [auto-ap.utils :refer [replace-by]] + [auto-ap.views.components.layouts + :refer + [appearing-side-bar side-bar-layout]] + [auto-ap.views.pages.data-page :as data-page] + [auto-ap.views.pages.transactions.common :refer [transaction-read]] + [auto-ap.views.pages.transactions.form :as edit] [auto-ap.views.pages.transactions.manual :as manual] - [auto-ap.views.utils :refer [bind-field date->str dispatch-event nf date-picker active-when with-user]] - [goog.string :as gstring] - [re-frame.core :as re-frame] [auto-ap.views.pages.transactions.side-bar :as side-bar] + [auto-ap.views.pages.transactions.table :as table] + [auto-ap.views.utils :refer [dispatch-event with-user]] + [clojure.set :as set] + [re-frame.core :as re-frame] [reagent.core :as reagent] - [auto-ap.status :as status] - [vimsical.re-frame.fx.track :as track])) + [vimsical.re-frame.fx.track :as track] + [auto-ap.status :as status])) - -(re-frame/reg-event-db - ::edit-completed - (fn [db [_ edit-transaction]] - (-> db - (update-in [::transaction-page :transactions] - replace-by :id (assoc edit-transaction :class "live-added"))))) - (re-frame/reg-event-fx ::manual-import-completed (fn [{:keys [db]} [_ {:keys [imported errors]}]] - {:dispatch [::params-change (::params db)] + {:dispatch [::params-change @(re-frame/subscribe [::data-page/params ::page])] :db (-> db (assoc-in [::notification :message] (str "Successfully imported " imported " transactions")) (assoc-in [::notification :errors] errors))})) -(re-frame/reg-sub - ::transaction-page - (fn [db] - (-> db ::transaction-page))) - -(re-frame/reg-sub - ::params - :<- [::subs/client] - :<- [::side-bar/filter-params] - :<- [::table/table-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)))) +(defn data-params->query-params [params] + {:start (:start params 0) + :sort (:sort params) + :client-id (:id @(re-frame/subscribe [::subs/client])) + :vendor-id (:id (:vendor params)) + :date-range (:date-range params) + :bank-account-id (:id (:bank-account params)) + :amount-gte (:amount-gte (:amount-range params)) + :amount-lte (:amount-lte (:amount-range params)) + :description (:description params) + :approval-status (condp = @(re-frame/subscribe [::subs/active-page]) + :transactions nil + :unapproved-transactions :unapproved + :requires-feedback-transactions :requires-feedback + :excluded-transactions :excluded + :approved-transactions :approved)}) (re-frame/reg-event-fx ::params-change [with-user] (fn [{:keys [user db ]} [_ params]] {:graphql {:token user - :owns-state {:single ::page} + :owns-state {:single [::data-page/page ::page]} :query-obj {:venia/queries [[:transaction_page - params + (data-params->query-params params) [[:transactions transaction-read] :total :start :end]]]} - :on-success [::received]} - :set-uri-params (dissoc params :status :client-id)})) + :on-success (fn [result] + [::data-page/received ::page (set/rename-keys (first (:transaction-page result)) + {:transactions :data})])}})) (re-frame/reg-event-fx ::unapprove-all @@ -84,38 +69,41 @@ {:db (-> (:db cofx) (assoc-in [:status :loading] true)) :graphql {:token (-> cofx :db :user) + :owns-state {:single ::unapprove-all} :query-obj {:venia/operation {:operation/type :mutation :operation/name "UnapproveTransactions"} :venia/queries [{:query/data [:unapprove-transactions - @(re-frame/subscribe [::params]) + (data-params->query-params params) [[:transactions transaction-read] :total :start :end]]}]} - :on-success [::received]}})) + :on-success (fn [result] + [::data-page/received ::page (set/rename-keys (first (:unapprove-transactions result)) + {:transactions :data})])}})) (re-frame/reg-event-fx ::unmounted (fn [{:keys [db]} _] - {:db (dissoc db ::table/table-params ::side-bar/filters ::side-bar/settled-filters ::transaction-page) - ::track/dispose {:id ::params}})) + {:dispatch [::data-page/dispose ::page] + ::track/dispose {:id ::params} + ::forward/dispose {:id ::updated}})) (re-frame/reg-event-fx ::mounted (fn [{:keys [db]} _] {::track/register {:id ::params - :subscription [::params] - :event-fn (fn [params] [::params-change params])}})) + :subscription [::data-page/params ::page] + :event-fn (fn [params] + [::params-change params])} + ::forward/register {:id ::updated + :events #{::edit/edited} + :event-fn (fn [[_ edited-transaction]] + [::data-page/updated-entity ::page edited-transaction])}})) + -(re-frame/reg-event-db - ::received - (fn [db [_ data]] - (-> db - (update ::transaction-page merge (or (first (:transaction-page data)) - (first (:unapprove-transactions data)))) - (assoc-in [:status :loading] false)))) (re-frame/reg-sub @@ -125,10 +113,11 @@ (defn content [] (let [notification (re-frame/subscribe [::notification]) - current-client @(re-frame/subscribe [::subs/client]) - user @(re-frame/subscribe [::subs/user])] + user @(re-frame/subscribe [::subs/user]) + params @(re-frame/subscribe [::data-page/params ::page])] [:div [:h1.title "Transactions"] + [status/status-notification {:statuses [[::status/single ::unapprove-all]]}] (when (= "admin" (:user/role user)) [:div (when (:message @notification) @@ -146,11 +135,12 @@ [:div.buttons [:button.button.is-outlined.is-primary {:on-click (dispatch-event [::manual/opening])} "Manual Yodlee Import"] - [:button.button.is-outlined.is-danger {:on-click (dispatch-event [::unapprove-all])} + [:button.button.is-outlined.is-danger {:on-click (dispatch-event [::unapprove-all params]) + :class (status/class-for @(re-frame/subscribe [::status/single ::unapprove-all])) + :disabled (status/disabled-for @(re-frame/subscribe [::status/single ::unapprove-all]))} "Unapprove all"]]]]) [table/table {:id :transactions - :transaction-page @(re-frame/subscribe [::transaction-page]) - :status @(re-frame/subscribe [::status/single ::page])}]])) + :data-page ::page}]])) (defn transactions-page [{:keys [approval-status]}] @@ -160,17 +150,14 @@ :component-did-mount #(re-frame/dispatch [::mounted]) :reagent-render (fn [] - (let [{transaction-bar-active? :active?} @(re-frame/subscribe [::forms/form ::edit/form]) - params @(re-frame/subscribe [::params]) ;; Keep to make sure it doens'nt get disposed - ap @(re-frame/subscribe [::subs/active-page]) - user (re-frame/subscribe [::subs/user])] + (let [{transaction-bar-active? :active?} @(re-frame/subscribe [::forms/form ::edit/form])] [side-bar-layout - {:side-bar [side-bar/side-bar] + {:side-bar [side-bar/side-bar {:data-page ::page}] :main [:div ^{:key approval-status} [content]] :bottom [:div [manual/modal {:import-completed [::manual-import-completed ]}]] :right-side-bar [appearing-side-bar {:visible? transaction-bar-active?} - [edit/form {:edit-completed [::edit-completed]}]]}]))})) + [edit/form]]}]))})) diff --git a/src/cljs/auto_ap/views/pages/transactions/form.cljs b/src/cljs/auto_ap/views/pages/transactions/form.cljs index 649b6f64..4f331abf 100644 --- a/src/cljs/auto_ap/views/pages/transactions/form.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/form.cljs @@ -81,18 +81,19 @@ (re-frame/reg-event-fx ::saving [with-user (forms/triggers-loading ::form) (forms/in-form ::form)] - (fn [{:keys [user]} [_ params]] + (fn [{:keys [user]} [_]] {:graphql {:token user :query-obj @(re-frame/subscribe [::submit-query]) - :on-success [::edited params] + :on-success (fn [result] + [::edited (:edit-transaction result)]) :on-error [::forms/save-error ::form]}})) (re-frame/reg-event-fx ::matching [with-user (forms/triggers-loading ::form) (forms/in-form ::form)] - (fn [{{{:keys [id]} :data} :db user :user} [_ params payment-id]] + (fn [{{{:keys [id]} :data} :db user :user} [_ payment-id]] {:graphql {:token user :query-obj {:venia/operation {:operation/type :mutation @@ -101,13 +102,14 @@ {:transaction_id id :payment-id payment-id} transaction-read]}]} - :on-success [::edited params] + :on-success (fn [result] + [::edited (:match-transaction result)]) :on-error [::forms/save-error ::form]}})) (re-frame/reg-event-fx ::matching-rule [with-user (forms/triggers-loading ::form) (forms/in-form ::form)] - (fn [{{{:keys [id]} :data} :db user :user} [_ params transaction-rule-id]] + (fn [{{{:keys [id]} :data} :db user :user} [_ transaction-rule-id]] {:graphql {:token user :query-obj {:venia/operation {:operation/type :mutation @@ -116,14 +118,16 @@ {:transaction-ids [id] :transaction-rule-id transaction-rule-id} transaction-read]}]} - :on-success [::edited params] + :on-success (fn [result] + [::edited (first (:match-transaction-rules result))]) :on-error [::forms/save-error ::form]}})) (re-frame/reg-event-fx ::edited [(forms/triggers-stop ::form)] - (fn [{:keys [db]} [_ {:keys [edit-completed]} {:keys [edit-transaction match-transaction match-transaction-rules]}]] - {:dispatch (conj edit-completed (or edit-transaction match-transaction (first match-transaction-rules)))})) + (fn [{:keys [db]} [_ transaction]] + {})) + (re-frame/reg-event-db ::manual-match @@ -158,7 +162,7 @@ [:tr [:td.no-border note] [:td.no-border - [:a.button.is-primary.is-small {:on-click (dispatch-event [::matching-rule params id])} + [:a.button.is-primary.is-small {:on-click (dispatch-event [::matching-rule id])} "Use this rule"]]])]]) @@ -177,11 +181,11 @@ [:td.no-border (:name vendor)] [:td.no-border (when check-number (str "Check " check-number " ")) memo] [:td.no-border - [:a.button.is-primary.is-small {:on-click (dispatch-event [::matching params id])} + [:a.button.is-primary.is-small {:on-click (dispatch-event [::matching id])} "Match"]]]))]]) -(defn form [{:keys [edit-completed]}] +(defn form [_] [layouts/side-bar {:on-close (dispatch-event [::forms/form-closing ::form])} (let [change-event [::forms/change ::form] {:keys [data] } @(re-frame/subscribe [::forms/form ::form]) @@ -190,7 +194,7 @@ is-admin? @(re-frame/subscribe [::subs/is-admin?]) should-disable-for-client? (and (not is-admin?) (not= :requires-feedback (:original-status data)))] - [form {:title "Transaction" :edit-completed edit-completed} + [form {:title "Transaction"} (when (and @(re-frame/subscribe [::subs/is-admin?]) (get-in data [:yodlee-merchant])) @@ -221,15 +225,13 @@ (not (:matched-rule data)) (not (:payment data)) is-admin?) - [potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data) - :edit-completed edit-completed}] + [potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data)}] (and (seq (:potential-payment-matches data)) (not (:payment data)) is-admin?) - [potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data) - :edit-completed edit-completed}] + [potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data)}] (and (not (seq (:potential-payment-matches data))) (not (seq (:potential-transaction-rule-matches data)))) diff --git a/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs b/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs index f2a3ea5b..114ae320 100644 --- a/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs @@ -7,90 +7,12 @@ [auto-ap.views.components.bank-account-filter :refer [bank-account-filter]] [auto-ap.views.components.typeahead :refer [typeahead-entity]] [bidi.bidi :as bidi] - [re-frame.core :as re-frame])) -(re-frame/reg-sub - ::specific-filters - (fn [db ] - (::filters db {}))) - -(re-frame/reg-sub - ::filters - :<- [::specific-filters] - :<- [::subs/vendors-by-id] - :<- [::subs/bank-accounts-by-id] - :<- [::subs/query-params] - (fn [[specific-filters vendors-by-id bank-accounts-by-id query-params] ] - - - (let [url-filters (-> query-params - (select-keys #{:vendor-id - :bank-account-id - :date-range - :description - :amount-gte - :amount-lte})) - url-filters {:vendor (when-let [vendor-id (:vendor-id url-filters)] - {:id (str vendor-id) - :name (get-in vendors-by-id [(str vendor-id) :name] "Loading...")}) - :bank-account (when-let [bank-account-id (:bank-account-id url-filters)] - {:id (str bank-account-id) - :name (get-in bank-accounts-by-id [(str bank-account-id) :name] "Loading...")}) - :amount-range {:amount-gte (:amount-gte url-filters) - :amount-lte (:amount-lte url-filters)} - :description (:description url-filters) - :date-range (:date-range url-filters)}] - (merge url-filters specific-filters )))) - -(re-frame/reg-sub - ::filter - :<- [::filters] - (fn [filters [_ which]] - (filters which))) - -(re-frame/reg-sub - ::settled-filters - (fn [db ] - (::settled-filters db))) - -(re-frame/reg-sub - ::filter-params - :<- [::settled-filters] - :<- [::filters] - :<- [::subs/active-page] - (fn [[settled-filters filters ap]] - (let [filters (or settled-filters filters)] - {:vendor-id (:id (:vendor filters)) - :date-range (:date-range filters) - :bank-account-id (:id (:bank-account filters)) - :amount-gte (:amount-gte (:amount-range filters)) - :amount-lte (:amount-lte (:amount-range filters)) - :description (:description filters) - :approval-status (condp = ap - :transactions nil - :unapproved-transactions :unapproved - :requires-feedback-transactions :requires-feedback - :excluded-transactions :excluded - :approved-transactions :approved)}))) -(re-frame/reg-event-fx - ::filters-settled - (fn [{:keys [db]} [_ & params]] - {:db (assoc db ::settled-filters @(re-frame/subscribe [::filters]))})) + [re-frame.core :as re-frame] + [auto-ap.views.pages.data-page :as data-page])) -(re-frame/reg-event-fx - ::filter-changed - (fn [{:keys [db]} [_ & params]] - (let [[a b c] params - [which val] (if (= 3 (count params)) - [(into [a] b) c] - [[a] b])] - {:db (assoc-in db (into [::filters] which) val) - :dispatch-debounce - {:event [::filters-settled] - :time 800 - :key ::filters}}))) -(defn side-bar [] +(defn side-bar [{:keys [data-page]}] (let [ap @(re-frame/subscribe [::subs/active-page]) user @(re-frame/subscribe [::subs/user])] [:div @@ -131,34 +53,35 @@ [:p.menu-label "Bank Account"] [:div [bank-account-filter - {:on-change-event [::filter-changed :bank-account] - :value @(re-frame/subscribe [::filter :bank-account]) + {:on-change-event [::data-page/filter-changed data-page :bank-account] + :value @(re-frame/subscribe [::data-page/filter data-page :bank-account]) :bank-accounts @(re-frame/subscribe [::subs/bank-accounts])}]] [:p.menu-label "Date Range"] [:div [date-range-filter - {:on-change-event [::filter-changed :date-range] - :value @(re-frame/subscribe [::filter :date-range])}]] + {:on-change-event [::data-page/filter-changed data-page :date-range] + :value @(re-frame/subscribe [::data-page/filter data-page :date-range])}]] - [:p.menu-label "Amount"] + [:p.menu-label "Amount"] [:div [number-filter - {:on-change-event [::filter-changed :amount-range] - :value @(re-frame/subscribe [::filter :amount-range])}]] + {:on-change-event [::data-page/filter-changed data-page :amount-range] + :value @(re-frame/subscribe [::data-page/filter data-page :amount-range])}]] [:p.menu-label "Vendor"] [:div [typeahead-entity {:matches @(re-frame/subscribe [::subs/vendors]) - :on-change #(re-frame/dispatch [::filter-changed :vendor %]) + :on-change #(re-frame/dispatch [::data-page/filter-changed data-page :vendor %]) + :include-keys [:name :id] :match->text :name :type "typeahead-entity" - :value @(re-frame/subscribe [::filter :vendor])}]] + :value @(re-frame/subscribe [::data-page/filter data-page :vendor])}]] [:p.menu-label "Description"] [:div [:div.field [:div.control [:input.input {:placeholder "CHECK 123 ABC" - :value @(re-frame/subscribe [::filter :description]) - :on-change (dispatch-value-change [::filter-changed :description])} ]]]]])) + :value @(re-frame/subscribe [::data-page/filter data-page :description]) + :on-change (dispatch-value-change [::data-page/filter-changed data-page :description])} ]]]]])) diff --git a/src/cljs/auto_ap/views/pages/transactions/table.cljs b/src/cljs/auto_ap/views/pages/transactions/table.cljs index d412f9b4..4bdff1c1 100644 --- a/src/cljs/auto_ap/views/pages/transactions/table.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/table.cljs @@ -12,7 +12,8 @@ [goog.string :as gstring] [re-frame.core :as re-frame] [auto-ap.views.components.buttons :as buttons] - [auto-ap.status :as status])) + [auto-ap.status :as status] + [auto-ap.views.pages.data-page :as data-page])) (re-frame/reg-event-fx ::editing-matches-found @@ -54,15 +55,13 @@ (fn [{table-params :db} [_ params :as z]] {:db (merge table-params params)})) -(defn table [{:keys [id transaction-page status]}] +(defn table [{:keys [id data-page ]}] (let [selected-client @(re-frame/subscribe [::subs/client]) + {:keys [data status]} @(re-frame/subscribe [::data-page/page data-page]) states @(re-frame/subscribe [::status/multi ::edits])] - [grid/grid {:on-params-change (fn [e] - (re-frame/dispatch [::params-changed e])) - :params @(re-frame/subscribe [::table-params]) - :status status + [grid/grid {:data-page data-page :column-count (if selected-client 6 7)} - [grid/controls transaction-page] + [grid/controls data] [grid/table {:fullwidth true} [grid/header {} [grid/row {} @@ -75,7 +74,7 @@ [grid/sortable-header-cell {:sort-key "status" :sort-name "Status" :style {:width "7em"}} "Status"] [grid/header-cell {:style {:width (action-cell-width 3)}}]]] [grid/body - (for [{:keys [client account vendor approval-status payment status bank-account description-original date amount id yodlee-merchant ] :as i} (:transactions transaction-page)] + (for [{:keys [client account vendor approval-status payment status bank-account description-original date amount id yodlee-merchant ] :as i} (:data data)] ^{:key id} [grid/row {:class (:class i)} (when-not selected-client