Kundenportal Vertragsanfragen ergänzen

This commit is contained in:
2026-05-08 20:01:57 +02:00
parent 2b1a9a456b
commit 5dc44e571f
9 changed files with 588 additions and 10 deletions

View File

@@ -5,6 +5,7 @@ import { authProfiles, historyitems } from "../../db/schema";
const columnMap: Record<string, any> = {
customers: historyitems.customer,
contracts: historyitems.contract,
members: historyitems.customer,
vendors: historyitems.vendor,
projects: historyitems.project,
@@ -30,6 +31,7 @@ const columnMap: Record<string, any> = {
const insertFieldMap: Record<string, string> = {
customers: "customer",
contracts: "contract",
members: "customer",
vendors: "vendor",
projects: "project",

View File

@@ -0,0 +1,221 @@
import { FastifyInstance } from "fastify"
import { and, eq } from "drizzle-orm"
import { authProfiles, contracts, contracttypes } from "../../../db/schema"
import { insertHistoryItem } from "../../utils/history"
async function getPortalCustomerId(server: FastifyInstance, req: any) {
const tenantId = req.user?.tenant_id
const userId = req.user?.user_id
if (!tenantId || !userId) return null
const [profile] = await server.db
.select({ customer_for_portal: authProfiles.customer_for_portal })
.from(authProfiles)
.where(and(
eq(authProfiles.tenant_id, tenantId),
eq(authProfiles.user_id, userId)
))
.limit(1)
return profile?.customer_for_portal || null
}
async function getPortalContract(server: FastifyInstance, req: any, contractId: number) {
const portalCustomerId = await getPortalCustomerId(server, req)
if (!portalCustomerId) return null
const [contract] = await server.db
.select({
id: contracts.id,
name: contracts.name,
tenant: contracts.tenant,
customer: contracts.customer,
contracttype: contracts.contracttype,
archived: contracts.archived,
})
.from(contracts)
.where(and(
eq(contracts.id, contractId),
eq(contracts.tenant, req.user?.tenant_id),
eq(contracts.customer, portalCustomerId),
eq(contracts.archived, false)
))
.limit(1)
return contract || null
}
function normalizeMessage(message: unknown) {
if (typeof message !== "string") return ""
return message.trim()
}
function appendMessage(text: string, message: string) {
return message ? `${text} Nachricht: ${message}` : text
}
function formatDateForHistory(value: string) {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return value
return new Intl.DateTimeFormat("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
timeZone: "Europe/Berlin",
}).format(date)
}
export default async function portalContractRoutes(server: FastifyInstance) {
server.post<{
Params: { id: string }
Body: { contracttype?: number | string; message?: string }
}>("/portal/contracts/:id/change-request", {
schema: {
tags: ["Portal"],
summary: "Request contract type change from customer portal",
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
body: {
type: "object",
required: ["contracttype"],
properties: {
contracttype: { anyOf: [{ type: "number" }, { type: "string" }] },
message: { type: "string", nullable: true },
},
},
},
}, async (req, reply) => {
const contractId = Number(req.params.id)
const requestedContracttypeId = Number(req.body.contracttype)
if (!Number.isInteger(contractId) || !Number.isInteger(requestedContracttypeId)) {
return reply.code(400).send({ error: "Ungültige Anfrage" })
}
const contract = await getPortalContract(server, req, contractId)
if (!contract) {
return reply.code(404).send({ error: "Vertrag nicht gefunden" })
}
const [requestedContracttype] = await server.db
.select({
id: contracttypes.id,
name: contracttypes.name,
})
.from(contracttypes)
.where(and(
eq(contracttypes.id, requestedContracttypeId),
eq(contracttypes.tenant, req.user?.tenant_id),
eq(contracttypes.archived, false)
))
.limit(1)
if (!requestedContracttype) {
return reply.code(400).send({ error: "Ungültiger Vertragstyp" })
}
const [currentContracttype] = contract.contracttype
? await server.db
.select({
id: contracttypes.id,
name: contracttypes.name,
})
.from(contracttypes)
.where(and(
eq(contracttypes.id, contract.contracttype),
eq(contracttypes.tenant, req.user?.tenant_id)
))
.limit(1)
: []
const message = normalizeMessage(req.body.message)
const oldName = currentContracttype?.name || "Ohne Vertragstyp"
const newName = requestedContracttype.name
const text = appendMessage(
`Kundenportal: Änderung des Vertragstyps von "${oldName}" auf "${newName}" angefragt.`,
message
)
await insertHistoryItem(server, {
tenant_id: req.user?.tenant_id,
created_by: req.user?.user_id || null,
entity: "contracts",
entityId: contract.id,
action: "unchanged",
oldVal: { contracttype: contract.contracttype, name: oldName },
newVal: { contracttype: requestedContracttype.id, name: newName },
text,
})
return { success: true, message: "Ihre Anfrage wurde übermittelt." }
})
server.post<{
Params: { id: string }
Body: { requestedEndDate?: string; message?: string }
}>("/portal/contracts/:id/cancellation-request", {
schema: {
tags: ["Portal"],
summary: "Request contract cancellation from customer portal",
params: {
type: "object",
required: ["id"],
properties: {
id: { type: "string" },
},
},
body: {
type: "object",
required: ["requestedEndDate"],
properties: {
requestedEndDate: { type: "string" },
message: { type: "string", nullable: true },
},
},
},
}, async (req, reply) => {
const contractId = Number(req.params.id)
const requestedEndDate = typeof req.body.requestedEndDate === "string"
? req.body.requestedEndDate.trim()
: ""
if (!Number.isInteger(contractId) || !requestedEndDate) {
return reply.code(400).send({ error: "Ungültige Anfrage" })
}
const parsedDate = new Date(requestedEndDate)
if (Number.isNaN(parsedDate.getTime())) {
return reply.code(400).send({ error: "Ungültiges Kündigungsdatum" })
}
const contract = await getPortalContract(server, req, contractId)
if (!contract) {
return reply.code(404).send({ error: "Vertrag nicht gefunden" })
}
const message = normalizeMessage(req.body.message)
const text = appendMessage(
`Kundenportal: Kündigung zum ${formatDateForHistory(requestedEndDate)} angefragt.`,
message
)
await insertHistoryItem(server, {
tenant_id: req.user?.tenant_id,
created_by: req.user?.user_id || null,
entity: "contracts",
entityId: contract.id,
action: "unchanged",
newVal: { requestedEndDate },
text,
})
return { success: true, message: "Ihre Anfrage wurde übermittelt." }
})
}