User can set up sales queries on their own now.

This commit is contained in:
2022-04-07 08:22:50 -07:00
parent a4c823c9ff
commit 45cf97a480
7 changed files with 288 additions and 224 deletions

View File

@@ -12,6 +12,7 @@
[auto-ap.datomic.migrate.invoice-converter
:refer [add-import-status-existing-invoices]]
[auto-ap.datomic.migrate.ledger :as ledger]
[auto-ap.datomic.migrate.queries :as queries]
[auto-ap.datomic.migrate.plaid :as plaid]
[auto-ap.datomic.migrate.sales :as sales]
[auto-ap.datomic.migrate.vendors :as vendors]
@@ -501,7 +502,8 @@
reports/norms-map
plaid/norms-map
audit/norms-map
vendors/norms-map)]
vendors/norms-map
queries/norms-map)]
(println "Conforming database...")
(c/ensure-conforms conn norms-map)
#_(when (not (seq args))

View File

@@ -4,16 +4,17 @@
[auto-ap.graphql.utils :refer [->graphql assert-admin can-see-client? is-admin?]]
[auto-ap.utils :refer [by]]
[auto-ap.yodlee.core :refer [in-memory-cache]]
[auto-ap.routes.queries :as q]
[auto-ap.square.core :as square]
[com.walmartlabs.lacinia.util :refer [attach-resolvers]]
[clj-time.coerce :as coerce]
[config.core :refer [env]]
[clojure.string :as str]
[unilog.context :as lc]
[clojure.tools.logging :as log]
[datomic.api :as d]
[clojure.java.io :as io]
[amazonica.aws.s3 :as s3]
#_{:clj-kondo/ignore [:unused-namespace]}
[yang.scheduler :as scheduler]
[mount.core :as mount])
(:import [org.apache.commons.codec.binary Base64]
@@ -51,7 +52,7 @@
[?ba :bank-account/plaid-account ?src]
[?ba :bank-account/yodlee-account-id ?src])]
new-db [:client/code client-code])
(filter (fn [[src cnt]]
(filter (fn [[_ cnt]]
(> cnt 1)))))
(throw (ex-info "Cannot reuse yodlee/plaid/intuit account" {:validation-error (str "Cannot reuse yodlee/plaid/intuit account")})))))
@@ -183,11 +184,11 @@
[(get-else $ ?e :journal-entry-line/credit 0.0) ?credit]]}
:args [(d/db conn) bank-account-id]})
debits (->> all-transactions
(map (fn [[e debit credit]]
(map (fn [[_ debit _]]
debit))
(reduce + 0.0))
credits (->> all-transactions
(map (fn [[e debit credit]]
(map (fn [[_ _ credit]]
credit))
(reduce + 0.0))
current-balance (if (= :bank-account-type/check (:bank-account/type (d/entity (d/db conn) bank-account-id)))
@@ -255,7 +256,7 @@
:start (scheduler/every (* 17 60 1000) refresh-current-balance)
:stop (scheduler/stop current-balance-worker))
(defn get-client [context args value]
(defn get-client [context _ _]
(->graphql
(->> (d-clients/get-all)
(filter #(can-see-client? (:id context) %))
@@ -276,6 +277,140 @@
(def sales-summary-query
"[:find ?d4 (sum ?total) (sum ?tax) (sum ?tip) (sum ?service-charge) (sum ?discount) (sum ?returns)
:with ?s
:in $
:where
[?s :sales-order/client [:client/code \"%s\"]]
[?s :sales-order/date ?d]
[?s :sales-order/total ?total]
[?s :sales-order/tax ?tax]
[?s :sales-order/tip ?tip]
[?s :sales-order/service-charge ?service-charge]
[?s :sales-order/returns ?returns]
[?s :sales-order/discount ?discount]
[(clj-time.coerce/to-date-time ?d) ?d2]
[(auto-ap.time/localize ?d2) ?d3]
[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4]
]")
(def sales-category-query
"[:find ?d4 ?n ?n2 (sum ?total) (sum ?tax) (sum ?discount)
:with ?s ?li
:in $
:where
[?s :sales-order/client [:client/code \"%s\"]]
[?s :sales-order/date ?d]
[?s :sales-order/line-items ?li]
[?li :order-line-item/category ?n]
[(get-else $ ?li :order-line-item/item-name \"\") ?n2]
[?li :order-line-item/total ?total]
[?li :order-line-item/tax ?tax]
[?li :order-line-item/discount ?discount]
[(clj-time.coerce/to-date-time ?d) ?d2]
[(auto-ap.time/localize ?d2) ?d3]
[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4]
]")
(def expected-deposits-query
"[:find ?d4 ?t ?f
:in $
:where
[?c :client/code \"%s\"]
[?s :expected-deposit/client ?c]
[?s :expected-deposit/total ?t]
[?s :expected-deposit/fee ?f]
[?s :expected-deposit/sales-date ?date]
[(clj-time.coerce/to-date-time ?date) ?d2]
[(auto-ap.time/localize ?d2) ?d3]
[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4]
]")
(def tenders-query
"[:find ?d4 ?type ?p2 (sum ?total) (sum ?tip)
:with ?charge
:in $
:where
[?c :client/code \"%s\"]
[?s :sales-order/client ?c]
[?s :sales-order/charges ?charge]
[?charge :charge/type-name ?type]
[?charge :charge/total ?total]
[?charge :charge/tip ?tip]
[(get-else $ ?charge :charge/processor :na) ?ccp]
[(get-else $ ?ccp :db/ident :na) ?p]
[(name ?p) ?p2]
[?s :sales-order/date ?date]
[(clj-time.coerce/to-date-time ?date) ?d2]
[(auto-ap.time/localize ?d2) ?d3]
[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4]
]")
(def refunds-query
"[:find ?d4 ?t (sum ?total) (sum ?fee)
:with ?r
:in $
:where
[?r :sales-refund/client [:client/code \"%s\"]]
[?r :sales-refund/total ?total]
[?r :sales-refund/fee ?fee]
[?r :sales-refund/date ?date]
[?r :sales-refund/type ?t]
[(clj-time.coerce/to-date-time ?date) ?d2]
[(auto-ap.time/localize ?d2) ?d3]
[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4]]")
(defn setup-sales-queries [context args _]
(assert-admin (:id context))
(let [{client-code :client/code} (d/pull (d/db conn) '[:client/code] (:client_id args))]
(q/put-query (str (UUID/randomUUID))
(format sales-summary-query client-code)
(str "sales query for " client-code)
(str client-code "-sales-summary")
[:client/code client-code]
)
(q/put-query (str (UUID/randomUUID))
(format sales-category-query client-code)
(str "sales category query for " client-code)
(str client-code "-sales-category")
[:client/code client-code]
)
(q/put-query (str (UUID/randomUUID))
(format expected-deposits-query client-code)
(str "expected deposit query for " client-code)
(str client-code "-expected-deposit")
[:client/code client-code]
)
(q/put-query (str (UUID/randomUUID))
(format tenders-query client-code)
(str "tender query for " client-code)
(str client-code "-tender")
[:client/code client-code]
)
(q/put-query (str (UUID/randomUUID))
(format refunds-query client-code)
(str "refunds query for " client-code)
(str client-code "-refund")
[:client/code client-code])
(let [sales-summary-id (:saved-query/guid (d/pull (d/db auto-ap.datomic/conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-sales-summary")]))
sales-category-id (:saved-query/guid (d/pull (d/db auto-ap.datomic/conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-sales-category")]))
expected-deposit-id (:saved-query/guid (d/pull (d/db auto-ap.datomic/conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-expected-deposit")]))
tender-id (:saved-query/guid (d/pull (d/db auto-ap.datomic/conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-tender")]))
refund-id (:saved-query/guid (d/pull (d/db auto-ap.datomic/conn) [:saved-query/guid] [:saved-query/lookup-key (str client-code "-refund")]))]
{:message (str/join "\n"
[
(str "For " client-code ":")
(str "Sales: " "https://app.integreatconsult.com/api/queries/" sales-summary-id "/results/json")
(str "Sales Category: " "https://app.integreatconsult.com/api/queries/" sales-category-id "/results/json")
(str "Expected Deposits: " "https://app.integreatconsult.com/api/queries/" expected-deposit-id "/results/json")
(str "Tenders: " "https://app.integreatconsult.com/api/queries/" tender-id "/results/json")
(str "Refund: " "https://app.integreatconsult.com/api/queries/" refund-id "/results/json")])})))
(def objects
{:location_match
@@ -353,7 +488,10 @@
(def mutations
{:edit_client {:type :client
:args {:edit_client {:type :edit_client}}
:resolve :mutation/edit-client}})
:resolve :mutation/edit-client}
:setup_sales_queries {:type :message
:args {:client_id {:type :id}}
:resolve :mutation/setup-sales-queries}})
(def input-objects
{:edit_location_match {:fields {:location {:type 'String}
@@ -418,7 +556,8 @@
(def resolvers
{:get-client get-client
:mutation/edit-client edit-client})
:mutation/edit-client edit-client
:mutation/setup-sales-queries setup-sales-queries})
(defn attach [schema]

View File

@@ -40,13 +40,12 @@
:user/name (:name profile)})
]
(log/info "authenticated as user" user)
;; TODO - these namespaces are not being transmitted/deserialized properly
(if (and token user)
(let [jwt (jwt/sign {:user (:name profile)
:exp (time/plus (time/now) (time/days 30))
:user/clients (map (fn [c]
(dissoc c :client/bank-accounts :client/location-matches :client/forecasted-transactions :client/matches :client/week-a-debits :client/week-a-credits :client/week-b-debits :client/week-b-credits :client/signature-file :client/address :client/emails :client/square-auth-token :client/square-locations))
(select-keys c [:client/code :db/id :client/name :client/locations]))
(:user/clients user))
:user/role (name (:user/role user))
:user/name (:name profile)}

View File

@@ -41,17 +41,27 @@
(into [(d/db conn)] (clojure.edn/read-string (get query-params "args" "[]")))))))))
(defn put-query [id body note]
(s3/put-object :bucket-name (:data-bucket env)
:key (str "queries/" id)
:input-stream (io/make-input-stream (.getBytes body) {})
:metadata {:content-type "application/text"
:user-metadata {:note note}})
{:body {:query body
:id id
:results-url (str "/api/queries/" id "/results")
:csv-results-url (str "/api/queries/" id "/results/csv")
:json-results-url (str "/api/queries/" id "/results/json")}})
(defn put-query [guid body note & [lookup-key client]]
(let [guid (if lookup-key
(or (:saved-query/guid (d/pull (d/db conn) [:saved-query/guid] [:saved-query/lookup-key lookup-key]))
guid)
guid)]
@(d/transact conn [(cond->
{:saved-query/guid guid
:saved-query/description note
:saved-query/key (str "queries/" guid)}
client (assoc :saved-query/client client)
lookup-key (assoc :saved-query/lookup-key lookup-key))])
(s3/put-object :bucket-name (:data-bucket env)
:key (str "queries/" guid)
:input-stream (io/make-input-stream (.getBytes body) {})
:metadata {:content-type "application/text"
:user-metadata {:note note}})
{:body {:query body
:id guid
:results-url (str "/api/queries/" guid "/results")
:csv-results-url (str "/api/queries/" guid "/results/csv")
:json-results-url (str "/api/queries/" guid "/results/json")}}))
(def json-routes
(context "/queries" []

View File

@@ -24,50 +24,68 @@
(defn auth-header
([cob-session] (str "Bearer " cob-session)))
(defn retry-thrice
([x] (retry-thrice x 0))
([x i]
(try
(x)
(catch Exception e
(if (>= i 3)
(throw e)
(do
(Thread/sleep 5000)
(retry-thrice x (inc i))))))))
(def other-config
(if (:yodlee2-proxy-host env)
{:proxy-host (:yodlee2-proxy-host env)
:proxy-port (:yodlee2-proxy-port env)
:retry-handler (fn [ex try-count http-context]
:socket-timeout 60000
:connection-timeout 60000
:retry-handler (fn [ex _ _]
(log/error "yodlee Error." ex)
false)}
{:retry-handler (fn [ex try-count http-context]
{:retry-handler (fn [ex _ _]
(log/error "yodlee Error." ex)
false)}))
false)
:socket-timeout 60000
:connection-timeout 60000}))
(def base-headers {"Api-Version" "1.1"
"Content-Type" "application/json"})
(defn login-cobrand []
(-> (str (:yodlee2-base-url env) "/auth/token")
(client/post (merge {:headers (assoc base-headers
"loginName" (:yodlee2-admin-user env)
"Content-Type" "application/x-www-form-urlencoded")
:body (str "clientId=" (:yodlee2-client-id env) " &secret=" (:yodlee2-client-secret env))
:as :json}
other-config)
)
:body
:token
:accessToken))
(retry-thrice
(fn []
(-> (str (:yodlee2-base-url env) "/auth/token")
(client/post (merge {:headers (assoc base-headers
"loginName" (:yodlee2-admin-user env)
"Content-Type" "application/x-www-form-urlencoded")
:body (str "clientId=" (:yodlee2-client-id env) " &secret=" (:yodlee2-client-secret env))
:as :json}
other-config)
)
:body
:token
:accessToken))))
(defn login-user [client-code]
(log/info "logging in as " client-code)
(-> (str (:yodlee2-base-url env) "/auth/token")
(client/post (merge {:headers (assoc base-headers
"loginName" (if (<= (count client-code) 3)
(str client-code client-code)
client-code)
"Content-Type" "application/x-www-form-urlencoded")
:body (str "clientId=" (:yodlee2-client-id env) " &secret=" (:yodlee2-client-secret env))
:as :json}
other-config)
)
:body
:token
:accessToken))
(retry-thrice
(fn []
(log/info "logging in as " client-code)
(-> (str (:yodlee2-base-url env) "/auth/token")
(client/post (merge {:headers (assoc base-headers
"loginName" (if (<= (count client-code) 3)
(str client-code client-code)
client-code)
"Content-Type" "application/x-www-form-urlencoded")
:body (str "clientId=" (:yodlee2-client-id env) " &secret=" (:yodlee2-client-secret env))
:as :json}
other-config)
)
:body
:token
:accessToken))))
(defn get-accounts [client-code ]
(let [cob-session (login-user (client-code->login client-code))]
@@ -93,14 +111,16 @@
[])))
(defn get-provider-accounts [client-code ]
(log/info "logging in user " client-code)
(let [cob-session (login-user (client-code->login client-code))]
(-> (str (:yodlee2-base-url env) "/providerAccounts")
(-> (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session )})
:as :json}
other-config))
:body
:providerAccount))))
(retry-thrice
(fn []
(log/info "logging in user " client-code)
(let [cob-session (login-user (client-code->login client-code))]
(-> (str (:yodlee2-base-url env) "/providerAccounts")
(-> (client/get (merge {:headers (merge base-headers {"Authorization" (auth-header cob-session )})
:as :json}
other-config))
:body
:providerAccount))))))
@@ -323,7 +343,6 @@
(defn reauthenticate [client-code pa data]
(println client-code)
(try
(-> (str (:yodlee2-base-url env) "/providerAccounts?providerAccountIds=" pa)

View File

@@ -15,7 +15,7 @@
[clj-time.core :as t]
[clojure.java.io :as io]
[clojure.string :as str]
[auto-ap.routes.queries :as q]
[amazonica.aws.s3 :as s3])
(:import [org.apache.commons.io.input BOMInputStream]
java.util.UUID))
@@ -477,139 +477,6 @@
(println "failed " e)))
(async/<!! (async/into [] output-chan)))))
(def sales-summary-query
"[:find ?d4 (sum ?total) (sum ?tax) (sum ?tip) (sum ?service-charge) (sum ?discount) (sum ?returns)
:with ?s
:in $
:where
[?s :sales-order/client [:client/code \"%s\"]]
[?s :sales-order/date ?d]
[?s :sales-order/total ?total]
[?s :sales-order/tax ?tax]
[?s :sales-order/tip ?tip]
[?s :sales-order/service-charge ?service-charge]
[?s :sales-order/returns ?returns]
[?s :sales-order/discount ?discount]
[(clj-time.coerce/to-date-time ?d) ?d2]
[(auto-ap.time/localize ?d2) ?d3]
[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4]
]")
(def sales-category-query
"[:find ?d4 ?n ?n2 (sum ?total) (sum ?tax) (sum ?discount)
:with ?s ?li
:in $
:where
[?s :sales-order/client [:client/code \"%s\"]]
[?s :sales-order/date ?d]
[?s :sales-order/line-items ?li]
[?li :order-line-item/category ?n]
[(get-else $ ?li :order-line-item/item-name \"\") ?n2]
[?li :order-line-item/total ?total]
[?li :order-line-item/tax ?tax]
[?li :order-line-item/discount ?discount]
[(clj-time.coerce/to-date-time ?d) ?d2]
[(auto-ap.time/localize ?d2) ?d3]
[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4]
]")
(def expected-deposits-query
"[:find ?d4 ?t ?f
:in $
:where
[?c :client/code \"%s\"]
[?s :expected-deposit/client ?c]
[?s :expected-deposit/total ?t]
[?s :expected-deposit/fee ?f]
[?s :expected-deposit/sales-date ?date]
[(clj-time.coerce/to-date-time ?date) ?d2]
[(auto-ap.time/localize ?d2) ?d3]
[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4]
]")
(def tenders-query
"[:find ?d4 ?type ?p2 (sum ?total) (sum ?tip)
:with ?charge
:in $
:where
[?c :client/code \"%s\"]
[?s :sales-order/client ?c]
[?s :sales-order/charges ?charge]
[?charge :charge/type-name ?type]
[?charge :charge/total ?total]
[?charge :charge/tip ?tip]
[(get-else $ ?charge :charge/processor :na) ?ccp]
[(get-else $ ?ccp :db/ident :na) ?p]
[(name ?p) ?p2]
[?s :sales-order/date ?date]
[(clj-time.coerce/to-date-time ?date) ?d2]
[(auto-ap.time/localize ?d2) ?d3]
[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4]
]")
(def refunds-query
"[:find ?d4 ?t (sum ?total) (sum ?fee)
:with ?r
:in $
:where
[?r :sales-refund/client [:client/code \"%s\"]]
[?r :sales-refund/total ?total]
[?r :sales-refund/fee ?fee]
[?r :sales-refund/date ?date]
[?r :sales-refund/type ?t]
[(clj-time.coerce/to-date-time ?date) ?d2]
[(auto-ap.time/localize ?d2) ?d3]
[(auto-ap.time/unparse-local ?d3 auto-ap.time/normal-date) ?d4]]")
(defn setup-sales-queries [client-code]
(let [sales-summary-id (or (first (find-queries [client-code "service-charge"]))
(str (UUID/randomUUID)))
sales-category-id (or (first (find-queries [client-code "item-name"]))
(str (UUID/randomUUID)))
expected-deposit-id (or (first (find-queries [client-code "expected-deposit"]))
(str (UUID/randomUUID)))
tender-id (or (first (find-queries [client-code ":charge"]))
(str (UUID/randomUUID)))
refund-id (or (first (find-queries [client-code "sales-refund"]))
(str (UUID/randomUUID)))]
(map (comp :json-results-url :body)
[(q/put-query sales-summary-id
(format sales-summary-query client-code)
(str "sales query for " client-code)
)
(q/put-query sales-category-id
(format sales-category-query client-code)
(str "sales category query for " client-code)
)
(q/put-query expected-deposit-id
(format expected-deposits-query client-code)
(str "expected deposit query for " client-code)
)
(q/put-query tender-id
(format tenders-query client-code)
(str "tender query for " client-code))
(q/put-query refund-id
(format refunds-query client-code)
(str "refunds query for " client-code)
)])))
(defn print-sales-queries [client-code]
(let [sales-summary-id (first (find-queries [client-code "service-charge"]))
sales-category-id (first (find-queries [client-code "item-name"]))
expected-deposit-id (first (find-queries [client-code "expected-deposit"]))
tender-id (first (find-queries [client-code ":charge"]))
refund-id (first (find-queries [client-code "sales-refund"]))]
(println "For" client-code ":")
(println "Sales: " (str "https://app.integreatconsult.com/api/queries/" sales-summary-id "/results/json"))
(println "Sales Category: " (str "https://app.integreatconsult.com/api/queries/" sales-category-id "/results/json"))
(println "Expected Deposits: " (str "https://app.integreatconsult.com/api/queries/" expected-deposit-id "/results/json"))
(println "Tenders: " (str "https://app.integreatconsult.com/api/queries/" tender-id "/results/json"))
(println "Refund: " (str "https://app.integreatconsult.com/api/queries/" refund-id "/results/json"))
(println "")))
(defn historical-load-sales [client-code days]
(let [client (d/pull (d/db auto-ap.datomic/conn)

View File

@@ -3,9 +3,11 @@
[clojure.string :as str]
[re-frame.core :as re-frame]
[auto-ap.views.pages.admin.clients.form :as form]
[auto-ap.views.utils :refer [action-cell-width date->str]]
[auto-ap.views.utils :refer [action-cell-width date->str with-user]]
[auto-ap.views.components.grid :as grid]
[auto-ap.views.components.buttons :as buttons]))
[auto-ap.views.components.modal :as modal]
[auto-ap.views.components.buttons :as buttons]
[auto-ap.status :as status]))
(re-frame/reg-sub
::specific-params
@@ -17,6 +19,28 @@
(fn [{:keys [db]} [_ p]]
{:db (assoc db ::params p)}))
(re-frame/reg-event-fx
::sales-queries-setup
(fn [_ [_ results]]
{:dispatch [::modal/modal-requested {:title "Sales Queries"
:body [:div [:pre (:message (:setup-sales-queries results))]]}]}))
(re-frame/reg-event-fx
::setup-sales-queries
[with-user]
(fn [{:keys [user]} [_ client-id]]
{:graphql
{:token user
:owns-state {:multi ::setup-sales-queries
:which client-id}
:query-obj {:venia/operation {:operation/type :mutation
:operation/name "SetupSalesQueries"}
:venia/queries [{:query/data [:setup-sales-queries
{:client-id client-id}
[:message]]}]}
:on-success [::sales-queries-setup]}}
))
(re-frame/reg-sub
::params
:<- [::specific-params]
@@ -26,33 +50,37 @@
(defn clients-table [{:keys [page status]}]
[grid/grid {:on-params-change (fn [p]
(re-frame/dispatch [::params-changed p]))
:status status
:params @(re-frame/subscribe [::params])
:column-count 5}
[grid/controls page]
[grid/table {:fullwidth true}
[grid/header
[grid/row {}
[grid/header-cell {} "Name"]
[grid/header-cell {:style {:width "20em"}} "Code"]
[grid/header-cell {} "Locations"]
[grid/header-cell {} "Locked Until"]
[grid/header-cell {} "Email"]
[grid/header-cell {:style {:width (action-cell-width 1)}}]]
]
[grid/body
(for [{:keys [id name email locked-until code locations] :as c} (:data page)]
^{:key (str name "-" id )}
[grid/row {:id id}
[grid/cell {} name]
[grid/cell {} code]
[grid/cell {} (str/join ", " locations)]
[grid/cell {} [:div.tag (or (some-> locked-until date->str)
"Not locked"
(let [states @(re-frame/subscribe [::status/multi ::setup-sales-queries])]
[grid/grid {:on-params-change (fn [p]
(re-frame/dispatch [::params-changed p]))
:status status
:params @(re-frame/subscribe [::params])
:column-count 5}
[grid/controls page]
[grid/table {:fullwidth true}
[grid/header
[grid/row {}
[grid/header-cell {} "Name"]
[grid/header-cell {:style {:width "20em"}} "Code"]
[grid/header-cell {} "Locations"]
[grid/header-cell {} "Locked Until"]
[grid/header-cell {} "Email"]
[grid/header-cell {:style {:width (action-cell-width 2)}}]]
]
[grid/body
(for [{:keys [id name email locked-until code locations]} (:data page)]
^{:key (str name "-" id )}
[grid/row {:id id}
[grid/cell {} name]
[grid/cell {} code]
[grid/cell {} (str/join ", " locations)]
[grid/cell {} [:div.tag (or (some-> locked-until date->str)
"Not locked"
)]]
[grid/cell {} email]
[grid/cell {} [buttons/fa-icon {:event [::form/editing id]
:icon :fa-pencil}]]])]]])
)]]
[grid/cell {} email]
[grid/cell {} [:div.buttons [buttons/fa-icon {:event [::setup-sales-queries id]
:class (status/class-for (get states id))
:icon :fa-dollar}]
[buttons/fa-icon {:event [::form/editing id]
:icon :fa-pencil}]]]])]]]))