(ns auto-ap.ssr.components.aside (:require [auto-ap.client-routes :as client-routes] [auto-ap.permissions :refer [can?]] [auto-ap.routes.admin.clients :as ac-routes] [auto-ap.routes.admin.excel-invoices :as ei-routes] [auto-ap.routes.admin.import-batch :as ib-routes] [auto-ap.routes.admin.transaction-rules :as transaction-rules] [auto-ap.routes.admin.vendors :as v-routes] [auto-ap.routes.invoice :as invoice-route] [auto-ap.routes.outgoing-invoice :as oi-routes] [auto-ap.routes.payments :as payment-routes] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.hiccup-helper :as hh] [auto-ap.ssr.hx :as hx] [auto-ap.ssr.svg :as svg] [bidi.bidi :as bidi] [hiccup.util :as hu])) (defn menu-button- [params & children] [:div [:a (-> params (dissoc :icon) (assoc :type "button") (update :class (fn [c] (cond-> (or c "cursor-pointer flex items-center p-2 w-full text-sm rounded-lg transition duration-75 group hover:bg-gray-100 dark:hover:bg-gray-700 select-none") (:active? params) (hh/add-class "text-blue-600 font-extrabold dark:text-blue-100 bg-gray-100") (not (:active? params)) (hh/add-class "text-gray-600 dark:text-white")))) (assoc :hx-indicator "find .htmx-indicator") (assoc :hx-select "#app") (assoc :hx-target "#app") (assoc :hx-swap "innerHTML")) (when (:icon params) [:span {:class "flex-shrink-0 w-6 h-6 text-gray-400 transition duration-75 group-hover:text-blue-500 dark:text-gray-400 group-hover:scale-110 dark:group-hover:text-white mr-3"} (:icon params)]) (into [:span {:class "flex-1 text-left whitespace-nowrap"}] children) (when (get params "@click.prevent") [:svg {:aria-hidden "true", :class "w-6 h-6", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} [:path {:fill-rule "evenodd", :d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z", :clip-rule "evenodd"}]]) [:div.htmx-indicator.flex.items-center (svg/spinner-primary {:class "inline w-4 h-4 text-white"})]]]) (defn sub-menu- [params & children] [:ul (cond-> (update params :class (fnil hh/add-class "") "space-y-1.5 max-h-0 transition transition-all overflow-hidden") true (assoc ":class" (format "selected == '%s' ? 'py-0.5' : 'py-0'" (:selector params)) :x-ref "submenu" :style (cond-> {} (:active? params) (assoc "max-height" "400px")) ":style" (format "selected == '%s' ? 'max-height: ' + $refs.submenu.scrollHeight + 'px' : ''" (:selector params)))) (for [c children] [:li (update-in c [1 1 :class ] (fn [c] (hh/add-class (or c "") " flex items-center p-2 pl-11 w-full text-base font-normal rounded-lg transition duration-75 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700")))])]) (defn left-aside- [{:keys [nav page-specific]} & children] [:aside {:id "left-nav", :class "fixed top-0 left-0 pt-16 z-20 w-64 h-screen transition-transform", "x-transition:enter" "transition duration-500" "x-transition:enter-start" "-translate-x-full" "x-transition:enter-end" " translate-x-0" "x-transition:leave" "transition duration-500" "x-transition:leave-start" "translate-x-0" "x-transition:leave-end" " -translate-x-full" :aria-labelledby "left-nav" :x-show "leftNavShow" ":aria-hidden" "leftNavShow ? 'false' : 'true'"} [:template {:x-teleport "body"} [:div.fixed.inset-0.lg:hidden {:x-show "leftNavShow" :x-transition:enter "transition duration-500" :x-transition:enter-start "opacity-0" :x-transition:enter-end "opacity-100" :x-transition:leave "transition duration-500" :x-transition:leave-start "opacity-100" :x-transition:leave-end "opacity-0" "@click.capture.prevent" "leftNavShow=false"} [:div.fixed.inset-0.bg-gray-800.z-10.opacity-70]]] [:div {:class "overflow-y-auto py-5 px-3 h-full bg-gray-50 border-r border-gray-200 dark:bg-gray-800 dark:border-gray-700"} nav (when page-specific [:div {:class " pt-5 mt-5 space-y-2 border-t border-gray-200 dark:border-gray-700"} page-specific])]]) (defn main-aside-nav- [request] (let [selected (cond (#{::invoice-route/all-page ::invoice-route/unpaid-page ::invoice-route/voided-page ::invoice-route/paid-page ::oi-routes/new} (:matched-route request)) "invoices" (#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts} (:matched-route request)) "sales" (#{::payment-routes/all-page ::payment-routes/pending-page ::payment-routes/cleared-page ::payment-routes/voided-page } (:matched-route request)) "payments" :else nil)] [:ul {:class "space-y-1" :x-data (hx/json {:selected selected})} [:li (menu-button- {:icon svg/pie :href "/"} "Dashboard")] (when (can? (:identity request) {:subject :invoice-page}) (list (menu-button- {"@click.prevent" "if (selected == 'invoices') {selected = null } else { selected = 'invoices'} " :icon svg/accounting-invoice-mail} "Invoices") (sub-menu- {:selector "invoices" :active? (= "invoices" selected)} (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes ::invoice-route/all-page) {:date-range "year"}) :active? (= ::invoice-route/all-page (:matched-route request)) :hx-boost "true"} "All") (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes ::invoice-route/paid-page) {:date-range "year"}) :active? (= ::invoice-route/paid-page (:matched-route request)) :hx-boost "true"} "Paid") (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes ::invoice-route/unpaid-page) {:date-range "year"}) :active? (= ::invoice-route/unpaid-page (:matched-route request)) :hx-boost "true"} "Unpaid") (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes ::invoice-route/voided-page) {:date-range "year"}) :active? (= ::invoice-route/voided-page (:matched-route request)) :hx-boost "true"} "Voided") (when (can? (:identity request) {:subject :invoice :activity :import}) (menu-button- {:href (bidi/path-for client-routes/routes :import-invoices)} "Import")) (when (can? (:identity request) {:subject :ar-invoice :activity :read}) (menu-button- {:href (bidi/path-for ssr-routes/only-routes ::oi-routes/new) :active? (= ::oi-routes/new (:matched-route request)) :hx-boost "true"} "Create outgoing"))))) (when (can? (:identity request) {:subject :sales :activity :read}) (list (menu-button- {:icon svg/receipt-register-1 "@click.prevent" "if (selected == 'sales') {selected = null } else { selected = 'sales'} "} "Sales") (sub-menu- {:selector "sales" :active? (= "sales" selected)} (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes :pos-sales) "?date-range=week") :active? (= :pos-sales (:matched-route request)) :hx-boost "true"} "Sales") (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes :pos-expected-deposits) "?date-range=week") :active? (= :pos-expected-deposits (:matched-route request)) :hx-boost "true"} "Expected Deposits") (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes :pos-tenders) "?date-range=week") :active? (= :pos-tenders (:matched-route request)) :hx-boost "true"} "Tenders") (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes :pos-refunds) "?date-range=week") :active? (= :pos-refunds (:matched-route request)) :hx-boost "true"} "Refunds") (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes :pos-cash-drawer-shifts) "?date-range=week") :active? (= :cash-drawer-shifts (:matched-route request)) :hx-boost "true"} "Cash drawer shifts")))) (menu-button- {"@click.prevent" "if (selected == 'payments') {selected = null } else { selected = 'payments'} " :icon svg/payments} "Payments") (sub-menu- {:selector "payments" :active? (= "payments" selected)} (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes ::payment-routes/all-page) {:date-range "month"}) :active? (= ::payment-routes/all-page (:matched-route request)) :hx-boost "true"} "All") (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes ::payment-routes/pending-page) {:date-range "month"}) :active? (= ::payment-routes/pending-page (:matched-route request)) :hx-boost "true"} "Pending") (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes ::payment-routes/cleared-page) {:date-range "month"}) :active? (= ::payment-routes/cleared-page (:matched-route request)) :hx-boost "true"} "Cleared") (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes ::payment-routes/voided-page) {:date-range "month"}) :active? (= ::payment-routes/voided-page (:matched-route request)) :hx-boost "true"} "Voided")) [:li {:x-data (hx/json {:open false})} (menu-button- {"@click.prevent" "if (selected == 'transactions') {selected = null } else { selected = 'transactions'} " :icon svg/bank} "Transactions") (sub-menu- {:selector "transactions" :active? (= "transactions" selected)} (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") (when (can? (:identity request) {:subject :transaction :activity :insights}) (menu-button- {:href (bidi/path-for ssr-routes/only-routes :transaction-insights)} "Insights")))] (when (can? (:identity request) {:subject :ledger-page}) (list (menu-button- {"@click.prevent" "if (selected == 'ledger') {selected = null } else { selected = 'ledger'} " :icon svg/receipt} "Ledger") (sub-menu- {:selector "ledger" :active? (= "ledger" selected)} (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") (when (can? (:identity request) {:subject :ledger :activity :import}) (menu-button- {:href (bidi/path-for client-routes/routes :external-import-ledger)} "External Ledger Import")))))])) (defn company-aside-nav- [_] [:ul {:class "space-y-2" :hx-boost "true"} [:li (menu-button- {:icon svg/vendors :href (bidi/path-for ssr-routes/only-routes :company) :hx-boost true} "My Company")] [:li (menu-button- {:icon svg/report :href (bidi/path-for ssr-routes/only-routes :company-reports) :hx-boost true} "Reports")] [:li (menu-button- {:icon svg/bank :href (bidi/path-for ssr-routes/only-routes :company-plaid) :hx-boost true} "Plaid Link")] [:li (menu-button- {:icon svg/bank :href (bidi/path-for ssr-routes/only-routes :company-yodlee) :hx-boost true} "Yodlee Link")] [:li (menu-button- {:icon svg/government-building :href (bidi/path-for ssr-routes/only-routes :company-1099) :hx-boost true} "1099 Vendor Info" )]]) (defn admin-aside-nav- [{:keys [matched-route] :as request}] [:ul {:class "space-y-2" :x-data (hx/json {:selected "nil"})} [:li (menu-button- {:icon svg/dashboard :active? (= :auto-ap.routes.admin/page matched-route) :href (bidi/path-for ssr-routes/only-routes :auto-ap.routes.admin/page) :hx-boost true} "Dashboard")] [:li (menu-button- {:icon svg/restaurant :active? (= ::ac-routes/page matched-route) :href (bidi/path-for ssr-routes/only-routes ::ac-routes/page) :hx-boost true} "Clients")] [:li (menu-button- {:icon svg/vendors :active? (= ::v-routes/page matched-route) :href (bidi/path-for ssr-routes/only-routes ::v-routes/page) :hx-boost true} "Vendors")] [:li (menu-button- {:icon svg/user :active? (= :users matched-route) :href (bidi/path-for ssr-routes/only-routes :users) :hx-boost true} "Users")] [:li (menu-button- {:icon svg/accounts :active? (= :admin-accounts matched-route) :href (bidi/path-for ssr-routes/only-routes :admin-accounts) :hx-boost true} "Accounts")] [:li (menu-button- {:icon svg/cog :active? (= ::transaction-rules/page matched-route) :href (bidi/path-for ssr-routes/only-routes ::transaction-rules/page) :hx-boost true} "Rules")] [:li (menu-button- {:icon svg/question :active? (= :admin-rules matched-route) :href (bidi/path-for ssr-routes/only-routes :admin-history) :hx-boost "true"} "History")] [:li (menu-button- {:icon svg/rabbit :active? (= :admin-jobs matched-route) :href (bidi/path-for ssr-routes/only-routes :admin-jobs) :hx-boost true} "Background Jobs")] (menu-button- {:icon svg/arrow-in "@click.prevent" "if (selected == 'import') {selected = null } else { selected = 'import'} "} "Import") (sub-menu- {:selector "import"} (menu-button- {:href (bidi/path-for ssr-routes/only-routes ::ei-routes/page) :active? (= ::ei-routes/page matched-route) :hx-boost true} "Excel Invoices") (menu-button- {:href (bidi/path-for ssr-routes/only-routes ::ib-routes/page) :active? (= ::ib-routes/page matched-route) :hx-boost true} "Import Batches") (menu-button- {:href (bidi/path-for ssr-routes/only-routes :admin-ezcater-xls) :active? (= :admin-ezcater-xls matched-route) :hx-boost "true"} "EZCater XLS Import"))])