feat: add Area validation + CRUD (src/lib/areas.ts)

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
toanhoang
2026-08-19 22:06:58 -04:00
co-authored by Claude Sonnet 5
parent 3078d9e8c5
commit 1e6dec9cf2
+92
View File
@@ -0,0 +1,92 @@
// Area — physical location tag for the office-move inventory walk (see
// prisma/schema.prisma's Area model comment). Same shape/conventions as
// src/lib/employees.ts: validate -> create (P2002 on duplicate name) ->
// list.
import { z } from "zod";
import { Prisma } from "@prisma/client";
import { prisma } from "./prisma";
export interface AreaRecord {
id: string;
name: string;
team: string | null;
created_at: string;
}
export interface CreateAreaInput {
name: string;
team?: string;
}
export type ValidationResult =
| { ok: true; value: CreateAreaInput }
| { ok: false; error: string };
function nonEmptyString(message: string) {
return z.string({ required_error: message, invalid_type_error: message }).refine(
(value) => value.trim() !== "",
{ message }
);
}
const createAreaSchema = z.object({
name: nonEmptyString("name is required and must be a non-empty string"),
team: z.string({ invalid_type_error: "team must be a string" }).optional(),
});
export function validateCreateAreaInput(input: {
name?: unknown;
team?: unknown;
}): ValidationResult {
const result = createAreaSchema.safeParse(input);
if (!result.success) {
const message = result.error.issues[0]?.message ?? "Invalid input";
return { ok: false, error: message };
}
return { ok: true, value: result.data };
}
function toRecord(row: {
id: string;
name: string;
team: string | null;
createdAt: Date;
}): AreaRecord {
return {
id: row.id,
name: row.name,
team: row.team,
created_at: row.createdAt.toISOString(),
};
}
export type CreateAreaResult = { ok: true; value: AreaRecord } | { ok: false; error: string };
/**
* Creates an Area, or returns the existing one if `name` already exists
* (idempotent-by-name) — this is deliberate, not just P2002 error handling:
* the bot's "reuse existing or type a new one" flow calls this every time a
* user picks/types an Area name, and re-submitting an Area the bot already
* showed as an existing option must succeed, not 409.
*/
export async function createOrGetArea(input: CreateAreaInput): Promise<CreateAreaResult> {
try {
const created = await prisma.area.create({
data: { name: input.name, team: input.team ?? null },
});
return { ok: true, value: toRecord(created) };
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
const existing = await prisma.area.findUnique({ where: { name: input.name } });
if (existing) {
return { ok: true, value: toRecord(existing) };
}
}
throw error;
}
}
export async function listAreas(): Promise<AreaRecord[]> {
const rows = await prisma.area.findMany({ orderBy: [{ name: "asc" }] });
return rows.map(toRecord);
}