KI-AGENT: Briefpapierpflege im Frontend ergänzen
This commit is contained in:
@@ -322,6 +322,11 @@ const links = computed(() => {
|
||||
to: "/settings/texttemplates",
|
||||
icon: "i-heroicons-clipboard-document-list",
|
||||
} : null,
|
||||
featureEnabled("settingsLetterheads") ? {
|
||||
label: "Briefpapiere",
|
||||
to: "/settings/letterheads",
|
||||
icon: "i-heroicons-document",
|
||||
} : null,
|
||||
featureEnabled("settingsTenant") ? {
|
||||
label: "Firmeneinstellungen",
|
||||
to: "/settings/tenant",
|
||||
|
||||
303
frontend/pages/settings/letterheads.vue
Normal file
303
frontend/pages/settings/letterheads.vue
Normal file
@@ -0,0 +1,303 @@
|
||||
<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 getFileForLetterhead = (letterhead) =>
|
||||
files.value.find((file) => file.path === letterhead.path)
|
||||
|
||||
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({ type: "Briefpapier" }))
|
||||
|
||||
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)"
|
||||
>
|
||||
PDF
|
||||
</UButton>
|
||||
<span v-else class="text-sm text-gray-500">Nicht zugeordnet</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>
|
||||
@@ -3298,6 +3298,11 @@ export const useDataStore = defineStore('data', () => {
|
||||
label: "Textvorlagen",
|
||||
labelSingle: "Textvorlage"
|
||||
},
|
||||
letterheads: {
|
||||
isArchivable: true,
|
||||
label: "Briefpapiere",
|
||||
labelSingle: "Briefpapier"
|
||||
},
|
||||
bankstatements: {
|
||||
isArchivable: true,
|
||||
label: "Kontobewegungen",
|
||||
|
||||
Reference in New Issue
Block a user