diff --git a/src/app/pets/[id]/edit/page.tsx b/src/app/pets/[id]/edit/page.tsx
new file mode 100644
index 0000000..d5f51d8
--- /dev/null
+++ b/src/app/pets/[id]/edit/page.tsx
@@ -0,0 +1,17 @@
+import { notFound } from "next/navigation";
+import { DetailBackLink } from "@/components/detail-back-link";
+import { PetForm } from "@/modules/pets/components/pet-form";
+import { getPet } from "@/modules/pets/server/queries";
+
+export default async function EditPetPage({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const pet = await getPet(id);
+ if (!pet) notFound();
+ return (
+
+ );
+}
diff --git a/src/app/pets/[id]/page.tsx b/src/app/pets/[id]/page.tsx
new file mode 100644
index 0000000..b8303d3
--- /dev/null
+++ b/src/app/pets/[id]/page.tsx
@@ -0,0 +1,33 @@
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { DetailBackLink } from "@/components/detail-back-link";
+import { getPet } from "@/modules/pets/server/queries";
+import { MedicalRecords } from "@/modules/pets/components/medical-records";
+
+export default async function PetPage({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const pet = await getPet(id);
+ if (!pet) notFound();
+ return (
+
+
+
+
+
{pet.name}
+
+ {[pet.species, pet.breed].filter(Boolean).join(" · ")}
+
+
+
+ Edit
+
+
+
+ Profile
+ {pet.birthDate && Born {pet.birthDate}
}
+ {pet.notes && {pet.notes}
}
+
+
+
+ );
+}
diff --git a/src/app/pets/new/page.tsx b/src/app/pets/new/page.tsx
new file mode 100644
index 0000000..380e21e
--- /dev/null
+++ b/src/app/pets/new/page.tsx
@@ -0,0 +1,12 @@
+import { DetailBackLink } from "@/components/detail-back-link";
+import { PetForm } from "@/modules/pets/components/pet-form";
+
+export default function NewPetPage() {
+ return (
+
+ );
+}
diff --git a/src/modules/pets/components/medical-records.tsx b/src/modules/pets/components/medical-records.tsx
new file mode 100644
index 0000000..dc905f0
--- /dev/null
+++ b/src/modules/pets/components/medical-records.tsx
@@ -0,0 +1,121 @@
+"use client";
+
+import { useTransition } from "react";
+import type { PetDetailDto } from "../server/queries";
+import { createAppointment, createPrescription, createVaccination } from "../server/actions";
+
+export function MedicalRecords({ pet }: { pet: PetDetailDto }) {
+ const [pending, startTransition] = useTransition();
+ function add(form: HTMLFormElement, action: () => Promise) {
+ startTransition(async () => {
+ await action();
+ form.reset();
+ location.reload();
+ });
+ }
+ return (
+
+
+ Appointments
+
+
+ {pet.appointments.map((item) => (
+ -
+ {item.title} — {new Date(item.appointmentAt).toLocaleString()}
+
+ ))}
+
+
+
+ Vaccinations
+
+
+ {pet.vaccinations.map((item) => (
+ -
+ {item.name} — {item.administeredOn}
+
+ ))}
+
+
+
+ Prescriptions
+
+
+ {pet.prescriptions.map((item) => (
+ -
+ {item.medication} — {item.dosage}
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/modules/pets/components/pet-form.tsx b/src/modules/pets/components/pet-form.tsx
new file mode 100644
index 0000000..be3fa4b
--- /dev/null
+++ b/src/modules/pets/components/pet-form.tsx
@@ -0,0 +1,81 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { useRouter } from "next/navigation";
+import type { PetDetailDto } from "../server/queries";
+import { createPet, updatePet } from "../server/actions";
+import { Input } from "@/components/ui/input";
+
+export function PetForm({ pet }: { pet?: PetDetailDto }) {
+ const router = useRouter();
+ const [isPending, startTransition] = useTransition();
+ const [error, setError] = useState(null);
+
+ function submit(event: React.FormEvent) {
+ event.preventDefault();
+ const data = new FormData(event.currentTarget);
+ const input = {
+ name: String(data.get("name") ?? ""),
+ species: String(data.get("species") ?? ""),
+ breed: String(data.get("breed") ?? "") || null,
+ birthDate: String(data.get("birthDate") ?? "") || null,
+ notes: String(data.get("notes") ?? "") || null,
+ images: pet?.images ?? [],
+ primaryImageUrl: pet?.primaryImageUrl ?? null,
+ };
+ setError(null);
+ startTransition(async () => {
+ try {
+ if (pet) {
+ await updatePet({ id: pet.id, ...input });
+ router.push(`/pets/${pet.id}`);
+ } else {
+ const created = await createPet(input);
+ router.push(`/pets/${created.id}`);
+ }
+ router.refresh();
+ } catch {
+ setError("Could not save pet.");
+ }
+ });
+ }
+
+ return (
+
+ );
+}
diff --git a/src/modules/pets/server/actions.ts b/src/modules/pets/server/actions.ts
index fd1709e..85e9e7a 100644
--- a/src/modules/pets/server/actions.ts
+++ b/src/modules/pets/server/actions.ts
@@ -196,6 +196,33 @@ export async function deleteAppointmentForScope(scope: Scope, input: { id: strin
await db.delete(petAppointments).where(eq(petAppointments.id, id));
}
+export async function createAppointment(input: z.input) {
+ const { household, user } = await getCurrentSession();
+ const row = await createAppointmentForScope(
+ { householdId: household.id, userId: user.id },
+ input,
+ );
+ revalidatePet(row.petId);
+ return row;
+}
+export async function updateAppointment(
+ input: { id: string } & z.input,
+) {
+ const { household, user } = await getCurrentSession();
+ const row = await updateAppointmentForScope(
+ { householdId: household.id, userId: user.id },
+ input,
+ );
+ revalidatePet(row.petId);
+ return row;
+}
+export async function deleteAppointment(input: { id: string }) {
+ const { household, user } = await getCurrentSession();
+ const row = await requireAppointment({ householdId: household.id, userId: user.id }, input.id);
+ await deleteAppointmentForScope({ householdId: household.id, userId: user.id }, input);
+ revalidatePet(row.petId);
+}
+
export async function createVaccinationForScope(
scope: Scope,
input: z.input,
@@ -258,6 +285,22 @@ export async function deleteVaccinationForScope(scope: Scope, input: { id: strin
await db.delete(petVaccinations).where(eq(petVaccinations.id, id));
}
+export async function createVaccination(input: z.input) {
+ const { household, user } = await getCurrentSession();
+ const row = await createVaccinationForScope(
+ { householdId: household.id, userId: user.id },
+ input,
+ );
+ revalidatePet(row.petId);
+ return row;
+}
+export async function deleteVaccination(input: { id: string }) {
+ const { household, user } = await getCurrentSession();
+ const row = await requireVaccination({ householdId: household.id, userId: user.id }, input.id);
+ await deleteVaccinationForScope({ householdId: household.id, userId: user.id }, input);
+ revalidatePet(row.petId);
+}
+
export async function createPrescriptionForScope(
scope: Scope,
input: z.input,
@@ -320,3 +363,19 @@ export async function deletePrescriptionForScope(scope: Scope, input: { id: stri
await cancelReminder("pets.prescription", id);
await db.delete(petPrescriptions).where(eq(petPrescriptions.id, id));
}
+
+export async function createPrescription(input: z.input) {
+ const { household, user } = await getCurrentSession();
+ const row = await createPrescriptionForScope(
+ { householdId: household.id, userId: user.id },
+ input,
+ );
+ revalidatePet(row.petId);
+ return row;
+}
+export async function deletePrescription(input: { id: string }) {
+ const { household, user } = await getCurrentSession();
+ const row = await requirePrescription({ householdId: household.id, userId: user.id }, input.id);
+ await deletePrescriptionForScope({ householdId: household.id, userId: user.id }, input);
+ revalidatePet(row.petId);
+}