Fix #7 Added Month Markings, Range Select

This commit is contained in:
2026-01-15 18:45:25 +01:00
parent e7fb2df5c7
commit d140251aa0
3 changed files with 245 additions and 196 deletions

View File

@@ -1,14 +1,9 @@
<script setup>
import dayjs from "dayjs";
// Zugriff auf $api und Toast Notification
const { $api } = useNuxtApp()
const {$api, $dayjs} = useNuxtApp()
const toast = useToast()
defineShortcuts({
'/': () => {
document.getElementById("searchinput").focus()
}
'/': () => document.getElementById("searchinput").focus()
})
const tempStore = useTempStore()
@@ -18,238 +13,280 @@ const route = useRoute()
const bankstatements = ref([])
const bankaccounts = ref([])
const filterAccount = ref([])
// Status für den Lade-Button
const isSyncing = ref(false)
const loadingDocs = ref(true) // Startet im Ladezustand
// Zeitraum-Optionen
const periodOptions = [
{label: 'Aktueller Monat', key: 'current_month'},
{label: 'Letzter Monat', key: 'last_month'},
{label: 'Aktuelles Quartal', key: 'current_quarter'},
{label: 'Letztes Quartal', key: 'last_quarter'},
{label: 'Benutzerdefiniert', key: 'custom'}
]
// Initialisierungswerte
const selectedPeriod = ref(periodOptions[0])
const dateRange = ref({
start: $dayjs().startOf('month').format('YYYY-MM-DD'),
end: $dayjs().endOf('month').format('YYYY-MM-DD')
})
const setupPage = async () => {
bankstatements.value = (await useEntities("bankstatements").select("*, statementallocations(*)", "date", false))
bankaccounts.value = await useEntities("bankaccounts").select()
if(bankaccounts.value.length > 0) filterAccount.value = bankaccounts.value
loadingDocs.value = true
try {
const [statements, accounts] = await Promise.all([
useEntities("bankstatements").select("*, statementallocations(*)", "valueDate", false),
useEntities("bankaccounts").select()
])
bankstatements.value = statements
bankaccounts.value = accounts
if (bankaccounts.value.length > 0 && filterAccount.value.length === 0) {
filterAccount.value = bankaccounts.value
}
// Erst nach dem Laden der Daten die Store-Werte anwenden
const savedBanking = tempStore.settings?.['banking'] || {}
if (savedBanking.periodKey) {
const found = periodOptions.find(p => p.key === savedBanking.periodKey)
if (found) selectedPeriod.value = found
}
if (savedBanking.range) {
dateRange.value = savedBanking.range
}
} catch (err) {
console.error("Setup Error:", err)
} finally {
loadingDocs.value = false
}
}
// Funktion für den Bankabruf
// Watcher für Schnellwahlen & Persistenz
watch([selectedPeriod, dateRange], ([newPeriod, newRange], [oldPeriod, oldRange]) => {
const now = $dayjs()
// Nur berechnen, wenn sich die Periode geändert hat
if (newPeriod.key !== oldPeriod?.key) {
switch (newPeriod.key) {
case 'current_month':
dateRange.value = {start: now.startOf('month').format('YYYY-MM-DD'), end: now.endOf('month').format('YYYY-MM-DD')}
break
case 'last_month':
const lastMonth = now.subtract(1, 'month')
dateRange.value = {start: lastMonth.startOf('month').format('YYYY-MM-DD'), end: lastMonth.endOf('month').format('YYYY-MM-DD')}
break
case 'current_quarter':
dateRange.value = {start: now.startOf('quarter').format('YYYY-MM-DD'), end: now.endOf('quarter').format('YYYY-MM-DD')}
break
case 'last_quarter':
const lastQuarter = now.subtract(1, 'quarter')
dateRange.value = {start: lastQuarter.startOf('quarter').format('YYYY-MM-DD'), end: lastQuarter.endOf('quarter').format('YYYY-MM-DD')}
break
}
}
// Speichern im Store
tempStore.modifyBankingPeriod(selectedPeriod.value.key, dateRange.value)
}, { deep: true })
const syncBankStatements = async () => {
isSyncing.value = true
try {
await $api('/api/functions/services/bankstatementsync', { method: 'POST' })
toast.add({
title: 'Erfolg',
description: 'Bankdaten wurden erfolgreich synchronisiert.',
icon: 'i-heroicons-check-circle',
color: 'green'
})
// Wichtig: Daten neu laden, damit die neuen Buchungen direkt sichtbar sind
await $api('/api/functions/services/bankstatementsync', {method: 'POST'})
toast.add({title: 'Erfolg', description: 'Bankdaten synchronisiert.', color: 'green'})
await setupPage()
} catch (error) {
console.error(error)
toast.add({
title: 'Fehler',
description: 'Beim Abrufen der Bankdaten ist ein Fehler aufgetreten.',
icon: 'i-heroicons-exclamation-circle',
color: 'red'
})
toast.add({title: 'Fehler', description: 'Fehler beim Abruf.', color: 'red'})
} finally {
isSyncing.value = false
}
}
const templateColumns = [
{
key: "account",
label: "Konto"
},{
key: "valueDate",
label: "Valuta"
},
{
key: "amount",
label: "Betrag"
},
{
key: "openAmount",
label: "Offener Betrag"
},
{
key: "partner",
label: "Name"
},
{
key: "text",
label: "Beschreibung"
}
{key: "account", label: "Konto"},
{key: "valueDate", label: "Valuta"},
{key: "amount", label: "Betrag"},
{key: "openAmount", label: "Offen"},
{key: "partner", label: "Name"},
{key: "text", label: "Beschreibung"}
]
const selectedColumns = ref(templateColumns)
const columns = computed(() => templateColumns.filter((column) => selectedColumns.value.includes(column)))
const searchString = ref(tempStore.searchStrings["bankstatements"] || '')
const selectedFilters = ref(tempStore.filters?.["banking"]?.["main"] || ['Nur offene anzeigen'])
const searchString = ref(tempStore.searchStrings["bankstatements"] ||'')
const clearSearchString = () => {
tempStore.clearSearchString("bankstatements")
searchString.value = ''
const shouldShowMonthDivider = (row, index) => {
if (index === 0) return true;
const prevRow = filteredRows.value[index - 1];
return $dayjs(row.valueDate).format('MMMM YYYY') !== $dayjs(prevRow.valueDate).format('MMMM YYYY');
}
const displayCurrency = (value, currency = "€") => {
return `${Number(value).toFixed(2).replace(".",",")} ${currency}`
}
const calculateOpenSum = (statement) => {
let startingAmount = 0
statement.statementallocations.forEach(item => {
startingAmount += item.amount
})
return (statement.amount - startingAmount).toFixed(2)
const allocated = statement.statementallocations?.reduce((acc, curr) => acc + curr.amount, 0) || 0;
return (statement.amount - allocated).toFixed(2);
}
const selectedFilters = ref(tempStore.filters?.["banking"]?.["main"] ? tempStore.filters["banking"]["main"] : ['Nur offene anzeigen'])
const filteredRows = computed(() => {
let temp = bankstatements.value
if (!bankstatements.value.length) return []
if(route.query.filter) {
console.log(route.query.filter)
temp = temp.filter(i => JSON.parse(route.query.filter).includes(i.id))
} else {
if(selectedFilters.value.includes("Nur offene anzeigen")){
temp = temp.filter(i => Number(calculateOpenSum(i)) !== 0)
}
let temp = [...bankstatements.value]
if(selectedFilters.value.includes("Nur positive anzeigen")){
temp = temp.filter(i => i.amount >= 0)
}
if(selectedFilters.value.includes("Nur negative anzeigen")){
temp = temp.filter(i => i.amount < 0)
}
// Filterung nach Datum
if (dateRange.value.start) {
temp = temp.filter(i => $dayjs(i.valueDate).isSameOrAfter($dayjs(dateRange.value.start), 'day'))
}
if (dateRange.value.end) {
temp = temp.filter(i => $dayjs(i.valueDate).isSameOrBefore($dayjs(dateRange.value.end), 'day'))
}
return useSearch(searchString.value, temp.filter(i => filterAccount.value.find(x => x.id === i.account)))
// Status Filter
if (selectedFilters.value.includes("Nur offene anzeigen")) {
temp = temp.filter(i => Number(calculateOpenSum(i)) !== 0)
}
if (selectedFilters.value.includes("Nur positive anzeigen")) {
temp = temp.filter(i => i.amount >= 0)
}
if (selectedFilters.value.includes("Nur negative anzeigen")) {
temp = temp.filter(i => i.amount < 0)
}
// Konto Filter & Suche
let results = temp.filter(i => filterAccount.value.find(x => x.id === i.account))
if (searchString.value) {
results = useSearch(searchString.value, results)
}
return results.sort((a, b) => $dayjs(b.valueDate).unix() - $dayjs(a.valueDate).unix())
})
setupPage()
const displayCurrency = (value) => `${Number(value).toFixed(2).replace(".", ",")}`
onMounted(() => {
setupPage()
})
</script>
<template>
<UDashboardNavbar title="Bankbuchungen" :badge="filteredRows.length">
<template #right>
<UButton
label="Bankabruf"
icon="i-heroicons-arrow-path"
color="primary"
variant="solid"
:loading="isSyncing"
@click="syncBankStatements"
class="mr-2"
/>
<UInput
id="searchinput"
name="searchinput"
v-model="searchString"
icon="i-heroicons-funnel"
autocomplete="off"
icon="i-heroicons-magnifying-glass"
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
@change="tempStore.modifySearchString('bankstatements',searchString)"
>
<template #trailing>
<UKbd value="/" />
</template>
</UInput>
<UButton
icon="i-heroicons-x-mark"
variant="outline"
color="rose"
@click="clearSearchString()"
v-if="searchString.length > 0"
/>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
<template #left>
<USelectMenu
:options="bankaccounts"
v-model="filterAccount"
option-attribute="iban"
multiple
by="id"
:ui-menu="{ width: 'min-w-max' }"
>
<template #label>
Konto
</template>
</USelectMenu>
<div class="flex items-center gap-3">
<USelectMenu
:options="bankaccounts"
v-model="filterAccount"
option-attribute="iban"
multiple
by="id"
placeholder="Konten"
class="w-48"
/>
<UDivider orientation="vertical" class="h-6"/>
<div class="flex items-center gap-2">
<USelectMenu
v-model="selectedPeriod"
:options="periodOptions"
class="w-44"
icon="i-heroicons-calendar-days"
/>
<div v-if="selectedPeriod.key === 'custom'" class="flex items-center gap-1">
<UInput type="date" v-model="dateRange.start" size="xs" class="w-32"/>
<UInput type="date" v-model="dateRange.end" size="xs" class="w-32"/>
</div>
<div v-else class="text-xs text-gray-400 hidden sm:block italic">
{{ $dayjs(dateRange.start).format('DD.MM.') }} - {{ $dayjs(dateRange.end).format('DD.MM.YYYY') }}
</div>
</div>
</div>
</template>
<template #right>
<USelectMenu
icon="i-heroicons-adjustments-horizontal-solid"
icon="i-heroicons-adjustments-horizontal"
multiple
v-model="selectedFilters"
:options="['Nur offene anzeigen','Nur positive anzeigen','Nur negative anzeigen']"
:color="selectedFilters.length > 0 ? 'primary' : 'white'"
:ui-menu="{ width: 'min-w-max' }"
@change="tempStore.modifyFilter('banking','main',selectedFilters)"
>
<template #label>
Filter
</template>
</USelectMenu>
/>
</template>
</UDashboardToolbar>
<UTable
:rows="filteredRows"
:columns="columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="(i) => router.push(`/banking/statements/edit/${i.id}`)"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Buchungen anzuzeigen' }"
>
<template #account-data="{row}">
{{row.account ? bankaccounts.find(i => i.id === row.account).iban : ""}}
</template>
<template #valueDate-data="{row}">
{{dayjs(row.valueDate).format("DD.MM.YY")}}
</template>
<template #amount-data="{row}">
<span
v-if="row.amount >= 0"
class="text-primary-500"
>{{String(row.amount.toFixed(2)).replace(".",",")}} </span>
<span
v-else-if="row.amount < 0"
class="text-rose-500"
>{{String(row.amount.toFixed(2)).replace(".",",")}} </span>
</template>
<template #openAmount-data="{row}">
{{displayCurrency(calculateOpenSum(row))}}
</template>
<template #partner-data="{row}">
<span
v-if="row.amount < 0"
>
{{row.credName}}
</span>
<span
v-else-if="row.amount > 0"
>
{{row.debName}}
</span>
</template>
</UTable>
<div class="overflow-y-auto relative" style="height: calc(100vh - 200px)">
<div v-if="loadingDocs" class="p-20 flex flex-col items-center justify-center">
<UProgress animation="carousel" class="w-1/3 mb-4" />
<span class="text-sm text-gray-500 italic">Bankbuchungen werden geladen...</span>
</div>
<table v-else class="w-full text-left border-collapse">
<thead class="sticky top-0 bg-white dark:bg-gray-900 z-10 shadow-sm">
<tr class="text-xs font-semibold text-gray-500 uppercase">
<th v-for="col in templateColumns" :key="col.key" class="p-4 border-b dark:border-gray-800">
{{ col.label }}
</th>
</tr>
</thead>
<tbody>
<template v-for="(row, index) in filteredRows" :key="row.id">
<tr v-if="shouldShowMonthDivider(row, index)">
<td colspan="6" class="bg-gray-50 dark:bg-gray-800/50 p-2 pl-4 text-sm font-bold text-primary-600 border-y dark:border-gray-800">
<div class="flex items-center gap-2">
<UIcon name="i-heroicons-calendar" class="w-4 h-4"/>
{{ $dayjs(row.valueDate).format('MMMM YYYY') }}
</div>
</td>
</tr>
<tr
class="hover:bg-gray-50 dark:hover:bg-gray-800/30 cursor-pointer border-b dark:border-gray-800 text-sm group"
@click="router.push(`/banking/statements/edit/${row.id}`)"
>
<td class="p-4 text-[10px] text-gray-400 font-mono truncate max-w-[150px]">
{{ row.account ? bankaccounts.find(i => i.id === row.account)?.iban : "" }}
</td>
<td class="p-4 whitespace-nowrap">{{ $dayjs(row.valueDate).format("DD.MM.YY") }}</td>
<td class="p-4 font-semibold">
<span :class="row.amount >= 0 ? 'text-green-600 dark:text-green-400' : 'text-rose-600 dark:text-rose-400'">
{{ displayCurrency(row.amount) }}
</span>
</td>
<td class="p-4 text-gray-400 italic text-xs">
{{ Number(calculateOpenSum(row)) !== 0 ? displayCurrency(calculateOpenSum(row)) : '-' }}
</td>
<td class="p-4 truncate max-w-[180px] font-medium">
{{ row.amount < 0 ? row.credName : row.debName }}
</td>
<td class="p-4 text-gray-500 truncate max-w-[350px] text-xs">
{{ row.text }}
</td>
</tr>
</template>
<tr v-if="filteredRows.length === 0">
<td colspan="6" class="p-32 text-center text-gray-400">
<div class="flex flex-col items-center">
<UIcon name="i-heroicons-magnifying-glass-circle" class="w-12 h-12 mb-3 opacity-20"/>
<p class="font-medium">Keine Buchungen gefunden</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<PageLeaveGuard :when="isSyncing"/>
</template>
<style scoped>
</style>
</template>

View File

@@ -3,10 +3,16 @@ import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import 'dayjs/locale/de'
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
import isSameOrAfter from "dayjs/plugin/isSameOrAfter"
import isSameOrBefore from "dayjs/plugin/isSameOrBefore"
dayjs.extend(duration)
dayjs.extend(relativeTime)
dayjs.extend(localizedFormat)
dayjs.extend(quarterOfYear)
dayjs.extend(isSameOrAfter)
dayjs.extend(isSameOrBefore)
dayjs.locale('de')
export default defineNuxtPlugin(() => {

View File

@@ -1,8 +1,7 @@
import {defineStore} from 'pinia'
import { defineStore } from 'pinia'
// @ts-ignore
export const useTempStore = defineStore('temp', () => {
const auth = useAuthStore()
const searchStrings = ref({})
@@ -20,21 +19,21 @@ export const useTempStore = defineStore('temp', () => {
filters: filters.value
}
await useNuxtApp().$api(`/api/profiles/${auth.profile.id}`,{
await useNuxtApp().$api(`/api/profiles/${auth.profile.id}`, {
method: 'PUT',
body: {temp_config: config}
body: { temp_config: config }
})
}
function setStoredTempConfig (config) {
searchStrings.value = config.searchStrings
columns.value = config.columns
pages.value = config.pages
settings.value = config.settings
function setStoredTempConfig(config) {
searchStrings.value = config.searchStrings || {}
columns.value = config.columns || {}
pages.value = config.pages || {}
settings.value = config.settings || {}
filters.value = config.filters || {}
}
function modifySearchString(type,input) {
function modifySearchString(type, input) {
searchStrings.value[type] = input
storeTempConfig()
}
@@ -44,28 +43,36 @@ export const useTempStore = defineStore('temp', () => {
storeTempConfig()
}
function modifyFilter(domain,type,input) {
if(!filters.value[domain]) filters.value[domain] = {}
function modifyFilter(domain, type, input) {
if (!filters.value[domain]) filters.value[domain] = {}
filters.value[domain][type] = input
storeTempConfig()
}
function modifyColumns(type,input) {
function modifyColumns(type, input) {
columns.value[type] = input
storeTempConfig()
}
function modifyPages(type,input) {
function modifyPages(type, input) {
pages.value[type] = input
storeTempConfig()
}
function modifySettings(type,input) {
function modifySettings(type, input) {
settings.value[type] = input
storeTempConfig()
}
// Spezifisch für das Banking-Datum
function modifyBankingPeriod(periodKey, range) {
if (!settings.value['banking']) settings.value['banking'] = {}
settings.value['banking'].periodKey = periodKey
settings.value['banking'].range = range
storeTempConfig()
}
return {
setStoredTempConfig,
@@ -79,8 +86,7 @@ export const useTempStore = defineStore('temp', () => {
modifyPages,
pages,
modifySettings,
modifyBankingPeriod, // Neue Funktion exportiert
settings
}
})