Ersetzt ungültige UTable-Empty-Props durch einen gemeinsamen Empty-State-Slot, damit leere Tabellen keine Objekt-/JSON-Ausgabe mehr anzeigen.
324 lines
9.5 KiB
Vue
324 lines
9.5 KiB
Vue
<script setup>
|
|
import { computed, onMounted, ref } from "vue"
|
|
|
|
const dataStore = useDataStore()
|
|
const toast = useToast()
|
|
|
|
const { select, create, update } = useEntities("letterheads")
|
|
|
|
const letterheads = ref([])
|
|
const files = ref([])
|
|
const loading = ref(true)
|
|
const saving = ref(false)
|
|
const modalOpen = ref(false)
|
|
const selectedFile = ref(null)
|
|
const itemInfo = ref({})
|
|
|
|
const documentTypeOptions = computed(() =>
|
|
Object.entries(dataStore.documentTypesForCreation || {})
|
|
.filter(([key]) => key !== "serialInvoices")
|
|
.map(([key, value]) => ({
|
|
key,
|
|
label: value.label || value.labelSingle || key
|
|
}))
|
|
)
|
|
|
|
const getDocumentTypeLabel = (type) =>
|
|
dataStore.documentTypesForCreation?.[type]?.label || type
|
|
|
|
const normalizePath = (path) =>
|
|
String(path || "").trim().replace(/^\/+/, "")
|
|
|
|
const getFileForLetterhead = (letterhead) => {
|
|
const letterheadPath = normalizePath(letterhead?.path)
|
|
if (!letterheadPath) return null
|
|
|
|
return files.value.find((file) => normalizePath(file.path) === letterheadPath) || null
|
|
}
|
|
|
|
const getFileNameForLetterhead = (letterhead) => {
|
|
const file = getFileForLetterhead(letterhead)
|
|
if (file?.name) return file.name
|
|
|
|
const path = normalizePath(letterhead?.path)
|
|
return path ? decodeURIComponent(path.split("/").pop()) : null
|
|
}
|
|
|
|
const sortedLetterheads = computed(() =>
|
|
[...letterheads.value].sort((a, b) => {
|
|
const aAll = !a.documentTypes?.length
|
|
const bAll = !b.documentTypes?.length
|
|
if (aAll !== bAll) return aAll ? -1 : 1
|
|
return String(a.name || "").localeCompare(String(b.name || ""), "de")
|
|
})
|
|
)
|
|
|
|
const refreshData = async () => {
|
|
loading.value = true
|
|
try {
|
|
const [loadedLetterheads, loadedFiles] = await Promise.all([
|
|
select("*", "name", true),
|
|
useEntities("files").select()
|
|
])
|
|
|
|
letterheads.value = loadedLetterheads
|
|
files.value = loadedFiles
|
|
} catch (error) {
|
|
toast.add({
|
|
title: "Briefpapiere konnten nicht geladen werden",
|
|
description: error?.message,
|
|
color: "error"
|
|
})
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const resetForm = () => {
|
|
itemInfo.value = {
|
|
name: "",
|
|
documentTypes: []
|
|
}
|
|
selectedFile.value = null
|
|
}
|
|
|
|
const openCreateModal = () => {
|
|
resetForm()
|
|
modalOpen.value = true
|
|
}
|
|
|
|
const openEditModal = (letterhead) => {
|
|
itemInfo.value = {
|
|
...JSON.parse(JSON.stringify(letterhead)),
|
|
documentTypes: Array.isArray(letterhead.documentTypes) ? [...letterhead.documentTypes] : []
|
|
}
|
|
selectedFile.value = null
|
|
modalOpen.value = true
|
|
}
|
|
|
|
const onFileChange = (event) => {
|
|
selectedFile.value = event.target.files?.[0] || null
|
|
}
|
|
|
|
const uploadLetterheadPdf = async () => {
|
|
if (!selectedFile.value) return null
|
|
|
|
if (selectedFile.value.type !== "application/pdf") {
|
|
throw new Error("Bitte wähle eine PDF-Datei aus.")
|
|
}
|
|
|
|
const formData = new FormData()
|
|
formData.append("file", selectedFile.value)
|
|
formData.append("meta", JSON.stringify({ filename: selectedFile.value.name }))
|
|
|
|
return await useNuxtApp().$api("/api/files/upload", {
|
|
method: "POST",
|
|
body: formData
|
|
})
|
|
}
|
|
|
|
const saveLetterhead = async () => {
|
|
if (!itemInfo.value.name?.trim()) {
|
|
toast.add({ title: "Name fehlt", description: "Bitte gib eine Bezeichnung ein.", color: "warning" })
|
|
return
|
|
}
|
|
|
|
if (!itemInfo.value.id && !selectedFile.value) {
|
|
toast.add({ title: "PDF fehlt", description: "Bitte lade ein Briefpapier als PDF hoch.", color: "warning" })
|
|
return
|
|
}
|
|
|
|
saving.value = true
|
|
try {
|
|
const uploaded = await uploadLetterheadPdf()
|
|
const payload = {
|
|
name: itemInfo.value.name.trim(),
|
|
documentTypes: Array.isArray(itemInfo.value.documentTypes) ? itemInfo.value.documentTypes : [],
|
|
path: uploaded?.path || itemInfo.value.path
|
|
}
|
|
|
|
if (itemInfo.value.id) {
|
|
await update(itemInfo.value.id, payload, true)
|
|
} else {
|
|
await create(payload, true)
|
|
}
|
|
|
|
modalOpen.value = false
|
|
await refreshData()
|
|
} catch (error) {
|
|
toast.add({
|
|
title: "Briefpapier konnte nicht gespeichert werden",
|
|
description: error?.message,
|
|
color: "error"
|
|
})
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
const archiveLetterhead = async (letterhead) => {
|
|
await update(letterhead.id, { archived: true }, true)
|
|
await refreshData()
|
|
}
|
|
|
|
const downloadLetterhead = async (letterhead) => {
|
|
const file = getFileForLetterhead(letterhead)
|
|
if (!file?.id) {
|
|
toast.add({
|
|
title: "Datei nicht gefunden",
|
|
description: "Für dieses Briefpapier konnte keine Datei zugeordnet werden.",
|
|
color: "warning"
|
|
})
|
|
return
|
|
}
|
|
|
|
await useFiles().downloadFile(file.id)
|
|
}
|
|
|
|
onMounted(refreshData)
|
|
</script>
|
|
|
|
<template>
|
|
<UDashboardNavbar title="Briefpapiere">
|
|
<template #right>
|
|
<UButton icon="i-heroicons-plus" @click="openCreateModal">
|
|
Briefpapier
|
|
</UButton>
|
|
</template>
|
|
</UDashboardNavbar>
|
|
|
|
<UDashboardPanelContent>
|
|
<UTable
|
|
:data="sortedLetterheads"
|
|
:loading="loading"
|
|
:columns="normalizeTableColumns([
|
|
{ key: 'name', label: 'Bezeichnung' },
|
|
{ key: 'documentTypes', label: 'Verwendung' },
|
|
{ key: 'path', label: 'Datei' },
|
|
{ key: 'actions', label: '' }
|
|
])"
|
|
>
|
|
<template #name-cell="{ row }">
|
|
<span class="font-medium text-gray-900 dark:text-white">
|
|
{{ row.original.name || "Briefpapier" }}
|
|
</span>
|
|
</template>
|
|
|
|
<template #documentTypes-cell="{ row }">
|
|
<div class="flex flex-wrap gap-1">
|
|
<UBadge v-if="!row.original.documentTypes?.length" color="primary" variant="soft">
|
|
Alle Dokumente
|
|
</UBadge>
|
|
<template v-else>
|
|
<UBadge
|
|
v-for="type in row.original.documentTypes"
|
|
:key="type"
|
|
color="gray"
|
|
variant="soft"
|
|
>
|
|
{{ getDocumentTypeLabel(type) }}
|
|
</UBadge>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<template #path-cell="{ row }">
|
|
<UButton
|
|
v-if="getFileForLetterhead(row.original)"
|
|
icon="i-heroicons-arrow-down-tray"
|
|
color="gray"
|
|
variant="ghost"
|
|
@click.stop="downloadLetterhead(row.original)"
|
|
>
|
|
{{ getFileNameForLetterhead(row.original) || "PDF" }}
|
|
</UButton>
|
|
<span v-else-if="getFileNameForLetterhead(row.original)" class="text-sm text-gray-700 dark:text-gray-300">
|
|
{{ getFileNameForLetterhead(row.original) }}
|
|
</span>
|
|
<span v-else class="text-sm text-gray-500">Keine Datei hinterlegt</span>
|
|
</template>
|
|
|
|
<template #actions-cell="{ row }">
|
|
<div class="flex justify-end gap-1">
|
|
<UButton
|
|
icon="i-heroicons-pencil-square"
|
|
color="gray"
|
|
variant="ghost"
|
|
@click="openEditModal(row.original)"
|
|
/>
|
|
<ButtonWithConfirm
|
|
color="error"
|
|
variant="ghost"
|
|
@confirmed="archiveLetterhead(row.original)"
|
|
>
|
|
<template #button>Archivieren</template>
|
|
<template #header>
|
|
<span class="font-bold">Briefpapier archivieren?</span>
|
|
</template>
|
|
Das Briefpapier "{{ row.original.name }}" wird archiviert.
|
|
</ButtonWithConfirm>
|
|
</div>
|
|
</template>
|
|
<template #empty>
|
|
<TableEmptyState label="Keine Briefpapiere gefunden" icon="i-heroicons-document" />
|
|
</template>
|
|
</UTable>
|
|
</UDashboardPanelContent>
|
|
|
|
<UModal v-model:open="modalOpen" :ui="{ width: 'sm:max-w-2xl' }">
|
|
<template #content>
|
|
<UCard>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-lg font-semibold">
|
|
{{ itemInfo.id ? "Briefpapier bearbeiten" : "Briefpapier erstellen" }}
|
|
</h3>
|
|
<UButton icon="i-heroicons-x-mark" color="gray" variant="ghost" @click="modalOpen = false" />
|
|
</div>
|
|
</template>
|
|
|
|
<div class="space-y-4">
|
|
<UFormField label="Bezeichnung" required>
|
|
<UInput v-model="itemInfo.name" icon="i-heroicons-tag" placeholder="z.B. Standard Briefpapier" />
|
|
</UFormField>
|
|
|
|
<UFormField label="Verwendung">
|
|
<USelectMenu
|
|
v-model="itemInfo.documentTypes"
|
|
:options="documentTypeOptions"
|
|
multiple
|
|
option-attribute="label"
|
|
value-attribute="key"
|
|
placeholder="Alle Dokumente"
|
|
/>
|
|
</UFormField>
|
|
|
|
<UFormField :label="itemInfo.id ? 'PDF ersetzen' : 'PDF hochladen'" :required="!itemInfo.id">
|
|
<input
|
|
type="file"
|
|
accept="application/pdf"
|
|
class="block w-full text-sm text-gray-700 file:mr-4 file:rounded-md file:border-0 file:bg-primary-50 file:px-3 file:py-2 file:text-sm file:font-medium file:text-primary-700 hover:file:bg-primary-100 dark:text-gray-200 dark:file:bg-primary-950 dark:file:text-primary-300"
|
|
@change="onFileChange"
|
|
>
|
|
</UFormField>
|
|
|
|
<div v-if="itemInfo.path" class="text-sm text-gray-500">
|
|
Aktuelle Datei: {{ itemInfo.path.split("/").pop() }}
|
|
</div>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<div class="flex justify-end gap-2">
|
|
<UButton color="gray" variant="ghost" @click="modalOpen = false">
|
|
Abbrechen
|
|
</UButton>
|
|
<UButton :loading="saving" icon="i-heroicons-check" @click="saveLetterhead">
|
|
Speichern
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
</UCard>
|
|
</template>
|
|
</UModal>
|
|
</template>
|