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
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:
16
backend/db/migrations/0065_document_templates.sql
Normal file
16
backend/db/migrations/0065_document_templates.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
CREATE TABLE IF NOT EXISTS "documenttemplates" (
|
||||
"id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"tenant" bigint NOT NULL REFERENCES "tenants"("id"),
|
||||
"name" text NOT NULL,
|
||||
"document_type" text NOT NULL,
|
||||
"template_data" jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
"default" boolean NOT NULL DEFAULT false,
|
||||
"archived" boolean NOT NULL DEFAULT false,
|
||||
"updated_at" timestamptz,
|
||||
"updated_by" uuid REFERENCES "auth_users"("id"),
|
||||
"created_by" uuid REFERENCES "auth_users"("id")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "documenttemplates_tenant_type_idx"
|
||||
ON "documenttemplates" ("tenant", "document_type");
|
||||
39
backend/db/schema/documenttemplates.ts
Normal file
39
backend/db/schema/documenttemplates.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
pgTable,
|
||||
bigint,
|
||||
text,
|
||||
timestamp,
|
||||
boolean,
|
||||
jsonb,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core"
|
||||
|
||||
import { tenants } from "./tenants"
|
||||
import { authUsers } from "./auth_users"
|
||||
|
||||
export const documenttemplates = pgTable("documenttemplates", {
|
||||
id: bigint("id", { mode: "number" })
|
||||
.primaryKey()
|
||||
.generatedByDefaultAsIdentity(),
|
||||
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
|
||||
tenant: bigint("tenant", { mode: "number" })
|
||||
.notNull()
|
||||
.references(() => tenants.id),
|
||||
|
||||
name: text("name").notNull(),
|
||||
documentType: text("document_type").notNull(),
|
||||
templateData: jsonb("template_data").notNull().default({}),
|
||||
default: boolean("default").notNull().default(false),
|
||||
archived: boolean("archived").notNull().default(false),
|
||||
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||
createdBy: uuid("created_by").references(() => authUsers.id),
|
||||
})
|
||||
|
||||
export type DocumentTemplate = typeof documenttemplates.$inferSelect
|
||||
export type NewDocumentTemplate = typeof documenttemplates.$inferInsert
|
||||
@@ -22,6 +22,7 @@ export * from "./contracttypes"
|
||||
export * from "./costcentres"
|
||||
export * from "./countrys"
|
||||
export * from "./createddocuments"
|
||||
export * from "./documenttemplates"
|
||||
export * from "./createdletters"
|
||||
export * from "./customers"
|
||||
export * from "./customerspaces"
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
contracttypes,
|
||||
costcentres,
|
||||
createddocuments,
|
||||
documenttemplates,
|
||||
customerinventoryitems,
|
||||
customerspaces,
|
||||
customers,
|
||||
@@ -237,6 +238,9 @@ export const resourceConfig = {
|
||||
texttemplates: {
|
||||
table: texttemplates
|
||||
},
|
||||
documenttemplates: {
|
||||
table: documenttemplates,
|
||||
},
|
||||
incominginvoices: {
|
||||
table: incominginvoices,
|
||||
mtmLoad: ["statementallocations","files"],
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import {useSum} from "~/composables/useSum.js";
|
||||
import CreateDocumentModal from "~/components/createDocumentModal.vue";
|
||||
import CreateDocumentFromTemplateModal from "~/components/createDocumentFromTemplateModal.vue";
|
||||
defineShortcuts({
|
||||
/*'/': () => {
|
||||
//console.log(searchinput)
|
||||
@@ -56,6 +58,7 @@ const dataStore = useDataStore()
|
||||
const tempStore = useTempStore()
|
||||
|
||||
const router = useRouter()
|
||||
const modal = useModal()
|
||||
const deliveryNoteLikeDocumentTypes = ['deliveryNotes', 'packingSlips']
|
||||
|
||||
const createddocuments = ref([])
|
||||
@@ -154,34 +157,17 @@ const selectItem = (item) => {
|
||||
Lieferscheine/Packscheine abrechnen
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="router.push(`/createDocument/edit/?${getAvailableQueryStringData({type: 'quotes'})}`)"
|
||||
icon="i-heroicons-plus"
|
||||
@click="modal.open(CreateDocumentModal, { queryStringData: props.queryStringData })"
|
||||
>
|
||||
+ Angebot
|
||||
+ Dokument
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="router.push(`/createDocument/edit/?${getAvailableQueryStringData({type: 'costEstimates'})}`)"
|
||||
icon="i-heroicons-document-duplicate"
|
||||
variant="outline"
|
||||
@click="modal.open(CreateDocumentFromTemplateModal, { queryStringData: props.queryStringData })"
|
||||
>
|
||||
+ Kostenschätzung
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="router.push(`/createDocument/edit/?${getAvailableQueryStringData({type: 'confirmationOrders'})}`)"
|
||||
>
|
||||
+ Auftragsbestätigung
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="router.push(`/createDocument/edit/?${getAvailableQueryStringData({type: 'deliveryNotes'})}`)"
|
||||
>
|
||||
+ Lieferschein
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="router.push(`/createDocument/edit/?${getAvailableQueryStringData({type: 'packingSlips'})}`)"
|
||||
>
|
||||
+ Packschein
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="router.push(`/createDocument/edit/?${getAvailableQueryStringData({type: 'advanceInvoices'})}`)"
|
||||
>
|
||||
+ Abschlagsrechnung
|
||||
+ Dokument aus Vorlage
|
||||
</UButton>
|
||||
<UButton
|
||||
@click="showFinalInvoiceConfig = true"
|
||||
@@ -238,12 +224,6 @@ const selectItem = (item) => {
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
<UButton
|
||||
@click="router.push(`/createDocument/edit/?${getAvailableQueryStringData({type: 'invoices'})}`)"
|
||||
>
|
||||
+ Rechnung
|
||||
</UButton>
|
||||
|
||||
<template #right>
|
||||
<USelectMenu
|
||||
v-model="selectedColumns"
|
||||
|
||||
@@ -331,6 +331,11 @@ const links = computed(() => {
|
||||
to: "/settings/texttemplates",
|
||||
icon: "i-heroicons-clipboard-document-list",
|
||||
} : null,
|
||||
featureEnabled("settingsDocumenttemplates") ? {
|
||||
label: "Dokumentenvorlagen",
|
||||
to: "/settings/documenttemplates",
|
||||
icon: "i-heroicons-document-duplicate",
|
||||
} : null,
|
||||
featureEnabled("settingsLetterheads") ? {
|
||||
label: "Briefpapiere",
|
||||
to: "/settings/letterheads",
|
||||
|
||||
94
frontend/components/createDocumentFromTemplateModal.vue
Normal file
94
frontend/components/createDocumentFromTemplateModal.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script setup>
|
||||
const dataStore = useDataStore()
|
||||
const modal = useModal()
|
||||
const router = useRouter()
|
||||
const templates = ref([])
|
||||
const selectedType = ref(null)
|
||||
const loading = ref(true)
|
||||
const props = defineProps({
|
||||
queryStringData: { type: String, default: "" },
|
||||
})
|
||||
|
||||
const documentTypes = computed(() => Object.entries(dataStore.documentTypesForCreation || {})
|
||||
.filter(([key]) => key !== 'serialInvoices')
|
||||
.map(([key, value]) => ({ key, ...value })))
|
||||
const visibleTemplates = computed(() => templates.value.filter(template => {
|
||||
return !selectedType.value || template.documentType === selectedType.value
|
||||
}))
|
||||
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
templates.value = await useEntities('documenttemplates').select('*')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const selectTemplate = (template) => {
|
||||
const query = new URLSearchParams(props.queryStringData)
|
||||
query.set('templateId', template.id)
|
||||
query.set('type', template.documentType)
|
||||
router.push(`/createDocument/edit?${query.toString()}`)
|
||||
modal.close()
|
||||
}
|
||||
|
||||
loadTemplates()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :ui="{ content: 'sm:max-w-3xl' }">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Dokument aus Vorlage erstellen</h2>
|
||||
<p class="mt-1 text-sm text-muted">Zuerst Dokumenttyp, anschließend die passende Vorlage auswählen.</p>
|
||||
</div>
|
||||
<UButton icon="i-heroicons-x-mark" color="gray" variant="ghost" @click="modal.close()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UFormField label="Dokumenttyp" required>
|
||||
<USelectMenu
|
||||
v-model="selectedType"
|
||||
:items="documentTypes"
|
||||
value-key="key"
|
||||
label-key="labelSingle"
|
||||
class="w-full"
|
||||
placeholder="Dokumenttyp auswählen"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<div class="mt-5 space-y-2">
|
||||
<p class="text-sm font-medium text-highlighted">Vorlagen</p>
|
||||
<div v-if="loading" class="py-6 text-center text-sm text-muted">Vorlagen werden geladen …</div>
|
||||
<div v-else-if="visibleTemplates.length === 0" class="rounded-lg border border-dashed border-default p-6 text-center text-sm text-muted">
|
||||
Keine Vorlagen für diesen Dokumenttyp vorhanden.
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<button
|
||||
v-for="template in visibleTemplates"
|
||||
:key="template.id"
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between rounded-lg border border-default p-3 text-left transition hover:border-primary hover:bg-primary/5"
|
||||
@click="selectTemplate(template)"
|
||||
>
|
||||
<span>
|
||||
<span class="block font-medium text-highlighted">{{ template.name }}</span>
|
||||
<span class="text-xs text-muted">{{ dataStore.documentTypesForCreation[template.documentType]?.labelSingle }}</span>
|
||||
</span>
|
||||
<UBadge v-if="template.default" color="primary" variant="soft">Standard</UBadge>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<UButton color="neutral" variant="ghost" @click="modal.close()">Abbrechen</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
62
frontend/components/createDocumentModal.vue
Normal file
62
frontend/components/createDocumentModal.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup>
|
||||
const dataStore = useDataStore()
|
||||
const modal = useModal()
|
||||
const router = useRouter()
|
||||
const props = defineProps({
|
||||
queryStringData: { type: String, default: "" },
|
||||
projectId: { type: [String, Number], default: null },
|
||||
customerId: { type: [String, Number], default: null },
|
||||
})
|
||||
|
||||
const documentTypes = computed(() => Object.entries(dataStore.documentTypesForCreation || {})
|
||||
.filter(([key]) => key !== 'serialInvoices')
|
||||
.map(([key, value]) => ({ key, ...value })))
|
||||
|
||||
const createDocument = (type) => {
|
||||
const query = new URLSearchParams(props.queryStringData)
|
||||
query.set('type', type)
|
||||
if (props.projectId) query.set('project', props.projectId)
|
||||
if (props.customerId) query.set('customer', props.customerId)
|
||||
router.push(`/createDocument/edit?${query.toString()}`)
|
||||
modal.close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :ui="{ content: 'sm:max-w-3xl' }">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Dokument erstellen</h2>
|
||||
<p class="mt-1 text-sm text-muted">Wähle den gewünschten Ausgangsbeleg.</p>
|
||||
</div>
|
||||
<UButton icon="i-heroicons-x-mark" color="gray" variant="ghost" @click="modal.close()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<UButton
|
||||
v-for="documentType in documentTypes"
|
||||
:key="documentType.key"
|
||||
block
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
class="flex h-auto min-h-24 flex-col items-start justify-center gap-1 p-4 text-left"
|
||||
@click="createDocument(documentType.key)"
|
||||
>
|
||||
<span class="font-semibold text-highlighted">{{ documentType.labelSingle }}</span>
|
||||
<span class="text-xs text-muted">{{ documentType.label }}</span>
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<UButton color="neutral" variant="ghost" @click="modal.close()">Abbrechen</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
81
frontend/pages/settings/documenttemplates.vue
Normal file
81
frontend/pages/settings/documenttemplates.vue
Normal 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>
|
||||
Reference in New Issue
Block a user