@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user