fix #141
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 15s
Build and Push Docker Images / build-frontend (push) Successful in 57s

fix #142
This commit is contained in:
2026-03-22 22:10:41 +01:00
parent 11a242d70d
commit 7c644c941a
6 changed files with 152 additions and 52 deletions

View File

@@ -1,23 +1,22 @@
<script setup> <script setup>
// Falls useDropZone nicht auto-importiert wird:
// import { useDropZone } from '@vueuse/core'
const props = defineProps({ const props = defineProps({
fileData: { fileData: {
type: Object, type: Object,
default: { default: () => ({
type: null type: null
} })
} }
}) })
const emit = defineEmits(["uploadFinished"]) const emit = defineEmits(["uploadFinished"])
const modal = useModal() const modal = useModal()
// const profileStore = useProfileStore() // Wird im Snippet nicht genutzt, aber ich lasse es drin
const uploadInProgress = ref(false) const uploadInProgress = ref(false)
const availableFiletypes = ref([]) const availableFiletypes = ref([])
const localFileData = reactive({
...props.fileData
})
// 1. State für die Dateien und die Dropzone Referenz // 1. State für die Dateien und die Dropzone Referenz
const selectedFiles = ref([]) const selectedFiles = ref([])
@@ -58,10 +57,8 @@ const uploadFiles = async () => {
uploadInProgress.value = true; uploadInProgress.value = true;
let fileData = props.fileData const { typeEnabled, ...fileData } = localFileData
delete fileData.typeEnabled
// 4. Hier nutzen wir nun selectedFiles.value statt document.getElementById
await useFiles().uploadFiles(fileData, selectedFiles.value, [], true) await useFiles().uploadFiles(fileData, selectedFiles.value, [], true)
uploadInProgress.value = false; uploadInProgress.value = false;
@@ -80,12 +77,11 @@ const fileNames = computed(() => {
<UModal> <UModal>
<template #content> <template #content>
<div ref="dropZoneRef" class="relative h-full flex flex-col"> <div ref="dropZoneRef" class="relative h-full flex flex-col">
<div <div
v-if="isOverDropZone" v-if="isOverDropZone"
class="absolute inset-0 z-50 flex items-center justify-center bg-primary-500/10 border-2 border-primary-500 border-dashed rounded-lg backdrop-blur-sm transition-all" class="absolute inset-0 z-50 flex items-center justify-center rounded-lg border-2 border-dashed border-primary-500 bg-primary-500/10 backdrop-blur-sm transition-all"
> >
<span class="text-xl font-bold text-primary-600 bg-white/80 px-4 py-2 rounded shadow-sm"> <span class="rounded bg-white/80 px-4 py-2 text-xl font-bold text-primary-600 shadow-sm">
Dateien hier ablegen Dateien hier ablegen
</span> </span>
</div> </div>
@@ -130,16 +126,17 @@ const fileNames = computed(() => {
class="mt-3" class="mt-3"
> >
<USelectMenu <USelectMenu
option-attribute="name" :items="availableFiletypes"
value-attribute="id" v-model="localFileData.type"
searchable value-key="id"
searchable-placeholder="Suchen..." label-key="name"
:options="availableFiletypes" :search-input="{ placeholder: 'Suchen...' }"
v-model="props.fileData.type" :filter-fields="['name']"
:disabled="!props.fileData.typeEnabled" :disabled="!localFileData.typeEnabled"
class="w-full"
> >
<template #label> <template #default>
<span v-if="availableFiletypes.find(x => x.id === props.fileData.type)">{{availableFiletypes.find(x => x.id === props.fileData.type).name}}</span> <span v-if="availableFiletypes.find(x => x.id === localFileData.type)">{{ availableFiletypes.find(x => x.id === localFileData.type).name }}</span>
<span v-else>Kein Typ ausgewählt</span> <span v-else>Kein Typ ausgewählt</span>
</template> </template>
</USelectMenu> </USelectMenu>
@@ -159,5 +156,4 @@ const fileNames = computed(() => {
</template> </template>
<style scoped> <style scoped>
/* Optional: Animationen für das Overlay */
</style> </style>

View File

@@ -1,4 +1,6 @@
<script setup> <script setup>
import { parseDate } from "@internationalized/date"
const {$api, $dayjs} = useNuxtApp() const {$api, $dayjs} = useNuxtApp()
const toast = useToast() const toast = useToast()
@@ -34,6 +36,12 @@ const periodOptions = [
{label: 'Benutzerdefiniert', key: 'custom'} {label: 'Benutzerdefiniert', key: 'custom'}
] ]
const bankingFilterItems = [
{ label: 'Nur offene anzeigen', value: 'Nur offene anzeigen' },
{ label: 'Nur positive anzeigen', value: 'Nur positive anzeigen' },
{ label: 'Nur negative anzeigen', value: 'Nur negative anzeigen' }
]
// Initialisierungswerte // Initialisierungswerte
const selectedPeriod = ref(periodOptions[0]) const selectedPeriod = ref(periodOptions[0])
const dateRange = ref({ const dateRange = ref({
@@ -41,6 +49,19 @@ const dateRange = ref({
end: $dayjs().endOf('month').format('YYYY-MM-DD') end: $dayjs().endOf('month').format('YYYY-MM-DD')
}) })
const getCalendarValue = (value) => {
if (!value) return undefined
const formatted = $dayjs(value).format('YYYY-MM-DD')
return formatted ? parseDate(formatted) : undefined
}
const setDateRangeFromCalendar = (field, value) => {
dateRange.value[field] = value ? value.toString() : ""
}
const getDateButtonLabel = (value) => value ? $dayjs(value).format('DD.MM.YYYY') : 'Kein Datum'
const setDateRangeFieldToToday = (field) => { const setDateRangeFieldToToday = (field) => {
dateRange.value[field] = $dayjs().format('YYYY-MM-DD') dateRange.value[field] = $dayjs().format('YYYY-MM-DD')
} }
@@ -496,30 +517,77 @@ onMounted(() => {
<template #left> <template #left>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<USelectMenu <USelectMenu
:options="bankaccounts" :items="bankaccounts"
v-model="filterAccount" v-model="filterAccount"
option-attribute="iban" value-key="id"
label-key="iban"
multiple multiple
by="id" by="id"
placeholder="Konten" placeholder="Konten"
class="w-48" class="w-48"
/> >
<template #default>
{{ filterAccount.length > 0 ? `${filterAccount.length} Kont${filterAccount.length > 1 ? 'en' : 'o'}` : 'Konten' }}
</template>
</USelectMenu>
<USeparator orientation="vertical" class="h-6"/> <USeparator orientation="vertical" class="h-6"/>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<USelectMenu <USelectMenu
v-model="selectedPeriod" v-model="selectedPeriod"
:options="periodOptions" :items="periodOptions"
value-key="key"
label-key="label"
class="w-44" class="w-44"
icon="i-heroicons-calendar-days" icon="i-heroicons-calendar-days"
/> >
<div v-if="selectedPeriod.key === 'custom'" class="flex items-center gap-1"> <template #default>
{{ selectedPeriod?.label || 'Zeitraum' }}
</template>
</USelectMenu>
<div v-if="selectedPeriod === 'custom'" class="flex items-center gap-1">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<UInput type="date" v-model="dateRange.start" size="xs" class="w-32"/> <UPopover class="w-full" :content="{ side: 'bottom', align: 'start' }">
<UButton size="2xs" color="gray" variant="soft" label="Heute" @click="setDateRangeFieldToToday('start')" /> <UButton
block
color="neutral"
variant="outline"
class="w-36 justify-start"
icon="i-heroicons-calendar"
:label="getDateButtonLabel(dateRange.start)"
/>
<template #content>
<div class="p-2">
<UCalendar
:model-value="getCalendarValue(dateRange.start)"
@update:model-value="setDateRangeFromCalendar('start', $event)"
:week-starts-on="1"
/>
</div>
</template>
</UPopover>
</div> </div>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<UInput type="date" v-model="dateRange.end" size="xs" class="w-32"/> <UPopover class="w-full" :content="{ side: 'bottom', align: 'start' }">
<UButton size="2xs" color="gray" variant="soft" label="Heute" @click="setDateRangeFieldToToday('end')" /> <UButton
block
color="neutral"
variant="outline"
class="w-36 justify-start"
icon="i-heroicons-calendar"
:label="getDateButtonLabel(dateRange.end)"
/>
<template #content>
<div class="p-2">
<UCalendar
:model-value="getCalendarValue(dateRange.end)"
@update:model-value="setDateRangeFromCalendar('end', $event)"
:week-starts-on="1"
/>
</div>
</template>
</UPopover>
</div> </div>
</div> </div>
<div v-else class="text-xs text-gray-400 hidden sm:block italic"> <div v-else class="text-xs text-gray-400 hidden sm:block italic">
@@ -534,9 +602,15 @@ onMounted(() => {
icon="i-heroicons-adjustments-horizontal" icon="i-heroicons-adjustments-horizontal"
multiple multiple
v-model="selectedFilters" v-model="selectedFilters"
:options="['Nur offene anzeigen','Nur positive anzeigen','Nur negative anzeigen']" :items="bankingFilterItems"
@change="tempStore.modifyFilter('banking','main',selectedFilters)" value-key="value"
/> label-key="label"
@update:model-value="tempStore.modifyFilter('banking','main',selectedFilters)"
>
<template #default>
Filter
</template>
</USelectMenu>
</template> </template>
</UDashboardToolbar> </UDashboardToolbar>

View File

@@ -687,9 +687,9 @@ setup()
</div> </div>
<div v-if="!topEntitySuggestion" class="mb-3"> <div v-if="!topEntitySuggestion" class="mb-3">
<div class="text-xs font-bold uppercase tracking-wide text-primary-700 dark:text-primary-300">Automatische Belegvorschlaege</div> <div class="text-xs font-bold uppercase tracking-wide text-primary-700 dark:text-primary-300">Automatische Belegvorschläge</div>
<div class="mt-1 text-xs text-gray-600 dark:text-gray-300"> <div class="mt-1 text-xs text-gray-600 dark:text-gray-300">
Kein eindeutiger Kunde oder Lieferant erkannt. Vorschlaege basieren auf Betrag und Verwendungszweck. Kein eindeutiger Kunde oder Lieferant erkannt. Vorschläge basieren auf Betrag und Verwendungszweck.
</div> </div>
</div> </div>

View File

@@ -76,11 +76,15 @@ const setupPage = async () => {
// --- Global Drag & Drop (Auto-Open Upload Modal) --- // --- Global Drag & Drop (Auto-Open Upload Modal) ---
let dragCounter = 0 let dragCounter = 0
const uploadModalOpening = ref(false)
const handleGlobalDragEnter = (e) => { const handleGlobalDragEnter = (e) => {
dragCounter++ dragCounter++
if (draggedItem.value) return if (draggedItem.value) return
if (uploadModalOpening.value) return
if (e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.includes('Files')) { if (e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.includes('Files')) {
uploadModalOpening.value = true
modal.open(DocumentUploadModal, { modal.open(DocumentUploadModal, {
fileData: { fileData: {
folder: currentFolder.value?.id, folder: currentFolder.value?.id,
@@ -91,6 +95,9 @@ const handleGlobalDragEnter = (e) => {
setupPage() setupPage()
dragCounter = 0 dragCounter = 0
} }
}).finally(() => {
dragCounter = 0
uploadModalOpening.value = false
}) })
} }
} }

View File

@@ -93,6 +93,17 @@ const DASHBOARD_WIDGETS = [
const widgetDefinitions = Object.fromEntries(DASHBOARD_WIDGETS.map((widget) => [widget.id, widget])) const widgetDefinitions = Object.fromEntries(DASHBOARD_WIDGETS.map((widget) => [widget.id, widget]))
function getDefaultDashboardWidgets() {
return DASHBOARD_WIDGETS.map((definition) => ({
id: definition.id,
x: definition.defaultLayout.x,
y: definition.defaultLayout.y,
w: definition.defaultLayout.w,
h: definition.defaultLayout.h,
visible: true
}))
}
function normalizeNumber(value, fallback) { function normalizeNumber(value, fallback) {
const parsed = Number(value) const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : fallback return Number.isFinite(parsed) ? parsed : fallback
@@ -303,8 +314,13 @@ function removeWidget(id) {
} }
function resetDashboard() { function resetDashboard() {
widgets.value = normalizeDashboardWidgets() widgets.value = getDefaultDashboardWidgets()
persistWidgets() persistWidgets()
toast.add({
title: "Dashboard zurückgesetzt",
description: "Das Standardlayout wurde wiederhergestellt.",
color: "primary"
})
} }
const visibleWidgets = computed(() => const visibleWidgets = computed(() =>
@@ -378,6 +394,15 @@ onBeforeUnmount(() => {
> >
Karte hinzufügen Karte hinzufügen
</UButton> </UButton>
<UButton
v-if="isEditMode"
icon="i-heroicons-arrow-path"
color="gray"
variant="ghost"
@click="resetDashboard"
>
Standardlayout
</UButton>
<UButton <UButton
v-if="isEditMode" v-if="isEditMode"
icon="i-heroicons-squares-2x2" icon="i-heroicons-squares-2x2"

View File

@@ -391,14 +391,13 @@ const isDistinctFilterActive = (columnKey) => {
<UPagination <UPagination
v-if="initialSetupDone && items.length > 0" v-if="initialSetupDone && items.length > 0"
:disabled="loading" :disabled="loading"
v-model="page" v-model:page="page"
:page-count="pageLimit" :items-per-page="pageLimit"
:total="itemsMeta.total" :total="itemsMeta.total"
@update:modelValue="(i) => changePage(i)" @update:page="changePage"
show-first :show-edges="true"
show-last first-icon="i-heroicons-chevron-double-left"
:first-button="{ icon: 'i-heroicons-chevron-double-left', color: 'gray' }" last-icon="i-heroicons-chevron-double-right"
:last-button="{ icon: 'i-heroicons-chevron-double-right', trailing: true, color: 'gray' }"
/> />
</template> </template>
@@ -653,14 +652,13 @@ const isDistinctFilterActive = (columnKey) => {
<UPagination <UPagination
v-if="initialSetupDone && items.length > 0" v-if="initialSetupDone && items.length > 0"
:disabled="loading" :disabled="loading"
v-model="page" v-model:page="page"
:page-count="pageLimit" :items-per-page="pageLimit"
:total="itemsMeta.total" :total="itemsMeta.total"
@update:modelValue="(i) => changePage(i)" @update:page="changePage"
show-first :show-edges="true"
show-last first-icon="i-heroicons-chevron-double-left"
:first-button="{ icon: 'i-heroicons-chevron-double-left', color: 'gray' }" last-icon="i-heroicons-chevron-double-right"
:last-button="{ icon: 'i-heroicons-chevron-double-right', trailing: true, color: 'gray' }"
/> />
</div> </div>