Implement the SSR/alpine/htmx manual transaction import, wiring the already-declared but unhandled ::external-import-page/parse/import routes. Mirrors the SSR ledger import: paste the exact master-branch Yodlee positional-column TSV, review parsed rows in an editable grid with per-row error/warning badges, and import. Every master validation is preserved and the existing import.transactions engine is reused unchanged (via import.manual/import-batch), so core components are untouched. - New ns auto-ap.ssr.transaction.import (page, paste/parse, editable grid, two-tier validation, import handler) + admin-only transactions Import nav. - Two-tier validation: fixable problems (bad date/amount, unknown client or bank-account code, missing fields) are hard errors that block the whole batch; inherent skip-conditions (non-POSTED, before start-date/locked, already-imported) are warnings computed from the engine's own categorize-transaction so the grid preview matches the import result. - Tests: failing-first Playwright e2e (e2e/transaction-import.spec.ts) plus unit/integration coverage (ssr/transaction/import_test.clj, 10 tests). - Deterministic bank-account code in the e2e seed. Plan: docs/plans/2026-06-01-001-feat-manual-transaction-import-ssr-plan.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
513 lines
28 KiB
Clojure
513 lines
28 KiB
Clojure
(ns auto-ap.ssr.components.aside
|
|
(:require
|
|
[auto-ap.graphql.utils :refer [is-admin?]]
|
|
[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.dashboard :as dashboard]
|
|
[auto-ap.routes.invoice :as invoice-route]
|
|
[auto-ap.routes.ledger :as ledger-routes]
|
|
[auto-ap.routes.outgoing-invoice :as oi-routes]
|
|
[auto-ap.routes.payments :as payment-routes]
|
|
[auto-ap.routes.pos.sales-summaries :as ss-routes]
|
|
[auto-ap.routes.transactions :as transaction-routes]
|
|
[auto-ap.ssr-routes :as ssr-routes]
|
|
[auto-ap.ssr.components.tags :as tags]
|
|
[auto-ap.ssr.hiccup-helper :as hh]
|
|
[auto-ap.ssr.hx :as hx]
|
|
[auto-ap.ssr.svg :as svg]
|
|
[auto-ap.time :as atime]
|
|
[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" "900px"))
|
|
":style" (format "selected == '%s' ? 'max-height: ' + $el.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- transaction-nav-params [request]
|
|
(let [qp (:query-params request)]
|
|
(cond-> {}
|
|
(:amount-gte qp) (assoc :amount-gte (:amount-gte qp))
|
|
(:amount-lte qp) (assoc :amount-lte (:amount-lte qp))
|
|
(:vendor qp) (assoc :vendor (:db/id (:vendor qp)))
|
|
(:account qp) (assoc :account (:db/id (:account qp)))
|
|
(:bank-account qp) (assoc :bank-account (:db/id (:bank-account qp)))
|
|
(:linked-to qp) (assoc :linked-to (:linked-to qp))
|
|
(:description qp) (assoc :description (:description qp))
|
|
(:location qp) (assoc :location (:location qp))
|
|
(:client-id qp) (assoc :client-id (:client-id qp))
|
|
(:unresolved qp) (assoc :unresolved (:unresolved qp))
|
|
(:potential-duplicates qp) (assoc :potential-duplicates (:potential-duplicates qp))
|
|
(:start-date qp) (assoc :start-date (atime/unparse-local (:start-date qp) atime/normal-date))
|
|
(:end-date qp) (assoc :end-date (atime/unparse-local (:end-date qp) atime/normal-date))
|
|
(:per-page qp) (assoc :per-page (:per-page qp)))))
|
|
|
|
(defn- transaction-nav-url [request route & {:keys [default-params] :or {default-params {:date-range "month"}}}]
|
|
(let [preserved (transaction-nav-params request)]
|
|
(hu/url (bidi/path-for ssr-routes/only-routes route)
|
|
{:date-range "month"})))
|
|
|
|
(defn left-aside- [{:keys [nav page-specific]} & _]
|
|
[: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'"}
|
|
|
|
;; TODO this causes a leftNavShow error when hitting back button. maybe amke a container
|
|
[: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-100.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 ::invoice-route/import-page :invoice-glimpse :invoice-glimpse-textract-invoice} (:matched-route request))
|
|
"invoices"
|
|
|
|
(#{:pos-sales :pos-expected-deposits :pos-tenders :pos-refunds :pos-cash-drawer-shifts ::ss-routes/page} (:matched-route request))
|
|
"sales"
|
|
(#{::payment-routes/all-page ::payment-routes/pending-page ::payment-routes/cleared-page ::payment-routes/voided-page} (:matched-route request))
|
|
"payments"
|
|
(#{::transaction-routes/page ::transaction-routes/approved-page ::transaction-routes/unapproved-page ::transaction-routes/requires-feedback-page :transaction-insights} (:matched-route request))
|
|
"transactions"
|
|
(#{::ledger-routes/all-page ::ledger-routes/external-page ::ledger-routes/external-import-page ::ledger-routes/balance-sheet ::ledger-routes/cash-flows ::ledger-routes/profit-and-loss} (:matched-route request))
|
|
"ledger"
|
|
:else
|
|
nil)]
|
|
[:ul {:class "space-y-1"
|
|
:x-data (hx/json {:selected selected})}
|
|
|
|
[:li
|
|
(menu-button- {:icon svg/pie
|
|
:href (bidi/path-for ssr-routes/only-routes
|
|
::dashboard/page)}
|
|
"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 ssr-routes/only-routes
|
|
::invoice-route/import-page)
|
|
:active? (= ::invoice-route/import-page (:matched-route request))
|
|
:hx-boost "true"} "Import"))
|
|
|
|
#_(when (can? (:identity request)
|
|
{:subject :invoice
|
|
:activity :import})
|
|
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
|
:invoice-glimpse))
|
|
:active? (= :invoice-glimpse (:matched-route request))
|
|
:hx-boost "true"}
|
|
[:div.flex.gap-2
|
|
"Glimpse"
|
|
(tags/pill- {:color :secondary} "Beta")]))
|
|
|
|
(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? (= :pos-cash-drawer-shifts (:matched-route request))
|
|
:hx-boost "true"}
|
|
"Cash drawer shifts")
|
|
(menu-button- {:href (str (bidi/path-for ssr-routes/only-routes
|
|
::ss-routes/page)
|
|
"?date-range=week")
|
|
:active? (= ::ss-routes/page (:matched-route request))
|
|
:hx-boost "true"}
|
|
"Summaries"))))
|
|
|
|
(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 (transaction-nav-url request ::transaction-routes/page)
|
|
:active? (= ::transaction-routes/page (:matched-route request))
|
|
:hx-boost "true"
|
|
:hx-include "#transaction-filters"}
|
|
"All")
|
|
(menu-button- {:href (transaction-nav-url request ::transaction-routes/unapproved-page)
|
|
:active? (= ::transaction-routes/unapproved-page (:matched-route request))
|
|
:hx-boost "true"
|
|
:hx-include "#transaction-filters"}
|
|
"Unapproved")
|
|
(menu-button- {:href (transaction-nav-url request ::transaction-routes/requires-feedback-page)
|
|
:active? (= ::transaction-routes/requires-feedback-page (:matched-route request))
|
|
:hx-boost "true"
|
|
:hx-include "#transaction-filters"}
|
|
"Client Review")
|
|
(menu-button- {:href (transaction-nav-url request ::transaction-routes/approved-page)
|
|
:active? (= ::transaction-routes/approved-page (:matched-route request))
|
|
:hx-boost "true"
|
|
:hx-include "#transaction-filters"}
|
|
"Approved")
|
|
(when (is-admin? (:identity request))
|
|
(menu-button- {:href (bidi/path-for ssr-routes/only-routes
|
|
::transaction-routes/external-import-page)
|
|
:active? (= ::transaction-routes/external-import-page (:matched-route request))
|
|
:hx-boost "true"}
|
|
"Import"))
|
|
(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}
|
|
"Reports")
|
|
(sub-menu- {:selector "ledger"
|
|
:active? (= "ledger" selected)}
|
|
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
|
::ledger-routes/all-page)
|
|
{:date-range "month"})
|
|
:active? (= ::ledger-routes/all-page (:matched-route request))
|
|
:hx-boost "true"}
|
|
[:div.flex.gap-2
|
|
"Register"
|
|
(tags/pill- {:color :secondary} "WIP")])
|
|
(when (is-admin? (:identity request))
|
|
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
|
::ledger-routes/external-page)
|
|
{:date-range "month"})
|
|
:active? (= ::ledger-routes/external-page (:matched-route request))
|
|
:hx-boost "true"}
|
|
[:div.flex.gap-2
|
|
"External Register"
|
|
(tags/pill- {:color :secondary} "WIP")]))
|
|
|
|
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
|
::ledger-routes/profit-and-loss))
|
|
:active? (= ::ledger-routes/profit-and-loss (:matched-route request))
|
|
:hx-boost "true"}
|
|
[:div.flex.gap-2
|
|
"Profit and loss"
|
|
(tags/pill- {:color :secondary} "WIP")])
|
|
|
|
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
|
::ledger-routes/cash-flows))
|
|
:active? (= ::ledger-routes/cash-flows (:matched-route request))
|
|
:hx-boost "true"}
|
|
[:div.flex.gap-2
|
|
"Cash flows"
|
|
(tags/pill- {:color :secondary} "WIP")])
|
|
|
|
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
|
::ledger-routes/balance-sheet))
|
|
:active? (= ::ledger-routes/balance-sheet (:matched-route request))
|
|
:hx-boost "true"}
|
|
[:div.flex.gap-2
|
|
"Balance Sheet"
|
|
(tags/pill- {:color :secondary} "WIP")])
|
|
|
|
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
|
::ledger-routes/external-import-page)
|
|
{:date-range "month"})
|
|
:active? (= ::ledger-routes/external-import-page (:matched-route request))
|
|
:hx-boost "true"}
|
|
[:div.flex.gap-2
|
|
"External Import"
|
|
(tags/pill- {:color :secondary} "WIP")]))))]))
|
|
|
|
(defn company-aside-nav- [request]
|
|
[:ul {:class "space-y-2" :hx-boost "true"}
|
|
[:li
|
|
(menu-button- {:icon svg/vendors
|
|
:active? (= :company (:matched-route request))
|
|
:href (bidi/path-for ssr-routes/only-routes
|
|
:company)
|
|
:hx-boost true}
|
|
"My Company")]
|
|
|
|
[:li
|
|
(menu-button- {:icon svg/report
|
|
:active? (= :company-reports (:matched-route request))
|
|
:href (bidi/path-for ssr-routes/only-routes
|
|
:company-reports)
|
|
:hx-boost true}
|
|
"Reports")]
|
|
[:li
|
|
(menu-button- {:icon svg/report
|
|
:active? (= :company-expense-report (:matched-route request))
|
|
:href (bidi/path-for ssr-routes/only-routes
|
|
:company-expense-report)
|
|
:hx-boost true}
|
|
"Expense Report")]
|
|
(when (can? (:identity request)
|
|
{:subject :reconciliation-report})
|
|
[:li
|
|
(menu-button- {:icon svg/report
|
|
:active? (= :company-reconciliation-report (:matched-route request))
|
|
:href (bidi/path-for ssr-routes/only-routes
|
|
:company-reconciliation-report)
|
|
:hx-boost true}
|
|
"Bank Sync Report")])
|
|
[:li
|
|
(menu-button- {:icon svg/bank
|
|
:active? (= :company-plaid (:matched-route request))
|
|
:href (bidi/path-for ssr-routes/only-routes
|
|
:company-plaid)
|
|
:hx-boost true}
|
|
"Plaid Link")]
|
|
[:li
|
|
(menu-button- {:icon svg/bank
|
|
:active? (= :company-yodlee (:matched-route request))
|
|
:href (bidi/path-for ssr-routes/only-routes
|
|
:company-yodlee)
|
|
:hx-boost true}
|
|
"Yodlee Link")]
|
|
[:li
|
|
(menu-button- {:icon svg/government-building
|
|
:active? (= :company-1099 (:matched-route request))
|
|
:href (bidi/path-for ssr-routes/only-routes
|
|
:company-1099)
|
|
:hx-boost true}
|
|
"1099 Vendor Info")]])
|
|
|
|
(defn admin-aside-nav- [{:keys [matched-route]}]
|
|
[: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"))])
|