feat: transactions page with server-side filtering and pagination
This commit is contained in:
parent
b1fbc921cd
commit
b71628290c
1032
resources/2024.csv
Normal file
1032
resources/2024.csv
Normal file
File diff suppressed because it is too large
Load Diff
1032
resources/2025.csv
Normal file
1032
resources/2025.csv
Normal file
File diff suppressed because it is too large
Load Diff
10
src/AccountTracking.Web/src/stores/txPeriod.ts
Normal file
10
src/AccountTracking.Web/src/stores/txPeriod.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const useTxPeriodStore = defineStore('txPeriod', () => {
|
||||||
|
const now = new Date()
|
||||||
|
const year = ref(now.getFullYear())
|
||||||
|
const month = ref<number | null>(null)
|
||||||
|
|
||||||
|
return { year, month }
|
||||||
|
})
|
||||||
@ -1 +1,162 @@
|
|||||||
<template><div>Transactions</div></template>
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { transactionsApi } from '../api'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
import { useTxPeriodStore } from '../stores/txPeriod'
|
||||||
|
import type { TransactionDto } from '../api/apiClient'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const auth = useAuthStore()
|
||||||
|
const period = useTxPeriodStore()
|
||||||
|
|
||||||
|
const items = ref<TransactionDto[]>([])
|
||||||
|
const totalCount = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const loading = ref(false)
|
||||||
|
const categories = ref<string[]>([])
|
||||||
|
const selectedCategory = ref<string | null>(null)
|
||||||
|
const search = ref('')
|
||||||
|
let searchTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
const MONTHS = [
|
||||||
|
{ title: 'Vše', value: null },
|
||||||
|
...Array.from({ length: 12 }, (_, i) => ({
|
||||||
|
title: new Date(2000, i, 1).toLocaleDateString('cs-CZ', { month: 'long' }),
|
||||||
|
value: i + 1
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
|
||||||
|
const YEARS = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i)
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
{ title: 'Datum', key: 'bookingDate', sortable: false },
|
||||||
|
{ title: 'Protiúčet / Zpráva', key: 'counterPartyName', sortable: false },
|
||||||
|
{ title: 'Kategorie', key: 'category', sortable: false },
|
||||||
|
{ title: 'Částka', key: 'amount', sortable: false, align: 'end' as const },
|
||||||
|
{ title: 'Zůstatek', key: 'balance', sortable: false, align: 'end' as const },
|
||||||
|
]
|
||||||
|
|
||||||
|
async function loadCategories() {
|
||||||
|
categories.value = await transactionsApi.categories()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTransactions() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result = await transactionsApi.list(
|
||||||
|
period.year,
|
||||||
|
period.month ?? undefined,
|
||||||
|
selectedCategory.value ?? undefined,
|
||||||
|
search.value || undefined,
|
||||||
|
page.value,
|
||||||
|
50
|
||||||
|
)
|
||||||
|
items.value = result.items ?? []
|
||||||
|
totalCount.value = result.totalCount ?? 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearchInput() {
|
||||||
|
if (searchTimeout) clearTimeout(searchTimeout)
|
||||||
|
searchTimeout = setTimeout(() => { page.value = 1; loadTransactions() }, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([() => period.year, () => period.month, selectedCategory, page], loadTransactions)
|
||||||
|
|
||||||
|
loadCategories()
|
||||||
|
loadTransactions()
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
auth.logout()
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAmount(val: number) {
|
||||||
|
return `${val.toLocaleString('cs-CZ')} Kč`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-app-bar>
|
||||||
|
<v-app-bar-title>Finance Tracker — Transakce</v-app-bar-title>
|
||||||
|
<template #append>
|
||||||
|
<v-btn to="/" variant="text" class="mr-2">Dashboard</v-btn>
|
||||||
|
<v-btn icon="mdi-logout" variant="text" @click="logout" />
|
||||||
|
</template>
|
||||||
|
</v-app-bar>
|
||||||
|
|
||||||
|
<v-main>
|
||||||
|
<v-container fluid class="pa-4">
|
||||||
|
<v-row class="mb-2">
|
||||||
|
<v-col cols="6" md="2">
|
||||||
|
<v-select
|
||||||
|
v-model="period.year"
|
||||||
|
:items="YEARS"
|
||||||
|
label="Rok"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" md="2">
|
||||||
|
<v-select
|
||||||
|
v-model="period.month"
|
||||||
|
:items="MONTHS"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
label="Měsíc"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedCategory"
|
||||||
|
:items="[{ title: 'Vše', value: null }, ...categories.map(c => ({ title: c, value: c }))]"
|
||||||
|
item-title="title"
|
||||||
|
item-value="value"
|
||||||
|
label="Kategorie"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="5">
|
||||||
|
<v-text-field
|
||||||
|
v-model="search"
|
||||||
|
label="Hledat"
|
||||||
|
prepend-inner-icon="mdi-magnify"
|
||||||
|
density="compact"
|
||||||
|
clearable
|
||||||
|
@input="onSearchInput"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-data-table-server
|
||||||
|
:headers="headers"
|
||||||
|
:items="items"
|
||||||
|
:items-length="totalCount"
|
||||||
|
:loading="loading"
|
||||||
|
:items-per-page="50"
|
||||||
|
v-model:page="page"
|
||||||
|
@update:options="loadTransactions"
|
||||||
|
>
|
||||||
|
<template #item.bookingDate="{ item }">
|
||||||
|
{{ item.bookingDate }}
|
||||||
|
</template>
|
||||||
|
<template #item.counterPartyName="{ item }">
|
||||||
|
<div>{{ item.counterPartyName }}</div>
|
||||||
|
<div class="text-caption text-medium-emphasis">{{ item.message }}</div>
|
||||||
|
</template>
|
||||||
|
<template #item.amount="{ item }">
|
||||||
|
<span :class="(item.amount ?? 0) < 0 ? 'text-error' : 'text-success'">
|
||||||
|
{{ formatAmount(item.amount ?? 0) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #item.balance="{ item }">
|
||||||
|
{{ formatAmount(item.balance ?? 0) }}
|
||||||
|
</template>
|
||||||
|
</v-data-table-server>
|
||||||
|
</v-container>
|
||||||
|
</v-main>
|
||||||
|
</template>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user