Add idle-observer, format code
This commit is contained in:
parent
534071cbe0
commit
7455a55766
|
|
@ -12,6 +12,7 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^7.0.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.0.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.1.2",
|
||||
"@idle-observer/vue3": "^0.2.0",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.5.1",
|
||||
|
|
@ -1248,6 +1249,21 @@
|
|||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@idle-observer/core": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@idle-observer/core/-/core-0.2.0.tgz",
|
||||
"integrity": "sha512-mr8dedtzGUGMo38oP4+gDGq/oGjY0f9aCddsCcOm5XZTDE5ALSL3zyvXT8+eOjMrwOVLkDXt4BoC7A9U6ImANw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@idle-observer/vue3": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@idle-observer/vue3/-/vue3-0.2.0.tgz",
|
||||
"integrity": "sha512-amI/uRRcHIdOI5x7wLGxGK3ewaBcljsIwoXQ16sDCnnHqAg8zwa9H1PMb3QzaRqH5b5Ck2MX0YOY6+pd2EEohQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@idle-observer/core": "0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
|
|
@ -4827,9 +4843,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.1.9",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
|
||||
"integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
|
||||
"version": "7.1.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
|
||||
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^7.0.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.0.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.1.2",
|
||||
"@idle-observer/vue3": "^0.2.0",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.5.1",
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@ function goToTransaction() {
|
|||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="transaction-card" @click="goToTransaction()">
|
||||
<div
|
||||
class="transaction-card"
|
||||
@click="goToTransaction()"
|
||||
>
|
||||
<!-- Top row contains timestamp and amount. -->
|
||||
<div class="transaction-card-top-row">
|
||||
<div>
|
||||
|
|
@ -40,16 +43,25 @@ function goToTransaction() {
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-mono align-right font-size-small" :class="{
|
||||
'text-positive': moneyStyle === 'positive',
|
||||
'text-negative': moneyStyle === 'negative',
|
||||
}">
|
||||
<div
|
||||
class="font-mono align-right font-size-small"
|
||||
:class="{
|
||||
'text-positive': moneyStyle === 'positive',
|
||||
'text-negative': moneyStyle === 'negative',
|
||||
}"
|
||||
>
|
||||
{{ formatMoney(tx.amount, tx.currency) }}
|
||||
</div>
|
||||
<div v-if="tx.creditedAccount !== null" class="font-size-small text-muted">
|
||||
<div
|
||||
v-if="tx.creditedAccount !== null"
|
||||
class="font-size-small text-muted"
|
||||
>
|
||||
Credited to <span class="text-normal font-bold">{{ tx.creditedAccount.name }}</span>
|
||||
</div>
|
||||
<div v-if="tx.debitedAccount !== null" class="font-size-small text-muted">
|
||||
<div
|
||||
v-if="tx.debitedAccount !== null"
|
||||
class="font-size-small text-muted"
|
||||
>
|
||||
Debited to <span class="text-normal font-bold">{{ tx.debitedAccount.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -61,13 +73,21 @@ function goToTransaction() {
|
|||
</div>
|
||||
|
||||
<!-- Bottom row contains other links. -->
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<div style="display: flex; justify-content: space-between">
|
||||
<div>
|
||||
<CategoryLabel :category="tx.category" v-if="tx.category" style="margin-left: 0" />
|
||||
<CategoryLabel
|
||||
:category="tx.category"
|
||||
v-if="tx.category"
|
||||
style="margin-left: 0"
|
||||
/>
|
||||
<AppBadge v-if="tx.vendor">{{ tx.vendor.name }}</AppBadge>
|
||||
</div>
|
||||
<div>
|
||||
<TagLabel v-for="tag in tx.tags" :key="tag" :tag="tag" />
|
||||
<TagLabel
|
||||
v-for="tag in tx.tags"
|
||||
:key="tag"
|
||||
:tag="tag"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,23 +28,42 @@ function incrementPage(step: number) {
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="page && page.totalElements > 0">
|
||||
<AppButton size="sm" :disabled="!page || page.isFirst" @click="updatePage(1)">
|
||||
<AppButton
|
||||
size="sm"
|
||||
:disabled="!page || page.isFirst"
|
||||
@click="updatePage(1)"
|
||||
>
|
||||
First Page
|
||||
</AppButton>
|
||||
|
||||
<AppButton size="sm" :disabled="!page || page.isFirst" @click="incrementPage(-1)">
|
||||
<AppButton
|
||||
size="sm"
|
||||
:disabled="!page || page.isFirst"
|
||||
@click="incrementPage(-1)"
|
||||
>
|
||||
Previous Page
|
||||
</AppButton>
|
||||
|
||||
<span style="min-width: 100px; text-align: center; display: inline-block;" class="font-size-xsmall">
|
||||
<span
|
||||
style="min-width: 100px; text-align: center; display: inline-block"
|
||||
class="font-size-xsmall"
|
||||
>
|
||||
Page <span class="font-bold">{{ page?.pageRequest.page }}</span> of {{ page?.totalPages }}
|
||||
</span>
|
||||
|
||||
<AppButton size="sm" :disabled="!page || page.isLast" @click="incrementPage(1)">
|
||||
<AppButton
|
||||
size="sm"
|
||||
:disabled="!page || page.isLast"
|
||||
@click="incrementPage(1)"
|
||||
>
|
||||
Next Page
|
||||
</AppButton>
|
||||
|
||||
<AppButton size="sm" :disabled="!page || page.isLast" @click="updatePage(page?.totalPages ?? 0)">
|
||||
<AppButton
|
||||
size="sm"
|
||||
:disabled="!page || page.isLast"
|
||||
@click="updatePage(page?.totalPages ?? 0)"
|
||||
>
|
||||
Last Page
|
||||
</AppButton>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -87,30 +87,58 @@ async function deleteTransaction() {
|
|||
}
|
||||
</script>
|
||||
<template>
|
||||
<AppPage :title="'Transaction ' + transaction.id" v-if="transaction">
|
||||
<AppPage
|
||||
:title="'Transaction ' + transaction.id"
|
||||
v-if="transaction"
|
||||
>
|
||||
<!-- Top-row with some badges for amount, vendor, and category. -->
|
||||
<div>
|
||||
<AppBadge size="lg" class="font-mono">
|
||||
<AppBadge
|
||||
size="lg"
|
||||
class="font-mono"
|
||||
>
|
||||
{{ transaction.currency.code }} {{ formatMoney(transaction.amount, transaction.currency) }}
|
||||
</AppBadge>
|
||||
<AppBadge size="md" v-if="transaction.vendor">
|
||||
<AppBadge
|
||||
size="md"
|
||||
v-if="transaction.vendor"
|
||||
>
|
||||
{{ transaction.vendor.name }}
|
||||
</AppBadge>
|
||||
<CategoryLabel v-if="transaction.category" :category="transaction.category" :clickable="true" />
|
||||
<CategoryLabel
|
||||
v-if="transaction.category"
|
||||
:category="transaction.category"
|
||||
:clickable="true"
|
||||
/>
|
||||
</div>
|
||||
<!-- Second row that lists all tags. -->
|
||||
<div v-if="transaction.tags.length > 0" class="mt-1">
|
||||
<TagLabel v-for="t in transaction.tags" :key="t" :tag="t" />
|
||||
<div
|
||||
v-if="transaction.tags.length > 0"
|
||||
class="mt-1"
|
||||
>
|
||||
<TagLabel
|
||||
v-for="t in transaction.tags"
|
||||
:key="t"
|
||||
:tag="t"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p>{{ transaction.description }}</p>
|
||||
|
||||
<div v-if="transaction.creditedAccount" class="my-1">
|
||||
<div
|
||||
v-if="transaction.creditedAccount"
|
||||
class="my-1"
|
||||
>
|
||||
<strong class="text-negative">Credited</strong> from
|
||||
<RouterLink :to="`/profiles/${getSelectedProfile(route)}/accounts/${transaction.creditedAccount.id}`">
|
||||
<RouterLink
|
||||
:to="`/profiles/${getSelectedProfile(route)}/accounts/${transaction.creditedAccount.id}`"
|
||||
>
|
||||
{{ transaction.creditedAccount.name }} (#{{ transaction.creditedAccount.numberSuffix }})
|
||||
</RouterLink>
|
||||
<div v-if="creditedAccountBalanceDiff" class="font-size-xsmall">
|
||||
<div
|
||||
v-if="creditedAccountBalanceDiff"
|
||||
class="font-size-xsmall"
|
||||
>
|
||||
Balance Before:
|
||||
<span class="font-mono">
|
||||
{{ formatMoney(creditedAccountBalanceDiff.before, transaction.currency) }}
|
||||
|
|
@ -122,12 +150,20 @@ async function deleteTransaction() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="transaction.debitedAccount" class="my-1">
|
||||
<div
|
||||
v-if="transaction.debitedAccount"
|
||||
class="my-1"
|
||||
>
|
||||
<strong class="text-positive">Debited</strong> to
|
||||
<RouterLink :to="`/profiles/${getSelectedProfile(route)}/accounts/${transaction.debitedAccount.id}`">
|
||||
<RouterLink
|
||||
:to="`/profiles/${getSelectedProfile(route)}/accounts/${transaction.debitedAccount.id}`"
|
||||
>
|
||||
{{ transaction.debitedAccount.name }} (#{{ transaction.debitedAccount.numberSuffix }})
|
||||
</RouterLink>
|
||||
<div v-if="debitedAccountBalanceDiff" class="font-size-xsmall">
|
||||
<div
|
||||
v-if="debitedAccountBalanceDiff"
|
||||
class="font-size-xsmall"
|
||||
>
|
||||
Balance Before:
|
||||
<span class="font-mono">
|
||||
{{ formatMoney(debitedAccountBalanceDiff.before, transaction.currency) }}
|
||||
|
|
@ -153,21 +189,39 @@ async function deleteTransaction() {
|
|||
|
||||
<div v-if="transaction.lineItems.length > 0">
|
||||
<h3>Line Items</h3>
|
||||
<LineItemCard v-for="item of transaction.lineItems" :key="item.idx" :line-item="item"
|
||||
:currency="transaction.currency" :total-count="transaction.lineItems.length" :editable="false" />
|
||||
<LineItemCard
|
||||
v-for="item of transaction.lineItems"
|
||||
:key="item.idx"
|
||||
:line-item="item"
|
||||
:currency="transaction.currency"
|
||||
:total-count="transaction.lineItems.length"
|
||||
:editable="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="transaction.attachments.length > 0">
|
||||
<h3>Attachments</h3>
|
||||
<AttachmentRow v-for="a in transaction.attachments" :attachment="a" :key="a.id" disabled />
|
||||
<AttachmentRow
|
||||
v-for="a in transaction.attachments"
|
||||
:attachment="a"
|
||||
:key="a.id"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<ButtonBar>
|
||||
<AppButton icon="wrench" @click="
|
||||
router.push(`/profiles/${getSelectedProfile(route)}/transactions/${transaction.id}/edit`)
|
||||
">
|
||||
<AppButton
|
||||
icon="wrench"
|
||||
@click="
|
||||
router.push(`/profiles/${getSelectedProfile(route)}/transactions/${transaction.id}/edit`)
|
||||
"
|
||||
>
|
||||
Edit
|
||||
</AppButton>
|
||||
<AppButton icon="trash" @click="deleteTransaction()">Delete</AppButton>
|
||||
<AppButton
|
||||
icon="trash"
|
||||
@click="deleteTransaction()"
|
||||
>Delete</AppButton
|
||||
>
|
||||
</ButtonBar>
|
||||
</AppPage>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,34 @@
|
|||
<script setup lang="ts">
|
||||
import { type Account, AccountApiClient } from '@/api/account';
|
||||
import { defaultPage, type Page, type PageRequest, type SortDir } from '@/api/pagination';
|
||||
import { getSelectedProfile } from '@/api/profile';
|
||||
import { TransactionApiClient, type TransactionCategory, type TransactionVendor, type TransactionsListItem } from '@/api/transaction';
|
||||
import AppBadge from '@/components/common/AppBadge.vue';
|
||||
import AppButton from '@/components/common/AppButton.vue';
|
||||
import AppPage from '@/components/common/AppPage.vue';
|
||||
import ButtonBar from '@/components/common/ButtonBar.vue';
|
||||
import AppForm from '@/components/common/form/AppForm.vue';
|
||||
import FormControl from '@/components/common/form/FormControl.vue';
|
||||
import FormGroup from '@/components/common/form/FormGroup.vue';
|
||||
import PaginationControls from '@/components/common/PaginationControls.vue';
|
||||
import TransactionCard from '@/components/TransactionCard.vue';
|
||||
import { computed, onMounted, ref, watch, type Ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import VueSelect from 'vue3-select-component';
|
||||
import { type Account, AccountApiClient } from '@/api/account'
|
||||
import { defaultPage, type Page, type PageRequest, type SortDir } from '@/api/pagination'
|
||||
import { getSelectedProfile } from '@/api/profile'
|
||||
import {
|
||||
TransactionApiClient,
|
||||
type TransactionCategory,
|
||||
type TransactionVendor,
|
||||
type TransactionsListItem,
|
||||
} from '@/api/transaction'
|
||||
import AppBadge from '@/components/common/AppBadge.vue'
|
||||
import AppButton from '@/components/common/AppButton.vue'
|
||||
import AppPage from '@/components/common/AppPage.vue'
|
||||
import ButtonBar from '@/components/common/ButtonBar.vue'
|
||||
import AppForm from '@/components/common/form/AppForm.vue'
|
||||
import FormControl from '@/components/common/form/FormControl.vue'
|
||||
import FormGroup from '@/components/common/form/FormGroup.vue'
|
||||
import PaginationControls from '@/components/common/PaginationControls.vue'
|
||||
import TransactionCard from '@/components/TransactionCard.vue'
|
||||
import { computed, onMounted, ref, watch, type Ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import VueSelect from 'vue3-select-component'
|
||||
|
||||
interface SortOption {
|
||||
label: string
|
||||
property: string
|
||||
}
|
||||
const SORT_PROPERTIES: SortOption[] = [
|
||||
{ label: "Timestamp", property: "txn.timestamp" },
|
||||
{ label: "Added at", property: "txn.added_at" },
|
||||
{ label: "Amount", property: "txn.amount" }
|
||||
{ label: 'Timestamp', property: 'txn.timestamp' },
|
||||
{ label: 'Added at', property: 'txn.added_at' },
|
||||
{ label: 'Amount', property: 'txn.amount' },
|
||||
]
|
||||
|
||||
const FETCH_DEBOUNCE_DELAY = 300
|
||||
|
|
@ -44,7 +49,7 @@ const searchQuery = ref('')
|
|||
const tagFilters: Ref<string[]> = ref([])
|
||||
const availableTags: Ref<string[]> = ref([])
|
||||
const tagOptions = computed(() => {
|
||||
return availableTags.value.map(tag => {
|
||||
return availableTags.value.map((tag) => {
|
||||
return { label: tag, value: tag }
|
||||
})
|
||||
})
|
||||
|
|
@ -52,22 +57,26 @@ const tagOptions = computed(() => {
|
|||
const vendorFilters = ref<number[]>([])
|
||||
const availableVendors = ref<TransactionVendor[]>([])
|
||||
const vendorOptions = computed(() => {
|
||||
return availableVendors.value.map(vendor => {
|
||||
return availableVendors.value.map((vendor) => {
|
||||
return { label: vendor.name, value: vendor.id }
|
||||
})
|
||||
})
|
||||
|
||||
const categoryFilters = ref<number[]>([])
|
||||
const availableCategories = ref<TransactionCategory[]>([])
|
||||
const categoryOptions = computed(() => availableCategories.value.map(category => {
|
||||
return { label: category.name, value: category.id }
|
||||
}))
|
||||
const categoryOptions = computed(() =>
|
||||
availableCategories.value.map((category) => {
|
||||
return { label: category.name, value: category.id }
|
||||
}),
|
||||
)
|
||||
|
||||
const accountFilters = ref<number[]>([])
|
||||
const availableAccounts = ref<Account[]>([])
|
||||
const accountOptions = computed(() => availableAccounts.value.map(acc => {
|
||||
return { label: `${acc.name} - #${acc.numberSuffix}`, value: acc.id }
|
||||
}))
|
||||
const accountOptions = computed(() =>
|
||||
availableAccounts.value.map((acc) => {
|
||||
return { label: `${acc.name} - #${acc.numberSuffix}`, value: acc.id }
|
||||
}),
|
||||
)
|
||||
|
||||
const minAmountFilter = ref<number | undefined>(undefined)
|
||||
const maxAmountFilter = ref<number | undefined>(undefined)
|
||||
|
|
@ -75,11 +84,11 @@ const maxAmountFilter = ref<number | undefined>(undefined)
|
|||
onMounted(async () => {
|
||||
loadFiltersFromRoute()
|
||||
const api = new TransactionApiClient(getSelectedProfile(route))
|
||||
api.getAllTags().then(tags => availableTags.value = tags)
|
||||
api.getVendors().then(vendors => availableVendors.value = vendors)
|
||||
api.getCategoriesFlattened().then(categories => availableCategories.value = categories)
|
||||
api.getAllTags().then((tags) => (availableTags.value = tags))
|
||||
api.getVendors().then((vendors) => (availableVendors.value = vendors))
|
||||
api.getCategoriesFlattened().then((categories) => (availableCategories.value = categories))
|
||||
const accountApi = new AccountApiClient(route)
|
||||
accountApi.getAccounts().then(accounts => availableAccounts.value = accounts)
|
||||
accountApi.getAccounts().then((accounts) => (availableAccounts.value = accounts))
|
||||
await fetchPage(1, 10)
|
||||
|
||||
watch(
|
||||
|
|
@ -92,14 +101,14 @@ onMounted(async () => {
|
|||
categoryFilters,
|
||||
accountFilters,
|
||||
minAmountFilter,
|
||||
maxAmountFilter
|
||||
maxAmountFilter,
|
||||
],
|
||||
() => {
|
||||
window.clearTimeout(fetchTimeoutId.value)
|
||||
fetchTimeoutId.value = window.setTimeout(() => {
|
||||
fetchPage(1, 10)
|
||||
}, FETCH_DEBOUNCE_DELAY)
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -117,9 +126,9 @@ async function fetchPage(pg: number, size: number) {
|
|||
sorts: [
|
||||
{
|
||||
attribute: selectedSort.value,
|
||||
dir: selectedSortDir.value
|
||||
}
|
||||
]
|
||||
dir: selectedSortDir.value,
|
||||
},
|
||||
],
|
||||
}
|
||||
const params = buildFiltersQuery()
|
||||
const urlWithParams = params.size == 0 ? route.path : route.path + '?' + params.toString()
|
||||
|
|
@ -139,25 +148,25 @@ async function fetchPage(pg: number, size: number) {
|
|||
function buildFiltersQuery(): URLSearchParams {
|
||||
const p = new URLSearchParams()
|
||||
if (searchQuery.value.trim().length > 0) {
|
||||
p.append("q", searchQuery.value.trim())
|
||||
p.append('q', searchQuery.value.trim())
|
||||
}
|
||||
for (const tag of tagFilters.value) {
|
||||
p.append("tag", tag)
|
||||
p.append('tag', tag)
|
||||
}
|
||||
for (const vendorId of vendorFilters.value) {
|
||||
p.append("vendor", vendorId + "")
|
||||
p.append('vendor', vendorId + '')
|
||||
}
|
||||
for (const categoryId of categoryFilters.value) {
|
||||
p.append("category", categoryId + "")
|
||||
p.append('category', categoryId + '')
|
||||
}
|
||||
for (const accountId of accountFilters.value) {
|
||||
p.append("account", accountId + "")
|
||||
p.append('account', accountId + '')
|
||||
}
|
||||
if (minAmountFilter.value !== undefined && minAmountFilter.value > 0) {
|
||||
p.append("min-amount", minAmountFilter.value * 100 + "")
|
||||
p.append('min-amount', minAmountFilter.value * 100 + '')
|
||||
}
|
||||
if (maxAmountFilter.value !== undefined && maxAmountFilter.value > 0) {
|
||||
p.append("max-amount", maxAmountFilter.value * 100 + "")
|
||||
p.append('max-amount', maxAmountFilter.value * 100 + '')
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
|
@ -177,16 +186,16 @@ function goToHome() {
|
|||
}
|
||||
|
||||
function loadFiltersFromRoute() {
|
||||
searchQuery.value = loadFirstParamValue("q") ?? ''
|
||||
tagFilters.value = loadAllParamValues("tag")
|
||||
vendorFilters.value = loadAllParamValues("vendor").map(s => parseInt(s))
|
||||
categoryFilters.value = loadAllParamValues("category").map(s => parseInt(s))
|
||||
accountFilters.value = loadAllParamValues("account").map(s => parseInt(s))
|
||||
const minAmount = loadFirstParamValue("min-amount")
|
||||
searchQuery.value = loadFirstParamValue('q') ?? ''
|
||||
tagFilters.value = loadAllParamValues('tag')
|
||||
vendorFilters.value = loadAllParamValues('vendor').map((s) => parseInt(s))
|
||||
categoryFilters.value = loadAllParamValues('category').map((s) => parseInt(s))
|
||||
accountFilters.value = loadAllParamValues('account').map((s) => parseInt(s))
|
||||
const minAmount = loadFirstParamValue('min-amount')
|
||||
if (minAmount !== undefined) {
|
||||
minAmountFilter.value = Math.round(parseInt(minAmount) / 100)
|
||||
}
|
||||
const maxAmount = loadFirstParamValue("max-amount")
|
||||
const maxAmount = loadFirstParamValue('max-amount')
|
||||
if (maxAmount !== undefined) {
|
||||
maxAmountFilter.value = Math.round(parseInt(maxAmount) / 100)
|
||||
}
|
||||
|
|
@ -205,7 +214,7 @@ function loadFirstParamValue(key: string): string | undefined {
|
|||
function loadAllParamValues(key: string): string[] {
|
||||
if (key in route.query) {
|
||||
if (Array.isArray(route.query[key])) {
|
||||
return route.query[key].filter(s => s !== null)
|
||||
return route.query[key].filter((s) => s !== null)
|
||||
} else if (route.query[key] !== null) {
|
||||
return [route.query[key]]
|
||||
}
|
||||
|
|
@ -217,43 +226,84 @@ function loadAllParamValues(key: string): string[] {
|
|||
<AppPage title="Transactions">
|
||||
<AppForm>
|
||||
<FormGroup>
|
||||
<FormControl label="Search" hint="Free-form text search against description, tags, vendor, category, account.">
|
||||
<input v-model="searchQuery" type="text" placeholder="Search for transactions..." />
|
||||
<FormControl
|
||||
label="Search"
|
||||
hint="Free-form text search against description, tags, vendor, category, account."
|
||||
>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search for transactions..."
|
||||
/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<div class="vueselect-control">
|
||||
<h5>Tag</h5>
|
||||
<VueSelect v-model="tagFilters" :options="tagOptions" placeholder="Select tags" is-multi />
|
||||
<VueSelect
|
||||
v-model="tagFilters"
|
||||
:options="tagOptions"
|
||||
placeholder="Select tags"
|
||||
is-multi
|
||||
/>
|
||||
</div>
|
||||
<div class="vueselect-control">
|
||||
<h5>Vendor</h5>
|
||||
<VueSelect v-model="vendorFilters" :options="vendorOptions" placeholder="Select vendors" is-multi />
|
||||
<VueSelect
|
||||
v-model="vendorFilters"
|
||||
:options="vendorOptions"
|
||||
placeholder="Select vendors"
|
||||
is-multi
|
||||
/>
|
||||
</div>
|
||||
<div class="vueselect-control">
|
||||
<h5>Category</h5>
|
||||
<VueSelect v-model="categoryFilters" :options="categoryOptions" placeholder="Select categories" is-multi />
|
||||
<VueSelect
|
||||
v-model="categoryFilters"
|
||||
:options="categoryOptions"
|
||||
placeholder="Select categories"
|
||||
is-multi
|
||||
/>
|
||||
</div>
|
||||
<div class="vueselect-control">
|
||||
<h5>Account</h5>
|
||||
<VueSelect v-model="accountFilters" :options="accountOptions" placeholder="Select accounts" is-multi />
|
||||
<VueSelect
|
||||
v-model="accountFilters"
|
||||
:options="accountOptions"
|
||||
placeholder="Select accounts"
|
||||
is-multi
|
||||
/>
|
||||
</div>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormControl label="Max Amount">
|
||||
<input v-model="maxAmountFilter" type="number" min="0" step="1" />
|
||||
<input
|
||||
v-model="maxAmountFilter"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Min Amount">
|
||||
<input v-model="minAmountFilter" type="number" min="0" step="1" />
|
||||
<input
|
||||
v-model="minAmountFilter"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormControl label="Sort By">
|
||||
<select v-model="selectedSort">
|
||||
<option v-for="sortOpt in SORT_PROPERTIES" :key="sortOpt.property" :value="sortOpt.property">{{
|
||||
sortOpt.label }}
|
||||
<option
|
||||
v-for="sortOpt in SORT_PROPERTIES"
|
||||
:key="sortOpt.property"
|
||||
:value="sortOpt.property"
|
||||
>
|
||||
{{ sortOpt.label }}
|
||||
</option>
|
||||
</select>
|
||||
</FormControl>
|
||||
|
|
@ -265,18 +315,36 @@ function loadAllParamValues(key: string): string[] {
|
|||
</FormControl>
|
||||
</FormGroup>
|
||||
<ButtonBar>
|
||||
<AppButton size="sm" icon="home" @click="goToHome()">Back to Homepage</AppButton>
|
||||
<AppButton size="sm" icon="trash" @click="clearFilters()">Clear Filters</AppButton>
|
||||
<AppButton
|
||||
size="sm"
|
||||
icon="home"
|
||||
@click="goToHome()"
|
||||
>Back to Homepage</AppButton
|
||||
>
|
||||
<AppButton
|
||||
size="sm"
|
||||
icon="trash"
|
||||
@click="clearFilters()"
|
||||
>Clear Filters</AppButton
|
||||
>
|
||||
</ButtonBar>
|
||||
</AppForm>
|
||||
|
||||
<PaginationControls :page="page" @update="(pr) => fetchPage(pr.page, pr.size)" class="align-right" />
|
||||
<PaginationControls
|
||||
:page="page"
|
||||
@update="(pr) => fetchPage(pr.page, pr.size)"
|
||||
class="align-right"
|
||||
/>
|
||||
<AppBadge size="sm">
|
||||
{{ page.totalElements }} search
|
||||
{{ page.totalElements == 1 ? 'result' : 'results' }}
|
||||
in {{ lastFetchTime }} milliseconds
|
||||
</AppBadge>
|
||||
<TransactionCard v-for="txn in page.items" :key="txn.id" :tx="txn" />
|
||||
<TransactionCard
|
||||
v-for="txn in page.items"
|
||||
:key="txn.id"
|
||||
:tx="txn"
|
||||
/>
|
||||
</AppPage>
|
||||
</template>
|
||||
<style lang="css" scoped>
|
||||
|
|
@ -285,7 +353,7 @@ function loadAllParamValues(key: string): string[] {
|
|||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.vueselect-control>h5 {
|
||||
.vueselect-control > h5 {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -61,16 +61,25 @@ async function checkAuth() {
|
|||
<div>
|
||||
<header class="app-header-bar">
|
||||
<div>
|
||||
<h1 class="app-header-text" @click="onHeaderClicked()">
|
||||
<h1
|
||||
class="app-header-text"
|
||||
@click="onHeaderClicked()"
|
||||
>
|
||||
Finnow
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<span class="app-user-widget" @click="router.push('/me')">
|
||||
<span
|
||||
class="app-user-widget"
|
||||
@click="router.push('/me')"
|
||||
>
|
||||
<font-awesome-icon icon="fa-user"></font-awesome-icon>
|
||||
</span>
|
||||
|
||||
<span class="app-logout-button" @click="authStore.onUserLoggedOut()">
|
||||
<span
|
||||
class="app-logout-button"
|
||||
@click="authStore.onUserLoggedOut()"
|
||||
>
|
||||
<font-awesome-icon icon="fa-solid fa-arrow-right-from-bracket"></font-awesome-icon>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -40,13 +40,25 @@ function goToSearch() {
|
|||
<template>
|
||||
<HomeModule title="Transactions">
|
||||
<template v-slot:default>
|
||||
<PaginationControls :page="transactions" @update="(pr) => fetchPage(pr)" class="align-right" />
|
||||
<TransactionCard v-for="tx in transactions.items" :key="tx.id" :tx="tx" />
|
||||
<PaginationControls
|
||||
:page="transactions"
|
||||
@update="(pr) => fetchPage(pr)"
|
||||
class="align-right"
|
||||
/>
|
||||
<TransactionCard
|
||||
v-for="tx in transactions.items"
|
||||
:key="tx.id"
|
||||
:tx="tx"
|
||||
/>
|
||||
<p v-if="transactions.totalElements === 0">You haven't added any transactions.</p>
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<AppButton icon="plus" @click="router.push(`/profiles/${getSelectedProfile(route)}/add-transaction`)">
|
||||
Add Transaction</AppButton>
|
||||
<AppButton
|
||||
icon="plus"
|
||||
@click="router.push(`/profiles/${getSelectedProfile(route)}/add-transaction`)"
|
||||
>
|
||||
Add Transaction</AppButton
|
||||
>
|
||||
<AppButton @click="goToSearch()">Search</AppButton>
|
||||
</template>
|
||||
</HomeModule>
|
||||
|
|
|
|||
Loading…
Reference in New Issue