From d7eced3e77859f8ecfd4d327b3fd9a555788bf5f Mon Sep 17 00:00:00 2001
From: florianfederspiel
Date: Fri, 8 May 2026 20:09:24 +0200
Subject: [PATCH] =?UTF-8?q?Vertragstypen=20f=C3=BCr=20=C3=84nderungsanfrag?=
=?UTF-8?q?en=20pflegen?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../migrations/0036_allowed_contracttypes.sql | 1 +
backend/db/migrations/meta/_journal.json | 7 ++++
backend/db/schema/contracts.ts | 1 +
backend/src/routes/portal/contracts.ts | 9 ++++
frontend/pages/customer-portal.vue | 42 ++++++++++++++++++-
frontend/stores/data.js | 9 ++++
6 files changed, 67 insertions(+), 2 deletions(-)
create mode 100644 backend/db/migrations/0036_allowed_contracttypes.sql
diff --git a/backend/db/migrations/0036_allowed_contracttypes.sql b/backend/db/migrations/0036_allowed_contracttypes.sql
new file mode 100644
index 0000000..3254325
--- /dev/null
+++ b/backend/db/migrations/0036_allowed_contracttypes.sql
@@ -0,0 +1 @@
+ALTER TABLE "contracts" ADD COLUMN "allowedContracttypes" jsonb DEFAULT '[]'::jsonb NOT NULL;
diff --git a/backend/db/migrations/meta/_journal.json b/backend/db/migrations/meta/_journal.json
index 8c4e6d3..5279f08 100644
--- a/backend/db/migrations/meta/_journal.json
+++ b/backend/db/migrations/meta/_journal.json
@@ -246,6 +246,13 @@
"when": 1778191200000,
"tag": "0035_contract_history",
"breakpoints": true
+ },
+ {
+ "idx": 35,
+ "version": "7",
+ "when": 1778194800000,
+ "tag": "0036_allowed_contracttypes",
+ "breakpoints": true
}
]
}
diff --git a/backend/db/schema/contracts.ts b/backend/db/schema/contracts.ts
index 2a32e67..c9dc973 100644
--- a/backend/db/schema/contracts.ts
+++ b/backend/db/schema/contracts.ts
@@ -52,6 +52,7 @@ export const contracts = pgTable(
contracttype: bigint("contracttype", { mode: "number" }).references(
() => contracttypes.id
),
+ allowedContracttypes: jsonb("allowedContracttypes").notNull().default([]),
bankingIban: text("bankingIban"),
bankingBIC: text("bankingBIC"),
diff --git a/backend/src/routes/portal/contracts.ts b/backend/src/routes/portal/contracts.ts
index 2a10745..d049dd6 100644
--- a/backend/src/routes/portal/contracts.ts
+++ b/backend/src/routes/portal/contracts.ts
@@ -32,6 +32,7 @@ async function getPortalContract(server: FastifyInstance, req: any, contractId:
tenant: contracts.tenant,
customer: contracts.customer,
contracttype: contracts.contracttype,
+ allowedContracttypes: contracts.allowedContracttypes,
archived: contracts.archived,
})
.from(contracts)
@@ -121,6 +122,14 @@ export default async function portalContractRoutes(server: FastifyInstance) {
return reply.code(400).send({ error: "Ungültiger Vertragstyp" })
}
+ const allowedContracttypes = Array.isArray(contract.allowedContracttypes)
+ ? contract.allowedContracttypes.map((id) => Number(id)).filter((id) => Number.isInteger(id))
+ : []
+
+ if (!allowedContracttypes.includes(requestedContracttype.id)) {
+ return reply.code(400).send({ error: "Dieser Vertragstyp steht für diesen Vertrag nicht zur Auswahl" })
+ }
+
const [currentContracttype] = contract.contracttype
? await server.db
.select({
diff --git a/frontend/pages/customer-portal.vue b/frontend/pages/customer-portal.vue
index 263bf23..e5a0008 100644
--- a/frontend/pages/customer-portal.vue
+++ b/frontend/pages/customer-portal.vue
@@ -165,9 +165,27 @@ function getContactLabel(contact: any) {
return contact.fullName || [contact.firstName, contact.lastName].filter(Boolean).join(" ") || contact.email || contact.id || "-"
}
+function getAllowedContracttypeIds(contract: any) {
+ if (!Array.isArray(contract?.allowedContracttypes)) return []
+ return contract.allowedContracttypes.map((id: any) => Number(id)).filter((id: number) => Number.isInteger(id))
+}
+
+function getAllowedContracttypes(contract: any) {
+ const allowedIds = getAllowedContracttypeIds(contract)
+ return contracttypes.value.filter((item: any) => allowedIds.includes(Number(item.id)))
+}
+
+const selectedContractAllowedContracttypes = computed(() => {
+ if (!selectedContract.value) return []
+ return getAllowedContracttypes(selectedContract.value)
+})
+
function openContractChangeRequest(contract: any) {
+ const allowedTypes = getAllowedContracttypes(contract)
selectedContract.value = contract
- contractChangeForm.contracttype = contract.contracttype?.id || null
+ contractChangeForm.contracttype = allowedTypes.some((item: any) => item.id === contract.contracttype?.id)
+ ? contract.contracttype.id
+ : allowedTypes[0]?.id || null
contractChangeForm.message = ""
contractChangeModalOpen.value = true
}
@@ -589,6 +607,7 @@ onMounted(async () => {
variant="soft"
icon="i-heroicons-pencil-square"
class="justify-center"
+ :disabled="getAllowedContracttypes(contract).length === 0"
@click="openContractChangeRequest(contract)"
>
Änderung anfragen
@@ -611,6 +630,22 @@ onMounted(async () => {
{{ contract.contracttype?.name || "Nicht hinterlegt" }}
+
+
Änderung möglich zu
+
+
+ {{ type.name }}
+
+
+
+ Keine Änderungstypen freigegeben
+
+
Ansprechpartner
@@ -751,12 +786,15 @@ onMounted(async () => {
+
+ Für diesen Vertrag sind aktuell keine Änderungstypen freigegeben.
+
diff --git a/frontend/stores/data.js b/frontend/stores/data.js
index 6abe4db..03d2493 100644
--- a/frontend/stores/data.js
+++ b/frontend/stores/data.js
@@ -853,6 +853,15 @@ export const useDataStore = defineStore('data', () => {
item.billingInterval = selectedContractType.billingInterval || null
},
inputColumn: "Allgemeines"
+ },{
+ key: "allowedContracttypes",
+ label: "Vertragstypen für Änderungsanfragen",
+ inputType: "select",
+ selectDataType: "contracttypes",
+ selectOptionAttribute: "name",
+ selectSearchAttributes: ["name"],
+ selectMultiple: true,
+ inputColumn: "Allgemeines"
},{
key: "active",
label: "Aktiv",