diff --git a/src/routes/portal.ts b/src/routes/portal.ts index a4c2b87..651c5e2 100644 --- a/src/routes/portal.ts +++ b/src/routes/portal.ts @@ -149,9 +149,89 @@ portalRouter.get("/pets", async (c) => { const clientId = c.get("portalClientId"); const clientPets = await db.select().from(pets).where(eq(pets.clientId, clientId)); - return c.json(clientPets.map(p => ({ id: p.id, name: p.name, breed: p.breed, weight: p.weightKg, birthDate: p.dateOfBirth, photoUrl: p.photoKey, notes: p.groomingNotes }))); + return c.json(clientPets.map(p => ({ + id: p.id, + name: p.name, + species: p.species, + breed: p.breed, + weightKg: p.weightKg ? Number(p.weightKg) : null, + dateOfBirth: p.dateOfBirth ? new Date(p.dateOfBirth).toISOString() : null, + healthAlerts: p.healthAlerts, + groomingNotes: p.groomingNotes, + cutStyle: p.cutStyle, + shampooPreference: p.shampooPreference, + specialCareNotes: p.specialCareNotes, + coatType: p.coatType, + petSizeCategory: p.petSizeCategory, + photoKey: p.photoKey, + photoUploadedAt: p.photoUploadedAt ? new Date(p.photoUploadedAt).toISOString() : null, + customFields: p.customFields, + }))); }); +const portalPetUpdateSchema = z.object({ + name: z.string().min(1).max(200).optional(), + species: z.string().min(1).max(100).optional(), + breed: z.string().max(200).optional().nullable(), + weightKg: z.number().positive().optional().nullable(), + dateOfBirth: z.string().datetime().optional().nullable(), + healthAlerts: z.string().max(2000).optional().nullable(), + groomingNotes: z.string().max(2000).optional().nullable(), + cutStyle: z.string().max(500).optional().nullable(), + shampooPreference: z.string().max(500).optional().nullable(), + specialCareNotes: z.string().max(2000).optional().nullable(), + coatType: z.enum(["smooth", "double", "wire", "curly", "long", "hairless"]).optional().nullable(), + petSizeCategory: z.enum(["small", "medium", "large", "xlarge"]).optional().nullable(), + customFields: z.record(z.string(), z.string()).optional(), +}); + +portalRouter.patch("/pets/:id", + zValidator("json", portalPetUpdateSchema), + async (c) => { + const db = getDb(); + const petId = c.req.param("id"); + const clientId = c.get("portalClientId"); + const body = c.req.valid("json"); + + const [existing] = await db.select().from(pets).where(eq(pets.id, petId)).limit(1); + if (!existing) return c.json({ error: "Not found" }, 404); + if (existing.clientId !== clientId) return c.json({ error: "Forbidden" }, 403); + + const { weightKg, dateOfBirth, ...rest } = body; + const [updated] = await db + .update(pets) + .set({ + ...rest, + weightKg: weightKg != null ? String(weightKg) : undefined, + dateOfBirth: dateOfBirth != null ? new Date(dateOfBirth) : undefined, + updatedAt: new Date(), + }) + .where(eq(pets.id, petId)) + .returning(); + + if (!updated) return c.json({ error: "Not found" }, 404); + + return c.json({ + id: updated.id, + name: updated.name, + species: updated.species, + breed: updated.breed, + weightKg: updated.weightKg ? Number(updated.weightKg) : null, + dateOfBirth: updated.dateOfBirth ? new Date(updated.dateOfBirth).toISOString() : null, + healthAlerts: updated.healthAlerts, + groomingNotes: updated.groomingNotes, + cutStyle: updated.cutStyle, + shampooPreference: updated.shampooPreference, + specialCareNotes: updated.specialCareNotes, + coatType: updated.coatType, + petSizeCategory: updated.petSizeCategory, + photoKey: updated.photoKey, + photoUploadedAt: updated.photoUploadedAt ? new Date(updated.photoUploadedAt).toISOString() : null, + customFields: updated.customFields, + }); + } +); + portalRouter.get("/invoices", async (c) => { const db = getDb(); const clientId = c.get("portalClientId");