feat: add Area validation + CRUD (src/lib/areas.ts)
Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3078d9e8c5
commit
1e6dec9cf2
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user