feat: transactions page with server-side filtering and pagination

This commit is contained in:
Martin 2026-03-20 01:16:40 +01:00
parent b1fbc921cd
commit b71628290c
6 changed files with 2247 additions and 12 deletions

1032
resources/2024.csv Normal file

File diff suppressed because it is too large Load Diff

1032
resources/2025.csv Normal file

File diff suppressed because it is too large Load Diff

View 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 }
})

View File

@ -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')}`
}
</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>