From 8dca62294734b4d289a26b51ea05fe1acb5799b7 Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Fri, 12 May 2023 15:17:54 -0700 Subject: [PATCH] Makes reports page work with new tailwind --- src/clj/auto_ap/datomic/reports.clj | 10 +- src/clj/auto_ap/ssr/company/company_1099.clj | 375 +++++++++---------- src/clj/auto_ap/ssr/company/reports.clj | 120 ++++++ src/clj/auto_ap/ssr/company_dropdown.clj | 20 +- src/clj/auto_ap/ssr/components.clj | 61 +-- src/clj/auto_ap/ssr/components/aside.clj | 15 +- src/clj/auto_ap/ssr/components/buttons.clj | 7 + src/clj/auto_ap/ssr/components/data_grid.clj | 59 ++- src/clj/auto_ap/ssr/components/paginator.clj | 61 +++ src/clj/auto_ap/ssr/core.clj | 4 + src/clj/auto_ap/ssr/svg.clj | 74 ++++ src/clj/auto_ap/ssr/utils.clj | 24 +- src/cljc/auto_ap/ssr_routes.cljc | 6 +- 13 files changed, 540 insertions(+), 296 deletions(-) create mode 100644 src/clj/auto_ap/ssr/company/reports.clj create mode 100644 src/clj/auto_ap/ssr/components/paginator.clj diff --git a/src/clj/auto_ap/datomic/reports.clj b/src/clj/auto_ap/datomic/reports.clj index 843e2026..66ce0cf2 100644 --- a/src/clj/auto_ap/datomic/reports.clj +++ b/src/clj/auto_ap/datomic/reports.clj @@ -12,6 +12,8 @@ [clj-time.coerce :as c] [datomic.api :as dc])) +(def default-read '[:db/id :report/client :report/created :report/url :report/name :report/creator]) + (defn raw-graphql-ids [db args] (let [query (cond-> {:query {:find [] :in ['$ ] @@ -43,8 +45,7 @@ (apply-pagination args)))) (defn graphql-results [ids db args] - (let [results (->> (pull-many db '[:db/id :report/client :report/created :report/url :report/name :report/creator] - ids) + (let [results (->> (pull-many db default-read ids) (map #(update % :report/created c/from-date)) (group-by :db/id))] (->> ids @@ -63,8 +64,3 @@ [(->> (graphql-results ids-to-retrieve db args)) matching-count])) - - - - - diff --git a/src/clj/auto_ap/ssr/company/company_1099.clj b/src/clj/auto_ap/ssr/company/company_1099.clj index e487cde3..e580c6bb 100644 --- a/src/clj/auto_ap/ssr/company/company_1099.clj +++ b/src/clj/auto_ap/ssr/company/company_1099.clj @@ -6,7 +6,7 @@ [auto-ap.ssr.components :as com] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.ui :refer [base-page]] - [auto-ap.ssr.utils :refer [html-response]] + [auto-ap.ssr.utils :refer [html-response form-data->map path->name]] [bidi.bidi :as bidi] [clojure.string :as str] [datomic.api :as dc] @@ -53,7 +53,6 @@ [(>= ?d #inst "2022-01-01T08:00")] [(< ?d #inst "2023-01-01T08:00")] [?p :payment/type :payment-type/check] - [?p :payment/amount ?a] [?p :payment/vendor ?v]] (dc/db conn) @@ -113,7 +112,6 @@ [(>= ?d #inst "2022-01-01T08:00")] [(< ?d #inst "2023-01-01T08:00")] [?p :payment/type :payment-type/check] - [?p :payment/amount ?a] [?p :payment/vendor ?v]] (dc/db conn) @@ -125,134 +123,97 @@ [(:client/code client ) amount])) (into [])))) +(defn row* [{:keys [client vendor amount flash?]}] + (com/data-grid-row + {:class (when flash? + "live-added")} + (com/data-grid-cell {} (:client/code client)) + (com/data-grid-cell + {} + [:div.flex.whitespace-nowrap.items-center.gap-4 + [:div [:div (:vendor/name vendor)] + [:div.text-sm.text-gray-400 + (or (-> vendor :vendor/legal-entity-name not-empty) + (str (-> vendor :vendor/legal-entity-first-name) " " + (-> vendor :vendor/legal-entity-middle-name) " " + (-> vendor :vendor/legal-entity-last-name)))]] + (when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)] + (com/pill + {:class "text-xs font-medium" + :color :primary} + (str/capitalize t99-type)) + )]) + (com/data-grid-cell + {:class "hidden md:table-cell"} + [:div.flex.gap-4 + (when-let [tin (-> vendor :vendor/legal-entity-tin)] + [:span {:class "text-xs font-medium py-0.5 "} + tin]) + (when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)] + (com/pill {:class "text-xs font-medium" + :color :yellow} + (name tin-type)))]) + (com/data-grid-cell + {:class "hidden lg:table-cell"} + (if (-> vendor :vendor/address :address/street1) + [:div + [:div (-> vendor :vendor/address :address/street1)] " " + [:div + (-> vendor :vendor/address :address/street2)] " " + [:div + (-> vendor :vendor/address :address/city) " " + (-> vendor :vendor/address :address/state) "," + (-> vendor :vendor/address :address/zip)]] + [:p.text-sm.italic.text-gray-400 "No address"])) + (com/data-grid-cell {} + (com/pill {:class "text-xs font-medium" + :color :primary} + "Paid $" (Math/round amount))) + (com/data-grid-right-stack-cell + {} + (if (cannot-overwrite? vendor) + [:div (com/link {:href "mailto:ben@integreatconsult.com"} "Contact Integreat")] + (com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes + :company-1099-vendor-dialog + :vendor-id (:db/id vendor)) + :hx-target "#modal-holder" + :hx-swap "outerHTML"} + svg/pencil))))) (defn table* [{:keys [identity session query-params hx-query-params]} & {:keys [flash-id]}] - (println hx-query-params) - (let [start (or (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) + (let [start (or (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) 0) - per-page (or (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) + per-page (or (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) 30) companies (get-1099-companies identity session) - total (count companies) + total (count companies) companies (subvec companies (Math/min start total) (Math/min (+ start per-page) total))] - [:div#vendor-table {:hx-get (bidi/path-for ssr-routes/only-routes - :company-1099-vendor-table - :request-method :get) - :hx-trigger "clientSelected from:body" - :hx-swap "outerHTML swap:300ms"} - (com/content-card {} - [:div {:class "flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"} - [:div - [:h1.text-2xl.mb-3.font-bold "1099 Vendor Info"] - [:div {:class "flex items-center flex-1 space-x-4"} - [:h5 - [:span "Total Vendors:"] - [:span {:class "dark:text-white pl-4"} (count companies)]]]] - [:div {:class "flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"} - (com/button {:color :primary} - (com/button-icon {} svg/refresh) - "Add new product") - (com/button {:color :secondary} - (com/button-icon {} svg/refresh) - "Update stocks 1/250") - (com/icon-button {} - svg/upload)]] - [:div {:class "overflow-x-auto"} - (apply com/data-grid {:headers [(com/data-grid-header {} "Client") - (com/data-grid-header {} "Vendor Name") - (com/data-grid-header {:class "hidden md:table-cell"} "TIN") - (com/data-grid-header {:class "hidden lg:table-cell"} "Address") - (com/data-grid-header {}) - (com/data-grid-header {})]} - (for [[client vendor amount] companies] - (com/data-grid-row - {:class (when (= flash-id - (:db/id vendor)) - "live-added")} - (com/data-grid-cell {} (:client/code client)) - (com/data-grid-cell - {} - [:div.flex.whitespace-nowrap.items-center.gap-4 - [:div [:div (:vendor/name vendor)] - [:div.text-sm.text-gray-400 - (or (-> vendor :vendor/legal-entity-name not-empty) - (str (-> vendor :vendor/legal-entity-first-name) " " - (-> vendor :vendor/legal-entity-middle-name) " " - (-> vendor :vendor/legal-entity-last-name)))]] - (when-let [t99-type (some-> vendor :vendor/legal-entity-1099-type :db/ident name)] - (com/pill - {:class "text-xs font-medium" - :color :primary} - (str/capitalize t99-type)) - )]) - (com/data-grid-cell - {:class "hidden md:table-cell"} - [:div.flex.gap-4 - (when-let [tin (-> vendor :vendor/legal-entity-tin)] - [:span {:class "text-xs font-medium py-0.5 "} - tin]) - (when-let [tin-type (some-> vendor :vendor/legal-entity-tin-type :db/ident name)] - (com/pill {:class "text-xs font-medium" - :color :yellow} - (name tin-type)))]) - (com/data-grid-cell - {:class "hidden lg:table-cell"} - (if (-> vendor :vendor/address :address/street1) - [:div - [:div (-> vendor :vendor/address :address/street1)] " " - [:div - (-> vendor :vendor/address :address/street2)] " " - [:div - (-> vendor :vendor/address :address/city) " " - (-> vendor :vendor/address :address/state) "," - (-> vendor :vendor/address :address/zip)]] - [:p.text-sm.italic.text-gray-400 "No address"])) - (com/data-grid-cell {} - (com/pill {:class "text-xs font-medium" - :color :primary} - "Paid $" (Math/round amount))) - (com/data-grid-right-stack-cell - {} - (if (cannot-overwrite? vendor) - [:div (com/link {:href "mailto:ben@integreatconsult.com"} "Contact Integreat")] - (com/icon-button {:hx-get (bidi/path-for ssr-routes/only-routes - :company-1099-vendor-dialog - :vendor-id (:db/id vendor)) - :hx-target "#modal-holder" - :hx-swap "outerHTML"} - svg/pencil))))))] - (com/paginator {:start start - :end (Math/min (+ start per-page) total) - :per-page per-page - :total total - :a-params (fn [page] - {:hx-get (str (bidi/path-for ssr-routes/only-routes - :company-1099-vendor-table - :request-method :get) - "?start=" (* page per-page)) - :hx-target "#vendor-table" - :hx-swap "outerHTML show:#app:top"})}))])) - -(defn form-data->map [form-data] - (reduce-kv - (fn [acc k v] - (cond (and (string? v) - (empty? v)) - acc - - :else - (assoc-in acc (->> (str/split k #"_") - (mapv #(apply keyword (str/split % #"/")))) - v))) - {} - form-data)) - -(defn path->name [k] - (cond (keyword? k) - (str (namespace k) "/" (name k)) - - (seq k) - (str/join "_" (map path->name k)) - :else k)) + (com/data-grid-card {:id "vendor-table" + :title "1099 Vendor Info" + :entity-name "vendors" + :route :company-1099-vendor-table + :start start + :per-page per-page + :total total + :action-buttons [(com/button {:color :primary} + (com/button-icon {} svg/refresh) + "Add new product") + (com/button {:color :secondary} + (com/button-icon {} svg/refresh) + "Update stocks 1/250") + (com/icon-button {} + svg/upload)] + :rows (for [[client vendor amount] companies] + (row* {:client client + :vendor vendor + :amount amount + :flash? (= flash-id + (:db/id vendor))})) + :headers [(com/data-grid-header {} "Client") + (com/data-grid-header {} "Vendor Name") + (com/data-grid-header {:class "hidden md:table-cell"} "TIN") + (com/data-grid-header {:class "hidden lg:table-cell"} "Address") + (com/data-grid-header {}) + (com/data-grid-header {})]}))) (defn vendor-save [{:keys [form-params identity route-params] :as request}] (when-not (cannot-overwrite? (dc/pull (dc/db conn) '[*] (Long/parseLong (:vendor-id route-params)))) @@ -265,8 +226,6 @@ (table* request :flash-id (Long/parseLong (:vendor-id route-params))) :headers {"hx-trigger" "closeModal"})) - - (defn vendor-dialog [request] (let [vendor (dc/pull (dc/db conn) '[* {:vendor/legal-entity-1099-type [:db/ident] :vendor/legal-entity-tin-type [:db/ident]}] (Long/parseLong (:vendor-id (:params request))))] ;; TODO perms @@ -276,86 +235,86 @@ :company-1099-vendor-save :request-method :post :vendor-id (Long/parseLong (:vendor-id (:params request))))) - :hx-target "#vendor-table" - :hx-swap "outerHTML swap:300ms"} - [:fieldset {:class "hx-disable"} - (com/modal-card {} - [:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name vendor)]] - [:div.space-y-6 - [:div.grid.grid-cols-6.gap-4 - [:h4.text-xl.border-b.col-span-6 "Address"] - [:div.col-span-6 - (com/field {:label "Street 1"} - (com/text-input {:name (path->name [:vendor/address :address/street1]) - :value (-> vendor :vendor/address :address/street1) - :placeholder "1700 Pennsylvania Ave" - :autofocus true}))] - [:div.col-span-6 - (com/field {:label "Street 2"} - (com/text-input {:name (path->name [:vendor/address :address/street2]) - :value (-> vendor :vendor/address :address/street2) - :placeholder "Suite 200"}))] - [:div.col-span-3 - (com/field {:label "City"} - (com/text-input {:name (path->name [:vendor/address :address/city]) - :value (-> vendor :vendor/address :address/city) - :placeholder "Cupertino"}))] - [:div.col-span-1 - (com/field {:label "State"} - (com/text-input {:name (path->name [:vendor/address :address/state]) - :value (-> vendor :vendor/address :address/state) - :placeholder "CA"}))] - [:div.col-span-2 - (com/field {:label "Zip"} - (com/text-input {:name (path->name [:vendor/address :address/zip]) - :value (-> vendor :vendor/address :address/zip) - :placeholder "98102"}))] - [:h4.text-xl.border-b.col-span-6 "Legal Entity"] - [:div.col-span-6 - (com/field {:label "Legal Entity Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-name]) - :value (-> vendor :vendor/legal-entity-name) - :placeholder "Good Restaurant LLC"}))] - [:div.col-span-6.text-center " - OR -"] - [:div.col-span-2 - (com/field {:label "First Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-first-name]) - :value (-> vendor :vendor/legal-entity-first-name) - :placeholder "John"}))] - [:div.col-span-2 - (com/field {:label "Middle Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-middle-name]) - :value (-> vendor :vendor/legal-entity-middle-name) - :placeholder "C."}))] - [:div.col-span-2 - (com/field {:label "Last Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-last-name]) - :value (-> vendor :vendor/legal-entity-last-name) - :placeholder "Riley"}))] - [:div.col-span-2 - (com/field {:label "TIN"} - (com/text-input {:name (path->name [:vendor/legal-entity-tin]) - :value (-> vendor :vendor/legal-entity-tin) - :placeholder "John"}))] - [:div.col-span-2 - (com/field {:label "TIN Type"} - (com/select {:name (path->name [:vendor/legal-entity-tin-type]) - :allow-blank? true - :value (some-> vendor :vendor/legal-entity-tin-type :db/ident name) - :options [["ein" "EIN"] - ["ssn" "SSN"]]}))] - [:div.col-span-2 - (com/field {:label "1099 Type"} - (com/select {:name (path->name [:vendor/legal-entity-1099-type]) - :allow-blank? true - :value (some-> vendor :vendor/legal-entity-1099-type :db/ident name) - :options [["none" "None"] - ["misc" "Misc"] - ["landlord" "Landlord"]]}))] - [:div.col-span-6 - (com/button {:color :primary} - "Save")]]] - [:div])]])))) + :hx-target "#vendor-table" + :hx-swap "outerHTML swap:300ms"} + [:fieldset {:class "hx-disable"} + (com/modal-card {} + [:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name vendor)]] + [:div.space-y-6 + [:div.grid.grid-cols-6.gap-4 + [:h4.text-xl.border-b.col-span-6 "Address"] + [:div.col-span-6 + (com/field {:label "Street 1"} + (com/text-input {:name (path->name [:vendor/address :address/street1]) + :value (-> vendor :vendor/address :address/street1) + :placeholder "1700 Pennsylvania Ave" + :autofocus true}))] + [:div.col-span-6 + (com/field {:label "Street 2"} + (com/text-input {:name (path->name [:vendor/address :address/street2]) + :value (-> vendor :vendor/address :address/street2) + :placeholder "Suite 200"}))] + [:div.col-span-3 + (com/field {:label "City"} + (com/text-input {:name (path->name [:vendor/address :address/city]) + :value (-> vendor :vendor/address :address/city) + :placeholder "Cupertino"}))] + [:div.col-span-1 + (com/field {:label "State"} + (com/text-input {:name (path->name [:vendor/address :address/state]) + :value (-> vendor :vendor/address :address/state) + :placeholder "CA"}))] + [:div.col-span-2 + (com/field {:label "Zip"} + (com/text-input {:name (path->name [:vendor/address :address/zip]) + :value (-> vendor :vendor/address :address/zip) + :placeholder "98102"}))] + [:h4.text-xl.border-b.col-span-6 "Legal Entity"] + [:div.col-span-6 + (com/field {:label "Legal Entity Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-name]) + :value (-> vendor :vendor/legal-entity-name) + :placeholder "Good Restaurant LLC"}))] + [:div.col-span-6.text-center " - OR -"] + [:div.col-span-2 + (com/field {:label "First Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-first-name]) + :value (-> vendor :vendor/legal-entity-first-name) + :placeholder "John"}))] + [:div.col-span-2 + (com/field {:label "Middle Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-middle-name]) + :value (-> vendor :vendor/legal-entity-middle-name) + :placeholder "C."}))] + [:div.col-span-2 + (com/field {:label "Last Name"} + (com/text-input {:name (path->name [:vendor/legal-entity-last-name]) + :value (-> vendor :vendor/legal-entity-last-name) + :placeholder "Riley"}))] + [:div.col-span-2 + (com/field {:label "TIN"} + (com/text-input {:name (path->name [:vendor/legal-entity-tin]) + :value (-> vendor :vendor/legal-entity-tin) + :placeholder "John"}))] + [:div.col-span-2 + (com/field {:label "TIN Type"} + (com/select {:name (path->name [:vendor/legal-entity-tin-type]) + :allow-blank? true + :value (some-> vendor :vendor/legal-entity-tin-type :db/ident name) + :options [["ein" "EIN"] + ["ssn" "SSN"]]}))] + [:div.col-span-2 + (com/field {:label "1099 Type"} + (com/select {:name (path->name [:vendor/legal-entity-1099-type]) + :allow-blank? true + :value (some-> vendor :vendor/legal-entity-1099-type :db/ident name) + :options [["none" "None"] + ["misc" "Misc"] + ["landlord" "Landlord"]]}))] + [:div.col-span-6 + (com/button {:color :primary} + "Save")]]] + [:div])]])))) (defn vendor-table [request] (html-response (table* request) diff --git a/src/clj/auto_ap/ssr/company/reports.clj b/src/clj/auto_ap/ssr/company/reports.clj new file mode 100644 index 00000000..62a502fa --- /dev/null +++ b/src/clj/auto_ap/ssr/company/reports.clj @@ -0,0 +1,120 @@ +(ns auto-ap.ssr.company.reports + (:require + [amazonica.aws.s3 :as s3] + [auto-ap.datomic :refer [conn]] + [auto-ap.datomic.reports :as r] + [auto-ap.graphql.utils :refer [assert-admin is-admin?]] + [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]] + [auto-ap.ssr.utils :refer [html-response]] + [auto-ap.time :as atime] + [bidi.bidi :as bidi] + [config.core :refer [env]] + [datomic.api :as dc] + [hiccup2.core :as hiccup])) + +(defn row* [{:keys [flash? report identity delete-after-settle?]}] + (com/data-grid-row + {:class (when flash? + "live-added") + "_" (hiccup/raw (when delete-after-settle?" on htmx:afterSettle wait 400ms then remove me"))} + (com/data-grid-cell + {} + (:report/name report)) + (com/data-grid-cell + {} + (when (:report/creator report) + (com/pill {:color :primary } + (:report/creator report)))) + (com/data-grid-cell + {} + (atime/unparse-local (:report/created report) + atime/normal-date)) + (com/data-grid-right-stack-cell + {} + (com/a-icon-button {:href (:report/url report)} + svg/download) + (when (is-admin? identity) + [:form + [:input {:type :hidden :name "id" :value (:db/id report)}] + (com/icon-button {:hx-delete (str (bidi/path-for ssr-routes/only-routes + :company-reports-delete + :request-method :delete)) + :hx-target "closest tr"} + svg/trash)])))) + +(defn table* [{:keys [client start per-page identity session flash-id]}] + (let [start (or start 0) + per-page (or per-page 30) + [reports total] (r/get-graphql {:id identity + :start start + :per-page per-page + :client-id (:db/id client) + :sort nil})] + (com/data-grid-card {:id "report-table" + :title "Reports" + :entity-name "reports" + :route :company-reports-table + :start start + :per-page per-page + :total total + :action-buttons [(com/button {:color :primary} + (com/button-icon {} svg/refresh) + "Add new product") + (com/button {:color :secondary} + (com/button-icon {} svg/refresh) + "Update stocks 1/250") + (com/icon-button {} + svg/upload)] + :rows (for [report reports] + (row* {:report report + :flash? (= flash-id + (:db/id report)) + :identity identity})) + :headers [(com/data-grid-header {} "Name") + (com/data-grid-header {:class "hidden md:table-cell"} "Created by") + (com/data-grid-header {:class "hidden md:table-cell"} "Created") + (com/data-grid-header {})]}))) + +(defn delete-report [{:keys [query-params hx-query-params form-params identity session] :as request}] + (let [[id-to-delete key] (first (dc/q '[:find ?i ?k + :in $ ?i + :where [?i :report/key ?k]] + (dc/db conn) + (some-> (get form-params "id") not-empty Long/parseLong))) + report (dc/pull (dc/db conn) r/default-read id-to-delete)] + (when id-to-delete + (s3/delete-object :bucket-name (:data-bucket env) + :key key) + @(dc/transact conn [[:db/retractEntity id-to-delete]])) + (html-response + (row* {:report report + :flash? true + :identity identity + :delete-after-settle? true})))) + +(defn reports-table [{:keys [query-params hx-query-params identity session] :as request}] + (html-response (table* {:client (:client (:session request)) + :start (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) + :per-page (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) + :identity identity + :session session}) + :headers {"hx-push-url" (str "?start=" (get (:query-params request) "start"))})) + +(defn page [{:keys [query-params hx-query-params identity session] :as request}] + (base-page + request + (com/page {:nav (com/company-aside-nav) + :active-client (:client (:session request)) + :identity (:identity request)} + (com/breadcrumbs {} + [:a {:href "#"} "My Company"] + [:a {:href "#"} "Reports"]) + (table* {:client (:client (:session request)) + :start (some-> (or (get query-params "start") (get hx-query-params "start")) not-empty (Long/parseLong )) + :per-page (some-> (or (get query-params "per-page") (get hx-query-params "per-page")) not-empty (Long/parseLong )) + :identity identity + :session session})) + nil)) diff --git a/src/clj/auto_ap/ssr/company_dropdown.clj b/src/clj/auto_ap/ssr/company_dropdown.clj index 8e60eaba..c5b900c7 100644 --- a/src/clj/auto_ap/ssr/company_dropdown.clj +++ b/src/clj/auto_ap/ssr/company_dropdown.clj @@ -12,14 +12,14 @@ (defn dropdown-search-results* [{:keys [options]}] [:ul - (for [option options] + (for [[id company-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" "_" (hiccup/raw "on click set value of <#company-search-value/> to @data-value then send selected to #company-dropdown") - :data-value (get option "key")} - (get option "value")]]])]) + :data-value id} + company-name]]])]) (defn get-clients [identity query] (dc/q '[:find ?c ?n @@ -33,10 +33,7 @@ (defn dropdown-search-results [{:keys [identity] :as request}] (html-response - (dropdown-search-results* {:options (->> (get-clients identity (get (:query-params request) "search-text")) - (map (fn [[k v]] - {"key" k - "value" v}))) + (dropdown-search-results* {:options (get-clients identity (get (:query-params request) "search-text")) :client (:client (:session request))}))) (defn dropdown [{:keys [client]}] @@ -79,14 +76,7 @@ :hx-target "#company-search-results" :hx-swap "innerHTML"} ]] [:input#company-search-value {:type "hidden" - :autocomplete "off" - :name "search-client" - :hx-put (bidi/path-for ssr-routes/only-routes - :active-client - :request-method :put) - :hx-target "#company-dropdown" - :hx-swap "outerHTML" - :hx-trigger "change changed"} ]] + :name "search-client"}]] [:div.divide-y.divide-gray-100 [:div#company-search-results {:class "h-48 px-3 pb-3 overflow-y-auto text-sm text-gray-700 dark:text-gray-200"}] [:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"} diff --git a/src/clj/auto_ap/ssr/components.clj b/src/clj/auto_ap/ssr/components.clj index f7d3fae5..75233aa2 100644 --- a/src/clj/auto_ap/ssr/components.clj +++ b/src/clj/auto_ap/ssr/components.clj @@ -8,13 +8,15 @@ [auto-ap.ssr.components.navbar :as navbar] [auto-ap.ssr.components.page :as page] [auto-ap.ssr.components.data-grid :as data-grid] - [auto-ap.ssr.components.tags :as tags])) + [auto-ap.ssr.components.tags :as tags] + [auto-ap.ssr.components.paginator :as paginator])) (def breadcrumbs breadcrumbs/breadcrumbs-) (def button buttons/button-) (def button-icon buttons/button-icon-) (def icon-button buttons/icon-button-) +(def a-icon-button buttons/a-icon-button-) (def modal dialog/modal-) (def modal-card dialog/modal-card-) @@ -44,62 +46,9 @@ :class (str "font-medium text-blue-600 dark:text-blue-500 hover:underline " class)}] children)) -(defn bound [x y z] - (cond - (< z x) - x - (< y x) - x - (> y z) - z - :else - y)) - -(def elipsis-button - [:p {:href "#", :class "flex items-center justify-center px-3 py-2 text-sm leading-tight text-gray-500 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400"} "..."]) - -(defn paginator- [{:keys [start per-page end total a-params]}] - (let [per-page (or per-page 20) - max-buttons 5 - buttons-before (Math/floor (/ max-buttons 2)) - total-pages (long (Math/max (long 1) (long (Math/ceil (/ total per-page))))) - current-page (long (Math/floor (/ start per-page))) - first-page-button (bound 0 (- current-page buttons-before) (- total-pages max-buttons)) - all-buttons (into [] (for [x (range total-pages)] - [:li - [:a (-> (a-params x) - (update - :class #(cond-> % - true (str " flex items-center justify-center px-3 py-2 text-sm leading-tight border ") - - (= current-page x) - (str " text-primary-600 bg-primary-50 border-primary-300 hover:bg-primary-100 hover:text-primary-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white") - - (not= current-page x) - (str " text-gray-500 bg-white border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"))) - (assoc :href "#")) - (inc x)]])) - last-page-button (Math/min (long total-pages) (long (+ max-buttons first-page-button))) - extended-last-page-button (when (not= last-page-button total-pages) - (list - elipsis-button - (last all-buttons))) +(def paginator paginator/paginator-) +(def data-grid-card data-grid/data-grid-card-) - extended-first-page-button (when (not= first-page-button 0) - (list - (first all-buttons) - elipsis-button))] - [:nav - [:ul {:class "inline-flex items-stretch -space-x-px"} - extended-first-page-button - (apply list (subvec all-buttons first-page-button last-page-button)) - extended-last-page-button]])) - -(defn paginator [{:keys [start per-page end total a-params] :as params}] - [:nav {:class "flex flex-col items-start justify-between p-4 space-y-3 md:flex-row md:items-center md:space-y-0", :aria-label "Table navigation"} - [:span {:class "text-sm font-normal text-gray-500 dark:text-gray-400"} - [:span {:class "font-semibold text-gray-900 dark:text-white"} (str (inc start)) "-" (str end) " of " (str total)]] - (paginator- params)]) diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index bf630990..f486f88c 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -1,6 +1,8 @@ (ns auto-ap.ssr.components.aside (:require [auto-ap.ssr.svg :as svg] - [hiccup2.core :as hiccup])) + [hiccup2.core :as hiccup] + [bidi.bidi :as bidi] + [auto-ap.ssr-routes :as ssr-routes])) (defn menu-button- [params & children] [:a (-> params @@ -188,7 +190,9 @@ [:ul {:class "space-y-2"} [:li - (menu-button- {:icon svg/report} + (menu-button- {:icon svg/report + :href (bidi/path-for ssr-routes/only-routes + :company-reports)} "Reports")] [:li (menu-button- {:icon svg/bank} @@ -198,5 +202,8 @@ "Vendors")] [:li - (menu-button- {:icon svg/government-building} - "1099 Vendor Info")]]) + (menu-button- {:icon svg/government-building + :href (bidi/path-for ssr-routes/only-routes + :company-1099)} + "1099 Vendor Info" + )]]) diff --git a/src/clj/auto_ap/ssr/components/buttons.clj b/src/clj/auto_ap/ssr/components/buttons.clj index 36f83a12..17b5f90e 100644 --- a/src/clj/auto_ap/ssr/components/buttons.clj +++ b/src/clj/auto_ap/ssr/components/buttons.clj @@ -16,6 +16,13 @@ (defn icon-button- [params & children] (into [:button (update params :class str " inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center p-3 text-sm font-medium border border-gray-300 dark:border-gray-700 text-center text-gray-500 hover:text-gray-800 rounded-lg dark:text-gray-400 dark:hover:text-gray-100") + [:div.htmx-indicator.flex.items-center + (svg/spinner {:class "inline w-4 h-4 text-white"})] + [:div.htmx-indicator-hidden.inline-flex.gap-2.items-center.justify-center (into [:div.h-4.w-4] children)]])) + +(defn a-icon-button- [params & children] + (into + [:a (update params :class str " inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center p-3 text-sm font-medium border border-gray-300 dark:border-gray-700 text-center text-gray-500 hover:text-gray-800 rounded-lg dark:text-gray-400 dark:hover:text-gray-100") [:div.h-4.w-4 children]])) (defn save-button- [params & children] diff --git a/src/clj/auto_ap/ssr/components/data_grid.clj b/src/clj/auto_ap/ssr/components/data_grid.clj index f6e33b9e..7a0dbf73 100644 --- a/src/clj/auto_ap/ssr/components/data_grid.clj +++ b/src/clj/auto_ap/ssr/components/data_grid.clj @@ -1,13 +1,17 @@ -(ns auto-ap.ssr.components.data-grid) +(ns auto-ap.ssr.components.data-grid + (:require + [auto-ap.ssr-routes :as ssr-routes] + [auto-ap.ssr.components.card :refer [content-card-]] + [auto-ap.ssr.components.paginator :refer [paginator-]] + [bidi.bidi :as bidi])) (defn header- [params & rest] (into [:th.px-4.py-3 {:scope "col" :class (:class params)} ] rest)) (defn row- [params & rest] - (into [:tr {:class (cond-> "border-b dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700" - - (:class params) (str " " (:class params)))}] rest)) + (into [:tr (update params + :class str " border-b dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700")] rest)) (defn cell- [params & rest] (into [:td.px-4.py-2 {:class (:class params)}] rest)) @@ -32,3 +36,50 @@ (into [:tbody] rest)]) + +(defn data-grid-card- [{:keys [id + route + title + entity-name + action-buttons + total + start + per-page + flash-id + headers + rows] :as params}] + [:div {:hx-get (bidi/path-for ssr-routes/only-routes + route + :request-method :get) + :hx-trigger "clientSelected from:body" + :hx-swap "outerHTML swap:300ms" + :id id} + (content-card- + {} + [:div {:class "flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"} + [:div + [:h1.text-2xl.mb-3.font-bold title] + [:div {:class "flex items-center flex-1 space-x-4"} + [:h5 + [:span (format "Total %s:" entity-name)] + [:span {:class "dark:text-white pl-4"} total]]]] + (into [:div {:class "flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"} + ] + action-buttons)] + [:div {:class "overflow-x-auto"} + (data-grid- {:headers headers + } + rows + + )] + (paginator- {:start start + :end (Math/min (+ start per-page) total) + :per-page per-page + :total total + :a-params (fn [page] + {:hx-get (str (bidi/path-for ssr-routes/only-routes + route + :request-method :get) + "?start=" (* page per-page)) + :hx-target (str "#" id) + :hx-swap "outerHTML show:#app:top"})}))]) diff --git a/src/clj/auto_ap/ssr/components/paginator.clj b/src/clj/auto_ap/ssr/components/paginator.clj new file mode 100644 index 00000000..c89caffb --- /dev/null +++ b/src/clj/auto_ap/ssr/components/paginator.clj @@ -0,0 +1,61 @@ +(ns auto-ap.ssr.components.paginator) + +(defn bound [x y z] + (cond + (< z x) + x + (< y x) + x + (> y z) + z + :else + y)) + +(def elipsis-button + [:p {:href "#", :class "flex items-center justify-center px-3 py-2 text-sm leading-tight text-gray-500 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400"} "..."]) + +(defn paginator-internal- [{:keys [start per-page end total a-params]}] + (let [per-page (or per-page 20) + max-buttons 5 + buttons-before (Math/floor (/ max-buttons 2)) + total-pages (long (Math/max (long 1) (long (Math/ceil (/ total per-page))))) + current-page (long (Math/floor (/ start per-page))) + first-page-button (bound 0 (- current-page buttons-before) (- total-pages max-buttons)) + all-buttons (into [] (for [x (range total-pages)] + [:li + [:a (-> (a-params x) + (update + :class #(cond-> % + true (str " flex items-center justify-center px-3 py-2 text-sm leading-tight border ") + + (= current-page x) + (str " text-primary-600 bg-primary-50 border-primary-300 hover:bg-primary-100 hover:text-primary-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white") + + (not= current-page x) + (str " text-gray-500 bg-white border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"))) + (assoc :href "#")) + (inc x)]])) + + + last-page-button (Math/min (long total-pages) (long (+ max-buttons first-page-button))) + + extended-last-page-button (when (not= last-page-button total-pages) + (list + elipsis-button + (last all-buttons))) + + extended-first-page-button (when (not= first-page-button 0) + (list + (first all-buttons) + elipsis-button))] + [:nav + [:ul {:class "inline-flex items-stretch -space-x-px"} + extended-first-page-button + (apply list (subvec all-buttons first-page-button last-page-button)) + extended-last-page-button]])) + +(defn paginator- [{:keys [start per-page end total a-params] :as params}] + [:nav {:class "flex flex-col items-start justify-between p-4 space-y-3 md:flex-row md:items-center md:space-y-0", :aria-label "Table navigation"} + [:span {:class "text-sm font-normal text-gray-500 dark:text-gray-400"} + [:span {:class "font-semibold text-gray-900 dark:text-white"} (str (inc start)) "-" (str end) " of " (str total)]] + (paginator-internal- params)]) diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index 32f1277b..70725b2a 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -8,6 +8,7 @@ [auto-ap.ssr.company.company-1099 :as company-1099] [auto-ap.ssr.search :as search] [auto-ap.ssr.company-dropdown :as company-dropdown] + [auto-ap.ssr.company.reports :as company-reports] [auto-ap.routes.ezcater-xls :as ezcater-xls])) ;; from auto-ap.ssr-routes, because they're shared @@ -24,6 +25,9 @@ :company-1099-vendor-table (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-table)) :company-1099-vendor-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-dialog)) :company-1099-vendor-save (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-save)) + :company-reports (wrap-client-redirect-unauthenticated (wrap-secure company-reports/page)) + :company-reports-table (wrap-client-redirect-unauthenticated (wrap-secure company-reports/reports-table)) + :company-reports-delete (wrap-client-redirect-unauthenticated (wrap-admin company-reports/delete-report)) :transaction-insights (wrap-client-redirect-unauthenticated (wrap-secure insights/page)) :transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-secure insights/insight-table)) :transaction-insight-rows (wrap-client-redirect-unauthenticated (wrap-secure insights/transaction-rows)) diff --git a/src/clj/auto_ap/ssr/svg.clj b/src/clj/auto_ap/ssr/svg.clj index be39d49b..c3a5d88f 100644 --- a/src/clj/auto_ap/ssr/svg.clj +++ b/src/clj/auto_ap/ssr/svg.clj @@ -197,3 +197,77 @@ (def drop-down [:svg {:class "w-4 h-4 ml-2", :aria-hidden "true", :fill "none", :stroke "currentColor", :viewbox "0 0 24 24", :xmlns "http://www.w3.org/2000/svg"} [:path {:stroke-linecap "round", :stroke-linejoin "round", :stroke-width "2", :d "M19 9l-7 7-7-7"}]]) + +(def download + [:svg + {:xmlns "http://www.w3.org/2000/svg", :viewBox "0 0 24 24"} + [:defs] + [:title "download-thick-bottom"] + [:path + {:d + "M5.5,11.5c-.275,0-.341.159-.146.354l6.292,6.293a.5.5,0,0,0,.709,0l6.311-6.275c.2-.193.13-.353-.145-.355L15.5,11.5V1.5a1,1,0,0,0-1-1h-5a1,1,0,0,0-1,1V11a.5.5,0,0,1-.5.5Z", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:path + {:d "M23.5,18.5v4a1,1,0,0,1-1,1H1.5a1,1,0,0,1-1-1v-4", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}]]) + +(def trash + [:svg + {:xmlns "http://www.w3.org/2000/svg", :viewBox "0 0 24 24"} + [:defs] + [:title "bin-1"] + [:path + {:d + "M21,4.5,19.188,21.709A2,2,0,0,1,17.2,23.5H6.8a2,2,0,0,1-1.989-1.791L3,4.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:line + {:x1 "0.5", + :y1 "4.5", + :x2 "23.5", + :y2 "4.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:path + {:d "M7.5,4.5v-3a1,1,0,0,1,1-1h7a1,1,0,0,1,1,1v3", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:line + {:x1 "12", + :y1 "9", + :x2 "12", + :y2 "19.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:line + {:x1 "16.5", + :y1 "9", + :x2 "16", + :y2 "19.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}] + [:line + {:x1 "7.5", + :y1 "9", + :x2 "8", + :y2 "19.5", + :fill "none", + :stroke "currentColor", + :stroke-linecap "round", + :stroke-linejoin "round"}]]) diff --git a/src/clj/auto_ap/ssr/utils.clj b/src/clj/auto_ap/ssr/utils.clj index 189762b8..929d5d64 100644 --- a/src/clj/auto_ap/ssr/utils.clj +++ b/src/clj/auto_ap/ssr/utils.clj @@ -2,7 +2,8 @@ (:require [auto-ap.logging :as alog] [config.core :refer [env]] - [hiccup2.core :as hiccup])) + [hiccup2.core :as hiccup] + [clojure.string :as str])) (defn html-response [hiccup & {:keys [status headers] :or {status 200 headers {}}}] {:status status @@ -39,3 +40,24 @@ (ex-message e)] :status 500))))))) +(defn form-data->map [form-data] + (reduce-kv + (fn [acc k v] + (cond (and (string? v) + (empty? v)) + acc + + :else + (assoc-in acc (->> (str/split k #"_") + (mapv #(apply keyword (str/split % #"/")))) + v))) + {} + form-data)) + +(defn path->name [k] + (cond (keyword? k) + (str (namespace k) "/" (name k)) + + (seq k) + (str/join "_" (map path->name k)) + :else k)) diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index 07aa8b7d..83db9fbb 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -18,7 +18,11 @@ "/1099" :company-1099 "/1099/table" {:get :company-1099-vendor-table} "/1099/vendor-dialog" {["/" [#"\d+" :vendor-id]] {:get :company-1099-vendor-dialog - :post :company-1099-vendor-save}}}}) + :post :company-1099-vendor-save}} + "/reports" {"" {:get :company-reports + :delete :company-reports-delete}} + "/reports/table" :company-reports-table + }}) (def only-routes ["/" routes])