@@ -1,23 +1,22 @@
|
||||
<script setup>
|
||||
// Falls useDropZone nicht auto-importiert wird:
|
||||
// import { useDropZone } from '@vueuse/core'
|
||||
|
||||
const props = defineProps({
|
||||
fileData: {
|
||||
type: Object,
|
||||
default: {
|
||||
default: () => ({
|
||||
type: null
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(["uploadFinished"])
|
||||
|
||||
const modal = useModal()
|
||||
// const profileStore = useProfileStore() // Wird im Snippet nicht genutzt, aber ich lasse es drin
|
||||
|
||||
const uploadInProgress = ref(false)
|
||||
const availableFiletypes = ref([])
|
||||
const localFileData = reactive({
|
||||
...props.fileData
|
||||
})
|
||||
|
||||
// 1. State für die Dateien und die Dropzone Referenz
|
||||
const selectedFiles = ref([])
|
||||
@@ -58,10 +57,8 @@ const uploadFiles = async () => {
|
||||
|
||||
uploadInProgress.value = true;
|
||||
|
||||
let fileData = props.fileData
|
||||
delete fileData.typeEnabled
|
||||
const { typeEnabled, ...fileData } = localFileData
|
||||
|
||||
// 4. Hier nutzen wir nun selectedFiles.value statt document.getElementById
|
||||
await useFiles().uploadFiles(fileData, selectedFiles.value, [], true)
|
||||
|
||||
uploadInProgress.value = false;
|
||||
@@ -80,12 +77,11 @@ const fileNames = computed(() => {
|
||||
<UModal>
|
||||
<template #content>
|
||||
<div ref="dropZoneRef" class="relative h-full flex flex-col">
|
||||
|
||||
<div
|
||||
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
|
||||
</span>
|
||||
</div>
|
||||
@@ -130,16 +126,17 @@ const fileNames = computed(() => {
|
||||
class="mt-3"
|
||||
>
|
||||
<USelectMenu
|
||||
option-attribute="name"
|
||||
value-attribute="id"
|
||||
searchable
|
||||
searchable-placeholder="Suchen..."
|
||||
:options="availableFiletypes"
|
||||
v-model="props.fileData.type"
|
||||
:disabled="!props.fileData.typeEnabled"
|
||||
:items="availableFiletypes"
|
||||
v-model="localFileData.type"
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
:search-input="{ placeholder: 'Suchen...' }"
|
||||
:filter-fields="['name']"
|
||||
:disabled="!localFileData.typeEnabled"
|
||||
class="w-full"
|
||||
>
|
||||
<template #label>
|
||||
<span v-if="availableFiletypes.find(x => x.id === props.fileData.type)">{{availableFiletypes.find(x => x.id === props.fileData.type).name}}</span>
|
||||
<template #default>
|
||||
<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>
|
||||
</template>
|
||||
</USelectMenu>
|
||||
@@ -159,5 +156,4 @@ const fileNames = computed(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Optional: Animationen für das Overlay */
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { parseDate } from "@internationalized/date"
|
||||
|
||||
const {$api, $dayjs} = useNuxtApp()
|
||||
const toast = useToast()
|
||||
|
||||
@@ -34,6 +36,12 @@ const periodOptions = [
|
||||
{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
|
||||
const selectedPeriod = ref(periodOptions[0])
|
||||
const dateRange = ref({
|
||||
@@ -41,6 +49,19 @@ const dateRange = ref({
|
||||
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) => {
|
||||
dateRange.value[field] = $dayjs().format('YYYY-MM-DD')
|
||||
}
|
||||
@@ -496,30 +517,77 @@ onMounted(() => {
|
||||
<template #left>
|
||||
<div class="flex items-center gap-3">
|
||||
<USelectMenu
|
||||
:options="bankaccounts"
|
||||
:items="bankaccounts"
|
||||
v-model="filterAccount"
|
||||
option-attribute="iban"
|
||||
value-key="id"
|
||||
label-key="iban"
|
||||
multiple
|
||||
by="id"
|
||||
placeholder="Konten"
|
||||
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"/>
|
||||
<div class="flex items-center gap-2">
|
||||
<USelectMenu
|
||||
v-model="selectedPeriod"
|
||||
:options="periodOptions"
|
||||
:items="periodOptions"
|
||||
value-key="key"
|
||||
label-key="label"
|
||||
class="w-44"
|
||||
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">
|
||||
<UInput type="date" v-model="dateRange.start" size="xs" class="w-32"/>
|
||||
<UButton size="2xs" color="gray" variant="soft" label="Heute" @click="setDateRangeFieldToToday('start')" />
|
||||
<UPopover class="w-full" :content="{ side: 'bottom', align: '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 class="flex items-center gap-1">
|
||||
<UInput type="date" v-model="dateRange.end" size="xs" class="w-32"/>
|
||||
<UButton size="2xs" color="gray" variant="soft" label="Heute" @click="setDateRangeFieldToToday('end')" />
|
||||
<UPopover class="w-full" :content="{ side: 'bottom', align: 'start' }">
|
||||
<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 v-else class="text-xs text-gray-400 hidden sm:block italic">
|
||||
@@ -534,9 +602,15 @@ onMounted(() => {
|
||||
icon="i-heroicons-adjustments-horizontal"
|
||||
multiple
|
||||
v-model="selectedFilters"
|
||||
:options="['Nur offene anzeigen','Nur positive anzeigen','Nur negative anzeigen']"
|
||||
@change="tempStore.modifyFilter('banking','main',selectedFilters)"
|
||||
/>
|
||||
:items="bankingFilterItems"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
@update:model-value="tempStore.modifyFilter('banking','main',selectedFilters)"
|
||||
>
|
||||
<template #default>
|
||||
Filter
|
||||
</template>
|
||||
</USelectMenu>
|
||||
</template>
|
||||
</UDashboardToolbar>
|
||||
|
||||
|
||||
@@ -687,9 +687,9 @@ setup()
|
||||
</div>
|
||||
|
||||
<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">
|
||||
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>
|
||||
|
||||
|
||||
@@ -76,11 +76,15 @@ const setupPage = async () => {
|
||||
|
||||
// --- Global Drag & Drop (Auto-Open Upload Modal) ---
|
||||
let dragCounter = 0
|
||||
const uploadModalOpening = ref(false)
|
||||
|
||||
const handleGlobalDragEnter = (e) => {
|
||||
dragCounter++
|
||||
if (draggedItem.value) return
|
||||
if (uploadModalOpening.value) return
|
||||
if (e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.includes('Files')) {
|
||||
uploadModalOpening.value = true
|
||||
|
||||
modal.open(DocumentUploadModal, {
|
||||
fileData: {
|
||||
folder: currentFolder.value?.id,
|
||||
@@ -91,6 +95,9 @@ const handleGlobalDragEnter = (e) => {
|
||||
setupPage()
|
||||
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]))
|
||||
|
||||
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) {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : fallback
|
||||
@@ -303,8 +314,13 @@ function removeWidget(id) {
|
||||
}
|
||||
|
||||
function resetDashboard() {
|
||||
widgets.value = normalizeDashboardWidgets()
|
||||
widgets.value = getDefaultDashboardWidgets()
|
||||
persistWidgets()
|
||||
toast.add({
|
||||
title: "Dashboard zurückgesetzt",
|
||||
description: "Das Standardlayout wurde wiederhergestellt.",
|
||||
color: "primary"
|
||||
})
|
||||
}
|
||||
|
||||
const visibleWidgets = computed(() =>
|
||||
@@ -378,6 +394,15 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
Karte hinzufügen
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="isEditMode"
|
||||
icon="i-heroicons-arrow-path"
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
@click="resetDashboard"
|
||||
>
|
||||
Standardlayout
|
||||
</UButton>
|
||||
<UButton
|
||||
v-if="isEditMode"
|
||||
icon="i-heroicons-squares-2x2"
|
||||
|
||||
@@ -391,14 +391,13 @@ const isDistinctFilterActive = (columnKey) => {
|
||||
<UPagination
|
||||
v-if="initialSetupDone && items.length > 0"
|
||||
:disabled="loading"
|
||||
v-model="page"
|
||||
:page-count="pageLimit"
|
||||
v-model:page="page"
|
||||
:items-per-page="pageLimit"
|
||||
:total="itemsMeta.total"
|
||||
@update:modelValue="(i) => changePage(i)"
|
||||
show-first
|
||||
show-last
|
||||
:first-button="{ icon: 'i-heroicons-chevron-double-left', color: 'gray' }"
|
||||
:last-button="{ icon: 'i-heroicons-chevron-double-right', trailing: true, color: 'gray' }"
|
||||
@update:page="changePage"
|
||||
:show-edges="true"
|
||||
first-icon="i-heroicons-chevron-double-left"
|
||||
last-icon="i-heroicons-chevron-double-right"
|
||||
/>
|
||||
|
||||
</template>
|
||||
@@ -653,14 +652,13 @@ const isDistinctFilterActive = (columnKey) => {
|
||||
<UPagination
|
||||
v-if="initialSetupDone && items.length > 0"
|
||||
:disabled="loading"
|
||||
v-model="page"
|
||||
:page-count="pageLimit"
|
||||
v-model:page="page"
|
||||
:items-per-page="pageLimit"
|
||||
:total="itemsMeta.total"
|
||||
@update:modelValue="(i) => changePage(i)"
|
||||
show-first
|
||||
show-last
|
||||
:first-button="{ icon: 'i-heroicons-chevron-double-left', color: 'gray' }"
|
||||
:last-button="{ icon: 'i-heroicons-chevron-double-right', trailing: true, color: 'gray' }"
|
||||
@update:page="changePage"
|
||||
:show-edges="true"
|
||||
first-icon="i-heroicons-chevron-double-left"
|
||||
last-icon="i-heroicons-chevron-double-right"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user