Kostenstellenhierarchie und Auswertung mit Unterkostenstellen ergänzt
This commit is contained in:
2
backend/db/migrations/0033_costcentres_parent.sql
Normal file
2
backend/db/migrations/0033_costcentres_parent.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "costcentres" ADD COLUMN "parent_costcentre" uuid;
|
||||
ALTER TABLE "costcentres" ADD CONSTRAINT "costcentres_parent_costcentre_costcentres_id_fk" FOREIGN KEY ("parent_costcentre") REFERENCES "public"."costcentres"("id") ON DELETE no action ON UPDATE no action;
|
||||
@@ -232,6 +232,13 @@
|
||||
"when": 1776298800000,
|
||||
"tag": "0032_manual_statementallocations_invoice_side",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 33,
|
||||
"version": "7",
|
||||
"when": 1777003200000,
|
||||
"tag": "0033_costcentres_parent",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ export const costcentres = pgTable("costcentres", {
|
||||
number: text("number").notNull(),
|
||||
name: text("name").notNull(),
|
||||
|
||||
parentCostcentre: uuid("parent_costcentre").references(() => costcentres.id),
|
||||
|
||||
vehicle: bigint("vehicle", { mode: "number" }).references(() => vehicles.id),
|
||||
|
||||
project: bigint("project", { mode: "number" }).references(() => projects.id),
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
sql,
|
||||
} from "drizzle-orm"
|
||||
|
||||
import { authProfiles } from "../../../db/schema";
|
||||
import { authProfiles, costcentres } from "../../../db/schema";
|
||||
import { resourceConfig } from "../../utils/resource.config";
|
||||
import { useNextNumberRangeNumber } from "../../utils/functions";
|
||||
import { getHistoryEntityLabel, insertHistoryItem } from "../../utils/history";
|
||||
@@ -260,6 +260,59 @@ function validateMemberPayload(payload: Record<string, any>) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function validateCostCentreParent(
|
||||
server: FastifyInstance,
|
||||
tenantId: number,
|
||||
costCentreId: string | null,
|
||||
parentCostcentreId: string | null
|
||||
) {
|
||||
if (!parentCostcentreId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hierarchyRows = await server.db
|
||||
.select({
|
||||
id: costcentres.id,
|
||||
parentCostcentre: costcentres.parentCostcentre,
|
||||
})
|
||||
.from(costcentres)
|
||||
.where(eq(costcentres.tenant, tenantId))
|
||||
|
||||
const hierarchyMap = new Map(
|
||||
hierarchyRows.map((row) => [row.id, row.parentCostcentre || null])
|
||||
)
|
||||
|
||||
if (!hierarchyMap.has(parentCostcentreId)) {
|
||||
return "Die übergeordnete Kostenstelle wurde nicht gefunden."
|
||||
}
|
||||
|
||||
if (costCentreId && parentCostcentreId === costCentreId) {
|
||||
return "Eine Kostenstelle kann nicht sich selbst als übergeordnete Kostenstelle haben."
|
||||
}
|
||||
|
||||
if (!costCentreId) {
|
||||
return null
|
||||
}
|
||||
|
||||
let currentParentId: string | null = parentCostcentreId
|
||||
const visited = new Set<string>()
|
||||
|
||||
while (currentParentId) {
|
||||
if (currentParentId === costCentreId) {
|
||||
return "Die ausgewählte übergeordnete Kostenstelle würde einen Zyklus erzeugen."
|
||||
}
|
||||
|
||||
if (visited.has(currentParentId)) {
|
||||
break
|
||||
}
|
||||
|
||||
visited.add(currentParentId)
|
||||
currentParentId = hierarchyMap.get(currentParentId) || null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function maskIban(iban: string) {
|
||||
if (!iban) return ""
|
||||
const cleaned = iban.replace(/\s+/g, "")
|
||||
@@ -730,6 +783,19 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
createData = prepared.data!
|
||||
}
|
||||
|
||||
if (resource === "costcentres") {
|
||||
const validationError = await validateCostCentreParent(
|
||||
server,
|
||||
req.user.tenant_id,
|
||||
null,
|
||||
createData.parentCostcentre || null
|
||||
)
|
||||
|
||||
if (validationError) {
|
||||
return reply.code(400).send({ error: validationError })
|
||||
}
|
||||
}
|
||||
|
||||
if (config.numberRangeHolder && !body[config.numberRangeHolder]) {
|
||||
const numberRangeResource = resource === "members" ? "customers" : resource
|
||||
const result = await useNextNumberRangeNumber(server, req.user.tenant_id, numberRangeResource)
|
||||
@@ -836,6 +902,21 @@ export default async function resourceRoutes(server: FastifyInstance) {
|
||||
}
|
||||
}
|
||||
|
||||
if (resource === "costcentres") {
|
||||
const validationError = await validateCostCentreParent(
|
||||
server,
|
||||
tenantId,
|
||||
oldRecord.id,
|
||||
Object.prototype.hasOwnProperty.call(data, "parentCostcentre")
|
||||
? data.parentCostcentre || null
|
||||
: oldRecord.parentCostcentre || null
|
||||
)
|
||||
|
||||
if (validationError) {
|
||||
return reply.code(400).send({ error: validationError })
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(data).forEach((key) => {
|
||||
const value = data[key]
|
||||
const shouldNormalize =
|
||||
|
||||
@@ -164,9 +164,13 @@ export const resourceConfig = {
|
||||
costcentres: {
|
||||
table: costcentres,
|
||||
searchColumns: ["name","number","description"],
|
||||
mtoLoad: ["vehicle","project","inventoryitem","branch"],
|
||||
mtoLoad: ["parentCostcentre","vehicle","project","inventoryitem","branch"],
|
||||
numberRangeHolder: "number",
|
||||
},
|
||||
parentCostcentre: {
|
||||
table: costcentres,
|
||||
searchColumns: ["name", "number", "description"],
|
||||
},
|
||||
branches: {
|
||||
table: branches,
|
||||
searchColumns: ["name","number","description"],
|
||||
|
||||
Reference in New Issue
Block a user