Adds advanced view
This commit is contained in:
@@ -232,6 +232,40 @@ const calendarYearPeriod = (date) => {
|
||||
return {end: formatDateMMDDYYYY(end), start: formatDateMMDDYYYY(start)};
|
||||
}
|
||||
|
||||
const getLastMonthPeriods = (date) => {
|
||||
if (!date) {
|
||||
date = new Date();
|
||||
} else {
|
||||
date = parseMMDDYYYY(date);
|
||||
}
|
||||
|
||||
// Get the first day of the current month
|
||||
const firstDayOfCurrentMonth = new Date(date.getFullYear(), date.getMonth(), 1);
|
||||
|
||||
// Get the last day of the previous month
|
||||
const lastDayOfPreviousMonth = dateFns.subDays(firstDayOfCurrentMonth, 1);
|
||||
|
||||
// Get the first day of the previous month
|
||||
const firstDayOfPreviousMonth = new Date(lastDayOfPreviousMonth.getFullYear(), lastDayOfPreviousMonth.getMonth(), 1);
|
||||
|
||||
// Get the same period for the previous year
|
||||
const firstDayOfPreviousYearMonth = new Date(firstDayOfPreviousMonth.getFullYear() - 1, firstDayOfPreviousMonth.getMonth(), 1);
|
||||
const lastDayOfPreviousYearMonth = dateFns.lastDayOfMonth(firstDayOfPreviousYearMonth);
|
||||
|
||||
// Create period objects
|
||||
const currentPeriod = {
|
||||
start: formatDateMMDDYYYY(firstDayOfPreviousMonth),
|
||||
end: formatDateMMDDYYYY(lastDayOfPreviousMonth)
|
||||
};
|
||||
|
||||
const previousYearPeriod = {
|
||||
start: formatDateMMDDYYYY(firstDayOfPreviousYearMonth),
|
||||
end: formatDateMMDDYYYY(lastDayOfPreviousYearMonth)
|
||||
};
|
||||
|
||||
return [currentPeriod, previousYearPeriod];
|
||||
};
|
||||
|
||||
initMultiDatepicker = function(elem, startingValue) {
|
||||
const modalParent = elem.closest('#modal-content');
|
||||
if (modalParent) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,16 +1,18 @@
|
||||
(ns auto-ap.ssr.components
|
||||
(:require [auto-ap.ssr.components.breadcrumbs :as breadcrumbs]
|
||||
[auto-ap.ssr.components.buttons :as buttons]
|
||||
[auto-ap.ssr.components.dialog :as dialog]
|
||||
[auto-ap.ssr.components.inputs :as inputs]
|
||||
[auto-ap.ssr.components.aside :as aside]
|
||||
[auto-ap.ssr.components.card :as card]
|
||||
[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.paginator :as paginator]
|
||||
[auto-ap.ssr.components.radio :as radio]))
|
||||
(:require
|
||||
[auto-ap.ssr.components.aside :as aside]
|
||||
[auto-ap.ssr.components.breadcrumbs :as breadcrumbs]
|
||||
[auto-ap.ssr.components.buttons :as buttons]
|
||||
[auto-ap.ssr.components.card :as card]
|
||||
[auto-ap.ssr.components.data-grid :as data-grid]
|
||||
[auto-ap.ssr.components.dialog :as dialog]
|
||||
[auto-ap.ssr.components.inputs :as inputs]
|
||||
[auto-ap.ssr.components.navbar :as navbar]
|
||||
[auto-ap.ssr.components.page :as page]
|
||||
[auto-ap.ssr.components.paginator :as paginator]
|
||||
[auto-ap.ssr.components.radio :as radio]
|
||||
[auto-ap.ssr.components.tabs :as tabs]
|
||||
[auto-ap.ssr.components.tags :as tags]))
|
||||
;; potemkin can be used here
|
||||
(def breadcrumbs breadcrumbs/breadcrumbs-)
|
||||
(def button buttons/button-)
|
||||
@@ -32,6 +34,7 @@
|
||||
(def modal-footer dialog/modal-footer-)
|
||||
(def tooltip buttons/tooltip-)
|
||||
|
||||
(def tabs tabs/tabs-)
|
||||
(def text-input inputs/text-input-)
|
||||
(def text-area inputs/text-area-)
|
||||
|
||||
|
||||
28
src/clj/auto_ap/ssr/components/tabs.clj
Normal file
28
src/clj/auto_ap/ssr/components/tabs.clj
Normal file
@@ -0,0 +1,28 @@
|
||||
(ns auto-ap.ssr.components.tabs
|
||||
(:require
|
||||
[auto-ap.ssr.hx :as hx]))
|
||||
|
||||
(defn tabs- [{:keys [tabs active]}]
|
||||
[:div.flex.flex-col.gap-2 {:x-data (hx/json {:activeTab active})}
|
||||
[:div {:class "text-sm font-medium text-center text-gray-500 border-b border-gray-200 dark:text-gray-400 dark:border-gray-700" }
|
||||
[:ul {:class "flex flex-wrap -mb-px"}
|
||||
(for [tab tabs]
|
||||
[:li {:class "me-2"}
|
||||
[:a {:href "#"
|
||||
:x-data (hx/json {:tabName (:name tab)})
|
||||
":data-active" (format "activeTab==tabName")
|
||||
"@click" (format "activeTab=tabName" )
|
||||
:class "inline-block data-[active]:text-blue-600 data-[active]:border-blue-600 p-4 border-b-2 rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300"}
|
||||
(:name tab)]])
|
||||
|
||||
|
||||
|
||||
#_[:li
|
||||
[:a {:class "inline-block p-4 text-gray-400 rounded-t-lg cursor-not-allowed dark:text-gray-500"} "Disabled"]]]]
|
||||
(for [tab tabs]
|
||||
[:div {:x-data (hx/json {:tabName (:name tab)})
|
||||
:x-show (format "activeTab==tabName")
|
||||
"x-transition:enter" "transition-opacity duration-300"
|
||||
"x-transition:enter-start" "opacity-0"
|
||||
"x-transition:enter-end" "opacity-100"}
|
||||
(:content tab) ])])
|
||||
@@ -37,6 +37,7 @@
|
||||
[auto-ap.ssr.search :as search]
|
||||
[auto-ap.ssr.transaction.insights :as insights]
|
||||
[auto-ap.ssr.users :as users]
|
||||
[auto-ap.ssr.transaction :as transaction]
|
||||
[auto-ap.ssr.vendor :as vendors]
|
||||
[ring.middleware.json :refer [wrap-json-response]]))
|
||||
|
||||
@@ -103,6 +104,7 @@
|
||||
(into admin-vendors/key->handler)
|
||||
(into admin-clients/key->handler)
|
||||
(into admin-rules/key->handler)
|
||||
(into transaction/key->handler)
|
||||
(into dashboard/key->handler)
|
||||
(into indicators/key->handler)
|
||||
(into payments/key->handler)
|
||||
|
||||
@@ -139,6 +139,69 @@
|
||||
:table table-contents
|
||||
:warning (not-empty (str/join "\n " (filter not-empty [warning (:warning report)])))}))))])
|
||||
|
||||
(defn- periods-dropdown []
|
||||
[:div {:x-data (hx/json {:periods (map (fn [p]
|
||||
{:start (atime/unparse-local (:start p) atime/normal-date)
|
||||
:end (atime/unparse-local (:end p) atime/normal-date)})
|
||||
(fc/field-value))
|
||||
:source_date (-> (fc/field-value)
|
||||
first
|
||||
:end
|
||||
(atime/unparse-local atime/normal-date))})
|
||||
|
||||
:x-init "$watch('periods', ds => source_date= ds.length > 0 ? ds[0].end : null)"}
|
||||
[:template {:x-for "(v,n) in periods"}
|
||||
[:div
|
||||
[:input {:type "hidden"
|
||||
":name" "'periods[' + n + '][start]'"
|
||||
:x-model "v.start"}]
|
||||
[:input {:type "hidden"
|
||||
":name" "'periods[' + n + '][end]'"
|
||||
:x-model "v.end"}]]]
|
||||
(com/a-button {"x-tooltip.on.click.theme.dropdown.placement.bottom.interactive" "{content: ()=> $refs.tooltip.innerHTML, allowHTML: true, appendTo: $root}"
|
||||
:indicator? false}
|
||||
[:template {:x-if "periods.length == 0"}
|
||||
[:span.text-left.text-gray-400 "None selected"]]
|
||||
[:template {:x-if "periods.length < 3 && periods.length > 0"}
|
||||
[:span.inline-flex.gap-2
|
||||
[:template {:x-for "p in periods"}
|
||||
(com/pill {:color :secondary}
|
||||
[:span {:x-text "p.start"}]
|
||||
" - "
|
||||
[:span {:x-text "p.end"}])]]]
|
||||
[:template {:x-if "periods.length >= 3"}
|
||||
(com/pill {:color :secondary}
|
||||
[:span {:x-text "periods.length"}]
|
||||
" periods selected")]
|
||||
[:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"}
|
||||
svg/drop-down])
|
||||
[:template {:x-ref "tooltip"}
|
||||
[:div.p-4.gap-2 {:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 ring-1 p-4 w-[700px] "}
|
||||
[:div.flex.flex-col.gap-2
|
||||
(com/tabs
|
||||
{:tabs [{:name "Quick"
|
||||
:content [:div.flex.flex.gap-2
|
||||
(com/calendar-input {:placeholder "12/21/2020" :x-model "source_date"})
|
||||
[:div.flex.flex-col.gap-2
|
||||
(com/a-button {"@click" "periods=getFourWeekPeriodsPeriods(source_date)"} "13 periods")
|
||||
(com/a-button {"@click" "periods=[calendarYearPeriod(source_date)]"} "Calendar year")
|
||||
(com/a-button {"@click" "periods=getLastMonthPeriods()"} "Last Month")
|
||||
(com/a-button {"@click" "periods=[]"} "Clear")]]}
|
||||
{:name "Advanced"
|
||||
:content [:div.flex.flex-col.gap-2 {:class "max-h-[500px] overflow-scroll"}
|
||||
[:template {:x-for "(p, i) in periods" ":key" "i"}
|
||||
[:div.flex.gap-2
|
||||
(com/text-input { :x-model "p.start" })
|
||||
(com/text-input { :x-model "p.end" })
|
||||
(com/a-icon-button {"@click.prevent.stop" "periods=periods.filter((_, i2) => i !== i2)"} svg/x)]
|
||||
#_(com/pill {:color :secondary}
|
||||
[:span {:x-text "p.start"}]
|
||||
" - "
|
||||
[:span {:x-text "p.end"}])]
|
||||
(com/button {"@click.prevent.stop" "periods.push({start: '', end: ''})" :class "w-32"} "Add new period")
|
||||
]}]
|
||||
:active "Quick"}) ]]]])
|
||||
|
||||
(defn form* [request & children]
|
||||
(let [params (or (:query-params request) {})]
|
||||
(fc/start-form
|
||||
@@ -166,53 +229,7 @@
|
||||
(fc/with-field :periods
|
||||
(com/validated-inline-field {:label "Periods"
|
||||
:errors (fc/field-errors)}
|
||||
[:div {:x-data (hx/json {:periods (map (fn [p]
|
||||
{:start (atime/unparse-local (:start p) atime/normal-date)
|
||||
:end (atime/unparse-local (:end p) atime/normal-date)})
|
||||
(fc/field-value))
|
||||
:source_date (-> (fc/field-value)
|
||||
first
|
||||
:end
|
||||
(atime/unparse-local atime/normal-date))})
|
||||
|
||||
:x-init "$watch('periods', ds => source_date= ds.length > 0 ? ds[0].end : null)"}
|
||||
[:template {:x-for "(v,n) in periods"}
|
||||
[:div
|
||||
(fc/with-field 0
|
||||
(fc/with-field :start
|
||||
[:input {:type "hidden"
|
||||
":name" "'periods[' + n + '][start]'"
|
||||
:x-model "v.start"}]))
|
||||
(fc/with-field 0
|
||||
(fc/with-field :end
|
||||
[:input {:type "hidden"
|
||||
":name" "'periods[' + n + '][end]'"
|
||||
:x-model "v.end"}]))]]
|
||||
(com/a-button {"x-tooltip.on.click.theme.dropdown.placement.bottom.interactive" "{content: ()=> $refs.tooltip.innerHTML, allowHTML: true, appendTo: $root}"
|
||||
:indicator? false}
|
||||
[:template {:x-if "periods.length == 0"}
|
||||
[:span.text-left.text-gray-400 "None selected"]]
|
||||
[:template {:x-if "periods.length < 3 && periods.length > 0"}
|
||||
[:span.inline-flex.gap-2
|
||||
[:template {:x-for "p in periods"}
|
||||
(com/pill {:color :secondary}
|
||||
[:span {:x-text "p.start"}]
|
||||
" - "
|
||||
[:span {:x-text "p.end"}])]]]
|
||||
[:template {:x-if "periods.length >= 3"}
|
||||
(com/pill {:color :secondary}
|
||||
[:span {:x-text "periods.length"}]
|
||||
" periods selected")]
|
||||
[:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"}
|
||||
svg/drop-down])
|
||||
[:template {:x-ref "tooltip"}
|
||||
[:div.p-4 {:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 ring-1 p-4"}
|
||||
[:div.flex.flex-col.gap-2
|
||||
(com/calendar-input {:placeholder "12/21/2020" :x-model "source_date"})
|
||||
(com/a-button {"@click" "periods=getFourWeekPeriodsPeriods(source_date)"} "13 periods")
|
||||
(com/a-button {"@click" "periods=[calendarYearPeriod(source_date)]"} "Calendar year")
|
||||
(com/a-button {"@click" "periods=[lastYearPeriod(source_date)]"} "Full year")
|
||||
(com/a-button {"@click" "periods=[]"} "Clear")]]]]))
|
||||
(periods-dropdown)))
|
||||
(fc/with-field :column-per-location
|
||||
(com/toggle {:name (fc/field-name)
|
||||
:checked (fc/field-value)}
|
||||
|
||||
389
src/clj/auto_ap/ssr/transaction.clj
Normal file
389
src/clj/auto_ap/ssr/transaction.clj
Normal file
@@ -0,0 +1,389 @@
|
||||
(ns auto-ap.ssr.transaction
|
||||
(:require
|
||||
[auto-ap.client-routes :as client-routes]
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-4 conn
|
||||
merge-query observable-query pull-many]]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.permissions :refer [can? wrap-must]]
|
||||
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||
[auto-ap.routes.transactions :as route]
|
||||
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.components.link-dropdown :refer [link-dropdown]]
|
||||
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.ledger :refer [wrap-ensure-bank-account-belongs]]
|
||||
[auto-ap.ssr.pos.common :refer [date-range-field*]]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.utils
|
||||
:refer [apply-middleware-to-all-handlers clj-date-schema
|
||||
entity-id html-response main-transformer strip
|
||||
wrap-merge-prior-hx wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[clj-time.coerce :as coerce]
|
||||
[datomic.api :as dc]
|
||||
[hiccup.util :as hu]
|
||||
[malli.core :as mc]))
|
||||
|
||||
(defn exact-match-id* [request]
|
||||
(if (nat-int? (:exact-match-id (:query-params request)))
|
||||
[:div {:x-data (hx/json {:exact_match (:exact-match-id (:query-params request))}) :id "exact-match-id-tag"}
|
||||
(com/hidden {:name "exact-match-id"
|
||||
"x-model" "exact_match"})
|
||||
(com/pill {:color :primary}
|
||||
[:span.inline-flex.space-x-2.items-center
|
||||
[:div "exact match"]
|
||||
[:div.w-3.h-3
|
||||
(com/link {"@click" "exact_match=null; $nextTick(() => $dispatch('change'))"}
|
||||
svg/x)]])]
|
||||
[:div {:id "exact-match-id-tag"}]))
|
||||
|
||||
(defn bank-account-filter* [request]
|
||||
[:div {:hx-trigger "clientSelected from:body"
|
||||
:hx-get (bidi.bidi/path-for ssr-routes/only-routes ::route/bank-account-filter)
|
||||
:hx-target "this"
|
||||
:hx-swap "outerHTML"}
|
||||
(when (:client request)
|
||||
(let [bank-account-belongs-to-client? (get (set (map :db/id (:client/bank-accounts (:client request))))
|
||||
(:db/id (:bank-account (:query-params request))))]
|
||||
(com/field {:label "Bank Account"}
|
||||
(com/radio-card {:size :small
|
||||
:name "bank-account"
|
||||
:value (or (when bank-account-belongs-to-client?
|
||||
(:db/id (:bank-account (:query-params request))))
|
||||
"")
|
||||
:options
|
||||
(into [{:value ""
|
||||
:content "All"}]
|
||||
(for [ba (:client/bank-accounts (:client request))]
|
||||
{:value (:db/id ba)
|
||||
:content (:bank-account/name ba)}))}))))])
|
||||
|
||||
(defn bank-account-filter [request]
|
||||
(html-response (bank-account-filter* request)))
|
||||
|
||||
(defn filters [request]
|
||||
[:form#transaction-filters {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
||||
"hx-get" (bidi/path-for ssr-routes/only-routes
|
||||
::route/table)
|
||||
"hx-target" "#entity-table"
|
||||
"hx-indicator" "#entity-table"}
|
||||
|
||||
(com/hidden {:name "status"
|
||||
:value (some-> (:status (:query-params request)) name)})
|
||||
[:fieldset.space-y-6
|
||||
(com/field {:label "Vendor"}
|
||||
(com/typeahead {:name "vendor"
|
||||
:id "vendor"
|
||||
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
||||
:value (:vendor (:query-params request))
|
||||
:value-fn :db/id
|
||||
:content-fn :vendor/name}))
|
||||
(com/field {:label "Bank Account"}
|
||||
(bank-account-filter* request))
|
||||
|
||||
(date-range-field* request)
|
||||
|
||||
(com/field {:label "Description"}
|
||||
(com/text-input {:name "description"
|
||||
:id "description"
|
||||
:class "hot-filter"
|
||||
:value (:description (:query-params request))
|
||||
:placeholder "e.g., Groceries"
|
||||
:size :small}))
|
||||
|
||||
(com/field {:label "Amount"}
|
||||
[:div.flex.space-x-4.items-baseline
|
||||
(com/money-input {:name "amount-gte"
|
||||
:id "amount-gte"
|
||||
:hx-preserve "true"
|
||||
:class "hot-filter w-20"
|
||||
:value (:amount-gte (:query-params request))
|
||||
:placeholder "0.01"
|
||||
:size :small})
|
||||
[:div.align-baseline
|
||||
"to"]
|
||||
(com/money-input {:name "amount-lte"
|
||||
:hx-preserve "true"
|
||||
:id "amount-lte"
|
||||
:class "hot-filter w-20"
|
||||
:value (:amount-lte (:query-params request))
|
||||
:placeholder "9999.34"
|
||||
:size :small})])
|
||||
(exact-match-id* request)]])
|
||||
|
||||
(defn fetch-ids [db {:keys [query-params route-params] :as request}]
|
||||
(let [valid-clients (extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
(:client-id query-params)
|
||||
(when (:client-code request)
|
||||
[:client/code (:client-code request)]))
|
||||
args query-params
|
||||
query
|
||||
(if (:exact-match-id args)
|
||||
{:query {:find '[?e]
|
||||
:in '[$ ?e [?c ...]]
|
||||
:where '[[?e :transaction/client ?c]]}
|
||||
:args [db
|
||||
(:exact-match-id args)
|
||||
valid-clients]}
|
||||
(cond-> {:query {:find []
|
||||
:in ['$ '[?clients ?start ?end]]
|
||||
:where '[[(iol-ion.query/scan-transactions $ ?clients ?start ?end) [[?e _ ?sort-default] ...]]]}
|
||||
:args [db
|
||||
[valid-clients
|
||||
(some-> (:start-date query-params) coerce/to-date)
|
||||
(some-> (:end-date query-params) coerce/to-date)]]}
|
||||
|
||||
(seq (:description args))
|
||||
(merge-query {:query {:in ['?description]
|
||||
:where ['[?e :transaction/description-original ?description]]}
|
||||
:args [(:description args)]})
|
||||
|
||||
(:amount-gte args)
|
||||
(merge-query {:query {:in ['?amount-gte]
|
||||
:where ['[?e :transaction/amount ?a]
|
||||
'[(>= ?a ?amount-gte)]]}
|
||||
:args [(:amount-gte args)]})
|
||||
|
||||
(:amount-lte args)
|
||||
(merge-query {:query {:in ['?amount-lte]
|
||||
:where ['[?e :transaction/amount ?a]
|
||||
'[(<= ?a ?amount-lte)]]}
|
||||
:args [(:amount-lte args)]})
|
||||
|
||||
(:db/id (:bank-account args))
|
||||
(merge-query {:query {:in ['?ba]
|
||||
:where ['[?e :transaction/bank-account ?ba]]}
|
||||
:args [(:db/id (:bank-account args))]})
|
||||
|
||||
(:vendor args)
|
||||
(merge-query {:query {:in ['?vendor-id]
|
||||
:where ['[?e :transaction/vendor ?vendor-id]]}
|
||||
:args [(:db/id (:vendor args))]})
|
||||
|
||||
|
||||
(:sort args) (add-sorter-fields {"client" ['[?e :transaction/client ?c]
|
||||
'[?c :client/name ?sort-client]]
|
||||
"vendor" '[(or-join [?e ?sort-vendor]
|
||||
(and
|
||||
[?e :transaction/vendor ?v]
|
||||
[?v :vendor/name ?sort-vendor])
|
||||
(and [(missing? $ ?e :transaction/vendor)]
|
||||
[(ground "") ?sort-vendor]))]
|
||||
"date" ['[?e :transaction/date ?sort-date]]
|
||||
"amount" ['[?e :transaction/amount ?sort-amount]]
|
||||
"description" ['[?e :transaction/description-original ?sort-description]]}
|
||||
args)
|
||||
|
||||
true
|
||||
(merge-query {:query {:find ['?sort-default '?e]}})))]
|
||||
|
||||
(->> (observable-query query)
|
||||
(apply-sort-4 (assoc query-params :default-asc? true))
|
||||
(apply-pagination query-params))))
|
||||
|
||||
(def default-read
|
||||
'[:transaction/amount
|
||||
:transaction/description-original
|
||||
:transaction/description-simple
|
||||
[ :transaction/date :xform clj-time.coerce/from-date]
|
||||
[ :transaction/post-date :xform clj-time.coerce/from-date]
|
||||
:transaction/type
|
||||
:transaction/status
|
||||
:db/id
|
||||
{:transaction/vendor [:vendor/name :db/id]
|
||||
:transaction/client [:client/name :client/code :db/id]
|
||||
:transaction/bank-account [:bank-account/numeric-code :bank-account/name]
|
||||
:transaction/accounts [{:transaction-account/account [:account/name :db/id]}
|
||||
:transaction-account/location
|
||||
:transaction-account/amount]
|
||||
:transaction/matched-rule [:matched-rule/name]}])
|
||||
|
||||
(defn hydrate-results [ids db _]
|
||||
(let [results (->> (pull-many db default-read ids)
|
||||
(group-by :db/id))
|
||||
results (->> ids
|
||||
(map results)
|
||||
(map first))]
|
||||
results))
|
||||
|
||||
(defn sum-amount [ids]
|
||||
(->>
|
||||
(dc/q {:find ['?id '?a]
|
||||
:in ['$ '[?id ...]]
|
||||
:where ['[?id :transaction/amount ?a]]}
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(map last)
|
||||
(reduce + 0.0)))
|
||||
|
||||
(defn fetch-page [request]
|
||||
(let [db (dc/db conn)
|
||||
{ids-to-retrieve :ids matching-count :count
|
||||
all-ids :all-ids} (fetch-ids db request)]
|
||||
|
||||
[(->> (hydrate-results ids-to-retrieve db request))
|
||||
matching-count
|
||||
(sum-amount all-ids)]))
|
||||
|
||||
(def query-schema (mc/schema
|
||||
[:maybe [:map {:date-range [:date-range :start-date :end-date]}
|
||||
[:sort {:optional true} [:maybe [:any]]]
|
||||
[:per-page {:optional true :default 25} [:maybe :int]]
|
||||
[:start {:optional true :default 0} [:maybe :int]]
|
||||
[:amount-gte {:optional true} [:maybe :double]]
|
||||
[:amount-lte {:optional true} [:maybe :double]]
|
||||
[:client-id {:optional true} [:maybe entity-id]]
|
||||
[:description {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||
[:vendor {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :vendor/name]}]]]
|
||||
[:bank-account {:optional true :default nil} [:maybe [:entity-map {:pull [:db/id :bank-account/numeric-code]}]]]
|
||||
#_[:status {:optional true} [:maybe (ref->enum-schema "transaction-status")]]
|
||||
[:exact-match-id {:optional true} [:maybe entity-id]]
|
||||
[:all-selected {:optional true :default nil} [:maybe :boolean]]
|
||||
[:selected {:optional true :default nil} [:maybe [:vector {:coerce? true}
|
||||
entity-id]]]
|
||||
[:start-date {:optional true}
|
||||
[:maybe clj-date-schema]]
|
||||
[:end-date {:optional true}
|
||||
[:maybe clj-date-schema]]]]))
|
||||
|
||||
|
||||
(def grid-page
|
||||
(helper/build {:id "entity-table"
|
||||
:nav com/main-aside-nav
|
||||
:check-boxes? true
|
||||
:page-specific-nav filters
|
||||
:fetch-page fetch-page
|
||||
:query-schema query-schema
|
||||
:parse-query-params (fn [p]
|
||||
(mc/decode query-schema p main-transformer))
|
||||
:action-buttons (fn [request]
|
||||
[(com/button {:color :primary
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
::route/new)}
|
||||
"Add Transaction")])
|
||||
:row-buttons (fn [request entity]
|
||||
[#_(when (and (can? (:identity request) {:subject :transaction :activity :edit}))
|
||||
(com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes
|
||||
::route/edit-wizard
|
||||
:db/id (:db/id entity))}
|
||||
svg/pencil))
|
||||
#_(when (and (can? (:identity request) {:subject :transaction :activity :delete}))
|
||||
(com/icon-button {:hx-delete (bidi/path-for ssr-routes/only-routes
|
||||
::route/delete
|
||||
:db/id (:db/id entity))
|
||||
:hx-confirm "Are you sure you want to delete this transaction?"}
|
||||
svg/trash))])
|
||||
|
||||
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)}
|
||||
"Transactions"]]
|
||||
:title (fn [r]
|
||||
"Transaction")
|
||||
:entity-name "register"
|
||||
:route ::route/table
|
||||
:csv-route ::route/csv
|
||||
:break-table (fn [request entity]
|
||||
(cond
|
||||
(= (-> request :query-params :sort first :name) "Vendor")
|
||||
(or (-> entity :transaction/vendor :vendor/name)
|
||||
"No vendor")
|
||||
|
||||
|
||||
|
||||
:else nil))
|
||||
:page->csv-entities (fn [[transactions]]
|
||||
transactions)
|
||||
:headers [{:key "id"
|
||||
:name "Id"
|
||||
:render-csv :db/id
|
||||
:render-for #{:csv}}
|
||||
{:key "client"
|
||||
:name "Client"
|
||||
:sort-key "client"
|
||||
:hide? (fn [args]
|
||||
(and (= (count (:clients args)) 1)
|
||||
(= 1 (count (:client/locations (:client args))))))
|
||||
:render (fn [x] [:div.flex.items-center.gap-2 (-> x :transaction/client :client/name)])
|
||||
:render-csv (fn [x] (-> x :transaction/client :client/name))}
|
||||
{:key "vendor"
|
||||
:name "Vendor"
|
||||
:sort-key "vendor"
|
||||
:render (fn [e]
|
||||
#_(alog/peek :vend e)
|
||||
(or
|
||||
(-> e :transaction/vendor :vendor/name)
|
||||
[:span.italic.text-gray-400 (-> e :transaction/description-simple)]))
|
||||
:render-csv (fn [e] (or (-> e :transaction/vendor :vendor/name)
|
||||
(-> e :transaction/description-simple)))}
|
||||
{:key "description"
|
||||
:name "Description"
|
||||
:sort-key "description"
|
||||
:render :transaction/description-original
|
||||
:render-csv :transaction/description-original}
|
||||
{:key "date"
|
||||
:sort-key "date"
|
||||
:name "Date"
|
||||
:show-starting "lg"
|
||||
:render (fn [{:transaction/keys [date]}]
|
||||
(some-> date (atime/unparse-local atime/normal-date)))}
|
||||
{:key "amount"
|
||||
:name "Amount"
|
||||
:sort-key "amount"
|
||||
:class "text-right"
|
||||
:render #(format "$%,.2f" (:transaction/amount %))
|
||||
:render-csv :transaction/amount }
|
||||
{:key "links"
|
||||
:name "Links"
|
||||
:show-starting "lg"
|
||||
:class "w-8"
|
||||
:render (fn [i]
|
||||
(link-dropdown
|
||||
(cond-> []
|
||||
(:transaction/payment i)
|
||||
(conj
|
||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::route/payment-page)
|
||||
{:exact-match-id (:db/id (:transaction/payment i))})
|
||||
:color :primary
|
||||
:content (format "Payment '%s'" (-> i :transaction/payment :payment/invoice-number))})
|
||||
|
||||
(and (:transaction/client-overrides i) (seq (:transaction/client-overrides i)))
|
||||
{:link (hu/url (bidi/path-for client-routes/routes
|
||||
:transactions)
|
||||
{:exact-match-id (:db/id i)})
|
||||
:color :primary
|
||||
:content "Client Overrides"})))
|
||||
:render-for #{:html}}]}))
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
|
||||
;; Handlers
|
||||
|
||||
|
||||
(def page (helper/page-route grid-page :parse-query-params? false))
|
||||
|
||||
(def table (helper/table-route grid-page :parse-query-params? false))
|
||||
|
||||
(defn csv [request]
|
||||
(helper/csv-route grid-page :parse-query-params? false request))
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{::route/page page
|
||||
::route/table table
|
||||
::route/csv csv
|
||||
::route/bank-account-filter bank-account-filter}
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-ensure-bank-account-belongs)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)
|
||||
(wrap-must {:activity :view :subject :transaction})
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
17
src/cljc/auto_ap/routes/transactions.cljc
Normal file
17
src/cljc/auto_ap/routes/transactions.cljc
Normal file
@@ -0,0 +1,17 @@
|
||||
(ns auto-ap.routes.transactions)
|
||||
|
||||
(def routes {"" {:get ::page}
|
||||
"/new" {:get ::new
|
||||
:post ::new-submit
|
||||
"/location-select" ::location-select
|
||||
"/account-typeahead" ::account-typeahead
|
||||
"/line-item" {:get ::new-line-item}}
|
||||
|
||||
"/external-new" ::external-page
|
||||
"/external-import-new" {"" ::external-import-page
|
||||
"/parse" ::external-import-parse
|
||||
"/import" ::external-import-import}
|
||||
|
||||
"/table" ::table
|
||||
"/csv" ::csv
|
||||
"/bank-account-filter" ::bank-account-filter })
|
||||
@@ -9,6 +9,8 @@
|
||||
[auto-ap.routes.dashboard :as d-routes]
|
||||
[auto-ap.routes.invoice :as i-routes]
|
||||
[auto-ap.routes.ledger :as l-routes]
|
||||
[auto-ap.routes.transactions :as t-routes]
|
||||
|
||||
[auto-ap.routes.admin.clients :as ac-routes]
|
||||
[auto-ap.routes.admin.sales-summaries :as ss-routes]
|
||||
[auto-ap.routes.admin.transaction-rules :as tr-routes]))
|
||||
@@ -51,6 +53,7 @@
|
||||
"/transaction-rule" tr-routes/routes
|
||||
"/excel-invoice" ei-routes/routes}
|
||||
"ledger" l-routes/routes
|
||||
"transaction2" t-routes/routes
|
||||
"transaction" {"/insights" {"" :transaction-insights
|
||||
"/table" :transaction-insight-table
|
||||
["/code/" [#"\d+" :transaction-id]] {:post :transaction-insight-code}
|
||||
|
||||
Reference in New Issue
Block a user