From f997c41abd9841b9f41a6e5efa3907cee5eb3bf6 Mon Sep 17 00:00:00 2001 From: Bryce Date: Sat, 14 Oct 2023 23:21:22 -0700 Subject: [PATCH] migrates accounts --- resources/input.css | 18 +- resources/public/output.css | 54 ++++ src/clj/auto_ap/datomic.clj | 2 + src/clj/auto_ap/ssr/admin/accounts.clj | 292 ++++++++++++++++++++++ src/clj/auto_ap/ssr/components/aside.clj | 6 +- src/clj/auto_ap/ssr/components/dialog.clj | 2 +- src/clj/auto_ap/ssr/components/inputs.clj | 37 +-- src/clj/auto_ap/ssr/core.clj | 4 +- src/clj/auto_ap/ssr/ui.clj | 3 +- src/clj/auto_ap/ssr/users.clj | 135 +++------- src/clj/auto_ap/ssr/utils.clj | 123 ++++++++- src/cljc/auto_ap/client_routes.cljc | 2 - src/cljc/auto_ap/ssr_routes.cljc | 7 +- 13 files changed, 559 insertions(+), 126 deletions(-) create mode 100644 src/clj/auto_ap/ssr/admin/accounts.clj diff --git a/resources/input.css b/resources/input.css index ef65f702..66146585 100644 --- a/resources/input.css +++ b/resources/input.css @@ -79,7 +79,7 @@ .choices { - @apply border-0 !important; + @apply border-0 mb-0 !important; } .choices__list--multiple { } @@ -110,3 +110,19 @@ @apply bg-green-500 border-gray-500 ring-blue-500 border-blue-500 !important; } +.choices__list--single .choices__item { + @apply w-auto flex !important; +} +.choices__list--single { + @apply w-auto !important; + +} + +.choices__list--single button { + @apply block relative m-0 h-auto !important; + +} + +.choices[data-type*="select-one"] .choices__button { + right:auto !important; +} diff --git a/resources/public/output.css b/resources/public/output.css index c6f28794..a87f6a67 100644 --- a/resources/public/output.css +++ b/resources/public/output.css @@ -1344,6 +1344,10 @@ input:checked + .toggle-bg { width: 50%; } +.w-16 { + width: 4rem; +} + .w-3 { width: 0.75rem; } @@ -1352,6 +1356,10 @@ input:checked + .toggle-bg { width: 0.875rem; } +.w-32 { + width: 8rem; +} + .w-36 { width: 9rem; } @@ -1384,6 +1392,10 @@ input:checked + .toggle-bg { width: 100%; } +.w-96 { + width: 24rem; +} + .max-w-2xl { max-width: 42rem; } @@ -1404,6 +1416,10 @@ input:checked + .toggle-bg { max-width: 1024px; } +.max-w-4xl { + max-width: 56rem; +} + .flex-1 { flex: 1 1 0%; } @@ -1420,6 +1436,14 @@ input:checked + .toggle-bg { flex-shrink: 1; } +.shrink-0 { + flex-shrink: 0; +} + +.grow-0 { + flex-grow: 0; +} + .basis-1\/4 { flex-basis: 25%; } @@ -1518,6 +1542,10 @@ input:checked + .toggle-bg { appearance: none; } +.grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + .grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } @@ -2257,6 +2285,11 @@ input:checked + .toggle-bg { color: rgb(97 145 37 / var(--tw-text-opacity)); } +.text-red-500 { + --tw-text-opacity: 1; + color: rgb(255 3 3 / var(--tw-text-opacity)); +} + .text-red-600 { --tw-text-opacity: 1; color: rgb(204 2 2 / var(--tw-text-opacity)); @@ -2485,6 +2518,7 @@ input:checked + .toggle-bg { } .choices { + margin-bottom: 0px !important; border-width: 0px !important; } @@ -2628,6 +2662,26 @@ input:checked + .toggle-bg { --tw-ring-color: rgb(0 156 234 / var(--tw-ring-opacity)) !important; } +.choices__list--single .choices__item { + display: flex !important; + width: auto !important; +} + +.choices__list--single { + width: auto !important; +} + +.choices__list--single button { + position: relative !important; + margin: 0px !important; + display: block !important; + height: auto !important; +} + +.choices[data-type*="select-one"] .choices__button { + right:auto !important; +} + .hover\:scale-105:hover { --tw-scale-x: 1.05; --tw-scale-y: 1.05; diff --git a/src/clj/auto_ap/datomic.clj b/src/clj/auto_ap/datomic.clj index 6ad41259..efeefc42 100644 --- a/src/clj/auto_ap/datomic.clj +++ b/src/clj/auto_ap/datomic.clj @@ -844,6 +844,8 @@ @(dc/transact conn (edn/read-string {:readers {'db/id id-literal 'db/fn construct}} (slurp (io/resource "functions.edn"))))) +(defn all-schema [] + (edn/read-string (slurp (io/resource "schema.edn")))) (defn transact-schema [conn] @(dc/transact conn diff --git a/src/clj/auto_ap/ssr/admin/accounts.clj b/src/clj/auto_ap/ssr/admin/accounts.clj new file mode 100644 index 00000000..a28e23df --- /dev/null +++ b/src/clj/auto_ap/ssr/admin/accounts.clj @@ -0,0 +1,292 @@ +(ns auto-ap.ssr.admin.accounts + (: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.utils + :refer [wrap-admin wrap-client-redirect-unauthenticated]] + [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 [entity-id + forced-vector + html-response + map->db-id-decoder + ref->enum-schema + ref->select-options + temp-id + wrap-schema-decode]] + [bidi.bidi :as bidi] + [clojure.string :as str] + [datomic.api :as dc] + [hiccup2.core :as hiccup] + [malli.core :as mc] + [ring.middleware.nested-params :refer [wrap-nested-params]])) + +(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 + :admin-account-table) + "hx-target" "#account-table" + "hx-indicator" "#account-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 "Cash" + :size :small})) + + (com/field {:label "Code"} + (com/text-input {:name "code" + :id "code" + :class "hot-filter" + :value (:code (:parsed-query-params request)) + :placeholder "11101" + :size :small}))]]) + +(def default-read '[:db/id + :account/code + :account/name + :account/numeric-code + :account/location + {[:account/type :xform iol-ion.query/ident] [:db/ident] + [:account/invoice-allowance :xform iol-ion.query/ident] [:db/ident] + [:account/vendor-allowance :xform iol-ion.query/ident] [:db/ident] + [:account/applicability :xform iol-ion.query/ident] [:db/ident] + :account/client-overrides [{:account-client-override/client [:client/name :db/id]} + :account-client-override/name + :db/id]}]) + +(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 :account/name ?n] + '[(clojure.string/upper-case ?n) ?sort-name]] + "code" ['[(get-else $ ?e :account/numeric-code 0) ?sort-code]] + + "type" ['[?e :account/type ?t] + '[?t :db/ident ?ti] + '[(name ?ti) ?sort-type]]} + query-params) + (some->> query-params :name not-empty) + (merge-query {:query {:find [] + :in ['?ns] + :where ['[?e :account/name ?an] + '[(clojure.string/upper-case ?an) ?upper-an] + '[(clojure.string/includes? ?upper-an ?ns)]]} + :args [(str/upper-case (:name query-params))]}) + + (some->> query-params :code) + (merge-query {:query {:find [] + :in ['?nc] + :where ['[?e :account/numeric-code ?nc] + ]} + :args [(:code query-params)]}) + + true + (merge-query {:query {:find ['?sort-default '?e] + :where ['[?e :account/code ?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])) + +(def grid-page + (helper/build {:id "account-table" + :nav (com/admin-aside-nav) + :page-specific-nav filters + :fetch-page fetch-page + :parse-query-params (comp + (query-params/parse-key :code query-params/parse-long) + (helper/default-parse-query-params grid-page)) + :row-buttons (fn [request entity] + [(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes + :admin-account-edit-dialog + :db/id (doto (:db/id entity) println))) + :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 + :admin-accounts)} + "Accounts"]] + :title "Accounts" + :entity-name "Account" + :route :admin-account-table + :headers [{:key "code" + :name "Code" + :sort-key "code" + :render :account/numeric-code} + + {:key "name" + :name "Name" + :sort-key "name" + :render :account/name} + {:key "type" + :name "Type" + :sort-key "type" + :render #(some->> % :account/type name (com/pill {:color :primary}))} + {:key "location" + :name "Location" + :sort-key "location" + :render :account/location}]})) + +(def row* (partial helper/row* grid-page)) +(def table* (partial helper/table* grid-page)) + +(defn account-edit-save [{:keys [params route-params] :as request}] + (let [_ @(dc/transact conn [[:upsert-entity (-> params (assoc :db/id (:db/id route-params)) (dissoc :id))]]) + new-account (some-> route-params :db/id (#(dc/pull (dc/db conn) default-read %)))] + + (html-response + (row* identity new-account {:flash? true}) + :headers {"hx-trigger" "closeModal" + "hx-retarget" (format "#account-table tr[data-id=\"%d\"]" (:db/id new-account))}))) + +(defn client-override* [override] + [:div.flex.gap-2.mb-2.client-override + [:div.w-96 + (com/typeahead {:name (format "account/client-overrides[%s][account-client-override/client]" (:db/id override)) + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes + :company-search) + :id (str "account-client-override-" (:db/id override)) + :value [(:db/id (:account-client-override/client override)) + (:client/name (:account-client-override/client override))]})] + [:div.w-96 + (com/text-input {:name (format "account/client-overrides[%s][account-client-override/name]" (:db/id override)) + :class "w-full" + :value (:account-client-override/name override)})] + [:div (com/a-icon-button {"_" (hiccup/raw "on click halt the event then transition the closest <.client-override />'s opacity to 0 then remove closest <.client-override />") } svg/x)]]) + +(defn account-edit-dialog [request] + (prn (:route-params request)) + (let [account (some-> request + :route-params + :db/id + (#(dc/pull (dc/db conn) default-read %)))] + (html-response + (com/modal + {:modal-class "max-w-4xl"} + [:form#edit-form {:hx-ext "response-targets" + :hx-post (str (bidi/path-for ssr-routes/only-routes + :admin-account-edit-save + :request-method :post + :db/id (:db/id account ))) + :hx-swap "outerHTML swap:300ms" + :hx-target-400 "#form-errors .error-content"} + [:fieldset {:class "hx-disable"} + (com/modal-card + {} + [:div.flex [:div.p-2 "Account"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:account/numeric-code account) " - " (:account/name account)]] + [:div.space-y-6 + (com/field {:label "Name"} + (com/text-input {:name "account/name" + :autofocus true + :class "w-32" + :value (:account/name account)})) + (com/field {:label "Account Type"} + (com/select {:name "account/type" + :class "w-36" + :id "type" + :value (some-> account :account/type name) + :options (ref->select-options "account-type")})) + (com/field {:label "Location"} + (com/text-input {:name "account/location" + :class "w-16" + :value (:account/location account)})) + + (com/field {:label "Invoice Allowance"} + (com/select {:name "account/invoice-allowance" + :value (name (:account/invoice-allowance account)) + :class "w-36" + :options (ref->select-options "allowance")})) + (com/field {:label "Vendor Allowance"} + (com/select {:name "account/vendor-allowance" + :class "w-36" + :value (name (:account/vendor-allowance account)) + :options (ref->select-options "allowance")})) + (com/field {:label "Applicability"} + (com/select {:name "account/applicability" + :class "w-36" + :value (name (:account/applicability account)) + :options (ref->select-options "account-applicability")})) + + (com/field {:label "Client Overrides" :id "client-overrides"} + (for [override (:account/client-overrides account)] + (client-override* override))) + (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes + :admin-account-client-override-new) + :hx-target "#client-overrides" + :hx-swap "beforeend"} + "New override") + [:div#form-errors [:span.error-content]] + (com/button {:color :primary :form "edit-form" :type "submit"} + "Save")] + [:div])]])))) + +(defn new-client-override [request] + (html-response + (client-override* {:db/id (str (java.util.UUID/randomUUID))}))) + +(def key->handler + {:admin-accounts (wrap-admin (helper/page-route grid-page)) + :admin-account-table (wrap-admin (helper/table-route grid-page)) + :admin-account-client-override-new (-> new-client-override wrap-admin wrap-client-redirect-unauthenticated) + :admin-account-edit-save (-> account-edit-save + wrap-admin + wrap-client-redirect-unauthenticated + (wrap-schema-decode + :route-schema (mc/schema [:map [:db/id entity-id]]) + :params-schema (mc/schema + [:map + [:account/name :string] + [:account/location [:maybe :string]] + [:account/type (ref->enum-schema "account-type")] + [:account/applicability (ref->enum-schema "account-applicability")] + [:account/invoice-allowance (ref->enum-schema "allowance")] + [:account/vendor-allowance (ref->enum-schema "allowance")] + [:account/client-overrides {:decode/json map->db-id-decoder} + (forced-vector [:map + [:db/id [:or entity-id temp-id]] + [:account-client-override/client [:or entity-id :string]] + [:account-client-override/name :string]])]])) + (wrap-nested-params)) + :admin-account-edit-dialog (-> account-edit-dialog + wrap-admin + wrap-client-redirect-unauthenticated + (wrap-schema-decode + :route-schema (mc/schema [:map [:db/id entity-id]])))}) diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index 6b7804ed..24c76639 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -299,12 +299,12 @@ "Vendors")] [:li (menu-button- {:icon svg/user - :href (bidi/path-for client-routes/routes - :admin-users)} + :href (bidi/path-for ssr-routes/only-routes + :users)} "Users")] [:li (menu-button- {:icon svg/accounts - :href (bidi/path-for client-routes/routes + :href (bidi/path-for ssr-routes/only-routes :admin-accounts)} "Accounts")] diff --git a/src/clj/auto_ap/ssr/components/dialog.clj b/src/clj/auto_ap/ssr/components/dialog.clj index 6ce3b034..c0b8c0d3 100644 --- a/src/clj/auto_ap/ssr/components/dialog.clj +++ b/src/clj/auto_ap/ssr/components/dialog.clj @@ -5,7 +5,7 @@ [:div [:div#modal-holder { :tabindex "-1", :class "fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full flex justify-center hidden" :aria-hidden true "_" (hiccup/raw "on closeModal transition <#modal-holder .modal-content /> opacity to 0.0 over 300ms then call hideModal() ")} - [:div {:class "relative w-full max-w-2xl max-h-full"} + [:div {:class (str "relative w-full max-h-full " (or (:modal-class params) " max-w-2xl "))} (into [:div#modal-content] children)] ] diff --git a/src/clj/auto_ap/ssr/components/inputs.clj b/src/clj/auto_ap/ssr/components/inputs.clj index e2999a6d..2777dae8 100644 --- a/src/clj/auto_ap/ssr/components/inputs.clj +++ b/src/clj/auto_ap/ssr/components/inputs.clj @@ -2,12 +2,15 @@ (:require [hiccup2.core :as hiccup])) + (defn select- [params & children] (into [:select (-> params (dissoc :allow-blank? :value :options) (update - :class str " 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")) + :class str " bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block 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") + + ) (cond->> (map (fn [[k v]] [:option {:value k :selected (= k (:value params))} v]) @@ -16,21 +19,22 @@ children)) (defn typeahead- [params] - [:div {} - [:select (-> params - (dissoc :url) - (dissoc :value) - (assoc :width "") - ) - (for [[k v] (:value params)] - [:option {:value k :selected true} v] - ) - ] + [:select (-> params + (dissoc :url) + (dissoc :value) + ) + (for [[k v] (if (:multiple params) + (:value params) + [(:value params)]) + :when k] + [:option {:value k :selected true} v] + ) + [:script {:lang "javascript"} (hiccup/raw (format " (function () { var element = document.getElementById('%s'); -var c = new Choices(element, {removeItems: true, removeItemButton:true, searchFloor: 3}); +var c = new Choices(element, {removeItems: true, removeItemButton:true, searchFloor: 3, searchPlaceholderValue: '%s'}); element.addEventListener('search', function (e) { let data = fetch('%s?q=' + e.detail.value) @@ -46,6 +50,7 @@ c.clearChoices(); " (:id params) + (:placeholder params) (:url params) ))]]) @@ -58,7 +63,7 @@ c.clearChoices(); [:input (-> params (update - :class str " 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") + :class str " bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block 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") (update :class #(str % (use-size size))) ) ]) @@ -67,7 +72,7 @@ c.clearChoices(); [:input (-> params (update - :class str " bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full 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-right appearance-none" + :class str " bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-blue-500 focus:border-blue-500 block 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-right appearance-none" ) (update :class #(str % (use-size size))) (assoc :type "number" @@ -80,7 +85,7 @@ c.clearChoices(); [:input (-> params (update - :class str " bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full 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") + :class str " bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-blue-500 focus:border-blue-500 block 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") (assoc :type "text") (assoc "_" (hiccup/raw "init initDatepicker(me)")) (assoc "hx-on" (hiccup/raw "changeDate: htmx.trigger(this, \"change\") @@ -90,6 +95,6 @@ c.clearChoices(); (defn field- [params & rest] (into - [:div + [:div {:id (:id params)} [:label {:class "block mb-2 text-sm font-medium text-gray-900 dark:text-white"} (:label params)]] rest)) diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index aa953176..6c6d3343 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -9,6 +9,7 @@ [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.admin.accounts :as admin-accounts] [auto-ap.ssr.company.reports :as company-reports] [auto-ap.ssr.company.yodlee :as company-yodlee] [auto-ap.ssr.invoice.glimpse :as invoice-glimpse] @@ -71,5 +72,6 @@ (into pos-tenders/key->handler) (into pos-cash-drawer-shifts/key->handler) (into pos-refunds/key->handler) - (into users/key->handler))) + (into users/key->handler) + (into admin-accounts/key->handler))) diff --git a/src/clj/auto_ap/ssr/ui.clj b/src/clj/auto_ap/ssr/ui.clj index 55cbcc8c..87ae5aae 100644 --- a/src/clj/auto_ap/ssr/ui.clj +++ b/src/clj/auto_ap/ssr/ui.clj @@ -23,7 +23,6 @@ [:link {:rel "icon" :type "image/png" :href "/favicon.png"}] [: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"}] @@ -37,9 +36,9 @@ [:script {:type "text/javascript" :src "https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.1.4/dist/js/datepicker-full.min.js"}] - #_[:link {:rel "stylesheet" :href "https://cdn.jsdelivr.net/npm/choices.js@9.0.1/public/assets/styles/base.min.css"}] [:link {:rel "stylesheet" :href "https://cdn.jsdelivr.net/npm/choices.js@9.0.1/public/assets/styles/choices.min.css"}] [:script {:src "https://cdn.jsdelivr.net/npm/choices.js@9.0.1/public/assets/scripts/choices.min.js"}] + [:script {:src "https://unpkg.com/htmx.org/dist/ext/response-targets.js"}] [: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"}] diff --git a/src/clj/auto_ap/ssr/users.clj b/src/clj/auto_ap/ssr/users.clj index 36e0b535..d85f5fef 100644 --- a/src/clj/auto_ap/ssr/users.clj +++ b/src/clj/auto_ap/ssr/users.clj @@ -10,21 +10,25 @@ query2]] [auto-ap.query-params :as query-params] [auto-ap.routes.auth :as auth] - [auto-ap.routes.utils :refer [wrap-admin wrap-client-redirect-unauthenticated]] + [auto-ap.routes.utils + :refer [wrap-admin wrap-client-redirect-unauthenticated]] [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.ssr.utils + :refer [entity-id + forced-vector + html-response + ref->enum-schema + wrap-schema-decode]] [auto-ap.time :as atime] [bidi.bidi :as bidi] [buddy.sign.jwt :as jwt] [clojure.string :as str] [config.core :refer [env]] [datomic.api :as dc] - [malli.core :as mc] - [malli.transform :as mt2] - [manifold.time :as mt])) + [malli.core :as mc])) (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" @@ -180,10 +184,10 @@ :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") + :hx-vals (format "{\"db/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))) + :db/id (:db/id entity))) :hx-target "#modal-holder" :hx-swap "outerHTML"} svg/pencil)]) @@ -228,12 +232,7 @@ (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 %))) ] + (let [user (some-> request :params :db/id (#(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) @@ -242,19 +241,9 @@ :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 - (#(dc/pull (dc/db conn) default-read %))) - _ @(dc/transact conn [ - [:upsert-entity {:db/id (:db/id user) - :user/role (keyword "user-role" (get form-params "role")) - :user/clients (some-> request :params :clients)}]]) - user (some-> request - :params - :user-id - (#(dc/pull (dc/db conn) default-read %)))] +(defn user-edit-save [{:keys [params route-params] :as request}] + (let [_ @(dc/transact conn [[:upsert-entity (-> params (assoc :db/id (:db/id route-params)) (dissoc :id))]]) + user (some-> request :route-params :db/id (#(dc/pull (dc/db conn) default-read %)))] (html-response (row* identity user {:flash? true}) @@ -263,24 +252,26 @@ (defn user-edit-dialog [request] (let [user (some-> request - :params - :user-id + :route-params + :db/id (#(dc/pull (dc/db conn) default-read %)))] (html-response (com/modal {} - [:form {:hx-post (str (bidi/path-for ssr-routes/only-routes + [:form {:hx-ext "response-targets" + :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"} + :db/id (:db/id user ))) + :hx-swap "outerHTML swap:300ms" + :hx-target-400 "#form-errors .error-content"} [: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" + (com/select {:name "user/role" :class "w-36" :autofocus true :id "role" @@ -292,7 +283,7 @@ ["user" "User"]] :size :small})) (com/field {:label "Clients"} - (com/typeahead {:name "clients" + (com/typeahead {:name "user/clients" :class "w-full" :multiple "multiple" :url (bidi/path-for ssr-routes/only-routes @@ -302,68 +293,12 @@ (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#form-errors [:span.error-content]] + (com/button {:color :primary :type "submit"} + "Save")] [:div])]])))) -(defn forced-vector [x] - [:vector {:decode/json {:enter (fn [x] - (if (sequential? x) - x - [x]) - )}} - x]) - -(def entity-id (mc/schema nat-int?)) - -(defn wrap-schema-decode [handler & {:keys [form query params]}] - (fn [{:keys [form-params query-params] :as request}] - (try - (handler (cond-> request - (and (:params request) params) - (assoc :params - (mc/coerce - params - (:params request) - (mt2/transformer - (mt2/key-transformer {:encode name :decode keyword}) - mt2/string-transformer - mt2/json-transformer) )) - - (and form form-params) - (assoc :parsed-form-params - (mc/coerce - form - form-params - (mt2/transformer - (mt2/key-transformer {:encode name :decode keyword}) - mt2/string-transformer - mt2/json-transformer) )) - - (and query query-params) - (assoc :parsed-query-params - (mc/coerce - form - form-params - (mt2/transformer - (mt2/key-transformer {:encode name :decode keyword}) - mt2/string-transformer - mt2/json-transformer) )))) - (catch Exception e - ;; TODO - {:status 400 - :body "error"})))) - - (def key->handler {:users (wrap-admin (helper/page-route grid-page)) :user-table (wrap-admin (helper/table-route grid-page)) @@ -371,15 +306,19 @@ wrap-admin wrap-client-redirect-unauthenticated (wrap-schema-decode - :params (mc/schema + :route-schema (mc/schema [:map [:db/id entity-id]]) + :params-schema (mc/schema [:map - [:user-id nat-int?] - [:clients (forced-vector entity-id)] - [:role [:enum {:decode/string #(keyword "user-role" %)} :user-role/admin :user-role/manager :user-role/power-user :user-role/user :user-role/none]]]))) + [:user/clients (forced-vector entity-id)] + [:user/role (ref->enum-schema "user-role")]]))) :user-edit-dialog (-> user-edit-dialog wrap-admin wrap-client-redirect-unauthenticated (wrap-schema-decode - :params (mc/schema [:map [:user-id entity-id]]))) - :user-impersonate (wrap-client-redirect-unauthenticated (wrap-admin impersonate))}) + :route-schema (mc/schema [:map [:db/id entity-id]]))) + :user-impersonate (-> impersonate + wrap-admin + wrap-client-redirect-unauthenticated + (wrap-schema-decode + :params-schema (mc/schema [:map [:db/id entity-id]])))}) diff --git a/src/clj/auto_ap/ssr/utils.clj b/src/clj/auto_ap/ssr/utils.clj index df5d8968..975ce879 100644 --- a/src/clj/auto_ap/ssr/utils.clj +++ b/src/clj/auto_ap/ssr/utils.clj @@ -1,9 +1,14 @@ (ns auto-ap.ssr.utils (:require + [auto-ap.datomic :refer [all-schema]] [auto-ap.logging :as alog] + [clojure.string :as str] [config.core :refer [env]] [hiccup2.core :as hiccup] - [clojure.string :as str])) + [malli.core :as mc] + [malli.error :as me] + [malli.transform :as mt2] + [ring.middleware.nested-params :refer [parse-nested-keys]])) (defn html-response [hiccup & {:keys [status headers oob] :or {status 200 headers {} oob []}}] {:status status @@ -68,3 +73,119 @@ (seq k) (str/join "_" (map path->name k)) :else k)) + + +(defn forced-vector [x] + [:vector {:decode/json {:enter (fn [x] + (if (sequential? x) + x + [x]) + )}} + x]) + +(def entity-id (mc/schema nat-int?)) +(def temp-id (mc/schema :string)) + +(defn str->keyword [s] + (if (string? s) + (let [[ns k] (str/split s #"/")] + (if (and ns k) + (keyword ns k) + (keyword s))) + s)) + +(defn keyword->str [k] + (subs (str k) 1)) + +(defn wrap-schema-decode [handler & {:keys [form-schema query-schema route-schema params-schema]}] + (fn [{:keys [form-params query-params params] :as request}] + (try + + (handler (cond-> request + (and (:params request) params-schema) + (assoc :params + (mc/coerce + params-schema + (:params request) + (mt2/transformer + (mt2/key-transformer {:encode keyword->str :decode str->keyword}) + mt2/string-transformer + mt2/json-transformer) )) + + (and (:route-params request) route-schema) + (assoc :route-params + (mc/coerce + route-schema + (:route-params request) + (mt2/transformer + (mt2/key-transformer {:encode keyword->str :decode str->keyword}) + mt2/string-transformer + mt2/json-transformer) )) + + (and form-schema form-params) + (assoc :parsed-form-params + (mc/coerce + form-schema + form-params + (mt2/transformer + (mt2/key-transformer {:encode keyword->str :decode str->keyword}) + mt2/string-transformer + mt2/json-transformer) )) + + (and query-schema query-params) + (assoc :parsed-query-params + (mc/coerce + query-schema + query-params + (mt2/transformer + (mt2/key-transformer {:encode name :decode keyword}) + mt2/string-transformer + mt2/json-transformer) )))) + (catch Exception e + (alog/warn ::validation-error :error e) + (html-response [:span.error-content.text-red-500 (str/join ", " + (mapcat identity + (-> e + (ex-data ) + :data + :explain + me/humanize + vals)))] + :status 400))))) + +(defn ref->enum-schema [n] + (into [:enum {:decode/string #(keyword n %)}] + (for [{:db/keys [ident]} (all-schema) + :when (= n (namespace ident))] + ident))) + +(defn ref->select-options [n & {:keys [allow-nil?]}] + (into (if allow-nil? + [["" ""]] + []) + (for [{:db/keys [ident]} (all-schema) + :when (= n (namespace ident))] + [(name ident) (str/replace (str/capitalize (name ident)) "-" " ")]))) + +(def map->db-id-decoder + {:enter (fn [x] + (into [] + (for [[k v] x] + (assoc v :db/id (cond (and (string? k) (re-find #"^\d+$" k)) + (Long/parseLong k) + (keyword? k) + (name k) + :else + k)))))}) + +(defn namespaceize-decoder [n] + {:exit (fn [m] + (when m + (reduce + (fn [m [k v]] + (if (= k "id") + (assoc m :db/id v) + (assoc m (keyword n (name k)) v))) + m + m)))}) + diff --git a/src/cljc/auto_ap/client_routes.cljc b/src/cljc/auto_ap/client_routes.cljc index cbaf1c0e..a0857e8b 100644 --- a/src/cljc/auto_ap/client_routes.cljc +++ b/src/cljc/auto_ap/client_routes.cljc @@ -10,9 +10,7 @@ "clients/" {"" :admin-clients [:id] {"" :admin-specific-client "/bank-accounts/" {[:bank-account] :admin-specific-bank-account}}} - "users" :admin-users "rules" :admin-rules - "accounts" :admin-accounts "import-batches" :admin-import-batches "jobs" :admin-jobs "vendors" :admin-vendors diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index a9ffd8e2..60b89f47 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -16,8 +16,13 @@ "/user" {"" :users "/table" :user-table "/impersonate" :user-impersonate - [[#"\d+" :user-id] "/edit"] {:get :user-edit-dialog + [[#"\d+" :db/id] "/edit"] {:get :user-edit-dialog :post :user-edit-save}} + "/account" {"" :admin-accounts + "/table" :admin-account-table + "/override/new" :admin-account-client-override-new + ["/" [#"\d+" :db/id] "/edit"] {:get :admin-account-edit-dialog + :post :admin-account-edit-save}} "/ezcater-xls" :admin-ezcater-xls} "transaction" {"/insights" {"" :transaction-insights "/table" :transaction-insight-table