diff --git a/notes.txt b/notes.txt index 982939d2..af5c7d31 100644 --- a/notes.txt +++ b/notes.txt @@ -1,5 +1 @@ -1. Make admin sidebar -2. Make transaction sidebar -3. Test power users 4. Maybe make dark mode persist -5. redirect when not authenticated diff --git a/resources/public/output.css b/resources/public/output.css index daa21bf4..251d5b1f 100644 --- a/resources/public/output.css +++ b/resources/public/output.css @@ -1380,6 +1380,10 @@ input:checked + .toggle-bg { flex: 1 1 0%; } +.flex-none { + flex: none; +} + .flex-shrink { flex-shrink: 1; } @@ -1480,6 +1484,14 @@ input:checked + .toggle-bg { flex-direction: column; } +.flex-wrap { + flex-wrap: wrap; +} + +.content-start { + align-content: flex-start; +} + .items-start { align-items: flex-start; } @@ -1512,6 +1524,10 @@ input:checked + .toggle-bg { justify-content: space-between; } +.justify-items-start { + justify-items: start; +} + .gap-1 { gap: 0.25rem; } @@ -1587,6 +1603,10 @@ input:checked + .toggle-bg { border-color: rgb(243 244 246 / var(--tw-divide-opacity)); } +.justify-self-start { + justify-self: start; +} + .overflow-auto { overflow: auto; } @@ -1797,6 +1817,21 @@ input:checked + .toggle-bg { background-color: rgb(253 246 178 / var(--tw-bg-opacity)); } +.bg-green-200 { + --tw-bg-opacity: 1; + background-color: rgb(201 225 171 / var(--tw-bg-opacity)); +} + +.bg-blue-200 { + --tw-bg-opacity: 1; + background-color: rgb(153 215 247 / var(--tw-bg-opacity)); +} + +.bg-red-200 { + --tw-bg-opacity: 1; + background-color: rgb(255 154 154 / var(--tw-bg-opacity)); +} + .bg-opacity-50 { --tw-bg-opacity: 0.5; } @@ -2346,6 +2381,21 @@ input:checked + .toggle-bg { background-color: rgb(255 255 255 / var(--tw-bg-opacity)); } +.hover\:bg-green-300:hover { + --tw-bg-opacity: 1; + background-color: rgb(175 211 130 / var(--tw-bg-opacity)); +} + +.hover\:bg-blue-300:hover { + --tw-bg-opacity: 1; + background-color: rgb(102 196 242 / var(--tw-bg-opacity)); +} + +.hover\:bg-red-300:hover { + --tw-bg-opacity: 1; + background-color: rgb(255 104 104 / var(--tw-bg-opacity)); +} + .hover\:text-blue-600:hover { --tw-text-opacity: 1; color: rgb(0 125 187 / var(--tw-text-opacity)); @@ -2437,6 +2487,21 @@ input:checked + .toggle-bg { --tw-ring-color: rgb(121 181 46 / var(--tw-ring-opacity)); } +.focus\:ring-green-200:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(201 225 171 / var(--tw-ring-opacity)); +} + +.focus\:ring-blue-200:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(153 215 247 / var(--tw-ring-opacity)); +} + +.focus\:ring-red-200:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(255 154 154 / var(--tw-ring-opacity)); +} + .group:hover .group-hover\:scale-110 { --tw-scale-x: 1.1; --tw-scale-y: 1.1; @@ -2549,6 +2614,26 @@ input:checked + .toggle-bg { background-color: rgb(99 49 18 / var(--tw-bg-opacity)); } +:is(.dark .dark\:bg-green-500) { + --tw-bg-opacity: 1; + background-color: rgb(121 181 46 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-green-700) { + --tw-bg-opacity: 1; + background-color: rgb(73 109 28 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-blue-700) { + --tw-bg-opacity: 1; + background-color: rgb(0 94 140 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:bg-red-700) { + --tw-bg-opacity: 1; + background-color: rgb(153 2 2 / var(--tw-bg-opacity)); +} + :is(.dark .dark\:bg-opacity-80) { --tw-bg-opacity: 0.8; } @@ -2662,6 +2747,16 @@ input:checked + .toggle-bg { background-color: rgb(73 109 28 / var(--tw-bg-opacity)); } +:is(.dark .dark\:hover\:bg-blue-600:hover) { + --tw-bg-opacity: 1; + background-color: rgb(0 125 187 / var(--tw-bg-opacity)); +} + +:is(.dark .dark\:hover\:bg-red-600:hover) { + --tw-bg-opacity: 1; + background-color: rgb(204 2 2 / var(--tw-bg-opacity)); +} + :is(.dark .dark\:hover\:text-blue-500:hover) { --tw-text-opacity: 1; color: rgb(0 156 234 / var(--tw-text-opacity)); diff --git a/scratch-sessions/ingest_ml.clj b/scratch-sessions/ingest_ml.clj index d8247bed..75ac1740 100644 --- a/scratch-sessions/ingest_ml.clj +++ b/scratch-sessions/ingest_ml.clj @@ -1,8 +1,8 @@ (ns ingest-ml - (:require [datomic.client.api :as dc] + (:require [datomic.api :as dc] [clojure.java.io :as io] [clojure.data.csv :as csv] - [iol-ion.tx :refer [upsert-entity]] + #_[iol-ion.tx :refer [upsert-entity]] [auto-ap.datomic :refer [conn]])) (println "hi") @@ -13,23 +13,22 @@ (defn reset-inference [] - (doseq [p (->> - (dc/q '[:find ?t - :where [?t :transaction/recommended-account]] - (dc/db conn)) - (map (fn [[t]] - `(upsert-entity - ~{:db/id t - :transaction/recommended-account nil - :tranasction/vendor-confidence nil - :transaction/account-confidence nil - :transaction/recommended-vendor nil}))) - - (partition-all 100))] - (dc/transact conn {:tx-data p}))) + (doseq [p (->> + (dc/q '[:find ?t + :where [?t :transaction/recommended-account]] + (dc/db conn)) + (map (fn [[t]] + [:upsert-entity {:db/id t + :transaction/recommended-account nil + :tranasction/vendor-confidence nil + :transaction/account-confidence nil + :transaction/recommended-vendor nil}])) + + (partition-all 100))] + @(dc/transact conn p))) (defn read-inference [] - (with-open [reader (io/reader "/mnt/data/dev2/ml-test/inference-outcome.csv")] + (with-open [reader (io/reader "data/inference-outcome.csv")] (->> (csv/read-csv reader) (into [] (comp @@ -43,7 +42,7 @@ (defn apply-inference [inference] (doseq [p (->> inference (partition-all 100))] - (dc/transact conn {:tx-data p}))) + @(dc/transact conn p))) (defn check-applied-inference [] @@ -69,3 +68,20 @@ (dc/db conn)) (shuffle) (take 10)))) + + +(defn random-infer [] + (let [n (rand-int 1000) + vs (into [] (map first (dc/q '[:find ?v :in $ :where [?v :vendor/name]] (dc/db conn)))) + as (into [] (map first (dc/q '[:find ?v :in $ :where [?v :account/name]] (dc/db conn))))] + (->> + (dc/qseq {:query '[:find ?t + :where [?t :transaction/client]] + :args [(dc/db conn)]}) + (map first) + (take n) + (map (fn [t] + {:db/id t + :transaction/recommended-account (rand-nth as) + :transaction/account-confidence (double (/ (double (rand-int 100)) 100.0)) + :transaction/recommended-vendor (rand-nth vs)}))))) diff --git a/scratch-sessions/pnl-export.repl b/scratch-sessions/pnl-export.repl new file mode 100644 index 00000000..ca27b7a8 --- /dev/null +++ b/scratch-sessions/pnl-export.repl @@ -0,0 +1,83 @@ +;; This buffer is for Clojure experiments and evaluation. + +;; Press C-j to evaluate the last expression. + +;; You can also press C-u C-j to evaluate the expression and pretty-print its result. +(dc/pull (dc/db conn) [:db/id] [:client/code "NGAK"]) + +(user/init-repl) + +(require '[auto-ap.datomic :refer [pull-attr pull-many]]) + +(csv/write-csv *out* + (let [clients (pull-many (dc/db conn) '[:db/id :client/code :client/locations] [[:client/code "NGAK"] [:client/code "NGOP"] #_#_[:client/code "NGRV"] [:client/code "NGE1"]]) + account->numeric-code (into {} (seq (dc/q '[:find ?i ?n + :in $ [?a ...] + :where [?i ?a ?n]] + (dc/db conn) [:account/numeric-code :bank-account/numeric-code]))) + account->name (let [lookup (into {} (seq (dc/q '[:find ?i ?n + :in $ + :where [?i :account/name ?n]] + (dc/db conn))))] + (fn [x] + (or (lookup x) "Bank Account"))) + account->location (into {} (seq (dc/q '[:find ?i ?l + :in $ + :where [?i :account/location ?l]] + (dc/db conn)))) + used-accounts (into #{} (for [c clients + d (dc/index-range (dc/db conn) + :journal-entry-line/client+account+location+date + [(:db/id c)] + [(inc (:db/id c))]) + :let [[_ account] (:v d)]] + account))] + (for [client clients + :let [all-entries (->> (dc/index-pull (dc/db conn) + {:index :avet + :selector [:db/id :journal-entry-line/running-balance :journal-entry-line/client+account+location+date] + :start [:journal-entry-line/client+account+location+date [(:db/id client)]] + :reverse false + :limit 1}) + (take-while (fn [curr] + (= (first (:journal-entry-line/client+account+location+date curr)) + (:db/id client))))) + account-lookup (->> all-entries + (reduce + (fn [acc curr] + (let [[client account location date] (:journal-entry-line/client+account+location+date curr) + numeric-code (account->numeric-code account)] + (assoc acc (format "%d-%d-%s-%s" client numeric-code location (atime/unparse-local (clj-time.coerce/to-date-time date) atime/iso-date)) (:journal-entry-line/running-balance curr)))) + (sorted-map)))] + a (sort-by account->numeric-code used-accounts) + :let [numeric-code (account->numeric-code a)] + l (or (account->location a) (:client/locations client)) + delta (range -60 0) + :let [date (t/plus (c/to-date-time #inst "2023-06-12T08:00:00") (t/days delta)) + date-str (atime/unparse-local date atime/iso-date)] + :when (and numeric-code (>= numeric-code 40000))] + (conj [(:client/code client) l numeric-code (account->name a) date-str] (with-precision 2 + (double (.setScale (bigdec (or + (some->> (subseq account-lookup >= (format "%d-%d-%s-%s" (:db/id client) numeric-code l "2000-01-01") <= (format "%d-%d-%s-%s" (:db/id client) numeric-code l date-str)) + last + last) + 0.0)) + 2 java.math.RoundingMode/HALF_UP)))))) + ) + +(dc/q '[:find (count ?x) + :where [?x :journal-entry-line/dirty true]] + (dc/db conn)) + +(let [clients (map first + (dc/q '[:find (pull ?c [:client/code :db/id]) + :where [?c :client/code]] + (dc/db conn)))] + (doseq [[c i] (map vector clients (range))] + (let [db (dc/db conn) + accounts-needing-rebuild (auto-ap.ledger/accounts-needing-rebuild db (:db/id c))] + (when (seq accounts-needing-rebuild) + (println "C" c "needs" (count accounts-needing-rebuild) "built") + #_(refresh-running-balance-accounts accounts-needing-rebuild clients c i db) + #_(mu/log ::client-completed))))) +, diff --git a/src/clj/auto_ap/routes/auth.clj b/src/clj/auto_ap/routes/auth.clj index 50698bf9..d6e4e2b9 100644 --- a/src/clj/auto_ap/routes/auth.clj +++ b/src/clj/auto_ap/routes/auth.clj @@ -20,18 +20,18 @@ (:jwt-secret env) {:alg :hs512})) -(defn oauth [{{:strs [code]} :query-params {:strs [host]} :headers}] +(defn oauth [{{:strs [code state]} :query-params {:strs [host]} :headers :as request}] (try (let [auth (-> "https://accounts.google.com/o/oauth2/token" - (http/post - {:form-params {"client_id" google-client-id - "client_secret" google-client-secret - "code" code - "redirect_uri" (str (:scheme env) "://" host "/api/oauth") - "grant_type" "authorization_code"} - :as :json}) + (http/post + {:form-params {"client_id" google-client-id + "client_secret" google-client-secret + "code" code + "redirect_uri" (str (:scheme env) "://" host "/api/oauth") + "grant_type" "authorization_code"} + :as :json}) :body) - + token (:access_token auth) profile (-> (http/get "https://www.googleapis.com/oauth2/v1/userinfo" {:headers {"Authorization" (str "Bearer " token)} :as :json}) @@ -54,19 +54,18 @@ _ (mu/log ::logged-in-as :auth auth)] ;; TODO - these namespaces are not being transmitted/deserialized properly - + (if (and token user) (let [jwt (jwt/sign auth (:jwt-secret env) {:alg :hs512})] - {:status 301 - :headers {"Location" (str "/?jwt=" jwt)} + :headers {"Location" (str (or (not-empty state) "/") "?jwt=" jwt)} :session {:identity (dissoc auth :exp)}}) {:status 401 :body "Couldn't authenticate"})) (catch Exception e - (log/warn e ) + (log/warn e) {:status 401 :body (str "Couldn't authenticate " (.toString e))}))) diff --git a/src/clj/auto_ap/routes/utils.clj b/src/clj/auto_ap/routes/utils.clj index ee803775..f8747c9d 100644 --- a/src/clj/auto_ap/routes/utils.clj +++ b/src/clj/auto_ap/routes/utils.clj @@ -8,30 +8,33 @@ (defn wrap-secure [handler] (fn [request] (cond (authenticated? request) - (handler request) + (handler request) + (get (:headers request) "hx-request") + {:status 401 + :headers {"hx-redirect" (str "/login?" + (url/map->query {"redirect-to" (:uri request)}))}} - (get (:headers request) "hx-request") - {:status 401 - :headers {"hx-redirect" "/login"}} - - :else - {:status 302 - :headers {"Location" "/login" }}))) + :else + {:status 302 + :headers {"Location" (str "/login?" + (url/map->query {"redirect-to" (:uri request)}))}}))) (defn wrap-admin [handler] (fn [request] (if (is-admin? (:identity request)) (handler request) - (do + (do (alog/warn ::unauthenticated) {:status 302 - :headers {"Location" "/login"}})))) + :headers {"Location" (str "/login?" + (url/map->query {"redirect-to" (:uri request)}))}})))) (defn wrap-client-redirect-unauthenticated [handler] (fn [request] (let [response (handler request)] (if (= 401 (get response :status)) - (-> response - (assoc-in [:headers "hx-redirect"] "/login/")) + (-> response + (assoc-in [:headers "hx-redirect"] (str "/login?" + (url/map->query {"redirect-to" (:uri request)})))) response)))) diff --git a/src/clj/auto_ap/ssr/company_dropdown.clj b/src/clj/auto_ap/ssr/company_dropdown.clj index 43d71702..5b3a31a7 100644 --- a/src/clj/auto_ap/ssr/company_dropdown.clj +++ b/src/clj/auto_ap/ssr/company_dropdown.clj @@ -126,16 +126,17 @@ function initCompanyDropdown() { (defn active-client [{:keys [identity params] :as request}] (let [client-id (some-> (or (:search-client params) (get params "search-client")) not-empty Long/parseLong)] + (println (format "HERE CLIENT ID '%s'" client-id)) (when client-id (assert-can-see-client identity client-id)) (let [new-session (assoc (:session request) :client (when client-id (dc/pull (dc/db conn) [:db/id :client/name :client/code] client-id)))] - (assoc - (html-response - (dropdown {:client (:client new-session) - :identity identity})) - :session - new-session - :headers - {"hx-trigger" "clientSelected"})))) + (assoc + (html-response + (dropdown {:client (:client new-session) + :identity identity})) + :session + new-session + :headers + {"hx-trigger" "clientSelected"})))) diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index 09505be4..5a885d99 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -150,69 +150,90 @@ (defn main-aside-nav- [] [:ul {:class "space-y-2"} - [:li - (menu-button- {:icon svg/pie} - "Dashboard")] - [:li - (menu-button- {:aria-controls "dropdown-invoices", - :data-collapse-toggle "dropdown-invoices" - :icon svg/accounting-invoice-mail} - "Invoices") - (sub-menu- {:id "dropdown-invoices"} - (menu-button- {:href "http://google.com"} - "All") - (menu-button- {:href "http://google.com"} - "Paid") - (menu-button- {:href "http://google.com"} - "Unpaid") - (menu-button- {:href "http://google.com"} - "Voided")) - ] - [:li - (menu-button- {:aria-controls "dropdown-sales", - :data-collapse-toggle "dropdown-sales" - :icon svg/receipt-register-1} - "Sales") - (sub-menu- {:id "dropdown-sales"} - (menu-button- {:href "Sales"} "Sales") - (menu-button- {:href "Sales"} "Expected Deposits") - (menu-button- {:href "Sales"} "Cash Shifts") - (menu-button- {:href "Sales"} "Tenders"))] - [:li - (menu-button- {:aria-controls "dropdown-payments" - :data-collapse-toggle "dropdown-payments" - :icon svg/payments} - "Payments") - (sub-menu- {:id "dropdown-payments"} - (menu-button- {:href "Sales"} "All") - (menu-button- {:href "Sales"} "Pending") - (menu-button- {:href "Sales"} "Cleared") - (menu-button- {:href "Sales"} "Voided"))] - - [:li - (menu-button- {:aria-controls "dropdown-transactions" - :data-collapse-toggle "dropdown-transactions" - :icon svg/bank} - "Transactions") - - (sub-menu- {:id "dropdown-transactions"} - (menu-button- {:href "Sales"} "All") - (menu-button- {:href "Sales"} "Unapproved") - (menu-button- {:href "Sales"} "Client Review") - (menu-button- {:href "Sales"} "Approved") - (menu-button- {:href "Sales"} "Insights"))] - [:li - (menu-button- {:aria-controls "dropdown-ledger" - :data-collapse-toggle "dropdown-ledger" - :icon svg/receipt} - "Ledger") - (sub-menu- {:id "dropdown-ledger"} - (menu-button- {:href "Sales"} "Register") - (menu-button- {:href "Sales"} "Profit & Loss") - (menu-button- {:href "Sales"} "Profit & Loss Detail") - (menu-button- {:href "Sales"} "Cash Flows") - (menu-button- {:href "Sales"} "Balance Sheet") - (menu-button- {:href "Sales"} "External Ledger Import"))]]) + [:li + (menu-button- {:icon svg/pie + :href "/"} + "Dashboard")] + [:li + (menu-button- {:aria-controls "dropdown-invoices", + :data-collapse-toggle "dropdown-invoices" + :icon svg/accounting-invoice-mail} + "Invoices") + (sub-menu- {:id "dropdown-invoices"} + (menu-button- {:href (bidi/path-for client-routes/routes + :invoices)} + "All") + (menu-button- {:href (bidi/path-for client-routes/routes + :paid-invoices)} + "Paid") + (menu-button- {:href (bidi/path-for client-routes/routes + :unpaid-invoices)} + "Unpaid") + (menu-button- {:href (bidi/path-for client-routes/routes + :voided-invoices)} + "Voided"))] + [:li + (menu-button- {:aria-controls "dropdown-sales", + :data-collapse-toggle "dropdown-sales" + :icon svg/receipt-register-1} + "Sales") + (sub-menu- {:id "dropdown-sales"} + (menu-button- {:href (bidi/path-for client-routes/routes + :sales-orders)} "Sales") + (menu-button- {:href (bidi/path-for client-routes/routes + :expected-deposits)} "Expected Deposits") + #_(menu-button- {:href "Sales"} "Cash Shifts") + #_(menu-button- {:href "Sales"} "Tenders"))] + [:li + (menu-button- {:aria-controls "dropdown-payments" + :data-collapse-toggle "dropdown-payments" + :icon svg/payments} + "Payments") + (sub-menu- {:id "dropdown-payments"} + (menu-button- {:href (bidi/path-for client-routes/routes + :payments)} "All") + (menu-button- {:href (bidi/path-for client-routes/routes + :payments)} "Pending") + (menu-button- {:href (bidi/path-for client-routes/routes + :payments)} "Cleared") + (menu-button- {:href (bidi/path-for client-routes/routes + :payments)} "Voided"))] + + [:li + (menu-button- {:aria-controls "dropdown-transactions" + :data-collapse-toggle "dropdown-transactions" + :icon svg/bank} + "Transactions") + + (sub-menu- {:id "dropdown-transactions"} + (menu-button- {:href (bidi/path-for client-routes/routes + :transactions)} "All") + (menu-button- {:href (bidi/path-for client-routes/routes + :unapproved-transactions)} "Unapproved") + (menu-button- {:href (bidi/path-for client-routes/routes + :requires-feedback-transactions)} "Client Review") + (menu-button- {:href (bidi/path-for client-routes/routes + :approved-transactions)} "Approved") + (menu-button- {:href (bidi/path-for ssr-routes/only-routes + :transaction-insights)} "Insights"))] + [:li + (menu-button- {:aria-controls "dropdown-ledger" + :data-collapse-toggle "dropdown-ledger" + :icon svg/receipt} + "Ledger") + (sub-menu- {:id "dropdown-ledger"} + (menu-button- {:href (bidi/path-for client-routes/routes + :ledger)} "Register") + (menu-button- {:href (bidi/path-for client-routes/routes + :profit-and-loss)} "Profit & Loss") + (menu-button- {:href (bidi/path-for client-routes/routes + :profit-and-loss-detail)} "Profit & Loss Detail") + (menu-button- {:href (bidi/path-for client-routes/routes + :cash-flows)} "Cash Flows") + (menu-button- {:href (bidi/path-for client-routes/routes + :balance-sheet)} "Balance Sheet") + (menu-button- {:href (bidi/path-for client-routes/routes + :external-import-ledger)} "External Ledger Import"))]]) (defn company-aside-nav- [] @@ -301,3 +322,5 @@ (menu-button- {:href (bidi/path-for ssr-routes/only-routes :admin-ezcater-xls) :hx-boost "true"} "EZCater XLS Import"))]]) + + diff --git a/src/clj/auto_ap/ssr/components/navbar.clj b/src/clj/auto_ap/ssr/components/navbar.clj index 5c044b3a..692b4cc8 100644 --- a/src/clj/auto_ap/ssr/components/navbar.clj +++ b/src/clj/auto_ap/ssr/components/navbar.clj @@ -1,50 +1,45 @@ (ns auto-ap.ssr.components.navbar (:require - [auto-ap.datomic :refer [conn pull-attr]] - [auto-ap.client-routes :as client-routes2] + [auto-ap.graphql.utils :refer [is-admin?]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.company-dropdown :as cd] [auto-ap.ssr.components.buttons :refer [icon-button-]] + [auto-ap.ssr.components.user-dropdown :as user-dropdown] [auto-ap.ssr.svg :as svg] - [bidi.bidi :as bidi] - [datomic.api :as dc] - [hiccup2.core :as hiccup] - [auto-ap.ssr.components.user-dropdown :as user-dropdown])) + [bidi.bidi :as bidi])) (defn navbar- [{:keys [client identity]}] [:nav {:class "fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700"} [:div {:class "px-3 py-3 lg:px-5 lg:pl-3"} - [:div {:class "flex items-center justify-between"} - [:div {:class "flex items-center justify-start"} - [:button { :aria-controls "left-nav", :id "left-nav-toggle" :type "button", :class "inline-flex items-center p-2 mt-2 ml-2 mr-2 text-sm text-gray-500 rounded-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"} - [:span {:class "sr-only"} "Open sidebar"] - [:svg {:class "w-6 h-6", :aria-hidden "true", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} - [:path {:clip-rule "evenodd", :fill-rule "evenodd", :d "M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z"}]]] - [:a {:href "/" :class "flex ml-2 md:mr-24"} - [:img {:src "/img/logo-big2.png", :class "h-10 mr-16", :alt "Integreat logo"}] - ] - ] - - [:div {:class "flex items-center gap-4"} - [:button.mt-1.lg:w-96.relative.hidden.lg:block {:class "bg-gray-50 hover:bg-gray-200 dark:hover:bg-gray-700 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 w-full pl-10 py-4 pr-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 gap-4 " - :hx-get (bidi/path-for ssr-routes/only-routes - :search) - :hx-target "#modal-holder" - :hx-swap "outerHTML"} - [:div {:class "absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-gray-500"} - [:div.w-4.h-4 svg/search] - [:span.ml-2 "Search"]]] - [:div {:class "hidden mr-3 -mb-1 sm:block"} - [:span]] - (icon-button- - {:id "toggleSidebarMobileSearch", :type "button", :class "p-2 text-gray-500 rounded-lg lg:hidden hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white" - :hx-get (bidi/path-for ssr-routes/only-routes - :search) - :hx-target "#modal-holder" - :hx-swap "outerHTML"} - svg/search) - (cd/dropdown {:client client :identity identity}) - - - (user-dropdown/dropdown {:identity identity}) - ]]]]) + [:div {:class "flex items-center justify-between"} + [:div {:class "flex items-center justify-start"} + [:button {:aria-controls "left-nav", :id "left-nav-toggle" :type "button", :class "inline-flex items-center p-2 mt-2 ml-2 mr-2 text-sm text-gray-500 rounded-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"} + [:span {:class "sr-only"} "Open sidebar"] + [:svg {:class "w-6 h-6", :aria-hidden "true", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} + [:path {:clip-rule "evenodd", :fill-rule "evenodd", :d "M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z"}]]] + [:a {:href "/" :class "flex ml-2 md:mr-24"} + [:img {:src "/img/logo-big2.png", :class "h-10 mr-16", :alt "Integreat logo"}]]] + + [:div {:class "flex items-center gap-4"} + + (when (is-admin? identity) + [:button.mt-1.lg:w-96.relative.hidden.lg:block {:class "bg-gray-50 hover:bg-gray-200 dark:hover:bg-gray-700 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 w-full pl-10 py-4 pr-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 gap-4 " + :hx-get (bidi/path-for ssr-routes/only-routes + :search) + :hx-target "#modal-holder" + :hx-swap "outerHTML"} + [:div {:class "absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-gray-500"} + [:div.w-4.h-4 svg/search] + [:span.ml-2 "Search"]]]) + [:div {:class "hidden mr-3 -mb-1 sm:block"} + [:span]] + (icon-button- + {:id "toggleSidebarMobileSearch", :type "button", :class "p-2 text-gray-500 rounded-lg lg:hidden hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white" + :hx-get (bidi/path-for ssr-routes/only-routes + :search) + :hx-target "#modal-holder" + :hx-swap "outerHTML"} + svg/search) + (cd/dropdown {:client client :identity identity}) + + (user-dropdown/dropdown {:identity identity})]]]]) diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index 0dfe92e2..171b4965 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -20,7 +20,7 @@ :admin-history (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page))) :admin-history-search (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page))) :admin-history-inspect (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/inspect))) - :active-client (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin company-dropdown/active-client))) + :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)) :company (wrap-client-redirect-unauthenticated (wrap-secure company/page)) @@ -35,11 +35,11 @@ :company-reports (wrap-client-redirect-unauthenticated (wrap-secure company-reports/page)) :company-reports-table (wrap-client-redirect-unauthenticated (wrap-secure company-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)) - :transaction-insight-approve (wrap-client-redirect-unauthenticated (wrap-secure insights/approve)) - :transaction-insight-explain (wrap-client-redirect-unauthenticated (wrap-secure insights/explain)) + :transaction-insights (wrap-client-redirect-unauthenticated (wrap-admin insights/page)) + :transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-admin insights/insight-table)) + :transaction-insight-rows (wrap-client-redirect-unauthenticated (wrap-admin insights/transaction-rows)) + :transaction-insight-approve (wrap-client-redirect-unauthenticated (wrap-admin insights/approve)) + :transaction-insight-explain (wrap-client-redirect-unauthenticated (wrap-admin insights/explain)) :admin-ezcater-xls (wrap-client-redirect-unauthenticated (wrap-admin ezcater-xls/page)) :search (wrap-client-redirect-unauthenticated (wrap-secure search/dialog-contents))}) diff --git a/src/clj/auto_ap/ssr/transaction/insights.clj b/src/clj/auto_ap/ssr/transaction/insights.clj index df3e0423..4c716ec1 100644 --- a/src/clj/auto_ap/ssr/transaction/insights.clj +++ b/src/clj/auto_ap/ssr/transaction/insights.clj @@ -205,7 +205,7 @@ (defn page [{:keys [identity matched-route session] :as request}] (base-page request - (com/page {:nav (com/admin-aside-nav) + (com/page {:nav (com/main-aside-nav) :active-client (:client (:session request)) :identity (:identity request) :app-params {:hx-get (bidi/path-for ssr-routes/only-routes diff --git a/src/cljs/auto_ap/views/components/layouts.cljs b/src/cljs/auto_ap/views/components/layouts.cljs index b67e0d39..e342fc32 100644 --- a/src/cljs/auto_ap/views/components/layouts.cljs +++ b/src/cljs/auto_ap/views/components/layouts.cljs @@ -1,23 +1,21 @@ (ns auto-ap.views.components.layouts (:require [auto-ap.events :as events] + [auto-ap.forms :as forms] + [auto-ap.forms.builder :as form-builder] [auto-ap.routes :as routes] + [auto-ap.ssr-routes :as ssr-routes] [auto-ap.subs :as subs] [auto-ap.views.components.modal :as modal] [auto-ap.views.components.search :as search] [auto-ap.views.components.vendor-dialog :as vendor-dialog] [auto-ap.views.utils - :refer [active-when - appearing - dispatch-event-with-propagation - login-url]] + :refer [active-when appearing dispatch-event-with-propagation login-url]] [bidi.bidi :as bidi] [clojure.string :as str] [re-frame.core :as re-frame] [reagent.core :as r] - [auto-ap.forms.builder :as form-builder] - [vimsical.re-frame.cofx.inject :as inject] - [auto-ap.forms :as forms])) + [vimsical.re-frame.cofx.inject :as inject])) (defn navbar-drop-down [{:keys [ class]} _] (let [!child (r/atom nil)] @@ -47,19 +45,19 @@ (defn login-dropdown [] (let [user (re-frame/subscribe [::subs/user])] (if @user - [navbar-drop-down {:header [:span [:span.icon [:i.fa.fa-user] ] + [navbar-drop-down {:header [:span [:span.icon [:i.fa.fa-user]] [:span (:user/name @user)]] :id ::account} [:div [:a {:class "navbar-item" - :href (bidi/path-for routes/routes :reports)} "My company"] + :href (bidi/path-for auto-ap.ssr-routes/only-routes :company)} "My company"] [:a.dropdown-item {:on-click (dispatch-event-with-propagation [::vendor-dialog/started {}])} "New Vendor"] [:a.dropdown-item {:on-click (dispatch-event-with-propagation [::vendor-dialog/edit {}])} "Edit Vendor"] (when (= "admin" (:user/role @user)) [:a {:class "navbar-item" :href (bidi/path-for routes/routes :admin)} "Administration"]) [:hr {:class "navbar-divider"}] - [:a.navbar-item {:on-click (fn [e] (.preventDefault e) (re-frame/dispatch [::events/logout]))} "Logout"]]] - [:a.navbar-item {:href login-url} "Login"]))) + [:a.navbar-item {:on-click (fn [e] (.preventDefault e) (re-frame/dispatch [::events/logout]))} "Logout"]]] + [:a.navbar-item {:href (login-url)} "Login"]))) (re-frame/reg-sub ::client-search diff --git a/src/cljs/auto_ap/views/pages/login.cljs b/src/cljs/auto_ap/views/pages/login.cljs index abfb9547..76b63982 100644 --- a/src/cljs/auto_ap/views/pages/login.cljs +++ b/src/cljs/auto_ap/views/pages/login.cljs @@ -1,7 +1,8 @@ (ns auto-ap.views.pages.login (:require [auto-ap.views.utils :refer [login-url]] - [re-frame.core :as re-frame])) + [re-frame.core :as re-frame] + [cemerick.url :as url])) (re-frame/reg-sub ::logout-reason @@ -17,9 +18,9 @@ [:div.notification.is-warning reason]) [:h1.title "Login"] [:div.box.slideInFromBelow - + [:img {:src "/img/logo-big.png"}] [:div - [:a.button.is-large.is-primary {:href login-url} "Login with Google"]]] + [:a.button.is-large.is-primary {:href (doto (login-url (get (:query (url/url (.-location js/window))) "redirect-to")) println)} "Login with Google"]]] [:p.has-text-gray "Copyright Integreat 2018"]]]]]]) diff --git a/src/cljs/auto_ap/views/pages/needs_activation.cljs b/src/cljs/auto_ap/views/pages/needs_activation.cljs index ffead368..27bc2d9d 100644 --- a/src/cljs/auto_ap/views/pages/needs_activation.cljs +++ b/src/cljs/auto_ap/views/pages/needs_activation.cljs @@ -12,11 +12,11 @@ (defn needs-activation-page [] - [:div + [:div [:h2 "Sorry, your user is not activated yet. Please have Ben Skinner enable your account. Click " [:a {:on-click (fn [] (re-frame/dispatch-sync [::relogin]) true) - :href login-url} + :href (login-url)} "here"] " to try again."]]) diff --git a/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs b/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs index 36a7d887..8e993836 100644 --- a/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/side_bar.cljs @@ -21,56 +21,55 @@ user @(re-frame/subscribe [::subs/user]) client @(re-frame/subscribe [::subs/client])] [:div - [:div [:p.menu-label "Type"] - [:ul.menu-list - [:li.menu-item - [:a.item {:href (bidi/path-for routes/routes :transactions) - :class [(active-when ap = :transactions)]} - [:span {:class "icon" :style {:font-size "25px"}}] - [:span {:class "name"} "All"]]] - [:li.menu-item - [:a.item {:href (bidi/path-for routes/routes :unapproved-transactions) - :class [(active-when ap = :unapproved-transactions)]} - [:span {:class "icon icon-task-list-text-1" :style {:font-size "25px"}}] - [:span {:class "name"} "Unapproved"]]] - [:li.menu-item - [:a.item {:href (bidi/path-for routes/routes :requires-feedback-transactions) - :class [(active-when ap = :requires-feedback-transactions)]} + [:div [:p.menu-label "Type"] + [:ul.menu-list + [:li.menu-item + [:a.item {:href (bidi/path-for routes/routes :transactions) + :class [(active-when ap = :transactions)]} + [:span {:class "icon" :style {:font-size "25px"}}] + [:span {:class "name"} "All"]]] + [:li.menu-item + [:a.item {:href (bidi/path-for routes/routes :unapproved-transactions) + :class [(active-when ap = :unapproved-transactions)]} + [:span {:class "icon icon-task-list-text-1" :style {:font-size "25px"}}] + [:span {:class "name"} "Unapproved"]]] + [:li.menu-item + [:a.item {:href (bidi/path-for routes/routes :requires-feedback-transactions) + :class [(active-when ap = :requires-feedback-transactions)]} - [:span {:class "icon icon-task-list-question" :style {:font-size "25px"}}] - - [:span {:class "name"} "Client Review"]]] - [:li.menu-item - [:a.item {:href (bidi/path-for routes/routes :approved-transactions) - :class [(active-when ap = :approved-transactions)]} + [:span {:class "icon icon-task-list-question" :style {:font-size "25px"}}] - [:span {:class "icon icon-task-list-check-1" :style {:font-size "25px"}}] - - [:span {:class "name"} "Approved"]]] - [:li.menu-item - [:a.item {:href (bidi/path-for routes/routes :excluded-transactions) - :class [(active-when ap = :excluded-transactions)]} + [:span {:class "name"} "Client Review"]]] + [:li.menu-item + [:a.item {:href (bidi/path-for routes/routes :approved-transactions) + :class [(active-when ap = :approved-transactions)]} - [:span {:class "icon icon-task-list-disable" :style {:font-size "25px"}}] - - [:span {:class "name"} "Excluded"]]] + [:span {:class "icon icon-task-list-check-1" :style {:font-size "25px"}}] + + [:span {:class "name"} "Approved"]]] + [:li.menu-item + [:a.item {:href (bidi/path-for routes/routes :excluded-transactions) + :class [(active-when ap = :excluded-transactions)]} + + [:span {:class "icon icon-task-list-disable" :style {:font-size "25px"}}] + + [:span {:class "name"} "Excluded"]]] + (when @(re-frame/subscribe [::subs/is-admin?]) [:li.menu-item [:a.item {:href (bidi/path-for ssr-routes/only-routes :transaction-insights) :class [(active-when ap = :transaction-insights)]} [:span {:class "icon icon-task-list-disable" :style {:font-size "25px"}}] - - [:span {:class "name"} "Insights"]]] - ]] - (when client - [:<> + + [:span {:class "name"} "Insights"]]])]] + (when client + [:<> [:p.menu-label "Bank Account"] - [:div + [:div [bank-account-filter {:on-change-event [::data-page/filter-changed data-page :bank-account] :value @(re-frame/subscribe [::data-page/filter data-page :bank-account]) :bank-accounts @(re-frame/subscribe [::subs/bank-accounts])}]]]) - [:p.menu-label "Financial Account"] [:div @@ -79,7 +78,7 @@ {:query i :client-id (:id client)} [:name :id]]) - :entity->text (fn [x ] (str (:numeric-code x) " - " (:name x))) + :entity->text (fn [x] (str (:numeric-code x) " - " (:name x))) :type "typeahead-v3" :on-change #(re-frame/dispatch [::data-page/filter-changed data-page :account (some-> % (select-keys [:name :id :numeric-code]))]) :value @(re-frame/subscribe [::data-page/filter data-page :account])}]] @@ -94,13 +93,11 @@ :on-change #(re-frame/dispatch [::data-page/filter-changed data-page :vendor %]) :value @(re-frame/subscribe [::data-page/filter data-page :vendor])}]] - - [:p.menu-label "Amount"] - [:div - [number-filter - {:on-change-event [::data-page/filter-changed data-page :amount-range] - :value @(re-frame/subscribe [::data-page/filter data-page :amount-range])}]] + [:div + [number-filter + {:on-change-event [::data-page/filter-changed data-page :amount-range] + :value @(re-frame/subscribe [::data-page/filter data-page :amount-range])}]] [:p.menu-label "Date Range"] [:div @@ -108,33 +105,31 @@ {:on-change-event [::data-page/filter-changed data-page :date-range] :value @(re-frame/subscribe [::data-page/filter data-page :date-range])}]] - [:p.menu-label "Location"] [:div.field [:div.control [:input.input {:placeholder "SC" :style {:width "3em"} :value @(re-frame/subscribe [::data-page/filter data-page :location]) - :on-change (dispatch-value-change [::data-page/filter-changed data-page :location ])} ]]] + :on-change (dispatch-value-change [::data-page/filter-changed data-page :location])}]]] - [:p.menu-label "Description"] - [:div - [:div.field - [:div.control [:input.input {:placeholder "CHECK 123 ABC" - :value @(re-frame/subscribe [::data-page/filter data-page :description]) - :on-change (dispatch-value-change [::data-page/filter-changed data-page :description])} ]]]] + [:p.menu-label "Description"] + [:div + [:div.field + [:div.control [:input.input {:placeholder "CHECK 123 ABC" + :value @(re-frame/subscribe [::data-page/filter data-page :description]) + :on-change (dispatch-value-change [::data-page/filter-changed data-page :description])}]]]] (when-let [exact-match-id @(re-frame/subscribe [::data-page/filter data-page :exact-match-id])] [:div [:p.menu-label "Specific Transaction"] [:span.tag.is-medium exact-match-id " " [:button.delete.is-small {:on-click (dispatch-event [::data-page/filter-changed data-page :exact-match-id nil])}]]]) - (when (= "admin" (:user/role user)) [:<> - + [:p.menu-label "Admin only"] - [:div + [:div [switch-field {:id "unresolved-only" :checked (boolean @(re-frame/subscribe [::data-page/filter data-page :unresolved])) :on-change (fn [e] @@ -149,7 +144,7 @@ [:button.delete.is-small {:on-click (dispatch-event [::data-page/filter-changed data-page :import-batch-id nil])}]]]) - [:div + [:div [switch-field {:id "potentially-duplicate" :checked (boolean @(re-frame/subscribe [::data-page/filter data-page :potential-duplicates])) :on-change (fn [e] diff --git a/src/cljs/auto_ap/views/utils.cljs b/src/cljs/auto_ap/views/utils.cljs index 86d2bab0..0326e38f 100644 --- a/src/cljs/auto_ap/views/utils.cljs +++ b/src/cljs/auto_ap/views/utils.cljs @@ -57,10 +57,16 @@ (when (apply f (into [active-page] rest)) " is-active")) -(def login-url - (let [client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com" - redirect-uri (js/encodeURI (str (.-origin (.-location js/window)) "/api/oauth"))] - (str "https://accounts.google.com/o/oauth2/auth?access_type=online&client_id=" client-id "&redirect_uri=" redirect-uri "&response_type=code&max_auth_age=0&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile"))) +(defn login-url + ([] (login-url nil)) + ([next] + + (let [client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com" + redirect-uri (js/encodeURI (str (.-origin (.-location js/window)) "/api/oauth" + ))] + (str "https://accounts.google.com/o/oauth2/auth?access_type=online&client_id=" client-id "&redirect_uri=" redirect-uri "&response_type=code&max_auth_age=0&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile" + (when next + (str "&state=" (js/encodeURIComponent next))))))) (defn dispatch-value-change [event] (fn [e] @@ -249,9 +255,9 @@ :user/role))))))) (defn query-params [] - (reduce-kv + (reduce-kv (fn [result k v] - (assoc result (keyword k) (edn/read-string v))) + (assoc result (keyword k) (try (edn/read-string v) (catch js/Error e v)))) {} (:query (cemerick.url/url (.-location js/window)))))