import { CreateCountrySchema, CreateMetroCitySchema, UpdateCountrySchema, UpdateMetroCitySchema, } from "@planarchy/shared"; import { Prisma } from "@planarchy/db"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc.js"; /** Convert nullable JSON to Prisma-compatible value (null → Prisma.JsonNull). */ function jsonOrNull(val: unknown): Prisma.InputJsonValue | typeof Prisma.JsonNull { if (val === null || val === undefined) return Prisma.JsonNull; return val as Prisma.InputJsonValue; } export const countryRouter = createTRPCRouter({ list: protectedProcedure .input(z.object({ isActive: z.boolean().optional() }).optional()) .query(async ({ ctx, input }) => { return ctx.db.country.findMany({ where: { ...(input?.isActive !== undefined ? { isActive: input.isActive } : {}), }, include: { metroCities: { orderBy: { name: "asc" } } }, orderBy: { name: "asc" }, }); }), getById: protectedProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) => { const country = await findUniqueOrThrow( ctx.db.country.findUnique({ where: { id: input.id }, include: { metroCities: { orderBy: { name: "asc" } }, _count: { select: { resources: true } }, }, }), "Country", ); return country; }), create: adminProcedure .input(CreateCountrySchema) .mutation(async ({ ctx, input }) => { const existing = await ctx.db.country.findUnique({ where: { code: input.code } }); if (existing) { throw new TRPCError({ code: "CONFLICT", message: `Country code "${input.code}" already exists` }); } return ctx.db.country.create({ data: { code: input.code, name: input.name, dailyWorkingHours: input.dailyWorkingHours, ...(input.scheduleRules !== undefined ? { scheduleRules: jsonOrNull(input.scheduleRules) } : {}), }, include: { metroCities: true }, }); }), update: adminProcedure .input(z.object({ id: z.string(), data: UpdateCountrySchema })) .mutation(async ({ ctx, input }) => { const existing = await findUniqueOrThrow( ctx.db.country.findUnique({ where: { id: input.id } }), "Country", ); if (input.data.code && input.data.code !== existing.code) { const conflict = await ctx.db.country.findUnique({ where: { code: input.data.code } }); if (conflict) { throw new TRPCError({ code: "CONFLICT", message: `Country code "${input.data.code}" already exists` }); } } return ctx.db.country.update({ where: { id: input.id }, data: { ...(input.data.code !== undefined ? { code: input.data.code } : {}), ...(input.data.name !== undefined ? { name: input.data.name } : {}), ...(input.data.dailyWorkingHours !== undefined ? { dailyWorkingHours: input.data.dailyWorkingHours } : {}), ...(input.data.scheduleRules !== undefined ? { scheduleRules: jsonOrNull(input.data.scheduleRules) } : {}), ...(input.data.isActive !== undefined ? { isActive: input.data.isActive } : {}), }, include: { metroCities: true }, }); }), // ─── Metro City ───────────────────────────────────────────── createCity: adminProcedure .input(CreateMetroCitySchema) .mutation(async ({ ctx, input }) => { await findUniqueOrThrow( ctx.db.country.findUnique({ where: { id: input.countryId } }), "Country", ); return ctx.db.metroCity.create({ data: { name: input.name, countryId: input.countryId }, }); }), updateCity: adminProcedure .input(z.object({ id: z.string(), data: UpdateMetroCitySchema })) .mutation(async ({ ctx, input }) => { return ctx.db.metroCity.update({ where: { id: input.id }, data: { ...(input.data.name !== undefined ? { name: input.data.name } : {}) }, }); }), deleteCity: adminProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { const city = await findUniqueOrThrow( ctx.db.metroCity.findUnique({ where: { id: input.id }, include: { _count: { select: { resources: true } } }, }), "Metro city", ); if (city._count.resources > 0) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: `Cannot delete metro city assigned to ${city._count.resources} resource(s)`, }); } await ctx.db.metroCity.delete({ where: { id: input.id } }); return { success: true }; }), });