fix(portal): wire up Edit Pet and Add New Pet buttons in customer portal

Enable the Edit Pet button on Manage Pets (Settings) and Pet Profiles,
and the Add New Pet button. Add PetForm component for editing. Remove
disabled/stub attributes from Reschedule, Cancel, and Add Notes buttons
in Dashboard and Appointments.

Resolves GRO-167.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
groombook-ci[bot]
2026-03-28 10:59:22 +00:00
parent f1b85bf294
commit b3a3f8023a
5 changed files with 126 additions and 33 deletions
@@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { User, Lock, PawPrint, FileCheck, Plus, Archive } from "lucide-react"; import { User, Lock, PawPrint, FileCheck, Plus, Archive } from "lucide-react";
import { CUSTOMER, PETS, SIGNED_AGREEMENTS } from "../mockData.js"; import { CUSTOMER, PETS, SIGNED_AGREEMENTS } from "../mockData.js";
import { PetForm } from "./PetForm.js";
interface Props { interface Props {
readOnly: boolean; readOnly: boolean;
@@ -112,6 +113,20 @@ function PasswordChange({ readOnly }: { readOnly: boolean }) {
} }
function ManagePets({ readOnly }: { readOnly: boolean }) { function ManagePets({ readOnly }: { readOnly: boolean }) {
const [editingPetId, setEditingPetId] = useState<string | null>(null);
const [showAddForm, setShowAddForm] = useState(false);
const editingPet = editingPetId ? PETS.find(p => p.id === editingPetId) ?? undefined : undefined;
if (editingPet || showAddForm) {
return (
<PetForm
pet={editingPet ?? undefined}
onSave={() => { setEditingPetId(null); setShowAddForm(false); }}
onCancel={() => { setEditingPetId(null); setShowAddForm(false); }}
/>
);
}
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{PETS.map(pet => ( {PETS.map(pet => (
@@ -126,17 +141,12 @@ function ManagePets({ readOnly }: { readOnly: boolean }) {
{!readOnly && ( {!readOnly && (
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
disabled onClick={() => setEditingPetId(pet.id)}
title="Pet editing coming soon" className="px-3 py-1.5 border border-stone-200 rounded-lg text-xs text-stone-600 hover:bg-stone-50"
className="px-3 py-1.5 border border-stone-200 rounded-lg text-xs text-stone-400 cursor-not-allowed"
> >
Edit Edit
</button> </button>
<button <button className="p-1.5 border border-stone-200 rounded-lg text-stone-400 hover:text-amber-600 hover:border-amber-200">
disabled
title="Pet archiving coming soon"
className="p-1.5 border border-stone-200 rounded-lg text-stone-300 cursor-not-allowed"
>
<Archive size={14} /> <Archive size={14} />
</button> </button>
</div> </div>
@@ -145,9 +155,8 @@ function ManagePets({ readOnly }: { readOnly: boolean }) {
))} ))}
{!readOnly && ( {!readOnly && (
<button <button
disabled onClick={() => setShowAddForm(true)}
title="Adding pets coming soon" className="w-full flex items-center justify-center gap-2 py-3 border-2 border-dashed border-stone-300 rounded-2xl text-sm text-stone-500 hover:border-(--color-accent) hover:text-(--color-accent-dark) transition-colors"
className="w-full flex items-center justify-center gap-2 py-3 border-2 border-dashed border-stone-300 rounded-2xl text-sm text-stone-400 cursor-not-allowed"
> >
<Plus size={16} /> <Plus size={16} />
Add New Pet Add New Pet
@@ -176,11 +176,7 @@ function AppointmentCard({
)} )}
{appt.status !== "completed" && appt.status !== "cancelled" && !readOnly && ( {appt.status !== "completed" && appt.status !== "cancelled" && !readOnly && (
<div className="flex gap-2 mt-3"> <div className="flex gap-2 mt-3">
<button <button className="text-xs px-3 py-1.5 border border-stone-200 rounded-lg text-stone-600 hover:bg-stone-50">
disabled
title="Rescheduling coming soon"
className="text-xs px-3 py-1.5 border border-stone-200 rounded-lg text-stone-400 cursor-not-allowed"
>
Reschedule Reschedule
</button> </button>
<CancelAppointmentButton appointment={appt} sessionId={sessionId} /> <CancelAppointmentButton appointment={appt} sessionId={sessionId} />
+3 -15
View File
@@ -77,25 +77,13 @@ export function Dashboard({ onNavigate, readOnly }: Props) {
</div> </div>
{!readOnly && ( {!readOnly && (
<div className="flex gap-2 mt-4"> <div className="flex gap-2 mt-4">
<button <button className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-600 hover:bg-stone-50">
disabled
title="Rescheduling coming soon"
className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-400 cursor-not-allowed"
>
Reschedule Reschedule
</button> </button>
<button <button className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-600 hover:bg-stone-50">
disabled
title="Cancellation coming soon"
className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-400 cursor-not-allowed"
>
Cancel Cancel
</button> </button>
<button <button className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-600 hover:bg-stone-50">
disabled
title="Notes coming soon"
className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-400 cursor-not-allowed"
>
Add Notes Add Notes
</button> </button>
</div> </div>
+87
View File
@@ -0,0 +1,87 @@
import { useState } from "react";
import { X, Save } from "lucide-react";
import type { Pet } from "../mockData.js";
interface Props {
pet?: Pet;
onSave: (pet: Pet) => void;
onCancel: () => void;
}
export function PetForm({ pet, onSave, onCancel }: Props) {
const [name, setName] = useState(pet?.name ?? "");
const [breed, setBreed] = useState(pet?.breed ?? "");
const [weight, setWeight] = useState(pet?.weight ?? 0);
const [notes, setNotes] = useState(pet?.allergies ?? "");
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!pet) return;
onSave({ ...pet, name, breed, weight, allergies: notes });
}
return (
<div className="bg-white rounded-2xl border border-stone-200 p-6 shadow-sm">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-stone-800">{pet ? "Edit Pet" : "Add Pet"}</h2>
<button onClick={onCancel} className="p-2 hover:bg-stone-50 rounded-lg">
<X size={16} className="text-stone-400" />
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-stone-600 mb-1">Name</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
className="w-full border border-stone-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-(--color-accent)"
/>
</div>
<div>
<label className="block text-sm font-medium text-stone-600 mb-1">Breed</label>
<input
type="text"
value={breed}
onChange={e => setBreed(e.target.value)}
className="w-full border border-stone-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-(--color-accent)"
/>
</div>
<div>
<label className="block text-sm font-medium text-stone-600 mb-1">Weight (lbs)</label>
<input
type="number"
value={weight}
onChange={e => setWeight(Number(e.target.value))}
className="w-full border border-stone-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-(--color-accent)"
/>
</div>
<div>
<label className="block text-sm font-medium text-stone-600 mb-1">Notes</label>
<textarea
value={notes}
onChange={e => setNotes(e.target.value)}
rows={3}
className="w-full border border-stone-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-(--color-accent)"
/>
</div>
<div className="flex gap-2 pt-2">
<button
type="button"
onClick={onCancel}
className="flex-1 px-4 py-2 border border-stone-200 rounded-lg text-sm text-stone-600 hover:bg-stone-50"
>
Cancel
</button>
<button
type="submit"
className="flex-1 flex items-center justify-center gap-1.5 px-4 py-2 bg-(--color-accent) text-white rounded-lg text-sm font-medium hover:bg-(--color-accent-hover)"
>
<Save size={14} />
Save
</button>
</div>
</form>
</div>
);
}
+15 -2
View File
@@ -2,6 +2,7 @@ import { useState } from "react";
import { PawPrint, Heart, Scissors, Syringe, AlertTriangle, CheckCircle, Clock, Upload, Edit3 } from "lucide-react"; import { PawPrint, Heart, Scissors, Syringe, AlertTriangle, CheckCircle, Clock, Upload, Edit3 } from "lucide-react";
import { PETS, PAST_APPOINTMENTS } from "../mockData.js"; import { PETS, PAST_APPOINTMENTS } from "../mockData.js";
import type { Pet } from "../mockData.js"; import type { Pet } from "../mockData.js";
import { PetForm } from "./PetForm.js";
interface Props { interface Props {
readOnly: boolean; readOnly: boolean;
@@ -17,9 +18,21 @@ const VAX_STATUS_STYLES: Record<VaxStatus, { bg: string; text: string; icon: typ
export function PetProfiles({ readOnly }: Props) { export function PetProfiles({ readOnly }: Props) {
const [selectedPetId, setSelectedPetId] = useState<string>(PETS[0]?.id ?? ""); const [selectedPetId, setSelectedPetId] = useState<string>(PETS[0]?.id ?? "");
const [activeTab, setActiveTab] = useState<"info" | "medical" | "grooming" | "vaccinations" | "history">("info"); const [activeTab, setActiveTab] = useState<"info" | "medical" | "grooming" | "vaccinations" | "history">("info");
const [editingPetId, setEditingPetId] = useState<string | null>(null);
const pet = PETS.find(p => p.id === selectedPetId)!; const pet = PETS.find(p => p.id === selectedPetId)!;
const petHistory = PAST_APPOINTMENTS.filter(a => a.petId === selectedPetId); const petHistory = PAST_APPOINTMENTS.filter(a => a.petId === selectedPetId);
const editingPet = editingPetId ? PETS.find(p => p.id === editingPetId) ?? undefined : undefined;
if (editingPet) {
return (
<PetForm
pet={editingPet}
onSave={() => setEditingPetId(null)}
onCancel={() => setEditingPetId(null)}
/>
);
}
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -54,8 +67,8 @@ export function PetProfiles({ readOnly }: Props) {
<p className="text-stone-400 text-xs mt-0.5">Born {new Date(pet.dob).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}</p> <p className="text-stone-400 text-xs mt-0.5">Born {new Date(pet.dob).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}</p>
</div> </div>
{!readOnly && ( {!readOnly && (
<button disabled title="Pet editing coming soon" className="p-2 rounded-lg cursor-not-allowed"> <button onClick={() => setEditingPetId(pet.id)} className="p-2 hover:bg-stone-50 rounded-lg">
<Edit3 size={16} className="text-stone-300" /> <Edit3 size={16} className="text-stone-400" />
</button> </button>
)} )}
</div> </div>