Files
FEDEO/frontend/pages/settings/letterheads.vue

322 lines
9.4 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: '' }
])"
:empty="{ icon: 'i-heroicons-document', label: 'Keine Briefpapiere gefunden' }"
>
<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>
</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>