Dokumentenvorlagen im Ausgangsbeleg-Editor ergänzen
All checks were successful
Build and Push Docker Images / build-frontend (push) Successful in 1m22s
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-api (push) Successful in 22s
Build and Push Docker Images / build-backend (push) Successful in 43s
Build and Push Docker Images / build-central-services-admin (push) Successful in 21s
Build and Push Docker Images / build-docs (push) Successful in 21s

This commit is contained in:
root
2026-08-31 18:49:40 +00:00
parent 6afc799f69
commit d90499a101
12 changed files with 397 additions and 39 deletions

View File

@@ -13,6 +13,8 @@ const router = useRouter()
const modal = useModal()
const auth = useAuthStore()
const toast = useToast()
const isTemplateMode = computed(() => Boolean(route.query.templateFromDocument || route.query.templateId || route.query.mode === "template"))
const templateName = ref("")
const quoteLikeDocumentTypes = ["quotes", "costEstimates"]
const deliveryNoteLikeDocumentTypes = ["deliveryNotes", "packingSlips"]
const documentStorageFallbackTypes = {
@@ -399,6 +401,24 @@ const setupPage = async () => {
if (route.query) {
if (route.query.type) itemInfo.value.type = route.query.type
if (route.query.templateFromDocument) {
const sourceDocument = await useEntities("createddocuments").selectSingle(route.query.templateFromDocument, '', false)
const sourceData = JSON.parse(JSON.stringify(sourceDocument || {}))
;["id", "createdAt", "tenant", "documentNumber", "documentDate", "state", "customer", "contact", "address", "project", "costcentre", "createddocument", "availableInPortal", "archived", "statementallocations", "files", "linkedDocument", "createddocuments", "serialexecution"].forEach((key) => delete sourceData[key])
Object.assign(itemInfo.value, sourceData)
itemInfo.value.type = route.query.type || sourceDocument?.type || itemInfo.value.type
templateName.value = `${dataStore.documentTypesForCreation[itemInfo.value.type]?.labelSingle || "Dokument"} Vorlage`
}
if (route.query.templateId) {
const template = await useEntities("documenttemplates").selectSingle(route.query.templateId, '', false)
if (template?.templateData) {
Object.assign(itemInfo.value, JSON.parse(JSON.stringify(template.templateData)))
itemInfo.value.type = template.documentType
templateName.value = template.name
}
}
if (!itemInfo.value.startText && !itemInfo.value.endText) {
setDocumentTypeConfig(true)
} else {
@@ -1756,6 +1776,31 @@ const saveSerialInvoice = async () => {
await router.push(`/createDocument/edit/${data.id}`)
}
const serializeTemplateData = () => {
const data = JSON.parse(JSON.stringify(itemInfo.value))
;["id", "createdAt", "tenant", "documentNumber", "documentDate", "state", "customer", "contact", "address", "project", "costcentre", "createddocument", "availableInPortal", "archived", "statementallocations", "files", "linkedDocument", "createddocuments", "serialexecution", "createdBy", "created_by"].forEach((key) => delete data[key])
return data
}
const saveDocumentTemplate = async () => {
if (!templateName.value.trim()) {
toast.add({ title: "Vorlagenname fehlt", description: "Bitte einen Namen für die Vorlage vergeben.", color: "error" })
return
}
const payload = {
name: templateName.value.trim(),
documentType: itemInfo.value.type,
templateData: serializeTemplateData(),
default: false,
}
const template = route.query.templateId
? await useEntities("documenttemplates").update(route.query.templateId, payload, true)
: await useEntities("documenttemplates").create(payload)
toast.add({ title: "Vorlage gespeichert", color: "success" })
await router.push(`/createDocument/edit?mode=template&templateId=${template.id || route.query.templateId}`)
}
const saveDocument = async (state, resetup = false) => {
itemInfo.value.state = state
@@ -2024,7 +2069,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<ArchiveButton
color="error"
type="createddocuments"
v-if="itemInfo.state === 'Entwurf' || itemInfo.type === 'serialInvoices'"
v-if="!isTemplateMode && (itemInfo.state === 'Entwurf' || itemInfo.type === 'serialInvoices')"
variant="outline"
@confirmed="useEntities('createddocuments').update(itemInfo.id,{archived: true}),
router.push('/')"
@@ -2032,27 +2077,46 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<UButton
icon="i-mdi-content-save"
@click="saveDocument('Entwurf',true)"
v-if="itemInfo.type !== 'serialInvoices' "
v-if="itemInfo.type !== 'serialInvoices' && !isTemplateMode"
:disabled="!itemInfo.customer"
>
Speichern
</UButton>
<UButton
v-if="isTemplateMode"
icon="i-mdi-content-save"
color="primary"
@click="saveDocumentTemplate"
>
Als Vorlage speichern
</UButton>
<UButton
@click="closeDocument"
v-if="itemInfo.id && itemInfo.type !== 'serialInvoices'"
v-if="itemInfo.id && itemInfo.type !== 'serialInvoices' && !isTemplateMode"
>
{{selectedTab === '0' ? "Vorschau zeigen" : "Fertigstellen"}}
</UButton>
<UButton
icon="i-mdi-content-save"
@click="saveSerialInvoice"
v-if="itemInfo.type === 'serialInvoices'"
v-if="itemInfo.type === 'serialInvoices' && !isTemplateMode"
>
Serienrechnung
</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<div v-if="isTemplateMode" class="px-5 pt-5">
<UAlert
color="primary"
variant="soft"
title="Vorlagenmodus"
description="Dieser Editor speichert ausschließlich eine Dokumentenvorlage. Es wird kein Ausgangsbeleg erstellt."
/>
<UFormField label="Vorlagenname" required class="mt-3">
<UInput v-model="templateName" placeholder="z. B. Standardrechnung" class="w-full" />
</UFormField>
</div>
<UTabs class="p-5" :items="tabItems" @update:model-value="onChangeTab" v-if="loaded" v-model="selectedTab">
<template #content="{item}">
<div v-if="item.label === 'Editor'">
@@ -2084,6 +2148,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<USelectMenu
:items="documentTypeItems"
v-model="itemInfo.type"
:disabled="isTemplateMode"
value-key="type"
label-key="label"
@update:model-value="setDocumentTypeConfig"

View File

@@ -21,10 +21,11 @@
variant="outline"
@click="clearSearchString()"
/>
<UButton
@click="router.push(`/createDocument/edit`)"
>
+ Ausgangsbeleg
<UButton icon="i-heroicons-plus" @click="modal.open(CreateDocumentModal)">
+ Dokument
</UButton>
<UButton icon="i-heroicons-document-duplicate" variant="outline" @click="modal.open(CreateDocumentFromTemplateModal)">
+ Dokument aus Vorlage
</UButton>
</template>
</UDashboardNavbar>
@@ -163,12 +164,15 @@
</template>
<script setup>
import CreateDocumentModal from "~/components/createDocumentModal.vue"
import CreateDocumentFromTemplateModal from "~/components/createDocumentFromTemplateModal.vue"
import dayjs from "dayjs";
import { ref, computed, reactive, watch } from 'vue';
const dataStore = useDataStore()
const tempStore = useTempStore()
const router = useRouter()
const modal = useModal()
const quoteLikeDocumentTypes = ['quotes', 'costEstimates']
const deliveryNoteLikeDocumentTypes = ['deliveryNotes', 'packingSlips']
@@ -209,7 +213,7 @@ defineShortcuts({
document.getElementById("searchinput").focus()
},
'+': () => {
router.push('/createDocument/edit')
modal.open(CreateDocumentModal)
},
'Enter': {
usingInput: true,

View File

@@ -226,6 +226,13 @@ const togglePortalRelease = async () => {
>
Kopieren
</UButton>
<UButton
icon="i-heroicons-document-duplicate"
variant="outline"
@click="router.push(`/createDocument/edit?templateFromDocument=${itemInfo.id}&type=${itemInfo.type}`)"
>
Als Vorlage übernehmen
</UButton>
<UButton
@click="openEmail"
icon="i-heroicons-envelope"

View File

@@ -0,0 +1,81 @@
<script setup>
const dataStore = useDataStore()
const router = useRouter()
const toast = useToast()
const templates = ref([])
const loading = ref(true)
const documentTypeItems = computed(() => dataStore.documentTypesForCreation || {})
const refresh = async () => {
loading.value = true
try {
templates.value = await useEntities("documenttemplates").select("*")
} catch (error) {
toast.add({ title: "Vorlagen konnten nicht geladen werden", description: error.message, color: "error" })
} finally {
loading.value = false
}
}
const openNew = () => router.push("/createDocument/edit?mode=template&type=invoices")
const openTemplate = (template) => router.push(`/createDocument/edit?mode=template&templateId=${template.id}`)
const archiveTemplate = async (template) => {
await useEntities("documenttemplates").update(template.id, { archived: true }, true)
await refresh()
}
const typeLabel = (type) => documentTypeItems.value[type]?.labelSingle || type
refresh()
</script>
<template>
<UDashboardNavbar title="Dokumentenvorlagen">
<template #right>
<UButton icon="i-heroicons-plus" @click="openNew">Neue Vorlage</UButton>
</template>
</UDashboardNavbar>
<UDashboardPanelContent>
<UAlert
class="mx-5 mt-2"
color="primary"
variant="soft"
title="Vorlagen für alle Ausgangsdokumente"
description="Dokumentenvorlagen verwenden denselben Editor wie die Ausgangsbelege. Jede Einstellung wird als Vorlage gespeichert."
/>
<UTable
class="mt-4"
:data="templates"
:loading="loading"
:columns="normalizeTableColumns([
{ key: 'name', label: 'Bezeichnung' },
{ key: 'documentType', label: 'Dokumenttyp' },
{ key: 'default', label: 'Standard' },
{ key: 'actions', label: '' }
])"
>
<template #name-cell="{ row }">
<span class="font-medium text-highlighted">{{ row.original.name }}</span>
</template>
<template #documentType-cell="{ row }">
<UBadge color="neutral" variant="soft">{{ typeLabel(row.original.documentType) }}</UBadge>
</template>
<template #default-cell="{ row }">
<UIcon v-if="row.original.default" name="i-heroicons-check-circle-20-solid" class="text-green-500" />
<span v-else class="text-muted">-</span>
</template>
<template #actions-cell="{ row }">
<div class="flex justify-end gap-1">
<UButton icon="i-heroicons-pencil-square" color="neutral" variant="ghost" @click="openTemplate(row.original)" />
<UButton icon="i-heroicons-archive-box" color="error" variant="ghost" @click="archiveTemplate(row.original)" />
</div>
</template>
<template #empty>
<TableEmptyState label="Keine Dokumentenvorlagen gefunden" icon="i-heroicons-document-duplicate" />
</template>
</UTable>
</UDashboardPanelContent>
</template>