This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
app/apps/api/src/routes/pets.ts
T
groombook-paperclip[bot] 14ed19497f feat: detailed pet profile attributes and grooming visit history (closes #13)
- Add cut_style, shampoo_preference, special_care_notes, custom_fields columns to pets table
- Add grooming_visit_logs table to track per-visit grooming details (cut, products, notes)
- Extend pets API to accept and return new profile fields
- Add /api/grooming-logs endpoint (GET by petId, POST, DELETE)
- Update Pet type with new fields; add GroomingVisitLog type
- Update Clients page: grooming preferences section in pet card, "Log visit" button,
  visit history panel showing last 3 visits, expanded pet form with grooming preferences

Co-authored-by: Groom Book CTO <cto@groombook.app>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-03-17 21:46:40 +00:00

93 lines
2.7 KiB
TypeScript

import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import { eq, getDb, pets } from "@groombook/db";
export const petsRouter = new Hono();
const createPetSchema = z.object({
clientId: z.string().uuid(),
name: z.string().min(1).max(200),
species: z.string().min(1).max(100),
breed: z.string().max(200).optional(),
weightKg: z.number().positive().optional(),
dateOfBirth: z.string().datetime().optional(),
healthAlerts: z.string().max(2000).optional(),
groomingNotes: z.string().max(2000).optional(),
cutStyle: z.string().max(500).optional(),
shampooPreference: z.string().max(500).optional(),
specialCareNotes: z.string().max(2000).optional(),
customFields: z.record(z.string(), z.string()).optional(),
});
const updatePetSchema = createPetSchema.partial().omit({ clientId: true });
petsRouter.get("/", async (c) => {
const db = getDb();
const clientId = c.req.query("clientId");
const query = db.select().from(pets);
if (clientId) {
const rows = await query.where(eq(pets.clientId, clientId));
return c.json(rows);
}
const rows = await query;
return c.json(rows);
});
petsRouter.get("/:id", async (c) => {
const db = getDb();
const [row] = await db
.select()
.from(pets)
.where(eq(pets.id, c.req.param("id")));
if (!row) return c.json({ error: "Not found" }, 404);
return c.json(row);
});
petsRouter.post("/", zValidator("json", createPetSchema), async (c) => {
const db = getDb();
const { weightKg, dateOfBirth, customFields, ...rest } = c.req.valid("json");
const [row] = await db
.insert(pets)
.values({
...rest,
weightKg: weightKg?.toString(),
dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : undefined,
customFields: customFields ?? {},
})
.returning();
return c.json(row, 201);
});
petsRouter.patch(
"/:id",
zValidator("json", updatePetSchema),
async (c) => {
const db = getDb();
const { weightKg, dateOfBirth, customFields, ...rest } = c.req.valid("json");
const [row] = await db
.update(pets)
.set({
...rest,
weightKg: weightKg?.toString(),
dateOfBirth: dateOfBirth ? new Date(dateOfBirth) : undefined,
...(customFields !== undefined ? { customFields } : {}),
updatedAt: new Date(),
})
.where(eq(pets.id, c.req.param("id")))
.returning();
if (!row) return c.json({ error: "Not found" }, 404);
return c.json(row);
}
);
petsRouter.delete("/:id", async (c) => {
const db = getDb();
const [row] = await db
.delete(pets)
.where(eq(pets.id, c.req.param("id")))
.returning();
if (!row) return c.json({ error: "Not found" }, 404);
return c.json({ ok: true });
});