import { CreateClientSchema, UpdateClientSchema } from "@capakraken/shared"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { createAuditEntry } from "../lib/audit.js"; import type { TRPCContext } from "../trpc.js"; import { assertClientCodeAvailable, assertClientDeletable, buildClientCreateData, buildClientListWhere, buildClientTree, buildClientUpdateData, findClientByIdentifier, } from "./client-support.js"; type ClientProcedureContext = Pick; type ClientIdentifierReadModel = { id: string; name: string; code: string | null; parentId: string | null; isActive: boolean; }; type ClientDetailReadModel = ClientIdentifierReadModel & { sortOrder: number; tags: string[]; createdAt: Date; updatedAt: Date; _count: { projects: number; children: number; }; }; function withAuditUser(userId: string | undefined) { return userId ? { userId } : {}; } export const clientIdInputSchema = z.object({ id: z.string() }); export const clientIdentifierInputSchema = z.object({ identifier: z.string().trim().min(1), }); export const clientListInputSchema = z .object({ parentId: z.string().nullable().optional(), isActive: z.boolean().optional(), search: z.string().optional(), }) .optional(); export const clientTreeInputSchema = z .object({ isActive: z.boolean().optional(), }) .optional(); export const clientUpdateInputSchema = z.object({ id: z.string(), data: UpdateClientSchema, }); export const clientBatchSortOrderInputSchema = z.array( z.object({ id: z.string(), sortOrder: z.number().int(), }), ); type ClientIdInput = z.infer; type ClientIdentifierInput = z.infer; type ClientListInput = z.infer; type ClientTreeInput = z.infer; type ClientCreateInput = z.infer; type ClientUpdateInput = z.infer; type ClientBatchSortOrderInput = z.infer; export async function listClients(ctx: ClientProcedureContext, input: ClientListInput) { return ctx.db.client.findMany({ where: buildClientListWhere(input ?? {}), include: { _count: { select: { children: true, projects: true } } }, orderBy: [{ sortOrder: "asc" }, { name: "asc" }], }); } export async function getClientTree(ctx: ClientProcedureContext, input: ClientTreeInput) { const all = await ctx.db.client.findMany({ where: { ...(input?.isActive !== undefined ? { isActive: input.isActive } : {}), }, orderBy: [{ sortOrder: "asc" }, { name: "asc" }], }); return buildClientTree(all); } export async function getClientById(ctx: ClientProcedureContext, input: ClientIdInput) { return findUniqueOrThrow( ctx.db.client.findUnique({ where: { id: input.id }, include: { parent: true, children: { orderBy: { sortOrder: "asc" } }, _count: { select: { projects: true, children: true } }, }, }), "Client", ); } export async function resolveClientByIdentifier( ctx: ClientProcedureContext, input: ClientIdentifierInput, ) { return findClientByIdentifier(ctx.db, input.identifier, { select: { id: true, name: true, code: true, parentId: true, isActive: true, }, }); } export async function getClientByIdentifier( ctx: ClientProcedureContext, input: ClientIdentifierInput, ) { return findClientByIdentifier(ctx.db, input.identifier, { include: { _count: { select: { projects: true, children: true } } }, }); } export async function createClient(ctx: ClientProcedureContext, input: ClientCreateInput) { if (input.parentId) { await findUniqueOrThrow( ctx.db.client.findUnique({ where: { id: input.parentId } }), "Parent client", ); } if (input.code) { await assertClientCodeAvailable(ctx.db, input.code); } const created = await ctx.db.client.create({ data: buildClientCreateData(input), }); void createAuditEntry({ db: ctx.db, entityType: "Client", entityId: created.id, entityName: created.name, action: "CREATE", ...withAuditUser(ctx.dbUser?.id), after: created as unknown as Record, source: "ui", }); return created; } export async function updateClient(ctx: ClientProcedureContext, input: ClientUpdateInput) { const existing = await findUniqueOrThrow( ctx.db.client.findUnique({ where: { id: input.id } }), "Client", ); if (input.data.code && input.data.code !== existing.code) { await assertClientCodeAvailable(ctx.db, input.data.code, existing.id); } const before = existing as unknown as Record; const updated = await ctx.db.client.update({ where: { id: input.id }, data: buildClientUpdateData(input.data), }); void createAuditEntry({ db: ctx.db, entityType: "Client", entityId: updated.id, entityName: updated.name, action: "UPDATE", ...withAuditUser(ctx.dbUser?.id), before, after: updated as unknown as Record, source: "ui", }); return updated; } export async function deactivateClient(ctx: ClientProcedureContext, input: ClientIdInput) { const updated = await ctx.db.client.update({ where: { id: input.id }, data: { isActive: false }, }); void createAuditEntry({ db: ctx.db, entityType: "Client", entityId: updated.id, entityName: updated.name, action: "UPDATE", ...withAuditUser(ctx.dbUser?.id), before: { isActive: true }, after: { isActive: false }, source: "ui", summary: "Deactivated Client", }); return updated; } export async function deleteClient(ctx: ClientProcedureContext, input: ClientIdInput) { const client = await findUniqueOrThrow( ctx.db.client.findUnique({ where: { id: input.id }, include: { _count: { select: { projects: true, children: true } } }, }), "Client", ); assertClientDeletable(client); await ctx.db.client.delete({ where: { id: input.id } }); void createAuditEntry({ db: ctx.db, entityType: "Client", entityId: client.id, entityName: client.name, action: "DELETE", ...withAuditUser(ctx.dbUser?.id), before: client as unknown as Record, source: "ui", }); return client; } export async function batchUpdateClientSortOrder( ctx: ClientProcedureContext, input: ClientBatchSortOrderInput, ) { await ctx.db.$transaction( input.map((item) => ctx.db.client.update({ where: { id: item.id }, data: { sortOrder: item.sortOrder }, }), ), ); for (const item of input) { void createAuditEntry({ db: ctx.db, entityType: "Client", entityId: item.id, action: "UPDATE", ...withAuditUser(ctx.dbUser?.id), after: { sortOrder: item.sortOrder }, source: "ui", summary: "Updated sort order", }); } return { ok: true }; }