This commit is contained in:
2025-12-27 12:56:54 +01:00
parent 7d4adbb3e4
commit d5999bfb20
9 changed files with 249 additions and 135 deletions

View File

@@ -1,4 +1,6 @@
<script setup >
<script setup>
// Falls useDropZone nicht auto-importiert wird:
// import { useDropZone } from '@vueuse/core'
const props = defineProps({
fileData: {
@@ -12,11 +14,35 @@ const props = defineProps({
const emit = defineEmits(["uploadFinished"])
const modal = useModal()
const profileStore = useProfileStore()
// const profileStore = useProfileStore() // Wird im Snippet nicht genutzt, aber ich lasse es drin
const uploadInProgress = ref(false)
const availableFiletypes = ref([])
// 1. State für die Dateien und die Dropzone Referenz
const selectedFiles = ref([])
const dropZoneRef = ref(null)
// 2. Setup der Dropzone
const onDrop = (files) => {
// Wenn Dateien gedroppt werden, speichern wir sie
// files ist hier meist ein Array, wir stellen sicher, dass es passt
selectedFiles.value = files || []
}
const { isOverDropZone } = useDropZone(dropZoneRef, {
onDrop,
// Verhindert, dass der Browser das Bild einfach öffnet
preventDefaultForDrop: true,
})
// 3. Handler für den klassischen Datei-Input Klick
const onFileInputChange = (e) => {
if (e.target.files) {
selectedFiles.value = Array.from(e.target.files)
}
}
const setup = async () => {
availableFiletypes.value = await useEntities("filetags").select()
}
@@ -24,81 +50,112 @@ const setup = async () => {
setup()
const uploadFiles = async () => {
// Validierung: Keine Dateien ausgewählt
if (!selectedFiles.value || selectedFiles.value.length === 0) {
alert("Bitte wählen Sie zuerst Dateien aus.") // Oder eine schönere Toast Notification
return
}
uploadInProgress.value = true;
let fileData = props.fileData
delete fileData.typeEnabled
await useFiles().uploadFiles(fileData, document.getElementById("fileUploadInput").files,[],true)
// 4. Hier nutzen wir nun selectedFiles.value statt document.getElementById
await useFiles().uploadFiles(fileData, selectedFiles.value, [], true)
uploadInProgress.value = false;
emit("uploadFinished")
modal.close()
}
// Helper Funktion um Dateinamen anzuzeigen (da das Input Feld leer bleibt beim Droppen)
const fileNames = computed(() => {
if (!selectedFiles.value.length) return ''
return selectedFiles.value.map(f => f.name).join(', ')
})
</script>
<template>
<UModal>
<UCard :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Datei hochladen
</h3>
<UButton
color="gray"
variant="ghost"
icon="i-heroicons-x-mark-20-solid"
class="-my-1"
@click="modal.close()"
:disabled="uploadInProgress"
/>
</div>
</template>
<div ref="dropZoneRef" class="relative h-full flex flex-col">
<UFormGroup
label="Datei:"
<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"
>
<UInput
type="file"
id="fileUploadInput"
multiple
accept="image/jpeg, image/png, image/gif, application/pdf"
/>
</UFormGroup>
<UFormGroup
label="Typ:"
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"
<span class="text-xl font-bold text-primary-600 bg-white/80 px-4 py-2 rounded shadow-sm">
Dateien hier ablegen
</span>
</div>
<UCard :ui="{ body: { base: 'flex-1' }, ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Datei hochladen
</h3>
<UButton
color="gray"
variant="ghost"
icon="i-heroicons-x-mark-20-solid"
class="-my-1"
@click="modal.close()"
:disabled="uploadInProgress"
/>
</div>
</template>
<UFormGroup
label="Datei:"
:help="selectedFiles.length > 0 ? `${selectedFiles.length} Datei(en) ausgewählt` : 'Ziehen Sie Dateien hierher oder klicken Sie'"
>
<template #label>
<span v-if="availableFiletypes.find(x => x.id === props.fileData.type)">{{availableFiletypes.find(x => x.id === props.fileData.type).name}}</span>
<span v-else>Keine Typ ausgewählt</span>
</template>
</USelectMenu>
</UFormGroup>
<UInput
v-if="selectedFiles.length === 0"
type="file"
id="fileUploadInput"
multiple
accept="image/jpeg, image/png, image/gif, application/pdf"
@change="onFileInputChange"
/>
<template #footer>
<UButton
@click="uploadFiles"
:loading="uploadInProgress"
:disabled="uploadInProgress"
>Hochladen</UButton>
</template>
</UCard>
<div v-if="selectedFiles.length > 0" class="mt-2 text-sm text-gray-500">
Ausgewählt: <span class="font-medium text-gray-700 dark:text-gray-300">{{ fileNames }}</span>
</div>
</UFormGroup>
<UFormGroup
label="Typ:"
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"
>
<template #label>
<span v-if="availableFiletypes.find(x => x.id === props.fileData.type)">{{availableFiletypes.find(x => x.id === props.fileData.type).name}}</span>
<span v-else>Kein Typ ausgewählt</span>
</template>
</USelectMenu>
</UFormGroup>
<template #footer>
<UButton
@click="uploadFiles"
:loading="uploadInProgress"
:disabled="uploadInProgress || selectedFiles.length === 0"
>Hochladen</UButton>
</template>
</UCard>
</div>
</UModal>
</template>
<style scoped>
/* Optional: Animationen für das Overlay */
</style>

View File

@@ -9,6 +9,7 @@ const props = defineProps({
</script>
<template>
<span v-if="props.row.infoData.streetNumber">{{props.row.infoData.streetNumber}},</span>
<span v-if="props.row.infoData.street">{{props.row.infoData.street}},</span>
<span v-if="props.row.infoData.special">{{props.row.infoData.special}},</span>
<span v-if="props.row.infoData.zip">{{props.row.infoData.zip}},</span>

View File

@@ -7,7 +7,7 @@ export default defineNuxtConfig({
}
},
modules: ['@pinia/nuxt', '@nuxt/ui', '@nuxtjs/supabase', "nuxt-editorjs", '@nuxtjs/fontaine', 'nuxt-viewport', 'nuxt-tiptap-editor', '@nuxtjs/leaflet'],
modules: ['@pinia/nuxt', '@nuxt/ui', '@nuxtjs/supabase', "nuxt-editorjs", '@nuxtjs/fontaine', 'nuxt-viewport', 'nuxt-tiptap-editor', '@nuxtjs/leaflet', '@vueuse/nuxt'],
ssr: false,

View File

@@ -13,6 +13,8 @@
"@capacitor/cli": "^7.0.0",
"@nuxtjs/leaflet": "^1.2.3",
"@nuxtjs/supabase": "^1.1.4",
"@vueuse/core": "^14.1.0",
"@vueuse/nuxt": "^14.1.0",
"nuxt": "^3.14.1592",
"nuxt-tiptap-editor": "^1.2.0",
"vue": "^3.5.13",
@@ -87,4 +89,4 @@
"zod": "^3.25.76",
"zpl-renderer-js": "^2.0.2"
}
}
}

View File

@@ -58,7 +58,7 @@ const clearSearchString = () => {
searchString.value = ''
}
const filterAccount = ref(bankaccounts || [])
const filterAccount = ref(bankaccounts.value || [])
const displayCurrency = (value, currency = "€") => {
return `${Number(value).toFixed(2).replace(".",",")} ${currency}`
@@ -150,7 +150,7 @@ setupPage()
</USelectMenu>
</template>
<template #right>
<USelectMenu
<!-- <USelectMenu
v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid"
:options="templateColumns"
@@ -162,7 +162,7 @@ setupPage()
<template #label>
Spalten
</template>
</USelectMenu>
</USelectMenu>-->
<USelectMenu
icon="i-heroicons-adjustments-horizontal-solid"
multiple

View File

@@ -12,7 +12,7 @@ defineShortcuts({
})
const dataStore = useDataStore()
const profileStore = useProfileStore()
const tempStore = useTempStore()
const route = useRoute()
const router = useRouter()
const mode = ref(route.params.mode || "show")
@@ -177,7 +177,14 @@ const removeAllocation = async (allocationId) => {
await setup()
}
const searchString = ref("")
const searchString = ref(tempStore.searchStrings["bankstatementsedit"] ||'')
const clearSearchString = () => {
searchString.value = ''
tempStore.clearSearchString("bankstatementsedit")
}
const filteredDocuments = computed(() => {
@@ -546,7 +553,15 @@ const archiveStatement = async () => {
class="mr-3 mt-3"
@click="removeAllocation(item.id)"
/>
<UButton
icon="i-heroicons-eye"
variant="outline"
color="primary"
class="mr-3 mt-3"
@click="navigateTo(`/createDocument/show/${item.createddocument}`)"
/>
</UCard>
</div>
</div>
@@ -780,6 +795,7 @@ const archiveStatement = async () => {
placeholder="Suche..."
class="hidden lg:block w-full mr-1"
@keydown.esc="$event.target.blur()"
@change="tempStore.modifySearchString('bankstatementsedit',searchString)"
>
<template #trailing>
<UKbd value="/" />
@@ -789,7 +805,7 @@ const archiveStatement = async () => {
variant="outline"
icon="i-heroicons-x-mark"
color="rose"
@click="searchString = ''"
@click="clearSearchString"
/>
</InputGroup>
</div>

View File

@@ -1,26 +1,26 @@
<template>
<UDashboardNavbar title="Ausgangsbelege" :badge="filteredRows.length">
<UDashboardNavbar :badge="filteredRows.length" title="Ausgangsbelege">
<template #right>
<UInput
id="searchinput"
v-model="searchString"
icon="i-heroicons-funnel"
autocomplete="off"
placeholder="Suche..."
class="hidden lg:block"
@keydown.esc="$event.target.blur()"
icon="i-heroicons-funnel"
placeholder="Suche..."
@change="tempStore.modifySearchString('createddocuments',searchString)"
@keydown.esc="$event.target.blur()"
>
<template #trailing>
<UKbd value="/" />
<UKbd value="/"/>
</template>
</UInput>
<UButton
v-if="searchString.length > 0"
color="rose"
icon="i-heroicons-x-mark"
variant="outline"
color="rose"
@click="clearSearchString()"
v-if="searchString.length > 0"
/>
<UButton
@click="router.push(`/createDocument/edit`)"
@@ -33,27 +33,27 @@
<template #right>
<USelectMenu
<!-- <USelectMenu
v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid"
:options="templateColumns"
multiple
class="hidden lg:block"
by="key"
class="hidden lg:block"
icon="i-heroicons-adjustments-horizontal-solid"
multiple
@change="tempStore.modifyColumns('createddocuments',selectedColumns)"
>
<template #label>
Spalten
</template>
</USelectMenu>
</USelectMenu>-->
<USelectMenu
v-if="selectableFilters.length > 0"
v-model="selectedFilters"
:color="selectedFilters.length > 0 ? 'primary' : 'white'"
:options="selectableFilters"
:ui-menu="{ width: 'min-w-max' }"
icon="i-heroicons-adjustments-horizontal-solid"
multiple
v-model="selectedFilters"
:options="selectableFilters"
:color="selectedFilters.length > 0 ? 'primary' : 'white'"
:ui-menu="{ width: 'min-w-max' }"
>
<template #label>
Filter
@@ -63,36 +63,36 @@
</UDashboardToolbar>
<UTabs :items="selectedTypes" class="m-3">
<template #default="{item}">
{{item.label}}
{{ item.label }}
<UBadge
variant="outline"
class="ml-2"
variant="outline"
>
{{filteredRows.filter(i => item.key === 'invoices' ? ['invoices','advanceInvoices','cancellationInvoices'].includes(i.type) : item.key === i.type).length}}
{{ filteredRows.filter(i => item.key === 'invoices' ? ['invoices', 'advanceInvoices', 'cancellationInvoices'].includes(i.type) : item.key === i.type).length }}
</UBadge>
</template>
<template #item="{item}">
<div style="height: 80vh; overflow-y: scroll">
<UTable
:rows="filteredRows.filter(i => item.key === 'invoices' ? ['invoices','advanceInvoices','cancellationInvoices'].includes(i.type) : item.key === i.type)"
:columns="columns"
class="w-full"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
@select="selectItem"
:empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'Keine Belege anzuzeigen' }"
:rows="filteredRows.filter(i => item.key === 'invoices' ? ['invoices','advanceInvoices','cancellationInvoices'].includes(i.type) : item.key === i.type)"
:ui="{ divide: 'divide-gray-200 dark:divide-gray-800' }"
class="w-full"
@select="selectItem"
>
<template #type-data="{row}">
{{dataStore.documentTypesForCreation[row.type].labelSingle}}
<!--
<span v-if="row.type === 'cancellationInvoices'"> zu {{row.linkedDocument.documentNumber}}</span>
-->
{{ dataStore.documentTypesForCreation[row.type].labelSingle }}
<!--
<span v-if="row.type === 'cancellationInvoices'"> zu {{row.linkedDocument.documentNumber}}</span>
-->
</template>
<template #state-data="{row}">
<span
v-if="row.state === 'Entwurf'"
class="text-rose-500"
>
{{row.state}}
{{ row.state }}
</span>
<!-- <span
v-if="row.state === 'Gebucht'"
@@ -104,49 +104,56 @@
v-if="row.state === 'Gebucht' && !items.find(i => i.createddocument && i.createddocument.id === row.id)"
class="text-primary-500"
>
{{row.state}}
{{ row.state }}
</span>
<span
v-else-if="row.state === 'Gebucht' && items.find(i => i.createddocument && i.createddocument.id === row.id && i.type === 'cancellationInvoices') && ['invoices','advanceInvoices'].includes(row.type)"
class="text-cyan-500"
>
Storniert mit {{items.find(i => i.createddocument && i.createddocument.id === row.id).documentNumber}}
Storniert mit {{ items.find(i => i.createddocument && i.createddocument.id === row.id).documentNumber }}
</span>
<span
v-else-if="row.state === 'Gebucht'"
class="text-primary-500"
>
{{row.state}}
{{ row.state }}
</span>
</template>
<template #partner-data="{row}">
<span v-if="row.customer && row.customer.name.length <21">{{row.customer ? row.customer.name : ""}}</span>
<span v-if="row.customer && row.customer.name.length <21">{{ row.customer ? row.customer.name : "" }}</span>
<UTooltip v-else-if="row.customer && row.customer.name.length > 20" :text="row.customer.name">
{{row.customer.name.substring(0,20)}}...
{{ row.customer.name.substring(0, 20) }}...
</UTooltip>
</template>
<template #reference-data="{row}">
<span v-if="row === filteredRows[selectedItem]" class="text-primary-500 font-bold">{{row.documentNumber}}</span>
<span v-else>{{row.documentNumber}}</span>
<span v-if="row === filteredRows[selectedItem]"
class="text-primary-500 font-bold">{{ row.documentNumber }}</span>
<span v-else>{{ row.documentNumber }}</span>
</template>
<template #date-data="{row}">
<span v-if="row.date">{{row.date ? dayjs(row.date).format("DD.MM.YY") : ''}}</span>
<span v-if="row.documentDate">{{row.documentDate ? dayjs(row.documentDate).format("DD.MM.YY") : ''}}</span>
<span v-if="row.date">{{ row.date ? dayjs(row.date).format("DD.MM.YY") : '' }}</span>
<span
v-if="row.documentDate">{{ row.documentDate ? dayjs(row.documentDate).format("DD.MM.YY") : '' }}</span>
</template>
<template #dueDate-data="{row}">
<span v-if="row.state === 'Gebucht' && row.paymentDays && ['invoices','advanceInvoices'].includes(row.type) && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id)" :class="dayjs(row.documentDate).add(row.paymentDays,'day').diff(dayjs()) <= 0 && !isPaid(row) ? ['text-rose-500'] : '' ">{{row.documentDate ? dayjs(row.documentDate).add(row.paymentDays,'day').format("DD.MM.YY") : ''}}</span>
<span
v-if="row.state === 'Gebucht' && row.paymentDays && ['invoices','advanceInvoices'].includes(row.type) && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id)"
:class="dayjs(row.documentDate).add(row.paymentDays,'day').diff(dayjs()) <= 0 && !isPaid(row) ? ['text-rose-500'] : '' ">{{ row.documentDate ? dayjs(row.documentDate).add(row.paymentDays, 'day').format("DD.MM.YY") : '' }}</span>
</template>
<template #paid-data="{row}">
<div v-if="(row.type === 'invoices' ||row.type === 'advanceInvoices') && row.state === 'Gebucht' && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id)">
<div
v-if="(row.type === 'invoices' ||row.type === 'advanceInvoices') && row.state === 'Gebucht' && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id)">
<span v-if="useSum().getIsPaid(row,items)" class="text-primary-500">Bezahlt</span>
<span v-else class="text-rose-600">Offen</span>
</div>
</template>
<template #amount-data="{row}">
<span v-if="row.type !== 'deliveryNotes'">{{displayCurrency(useSum().getCreatedDocumentSum(row,items))}}</span>
<span
v-if="row.type !== 'deliveryNotes'">{{ displayCurrency(useSum().getCreatedDocumentSum(row, items)) }}</span>
</template>
<template #amountOpen-data="{row}">
<span v-if="!['deliveryNotes','cancellationInvoices','quotes','confirmationOrders'].includes(row.type) && row.state !== 'Entwurf' && !useSum().getIsPaid(row,items) && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id) ">{{displayCurrency(useSum().getCreatedDocumentSum(row, items) - row.statementallocations.reduce((n,{amount}) => n + amount, 0))}}</span>
<span
v-if="!['deliveryNotes','cancellationInvoices','quotes','confirmationOrders'].includes(row.type) && row.state !== 'Entwurf' && !useSum().getIsPaid(row,items) && !items.find(i => i.linkedDocument && i.linkedDocument.id === row.id) ">{{ displayCurrency(useSum().getCreatedDocumentSum(row, items) - row.statementallocations.reduce((n, {amount}) => n + amount, 0)) }}</span>
</template>
</UTable>
</div>
@@ -175,14 +182,14 @@ defineShortcuts({
}
},
'arrowdown': () => {
if(selectedItem.value < filteredRows.value.length - 1) {
if (selectedItem.value < filteredRows.value.length - 1) {
selectedItem.value += 1
} else {
selectedItem.value = 0
}
},
'arrowup': () => {
if(selectedItem.value === 0) {
if (selectedItem.value === 0) {
selectedItem.value = filteredRows.value.length - 1
} else {
selectedItem.value -= 1
@@ -202,7 +209,7 @@ const selectedItem = ref(0)
const setupPage = async () => {
items.value = (await useEntities("createddocuments").select("*, customer(id,name), statementallocations(id,amount),linkedDocument(*)","documentNumber",true, true))
items.value = (await useEntities("createddocuments").select("*, customer(id,name), statementallocations(id,amount),linkedDocument(*)", "documentNumber", true, true))
}
setupPage()
@@ -309,7 +316,7 @@ const filteredRows = computed(() => {
}
})
if(selectedFilters.value.length > 0) {
if (selectedFilters.value.length > 0) {
selectedFilters.value.forEach(filterName => {
let filter = dataType.filters.find(i => i.name === filterName)
tempItems = tempItems.filter(filter.filterFunction)
@@ -317,7 +324,6 @@ const filteredRows = computed(() => {
}
tempItems = useSearch(searchString.value, tempItems)

View File

@@ -1,5 +1,3 @@
<template>
<UDashboardNavbar title="Serienrechnungen" :badge="filteredRows.length">
<template #right>
@@ -25,7 +23,7 @@
</UDashboardNavbar>
<UDashboardToolbar>
<template #right>
<USelectMenu
<!-- <USelectMenu
v-model="selectedColumns"
icon="i-heroicons-adjustments-horizontal-solid"
:options="templateColumns"
@@ -36,7 +34,7 @@
<template #label>
Spalten
</template>
</USelectMenu>
</USelectMenu>-->
<USelectMenu
v-model="selectedFilters"
icon="i-heroicons-adjustments-horizontal-solid"

View File

@@ -364,33 +364,67 @@ const clearSearchString = () => {
variant="outline"
v-if="Object.keys(selectedFiles).find(i => selectedFiles[i] === true)"
>Herunterladen</UButton>
<UModal
v-model="createFolderModalOpen"
>
<UCard>
<UModal v-model="createFolderModalOpen">
<UCard :ui="{ body: { base: 'space-y-4' } }">
<template #header>
Ordner Erstellen
</template>
<div class="flex items-center justify-between">
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
Ordner Erstellen
</h3>
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1" @click="createFolderModalOpen = false" />
</div>
</template>
<UFormGroup label="Name des Ordners" required>
<UInput
v-model="createFolderData.name"
placeholder="z.B. Rechnungen 2024"
autofocus
/>
</UFormGroup>
<UFormGroup
label="Ordner erstellen"
<UFormGroup
label="Standard Dateityp"
>
<USelectMenu
v-model="createFolderData.standardFiletype"
:options="filetags"
option-attribute="name"
value-attribute="id"
searchable
searchable-placeholder="Typ suchen..."
placeholder="Kein Standard-Typ"
clear-search-on-close
>
<UInput
v-model="createFolderData.name"
<template #label>
<span v-if="createFolderData.standardFiletype">
{{ filetags.find(t => t.id === createFolderData.standardFiletype)?.name }}
</span>
<span v-else class="text-gray-400">Kein Typ ausgewählt</span>
</template>
</USelectMenu>
</UFormGroup>
<div v-if="createFolderData.standardFiletype">
<UCheckbox
v-model="createFolderData.standardFiletypeIsOptional"
name="isOptional"
label="Dateityp ist optional"
help="Wenn deaktiviert, MUSS der Nutzer beim Upload diesen Typ verwenden."
/>
</UFormGroup>
</div>
<template #footer>
<UButton
@click="createFolder"
>
Erstellen
</UButton>
<div class="flex justify-end gap-2">
<UButton color="gray" variant="ghost" @click="createFolderModalOpen = false">
Abbrechen
</UButton>
<UButton @click="createFolder" :disabled="!createFolderData.name">
Erstellen
</UButton>
</div>
</template>
</UCard>
</UModal>
</template>
@@ -429,8 +463,8 @@ const clearSearchString = () => {
</td>
<td>
<span v-if="entry.type === 'file'" class="text-xl">{{dayjs(documents.find(i => i.id === entry.id).created_at).format("DD.MM.YY HH:mm")}}</span>
<span v-if="entry.type === 'folder'" class="text-xl">{{dayjs(currentFolders.find(i => i.id === entry.id).created_at).format("DD.MM.YY HH:mm")}}</span>
<span v-if="entry.type === 'file'" class="text-xl">{{dayjs(documents.find(i => i.id === entry.id).createdAt).format("DD.MM.YY HH:mm")}}</span>
<span v-if="entry.type === 'folder'" class="text-xl">{{dayjs(currentFolders.find(i => i.id === entry.id).createdAt).format("DD.MM.YY HH:mm")}}</span>
</td>
</tr>
</table>