feat(GRO-1174): add pet size/coat dropdowns to booking wizard

- Add Pet Size dropdown (Small, Medium, Large, X-Large) after breed field
- Add Coat Type dropdown (Smooth, Double, Curly, Wire, Long, Hairless)
- Pass petSizeCategory + petCoatType as query params to availability endpoint
- Include petSizeCategory + petCoatType in POST /appointments body
- Show "appointment" duration label on confirm (service duration only)
- Display pet size/coat on confirmation card when provided
- Pre-fill from URL params
- Reset form resets all new fields

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-05-20 02:44:14 +00:00
committed by Flea Flicker [agent]
parent b8eb39e15c
commit 29fa0bd02b
+57 -7
View File
@@ -13,6 +13,8 @@ interface BookingBody {
petName: string; petName: string;
petSpecies: string; petSpecies: string;
petBreed: string; petBreed: string;
petSizeCategory: string;
petCoatType: string;
notes: string; notes: string;
} }
@@ -123,6 +125,8 @@ export function BookPage() {
petName: "", petName: "",
petSpecies: "", petSpecies: "",
petBreed: "", petBreed: "",
petSizeCategory: "",
petCoatType: "",
notes: "", notes: "",
}); });
const [formError, setFormError] = useState<string | null>(null); const [formError, setFormError] = useState<string | null>(null);
@@ -136,7 +140,9 @@ export function BookPage() {
const petName = searchParams.get("petName"); const petName = searchParams.get("petName");
const petSpecies = searchParams.get("petSpecies"); const petSpecies = searchParams.get("petSpecies");
const petBreed = searchParams.get("petBreed"); const petBreed = searchParams.get("petBreed");
if (clientName || clientEmail || clientPhone || petName || petSpecies || petBreed) { const petSizeCategory = searchParams.get("petSizeCategory");
const petCoatType = searchParams.get("petCoatType");
if (clientName || clientEmail || clientPhone || petName || petSpecies || petBreed || petSizeCategory || petCoatType) {
setForm((f) => ({ setForm((f) => ({
...f, ...f,
...(clientName && { clientName }), ...(clientName && { clientName }),
@@ -145,6 +151,8 @@ export function BookPage() {
...(petName && { petName }), ...(petName && { petName }),
...(petSpecies && { petSpecies }), ...(petSpecies && { petSpecies }),
...(petBreed && { petBreed }), ...(petBreed && { petBreed }),
...(petSizeCategory && { petSizeCategory }),
...(petCoatType && { petCoatType }),
})); }));
} }
}, [searchParams]); }, [searchParams]);
@@ -168,14 +176,18 @@ export function BookPage() {
if (!selectedService || !date) return; if (!selectedService || !date) return;
setSlotsLoading(true); setSlotsLoading(true);
setSelectedSlot(null); setSelectedSlot(null);
fetch( const params = new URLSearchParams({
`/api/book/availability?serviceId=${encodeURIComponent(selectedService.id)}&date=${encodeURIComponent(date)}` serviceId: selectedService.id,
) date,
});
if (form.petSizeCategory) params.set("petSizeCategory", form.petSizeCategory);
if (form.petCoatType) params.set("petCoatType", form.petCoatType);
fetch(`/api/book/availability?${params.toString()}`)
.then((r) => r.json() as Promise<string[]>) .then((r) => r.json() as Promise<string[]>)
.then(setSlots) .then(setSlots)
.catch(() => setSlots([])) .catch(() => setSlots([]))
.finally(() => setSlotsLoading(false)); .finally(() => setSlotsLoading(false));
}, [selectedService, date]); }, [selectedService, date, form.petSizeCategory, form.petCoatType]);
function goToStep2(svc: Service) { function goToStep2(svc: Service) {
setSelectedService(svc); setSelectedService(svc);
@@ -214,6 +226,8 @@ export function BookPage() {
petName: form.petName, petName: form.petName,
petSpecies: form.petSpecies, petSpecies: form.petSpecies,
petBreed: form.petBreed || undefined, petBreed: form.petBreed || undefined,
petSizeCategory: form.petSizeCategory || undefined,
petCoatType: form.petCoatType || undefined,
notes: form.notes || undefined, notes: form.notes || undefined,
}), }),
}); });
@@ -494,6 +508,36 @@ export function BookPage() {
placeholder="Golden Retriever" placeholder="Golden Retriever"
/> />
</div> </div>
<div>
<label style={label}>Pet size</label>
<select
style={input}
value={form.petSizeCategory}
onChange={(e) => setForm((f) => ({ ...f, petSizeCategory: e.target.value }))}
>
<option value="">Select size</option>
<option value="small">Small (under 15 lbs)</option>
<option value="medium">Medium (1540 lbs)</option>
<option value="large">Large (4080 lbs)</option>
<option value="x-large">X-Large (over 80 lbs)</option>
</select>
</div>
<div>
<label style={label}>Coat type</label>
<select
style={input}
value={form.petCoatType}
onChange={(e) => setForm((f) => ({ ...f, petCoatType: e.target.value }))}
>
<option value="">Select coat</option>
<option value="smooth">Smooth</option>
<option value="double">Double</option>
<option value="curly">Curly</option>
<option value="wire">Wire</option>
<option value="long">Long</option>
<option value="hairless">Hairless</option>
</select>
</div>
<div> <div>
<label style={label}>Notes for groomer</label> <label style={label}>Notes for groomer</label>
<textarea <textarea
@@ -528,7 +572,7 @@ export function BookPage() {
<div> <div>
<div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Service</div> <div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Service</div>
<div style={{ fontWeight: 600 }}>{selectedService.name}</div> <div style={{ fontWeight: 600 }}>{selectedService.name}</div>
<div style={{ color: "#6b7280" }}>{fmtPrice(selectedService.basePriceCents)} · {fmtDuration(selectedService.durationMinutes)}</div> <div style={{ color: "#6b7280" }}>{fmtPrice(selectedService.basePriceCents)} · {fmtDuration(selectedService.durationMinutes)} appointment</div>
</div> </div>
<div> <div>
<div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Date & Time</div> <div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Date & Time</div>
@@ -545,6 +589,11 @@ export function BookPage() {
<div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Pet</div> <div style={{ color: "#9ca3af", fontSize: 12, fontWeight: 600, textTransform: "uppercase" }}>Pet</div>
<div style={{ fontWeight: 600 }}>{form.petName}</div> <div style={{ fontWeight: 600 }}>{form.petName}</div>
<div style={{ color: "#6b7280", textTransform: "capitalize" }}>{form.petSpecies}{form.petBreed ? ` · ${form.petBreed}` : ""}</div> <div style={{ color: "#6b7280", textTransform: "capitalize" }}>{form.petSpecies}{form.petBreed ? ` · ${form.petBreed}` : ""}</div>
{(form.petSizeCategory || form.petCoatType) && (
<div style={{ color: "#6b7280", fontSize: 12, marginTop: 2 }}>
{form.petSizeCategory ? `${form.petSizeCategory} · ` : ""}{form.petCoatType ? form.petCoatType : ""}
</div>
)}
</div> </div>
{form.notes && ( {form.notes && (
<div style={{ gridColumn: "1 / -1" }}> <div style={{ gridColumn: "1 / -1" }}>
@@ -599,7 +648,8 @@ export function BookPage() {
setResult(null); setResult(null);
setForm({ setForm({
serviceId: "", startTime: "", clientName: "", clientEmail: "", serviceId: "", startTime: "", clientName: "", clientEmail: "",
clientPhone: "", petName: "", petSpecies: "", petBreed: "", notes: "", clientPhone: "", petName: "", petSpecies: "", petBreed: "",
petSizeCategory: "", petCoatType: "", notes: "",
}); });
}} }}
> >