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