refactor(api): extract utilization category support

This commit is contained in:
2026-03-31 13:49:10 +02:00
parent daf3588cab
commit 6f69021fe5
3 changed files with 163 additions and 35 deletions
@@ -0,0 +1,76 @@
import type { Prisma, PrismaClient } from "@capakraken/db";
import {
CreateUtilizationCategorySchema,
UpdateUtilizationCategorySchema,
} from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
type UtilizationCategoryDb = Pick<PrismaClient, "utilizationCategory">;
type UtilizationCategoryListInput = {
isActive?: boolean | undefined;
};
type CreateUtilizationCategoryInput = z.infer<typeof CreateUtilizationCategorySchema>;
type UpdateUtilizationCategoryInput = z.infer<typeof UpdateUtilizationCategorySchema>;
export function buildUtilizationCategoryListWhere(
input: UtilizationCategoryListInput,
): Prisma.UtilizationCategoryWhereInput {
return {
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
};
}
export async function assertUtilizationCategoryCodeAvailable(
db: UtilizationCategoryDb,
code: string,
ignoreId?: string,
): Promise<void> {
const existing = await db.utilizationCategory.findUnique({ where: { code } });
if (existing && existing.id !== ignoreId) {
throw new TRPCError({
code: "CONFLICT",
message: `Code "${code}" already exists`,
});
}
}
export async function unsetDefaultUtilizationCategory(
db: UtilizationCategoryDb,
ignoreId?: string,
): Promise<void> {
await db.utilizationCategory.updateMany({
where: {
isDefault: true,
...(ignoreId ? { id: { not: ignoreId } } : {}),
},
data: { isDefault: false },
});
}
export function buildUtilizationCategoryCreateData(
input: CreateUtilizationCategoryInput,
): Prisma.UtilizationCategoryUncheckedCreateInput {
return {
code: input.code,
name: input.name,
...(input.description !== undefined ? { description: input.description } : {}),
sortOrder: input.sortOrder,
isDefault: input.isDefault,
};
}
export function buildUtilizationCategoryUpdateData(
input: UpdateUtilizationCategoryInput,
): Prisma.UtilizationCategoryUncheckedUpdateInput {
return {
...(input.code !== undefined ? { code: input.code } : {}),
...(input.name !== undefined ? { name: input.name } : {}),
...(input.description !== undefined ? { description: input.description } : {}),
...(input.sortOrder !== undefined ? { sortOrder: input.sortOrder } : {}),
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
...(input.isDefault !== undefined ? { isDefault: input.isDefault } : {}),
};
}
+14 -35
View File
@@ -2,20 +2,24 @@ import {
CreateUtilizationCategorySchema,
UpdateUtilizationCategorySchema,
} from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { createAuditEntry } from "../lib/audit.js";
import { adminProcedure, createTRPCRouter, planningReadProcedure } from "../trpc.js";
import {
assertUtilizationCategoryCodeAvailable,
buildUtilizationCategoryCreateData,
buildUtilizationCategoryListWhere,
buildUtilizationCategoryUpdateData,
unsetDefaultUtilizationCategory,
} from "./utilization-category-support.js";
export const utilizationCategoryRouter = createTRPCRouter({
list: planningReadProcedure
.input(z.object({ isActive: z.boolean().optional() }).optional())
.query(async ({ ctx, input }) => {
return ctx.db.utilizationCategory.findMany({
where: {
...(input?.isActive !== undefined ? { isActive: input.isActive } : {}),
},
where: buildUtilizationCategoryListWhere(input ?? {}),
orderBy: { sortOrder: "asc" },
});
}),
@@ -36,27 +40,15 @@ export const utilizationCategoryRouter = createTRPCRouter({
create: adminProcedure
.input(CreateUtilizationCategorySchema)
.mutation(async ({ ctx, input }) => {
const existing = await ctx.db.utilizationCategory.findUnique({ where: { code: input.code } });
if (existing) {
throw new TRPCError({ code: "CONFLICT", message: `Code "${input.code}" already exists` });
}
await assertUtilizationCategoryCodeAvailable(ctx.db, input.code);
// If setting as default, unset the current default first
if (input.isDefault) {
await ctx.db.utilizationCategory.updateMany({
where: { isDefault: true },
data: { isDefault: false },
});
await unsetDefaultUtilizationCategory(ctx.db);
}
const created = await ctx.db.utilizationCategory.create({
data: {
code: input.code,
name: input.name,
...(input.description !== undefined ? { description: input.description } : {}),
sortOrder: input.sortOrder,
isDefault: input.isDefault,
},
data: buildUtilizationCategoryCreateData(input),
});
void createAuditEntry({
@@ -82,32 +74,19 @@ export const utilizationCategoryRouter = createTRPCRouter({
);
if (input.data.code && input.data.code !== existing.code) {
const conflict = await ctx.db.utilizationCategory.findUnique({ where: { code: input.data.code } });
if (conflict) {
throw new TRPCError({ code: "CONFLICT", message: `Code "${input.data.code}" already exists` });
}
await assertUtilizationCategoryCodeAvailable(ctx.db, input.data.code, existing.id);
}
// If setting as default, unset others
if (input.data.isDefault) {
await ctx.db.utilizationCategory.updateMany({
where: { isDefault: true, id: { not: input.id } },
data: { isDefault: false },
});
await unsetDefaultUtilizationCategory(ctx.db, input.id);
}
const before = existing as unknown as Record<string, unknown>;
const updated = await ctx.db.utilizationCategory.update({
where: { id: input.id },
data: {
...(input.data.code !== undefined ? { code: input.data.code } : {}),
...(input.data.name !== undefined ? { name: input.data.name } : {}),
...(input.data.description !== undefined ? { description: input.data.description } : {}),
...(input.data.sortOrder !== undefined ? { sortOrder: input.data.sortOrder } : {}),
...(input.data.isActive !== undefined ? { isActive: input.data.isActive } : {}),
...(input.data.isDefault !== undefined ? { isDefault: input.data.isDefault } : {}),
},
data: buildUtilizationCategoryUpdateData(input.data),
});
void createAuditEntry({