diff --git a/backend/db/migrations/0065_document_templates.sql b/backend/db/migrations/0065_document_templates.sql new file mode 100644 index 0000000..99caf5f --- /dev/null +++ b/backend/db/migrations/0065_document_templates.sql @@ -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"); \ No newline at end of file diff --git a/backend/db/schema/documenttemplates.ts b/backend/db/schema/documenttemplates.ts new file mode 100644 index 0000000..b9a8c80 --- /dev/null +++ b/backend/db/schema/documenttemplates.ts @@ -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 diff --git a/backend/db/schema/index.ts b/backend/db/schema/index.ts index 3afc23c..e6d1e5e 100644 --- a/backend/db/schema/index.ts +++ b/backend/db/schema/index.ts @@ -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" diff --git a/backend/src/utils/resource.config.ts b/backend/src/utils/resource.config.ts index bf7e486..476c5cc 100644 --- a/backend/src/utils/resource.config.ts +++ b/backend/src/utils/resource.config.ts @@ -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"], diff --git a/frontend/components/EntityShowSubCreatedDocuments.vue b/frontend/components/EntityShowSubCreatedDocuments.vue index 272da37..febb98e 100644 --- a/frontend/components/EntityShowSubCreatedDocuments.vue +++ b/frontend/components/EntityShowSubCreatedDocuments.vue @@ -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 - + Angebot + + Dokument - + Kostenschätzung - - - + Auftragsbestätigung - - - + Lieferschein - - - + Packschein - - - + Abschlagsrechnung + + Dokument aus Vorlage { - - + Rechnung - - { 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", diff --git a/frontend/components/createDocumentFromTemplateModal.vue b/frontend/components/createDocumentFromTemplateModal.vue new file mode 100644 index 0000000..1431993 --- /dev/null +++ b/frontend/components/createDocumentFromTemplateModal.vue @@ -0,0 +1,94 @@ + + + + + + + + + + Dokument aus Vorlage erstellen + Zuerst Dokumenttyp, anschließend die passende Vorlage auswählen. + + + + + + + + + + + Vorlagen + Vorlagen werden geladen … + + Keine Vorlagen für diesen Dokumenttyp vorhanden. + + + + + {{ template.name }} + {{ dataStore.documentTypesForCreation[template.documentType]?.labelSingle }} + + Standard + + + + + + + Abbrechen + + + + + + diff --git a/frontend/components/createDocumentModal.vue b/frontend/components/createDocumentModal.vue new file mode 100644 index 0000000..729ceb2 --- /dev/null +++ b/frontend/components/createDocumentModal.vue @@ -0,0 +1,62 @@ + + + + + + + + + + Dokument erstellen + Wähle den gewünschten Ausgangsbeleg. + + + + + + + + {{ documentType.labelSingle }} + {{ documentType.label }} + + + + + + Abbrechen + + + + + + diff --git a/frontend/pages/createDocument/edit/[[id]].vue b/frontend/pages/createDocument/edit/[[id]].vue index 282d77c..3457654 100644 --- a/frontend/pages/createDocument/edit/[[id]].vue +++ b/frontend/pages/createDocument/edit/[[id]].vue @@ -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 = { Speichern + + Als Vorlage speichern + {{selectedTab === '0' ? "Vorschau zeigen" : "Fertigstellen"}} Serienrechnung + + + + + + @@ -2084,6 +2148,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = { - - + Ausgangsbeleg + + + Dokument + + + + Dokument aus Vorlage @@ -163,12 +164,15 @@ + + + + + Neue Vorlage + + + + + + + + + {{ row.original.name }} + + + {{ typeLabel(row.original.documentType) }} + + + + - + + + + + + + + + + + + +
Zuerst Dokumenttyp, anschließend die passende Vorlage auswählen.
Vorlagen
Wähle den gewünschten Ausgangsbeleg.