KI-AGENT: Briefpapierpflege im Frontend ergänzen
This commit is contained in:
@@ -322,6 +322,11 @@ const links = computed(() => {
|
|||||||
to: "/settings/texttemplates",
|
to: "/settings/texttemplates",
|
||||||
icon: "i-heroicons-clipboard-document-list",
|
icon: "i-heroicons-clipboard-document-list",
|
||||||
} : null,
|
} : null,
|
||||||
|
featureEnabled("settingsLetterheads") ? {
|
||||||
|
label: "Briefpapiere",
|
||||||
|
to: "/settings/letterheads",
|
||||||
|
icon: "i-heroicons-document",
|
||||||
|
} : null,
|
||||||
featureEnabled("settingsTenant") ? {
|
featureEnabled("settingsTenant") ? {
|
||||||
label: "Firmeneinstellungen",
|
label: "Firmeneinstellungen",
|
||||||
to: "/settings/tenant",
|
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",
|
label: "Textvorlagen",
|
||||||
labelSingle: "Textvorlage"
|
labelSingle: "Textvorlage"
|
||||||
},
|
},
|
||||||
|
letterheads: {
|
||||||
|
isArchivable: true,
|
||||||
|
label: "Briefpapiere",
|
||||||
|
labelSingle: "Briefpapier"
|
||||||
|
},
|
||||||
bankstatements: {
|
bankstatements: {
|
||||||
isArchivable: true,
|
isArchivable: true,
|
||||||
label: "Kontobewegungen",
|
label: "Kontobewegungen",
|
||||||
|
|||||||
Reference in New Issue
Block a user