From d9fec54062405380a6424092a518218cd55a2895 Mon Sep 17 00:00:00 2001 From: Bryce Date: Thu, 12 Oct 2023 21:55:37 -0700 Subject: [PATCH] Migrates user page to SSR --- api-key.txt | 1 + resources/input.css | 19 + resources/public/output.css | 84 +++++ resources/schema.edn | 4 + scratch-sessions/setup-user-last-login.repl | 14 + src/clj/auto_ap/datomic/users.clj | 3 +- src/clj/auto_ap/ssr/company.clj | 26 +- src/clj/auto_ap/ssr/components.clj | 1 + src/clj/auto_ap/ssr/components/inputs.clj | 39 ++- src/clj/auto_ap/ssr/core.clj | 29 +- src/clj/auto_ap/ssr/grid_page_helper.clj | 1 - src/clj/auto_ap/ssr/pos/sales_orders.clj | 7 +- src/clj/auto_ap/ssr/ui.clj | 8 +- src/clj/auto_ap/ssr/users.clj | 331 ++++++++++++++++++ src/cljc/auto_ap/ssr_routes.cljc | 6 + .../views/pages/admin/users/table.cljs | 2 - 16 files changed, 542 insertions(+), 33 deletions(-) create mode 100644 api-key.txt create mode 100644 scratch-sessions/setup-user-last-login.repl create mode 100644 src/clj/auto_ap/ssr/users.clj diff --git a/api-key.txt b/api-key.txt new file mode 100644 index 00000000..af08b0d9 --- /dev/null +++ b/api-key.txt @@ -0,0 +1 @@ +eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyIjoiQVBJIiwiZXhwIjoxNzgyNjYxMjQxLCJ1c2VyL3JvbGUiOiJhZG1pbiIsInVzZXIvbmFtZSI6IkFQSSJ9.Zt5kWBplVFSWaQ7GWud85s7B5ok8vEqIcww1AVZNmM26Y0O8m1-P_er9Mz1SLTX8sdBu4qFxqXk-9_ImS3Pl5A diff --git a/resources/input.css b/resources/input.css index 8653d69d..675234f2 100644 --- a/resources/input.css +++ b/resources/input.css @@ -76,3 +76,22 @@ .min-h-content { min-height: calc(100vh - 4em); } + + +.select2 { + @apply text-xs !important; +} +.select2-dropdown { + @apply p-2 mb-6 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 text-xs !important; +} + +.select2-selection__choice { + @apply bg-primary-100 dark:bg-primary-700 text-gray-900 dark:text-gray-200 !important; + +} +.select2-search { + @apply h-4 !important; +} +.select2-selection { + @apply py-2.5 !important; +} diff --git a/resources/public/output.css b/resources/public/output.css index e1644126..e225b687 100644 --- a/resources/public/output.css +++ b/resources/public/output.css @@ -1352,6 +1352,10 @@ input:checked + .toggle-bg { width: 0.875rem; } +.w-36 { + width: 9rem; +} + .w-4 { width: 1rem; } @@ -1546,6 +1550,10 @@ input:checked + .toggle-bg { flex-wrap: wrap; } +.place-items-center { + place-items: center; +} + .items-start { align-items: flex-start; } @@ -2476,6 +2484,82 @@ input:checked + .toggle-bg { min-height: calc(100vh - 4em); } +.select2 { + font-size: 0.75rem !important; + line-height: 1rem !important; +} + +.select2-dropdown { + margin-bottom: 1.5rem !important; + border-radius: 0.5rem !important; + border-width: 1px !important; + --tw-border-opacity: 1 !important; + border-color: rgb(209 213 219 / var(--tw-border-opacity)) !important; + --tw-bg-opacity: 1 !important; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)) !important; + padding: 0.5rem !important; + font-size: 0.75rem !important; + line-height: 1rem !important; + --tw-text-opacity: 1 !important; + color: rgb(17 24 39 / var(--tw-text-opacity)) !important; +} + +.select2-dropdown:focus { + --tw-border-opacity: 1 !important; + border-color: rgb(0 156 234 / var(--tw-border-opacity)) !important; + --tw-ring-opacity: 1 !important; + --tw-ring-color: rgb(0 156 234 / var(--tw-ring-opacity)) !important; +} + +:is(.dark .select2-dropdown) { + --tw-border-opacity: 1 !important; + border-color: rgb(75 85 99 / var(--tw-border-opacity)) !important; + --tw-bg-opacity: 1 !important; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)) !important; + --tw-text-opacity: 1 !important; + color: rgb(255 255 255 / var(--tw-text-opacity)) !important; +} + +:is(.dark .select2-dropdown)::-moz-placeholder { + --tw-placeholder-opacity: 1 !important; + color: rgb(156 163 175 / var(--tw-placeholder-opacity)) !important; +} + +:is(.dark .select2-dropdown)::placeholder { + --tw-placeholder-opacity: 1 !important; + color: rgb(156 163 175 / var(--tw-placeholder-opacity)) !important; +} + +:is(.dark .select2-dropdown:focus) { + --tw-border-opacity: 1 !important; + border-color: rgb(0 156 234 / var(--tw-border-opacity)) !important; + --tw-ring-opacity: 1 !important; + --tw-ring-color: rgb(0 156 234 / var(--tw-ring-opacity)) !important; +} + +.select2-selection__choice { + --tw-bg-opacity: 1 !important; + background-color: rgb(228 240 213 / var(--tw-bg-opacity)) !important; + --tw-text-opacity: 1 !important; + color: rgb(17 24 39 / var(--tw-text-opacity)) !important; +} + +:is(.dark .select2-selection__choice) { + --tw-bg-opacity: 1 !important; + background-color: rgb(73 109 28 / var(--tw-bg-opacity)) !important; + --tw-text-opacity: 1 !important; + color: rgb(229 231 235 / var(--tw-text-opacity)) !important; +} + +.select2-search { + height: 1rem !important; +} + +.select2-selection { + padding-top: 0.625rem !important; + padding-bottom: 0.625rem !important; +} + .hover\:scale-105:hover { --tw-scale-x: 1.05; --tw-scale-y: 1.05; diff --git a/resources/schema.edn b/resources/schema.edn index 9c2a6558..aaf3aaf4 100644 --- a/resources/schema.edn +++ b/resources/schema.edn @@ -1393,6 +1393,10 @@ :db/cardinality #:db{:ident :db.cardinality/one}, :db/doc "the id from the provider", :db/ident :user/provider-id} + {:db/valueType #:db{:ident :db.type/instant}, + :db/cardinality #:db{:ident :db.cardinality/one}, + :db/doc "The last time the user logged in, defaulted to the last action they took", + :db/ident :user/last-login} {:db/valueType #:db{:ident :db.type/ref}, :db/cardinality #:db{:ident :db.cardinality/one}, :db/doc "The role [:user :admin :none]", diff --git a/scratch-sessions/setup-user-last-login.repl b/scratch-sessions/setup-user-last-login.repl new file mode 100644 index 00000000..d69e6765 --- /dev/null +++ b/scratch-sessions/setup-user-last-login.repl @@ -0,0 +1,14 @@ + +(->> (dc/q '[:find ?u (max ?lat) + :in $ $$ + :where [?u :user/name] + [$$ ?u _ _ ?tx] + [?tx :db/txInstant ?lat]] + (dc/db conn) + (dc/history (dc/db conn))) + (map (fn [[u last-login]] + {:db/id u + :user/last-login last-login})) + (dc/transact conn) + deref) + diff --git a/src/clj/auto_ap/datomic/users.clj b/src/clj/auto_ap/datomic/users.clj index f10e83c6..1f71939c 100644 --- a/src/clj/auto_ap/datomic/users.clj +++ b/src/clj/auto_ap/datomic/users.clj @@ -31,7 +31,8 @@ :where [?e :user/provider ?provider] [?e :user/provider-id ?provider-id]] (dc/db conn) provider provider-id)) - result @(dc/transact conn [[:upsert-entity (cond-> (assoc new-user :db/id (or user-id "user")) + result @(dc/transact conn [[:upsert-entity (cond-> (assoc new-user :db/id (or user-id "user") + :user/last-login (java.util.Date.)) (not user-id) (assoc :user/role :user-role/none) is-first-user? (assoc :user/role :user-role/admin))]]) user-id (or user-id (get-in result [:tempids "user"]))] diff --git a/src/clj/auto_ap/ssr/company.clj b/src/clj/auto_ap/ssr/company.clj index 058bacb0..86b31b58 100644 --- a/src/clj/auto_ap/ssr/company.clj +++ b/src/clj/auto_ap/ssr/company.clj @@ -1,15 +1,19 @@ (ns auto-ap.ssr.company (:require - [auto-ap.datomic :refer [conn]] + [auto-ap.datomic :refer [conn pull-attr]] [auto-ap.datomic.clients :refer [full-read]] + [auto-ap.solr :as solr] + [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.ui :refer [base-page]] + [bidi.bidi :as bidi] [cemerick.url :as url] + [clojure.set :as set] + [clojure.string :as str] [config.core :refer [env]] [datomic.api :as dc] - [auto-ap.ssr-routes :as ssr-routes] - [bidi.bidi :as bidi])) + [ring.middleware.json :refer [wrap-json-response]])) (defn please-select-client-screen* [] [:div.grid.grid-cols-3 @@ -66,3 +70,19 @@ (main-content* {:client (:client request)})) "My Company")) +(defn search [{:keys [clients query-params]}] + (let [valid-client-ids (set (map :db/id clients)) + name-like-ids (when (not-empty (get query-params "q")) + (set (map (comp #(Long/parseLong %) :id) + (solr/query solr/impl "clients" + {"query" (format "_text_:(%s*)" (str/upper-case (solr/escape (get query-params "q")))) + "fields" "id" + "limit" 300})))) + valid-clients (for [n name-like-ids + :when (valid-client-ids n)] + {"id" n "text" (pull-attr (dc/db conn) :client/name n)} + )] + {:body {"results" valid-clients}})) + +(def search (wrap-json-response search)) + diff --git a/src/clj/auto_ap/ssr/components.clj b/src/clj/auto_ap/ssr/components.clj index 86dae15f..9845cc1a 100644 --- a/src/clj/auto_ap/ssr/components.clj +++ b/src/clj/auto_ap/ssr/components.clj @@ -28,6 +28,7 @@ (def money-input inputs/money-input-) (def date-input inputs/date-input-) (def select inputs/select-) +(def typeahead inputs/typeahead-) (def field inputs/field-) (def left-aside aside/left-aside-) diff --git a/src/clj/auto_ap/ssr/components/inputs.clj b/src/clj/auto_ap/ssr/components/inputs.clj index e9c9fc38..b607e55b 100644 --- a/src/clj/auto_ap/ssr/components/inputs.clj +++ b/src/clj/auto_ap/ssr/components/inputs.clj @@ -1,17 +1,50 @@ (ns auto-ap.ssr.components.inputs - (:require [hiccup2.core :as hiccup])) + (:require [hiccup2.core :as hiccup] + [auto-ap.ssr.svg :as svg] + [bidi.bidi :as bidi])) (defn select- [params & children] (into - [:select {:class "bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" + [:select {:class (str (:class params) " bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500") :name (:name params)} (cond->> (map (fn [[k v]] - [:option {:value k :selected (= v (:value params))} v]) + [:option {:value k :selected (= k (:value params))} v]) (:options params)) (:allow-blank? params) (conj [:option {:value "" :selected (not (:value params))} ""]))] children)) +(defn typeahead- [params] + [:div {:class (:class params)} + [:select (-> params + (dissoc :url) + (assoc :width "")) + (for [[k v] (:value params)] + [:option {:value k :selected true} v] + ) + [:script {:lang "javascript"} + (hiccup/raw (format "$(document).ready(function() {$(\"#%s\").select2({ajax: {url: '%s', dataType: 'json'}, minimumInputLength: 4, placeholder: \"%s\"})})" + (:id params) + (:url params) + (or (:placeholder params) + "Type to search")))]]]) + +(defn typeahead-results- [{:keys [options]}] + [:ul + (for [{:keys [id name]} options] + [:li + [:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"} + [:a {:href "#" + :class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300" + #_#_:hx-put (bidi/path-for ssr-routes/only-routes + :active-client + :request-method :put) + :hx-target "#company-dropdown" + :hx-headers (format "{\"x-clients\": \"[%d]\"}" id) + :hx-swap "outerHTML" + :hx-trigger "click"} + name]]])]) + (defn use-size [size] (if (= :small size) (str " " "text-xs p-2") diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index a26400b0..aa953176 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -1,24 +1,26 @@ (ns auto-ap.ssr.core (:require + [auto-ap.routes.ezcater-xls :as ezcater-xls] [auto-ap.routes.utils :refer [wrap-admin wrap-client-redirect-unauthenticated wrap-secure]] [auto-ap.ssr.admin.history :as history] [auto-ap.ssr.auth :as auth] - [auto-ap.ssr.transaction.insights :as insights] - [auto-ap.ssr.company.company-1099 :as company-1099] - [auto-ap.ssr.company.yodlee :as company-yodlee] - [auto-ap.ssr.company.plaid :as company-plaid] - [auto-ap.ssr.search :as search] + [auto-ap.ssr.company :as company] [auto-ap.ssr.company-dropdown :as company-dropdown] + [auto-ap.ssr.company.company-1099 :as company-1099] + [auto-ap.ssr.company.plaid :as company-plaid] [auto-ap.ssr.company.reports :as company-reports] + [auto-ap.ssr.company.yodlee :as company-yodlee] [auto-ap.ssr.invoice.glimpse :as invoice-glimpse] - [auto-ap.ssr.pos.sales-orders :as pos-sales] - [auto-ap.ssr.pos.refunds :as pos-refunds] - [auto-ap.ssr.pos.expected-deposits :as pos-expected-deposits] [auto-ap.ssr.pos.cash-drawer-shifts :as pos-cash-drawer-shifts] + [auto-ap.ssr.pos.expected-deposits :as pos-expected-deposits] + [auto-ap.ssr.pos.refunds :as pos-refunds] + [auto-ap.ssr.pos.sales-orders :as pos-sales] [auto-ap.ssr.pos.tenders :as pos-tenders] - [auto-ap.routes.ezcater-xls :as ezcater-xls] - [auto-ap.ssr.company :as company])) + [auto-ap.ssr.search :as search] + [auto-ap.ssr.transaction.insights :as insights] + [auto-ap.ssr.users :as users] + [ring.middleware.json :refer [wrap-json-response]])) ;; from auto-ap.ssr-routes, because they're shared @@ -31,7 +33,9 @@ :admin-history-inspect (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/inspect))) :active-client (wrap-client-redirect-unauthenticated (wrap-secure (wrap-secure company-dropdown/active-client))) :company-dropdown-search-results - (wrap-client-redirect-unauthenticated (wrap-secure company-dropdown/dropdown-search-results)) + (wrap-client-redirect-unauthenticated (wrap-secure (wrap-json-response company-dropdown/dropdown-search-results {}))) + :company-search + (wrap-client-redirect-unauthenticated (wrap-secure company/search)) :company (wrap-client-redirect-unauthenticated (wrap-secure company/page)) :company-1099 (wrap-client-redirect-unauthenticated (wrap-secure company-1099/page)) :company-1099-vendor-table (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-table)) @@ -66,5 +70,6 @@ (into pos-expected-deposits/key->handler) (into pos-tenders/key->handler) (into pos-cash-drawer-shifts/key->handler) - (into pos-refunds/key->handler))) + (into pos-refunds/key->handler) + (into users/key->handler))) diff --git a/src/clj/auto_ap/ssr/grid_page_helper.clj b/src/clj/auto_ap/ssr/grid_page_helper.clj index 50d666e4..36b1782c 100644 --- a/src/clj/auto_ap/ssr/grid_page_helper.clj +++ b/src/clj/auto_ap/ssr/grid_page_helper.clj @@ -184,7 +184,6 @@ valid-clients (->> valid-clients (take 20) set)] - (println "VALID CLIENTS ARE" valid-clients) (handler (assoc request :trimmed-clients valid-clients))))) (defn table-route [grid-spec] diff --git a/src/clj/auto_ap/ssr/pos/sales_orders.clj b/src/clj/auto_ap/ssr/pos/sales_orders.clj index 7398c9f7..4c23bdbd 100644 --- a/src/clj/auto_ap/ssr/pos/sales_orders.clj +++ b/src/clj/auto_ap/ssr/pos/sales_orders.clj @@ -9,10 +9,7 @@ pull-many query2]] [auto-ap.datomic.sales-orders :as d-sales] - [auto-ap.graphql.utils :refer [extract-client-ids]] [auto-ap.query-params :as query-params] - [auto-ap.routes.utils - :refer [wrap-client-redirect-unauthenticated wrap-secure]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] [auto-ap.ssr.grid-page-helper :as helper] @@ -22,8 +19,7 @@ [auto-ap.time :as atime] [bidi.bidi :as bidi] [clj-time.coerce :as c] - [datomic.api :as dc] - [malli.core :as m])) + [datomic.api :as dc])) (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" @@ -150,7 +146,6 @@ true (merge-query {:query {:find ['?sort-default '?e]}}))] - (clojure.pprint/pprint query) (cond->> (query2 query) true (apply-sort-3 query-params) true (apply-pagination query-params)))) diff --git a/src/clj/auto_ap/ssr/ui.clj b/src/clj/auto_ap/ssr/ui.clj index b10c0e4f..70041cfb 100644 --- a/src/clj/auto_ap/ssr/ui.clj +++ b/src/clj/auto_ap/ssr/ui.clj @@ -21,15 +21,12 @@ [:title (str "Integreat | " page-name)] [:link {:href "/css/font.min.css", :rel "stylesheet"}] [:link {:rel "icon" :type "image/png" :href "/favicon.png"}] - #_[:link {:rel "stylesheet", :href "/css/react-datepicker.min.inc.css"}] [:link {:rel "stylesheet", :href "/output.css"}] + [:script {:src "https://code.jquery.com/jquery-3.7.1.min.js"}] [:script {:src "https://unpkg.com/hyperscript.org@0.9.7/dist/_hyperscript.min.js"}] [:script {:src "https://unpkg.com/@popperjs/core@2.11.8/dist/umd/popper.min.js"}] [:script {:src "https://cdn.plaid.com/link/v2/stable/link-initialize.js"}] - #_[:script {:src "https://unpkg.com/htmx.org@1.8.4" - :integrity "sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV" - :crossorigin= "anonymous"}] [:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.min.js" :crossorigin= "anonymous"}] [:script {:src "https://unpkg.com/htmx.org/dist/ext/debug.js"}] @@ -38,7 +35,8 @@ [:link {:rel "stylesheet" :href "https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.1.4/dist/css/datepicker.min.css"}] [:script {:type "text/javascript" :src "https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.1.4/dist/js/datepicker-full.min.js"}] - [:script {:type "text/javascript", :src "https://cdn.jsdelivr.net/npm/@tarekraafat/autocomplete.js@10.2.7/dist/autoComplete.min.js"}] + [:script {:type "text/javascript", :src "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"}] + [:link {:rel "stylesheet" :href "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css"}] [:script {:src "https://unpkg.com/dropzone@5.9.3/dist/min/dropzone.min.js"}] [:link {:rel "stylesheet" :href "https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" :type "text/css"}] [:style diff --git a/src/clj/auto_ap/ssr/users.clj b/src/clj/auto_ap/ssr/users.clj new file mode 100644 index 00000000..8117babe --- /dev/null +++ b/src/clj/auto_ap/ssr/users.clj @@ -0,0 +1,331 @@ +(ns auto-ap.ssr.users + (:require + [auto-ap.datomic + :refer [add-sorter-fields + apply-pagination + apply-sort-3 + conn + merge-query + pull-many + query2]] + [auto-ap.query-params :as query-params] + [auto-ap.routes.auth :as auth] + [auto-ap.routes.utils :refer [wrap-admin]] + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components :as com] + [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.utils :refer [html-response]] + [auto-ap.time :as atime] + [bidi.bidi :as bidi] + [buddy.sign.jwt :as jwt] + [clj-http.client :as client] + [clojure.string :as str] + [config.core :refer [env]] + [datomic.api :as dc])) + +(defn filters [request] + [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" + "hx-get" (bidi/path-for ssr-routes/only-routes + :user-table) + "hx-target" "#user-table" + "hx-indicator" "#user-table"} + + [:fieldset.space-y-6 + (com/field {:label "Name"} + (com/text-input {:name "name" + :id "name" + :class "hot-filter" + :value (:name (:parsed-query-params request)) + :placeholder "Johnny Testerson" + :size :small})) + + (com/field {:label "Email"} + (com/text-input {:name "email" + :id "email" + :class "hot-filter" + :value (:name (:parsed-query-params request)) + :placeholder "hello@friend.com" + :size :small})) + + (com/field {:label "Role"} + (com/radio {:size :small + :name "role" + :options [{:value "" + :content "All"} + {:value "admin" + :content "Admin"} + {:value "power-user" + :content "Power user"} + {:value "manager" + :content "Manager"} + {:value "user" + :content "User"} + {:value "none" + :content "None"}]}))]]) + +(def default-read '[:db/id + :user/name + :user/email + :user/profile-image-url + [:user/last-login :xform clj-time.coerce/from-date] + {[:user/role :xform iol-ion.query/ident] [:db/ident] + + :user/clients [:client/code :db/id :client/locations :client/name]}]) + +(defn fetch-ids [db request] + (let [query-params (:parsed-query-params request) + query (cond-> {:query {:find [] + :in '[$ ] + :where '[]} + :args [db ]} + (:sort query-params) (add-sorter-fields {"name" ['[?e :user/name ?un] + '[(clojure.string/upper-case ?un) ?sort-name]] + "email" ['[(get-else $ ?e :user/email "") ?sort-email]] + + "role" ['[?e :user/role ?r] + '[?r :db/ident ?ri] + '[(name ?ri) ?sort-role]] + "last-login" ['[?e :user/last-login ?sort-last-login]]} + query-params) + (some->> query-params :name not-empty) + (merge-query {:query {:find [] + :in ['?ns] + :where ['[?e :user/name ?sn] + '[(clojure.string/upper-case ?sn) ?upper-sn] + '[(clojure.string/includes? ?upper-sn ?ns)]]} + :args [(str/upper-case (:name query-params))]}) + + (some->> query-params :email not-empty) + (merge-query {:query {:find [] + :in ['?es] + :where ['[?e :user/email ?se] + '[(clojure.string/upper-case ?se) ?upper-se] + '[(clojure.string/includes? ?upper-se ?es)]]} + :args [(str/upper-case (:email query-params))]}) + + (some->> query-params :role) + (merge-query {:query {:find [] + :in ['?r] + :where ['[?e :user/role ?r] + '[?r :db/ident ?ri]]} + :args [(some->> query-params :role)]}) + + + + true + (merge-query {:query {:find ['?sort-default '?e] + :where ['[?e :user/name ?un] + '[(clojure.string/upper-case ?un) ?sort-default]]}}))] + + (cond->> (query2 query) + true (apply-sort-3 query-params) + true (apply-pagination query-params)))) + +(defn hydrate-results [ids db _] + (let [results (->> (pull-many db default-read ids) + (group-by :db/id)) + refunds (->> ids + (map results) + (map first))] + refunds)) + +(defn fetch-page [request] + (let [db (dc/db conn) + {ids-to-retrieve :ids matching-count :count} (fetch-ids db request)] + + [(->> (hydrate-results ids-to-retrieve db request)) + matching-count])) + +(defn role->pill [role] + (com/pill {:color (cond (= :user-role/admin role) + :primary + + (= :user-role/manager role) + :secondary + + (= :user-role/power-user role) + :secondary + + (= :user-role/user role) + :yellow + + :else + :red)} + (name role))) + +(defn user->client-pills [user] + [:div.flex.space-x-2 + (for [{:client/keys [code]} (take 3 (:user/clients user))] + (com/pill {:color :primary} + code) + ) + (let [remainder (- (count (:user/clients user)) 3)] + (when (> remainder 0) + (com/pill {:color :white} + (format "%d more" remainder))))]) + +(def grid-page + (helper/build {:id "user-table" + :nav (com/admin-aside-nav) + :page-specific-nav filters + :fetch-page fetch-page + :parse-query-params (comp + (query-params/parse-key :role #(query-params/parse-keyword "user-role" %)) + (query-params/parse-key :total-gte query-params/parse-double) + (query-params/parse-key :total-lte query-params/parse-double) + (helper/default-parse-query-params grid-page)) + :row-buttons (fn [request entity] + [(com/button {:hx-post (str (bidi/path-for ssr-routes/only-routes + :user-impersonate)) + :hx-vals (format "{\"user-id\": \"%s\"}" (:db/id entity))} "Impersonate") + (com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes + :user-edit-dialog + :user-id (:db/id entity))) + :hx-target "#modal-holder" + :hx-swap "outerHTML"} + svg/pencil)]) + :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes + :admin)} + "Admin"] + + [:a {:href (bidi/path-for ssr-routes/only-routes + :users)} + "Users"]] + :title "Users" + :entity-name "User" + :route :user-table + :headers [{:key "name" + :name "Name" + :sort-key "name" + :render (fn [user] + [:div.flex.space-x-2.place-items-center + (when-let [profile-image (:user/profile-image-url user) ] + [:div.rounded-full.overflow-hidden.w-8.h-8.display-inline + [:img {:src profile-image }]]) + [:span.inline-block ](:user/name user)])} + + {:key "email" + :name "Email" + :sort-key "email" + :render #(-> % :user/email)} + {:key "role" + :name "Role" + :sort-key "role" + :render #(some-> % :user/role role->pill)} + {:key "last-login" + :name "Last login" + :sort-key "last-login" + :render #(some-> % (:user/last-login) (atime/unparse-local atime/standard-time))} + {:key "clients" + :name "Clients" + :render user->client-pills} + ]})) + +(def row* (partial helper/row* grid-page)) +(def table* (partial helper/table* grid-page)) + +(defn impersonate [request] + (let [user (some-> request + :form-params + (get "user-id") + not-empty + Long/parseLong + (#(dc/pull (dc/db conn) default-read %))) ] + {:status 200 + :headers {"hx-redirect" (str "/?jwt=" (jwt/sign (auth/user->jwt user "FAKE_TOKEN") + (:jwt-secret env) + {:alg :hs512})) + } + :session {:identity (dissoc (auth/user->jwt user "FAKE_TOKEN") + :exp)}})) + +(defn user-edit-save [{:keys [form-params] :as request}] + (let [user (some-> request + :params + :user-id + not-empty + Long/parseLong + (#(dc/pull (dc/db conn) default-read %))) + new-clients (map #(Long/parseLong %) + (cond-> (get form-params "clients") + (string? (get form-params "clients")) vector)) + + _ @(dc/transact conn [ + [:upsert-entity {:db/id (:db/id user) + :user/role (keyword "user-role" (get form-params "role")) + :user/clients new-clients}]]) + user (some-> request + :params + :user-id + not-empty + Long/parseLong + (#(dc/pull (dc/db conn) default-read %)))] + + (html-response + (row* identity user {:flash? true}) + :headers {"hx-trigger" "closeModal" + "hx-retarget" (format "#user-table tr[data-id=\"%d\"]" (:db/id user))}))) + +(defn user-edit-dialog [request] + (let [user (some-> request + :params + :user-id + not-empty + Long/parseLong + (#(dc/pull (dc/db conn) default-read %)))] + (html-response + (com/modal + {} + [:form {:hx-post (str (bidi/path-for ssr-routes/only-routes + :user-edit-save + :request-method :post + :user-id (:db/id user ))) + :hx-swap "outerHTML swap:300ms"} + [:fieldset {:class "hx-disable"} + (com/modal-card + {} + [:div.flex [:div.p-2 "User"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:user/name user)]] + [:div.space-y-6 + (com/field {:label "Role"} + (com/select {:name "role" + :class "w-36" + :id "role" + :value (name (:user/role user)) + :options [["none" "None"] + ["power-user" "Power user"] + ["manager" "Manager"] + ["admin" "Admin"] + ["user" "User"]] + :size :small})) + (com/field {:label "Clients"} + (com/typeahead {:name "clients" + :class "w-full" + :multiple "multiple" + :url (bidi/path-for ssr-routes/only-routes + :company-search) + :id "role" + :value (map + (fn [client] + [(:db/id client) (:client/name client)]) + (:user/clients user)) + #_#_:value (name (:user/role user)) + #_#_:options [["none" "None"] + ["power-user" "Power user"] + ["manager" "Manager"] + ["admin" "Admin"] + ["user" "User"]] + :size :small})) + (com/button {:color :primary} + "Save") + ] + + [:div])]])))) + +(def key->handler + {:users (wrap-admin (helper/page-route grid-page)) + :user-table (wrap-admin (helper/table-route grid-page)) + :user-edit-save (wrap-client-redirect-unauthenticated (wrap-admin user-edit-save)) + :user-edit-dialog (wrap-client-redirect-unauthenticated (wrap-admin user-edit-dialog)) + :user-impersonate (wrap-client-redirect-unauthenticated (wrap-admin impersonate))}) + diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index 282fc13d..a9ffd8e2 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -13,6 +13,11 @@ #"/search/?" :admin-history-search ["/" [#"\d+" :entity-id] #"/?"] :admin-history ["/inspect/" [#"\d+" :entity-id] #"/?"] :admin-history-inspect} + "/user" {"" :users + "/table" :user-table + "/impersonate" :user-impersonate + [[#"\d+" :user-id] "/edit"] {:get :user-edit-dialog + :post :user-edit-save}} "/ezcater-xls" :admin-ezcater-xls} "transaction" {"/insights" {"" :transaction-insights "/table" :transaction-insight-table @@ -33,6 +38,7 @@ "company" {"" :company "/dropdown" :company-dropdown-search-results + "/search" :company-search "/active" {:put :active-client} "/1099" :company-1099 "/1099/table" {:get :company-1099-vendor-table} diff --git a/src/cljs/auto_ap/views/pages/admin/users/table.cljs b/src/cljs/auto_ap/views/pages/admin/users/table.cljs index 7baa9576..a29209d4 100644 --- a/src/cljs/auto_ap/views/pages/admin/users/table.cljs +++ b/src/cljs/auto_ap/views/pages/admin/users/table.cljs @@ -22,8 +22,6 @@ ::impersonate [with-user] (fn [{:keys [db user]} [_ impersonate-jwt]] - (js/alert "HI") - {:http {:method "GET" :uri (str "/impersonate?jwt=" impersonate-jwt) :on-success [::impersonated impersonate-jwt]}}))