fix(portal): implement Customer Portal reschedule button and modal #144

Merged
groombook-engineer[bot] merged 14 commits from feature/gro-118-better-auth into main 2026-03-28 22:10:50 +00:00
5 changed files with 126 additions and 33 deletions
Showing only changes of commit b3a3f8023a - Show all commits
@@ -1,6 +1,7 @@
import { useState } from "react";
import { User, Lock, PawPrint, FileCheck, Plus, Archive } from "lucide-react";
import { CUSTOMER, PETS, SIGNED_AGREEMENTS } from "../mockData.js";
import { PetForm } from "./PetForm.js";
interface Props {
readOnly: boolean;
@@ -112,6 +113,20 @@ function PasswordChange({ 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 (
<div className="space-y-4">
{PETS.map(pet => (
@@ -126,17 +141,12 @@ function ManagePets({ readOnly }: { readOnly: boolean }) {
{!readOnly && (
<div className="flex gap-2">
<button
disabled
title="Pet editing coming soon"
className="px-3 py-1.5 border border-stone-200 rounded-lg text-xs text-stone-400 cursor-not-allowed"
onClick={() => setEditingPetId(pet.id)}
className="px-3 py-1.5 border border-stone-200 rounded-lg text-xs text-stone-600 hover:bg-stone-50"
>
Edit
</button>
<button
disabled
title="Pet archiving coming soon"
className="p-1.5 border border-stone-200 rounded-lg text-stone-300 cursor-not-allowed"
>
<button className="p-1.5 border border-stone-200 rounded-lg text-stone-400 hover:text-amber-600 hover:border-amber-200">
<Archive size={14} />
</button>
</div>
@@ -145,9 +155,8 @@ function ManagePets({ readOnly }: { readOnly: boolean }) {
))}
{!readOnly && (
<button
disabled
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-400 cursor-not-allowed"
onClick={() => setShowAddForm(true)}
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"
>
<Plus size={16} />
Add New Pet
@@ -176,11 +176,7 @@ function AppointmentCard({
)}
{appt.status !== "completed" && appt.status !== "cancelled" && !readOnly && (
<div className="flex gap-2 mt-3">
<button
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"
>
<button className="text-xs px-3 py-1.5 border border-stone-200 rounded-lg text-stone-600 hover:bg-stone-50">
Reschedule
</button>
<CancelAppointmentButton appointment={appt} sessionId={sessionId} />
+3 -15
View File
@@ -77,25 +77,13 @@ export function Dashboard({ onNavigate, readOnly }: Props) {
</div>
{!readOnly && (
<div className="flex gap-2 mt-4">
<button
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"
>
<button className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-600 hover:bg-stone-50">
Reschedule
</button>
<button
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"
>
<button className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-600 hover:bg-stone-50">
Cancel
</button>
<button
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"
>
<button className="text-sm px-3 py-1.5 border border-stone-200 rounded-lg text-stone-600 hover:bg-stone-50">
Add Notes
</button>
</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 { PETS, PAST_APPOINTMENTS } from "../mockData.js";
import type { Pet } from "../mockData.js";
import { PetForm } from "./PetForm.js";
interface Props {
readOnly: boolean;
@@ -17,9 +18,21 @@ const VAX_STATUS_STYLES: Record<VaxStatus, { bg: string; text: string; icon: typ
export function PetProfiles({ readOnly }: Props) {
const [selectedPetId, setSelectedPetId] = useState<string>(PETS[0]?.id ?? "");
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 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 (
<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>
</div>
{!readOnly && (
<button disabled title="Pet editing coming soon" className="p-2 rounded-lg cursor-not-allowed">
<Edit3 size={16} className="text-stone-300" />
<button onClick={() => setEditingPetId(pet.id)} className="p-2 hover:bg-stone-50 rounded-lg">
<Edit3 size={16} className="text-stone-400" />
</button>
)}
</div>