Persist pet profile changes via PATCH /api/portal/pets/{petId}
- handlePetSave is now async; calls PATCH before updating local state - API response used as source of truth for local state update - Error state shown on API failure; edit form NOT cleared on failure - Loading/saving indicator in PetForm while API call in flight Refs: GRO-1470 Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { X, Save, Plus, Star } from "lucide-react";
|
import { X, Save, Plus, Star, Loader2 } from "lucide-react";
|
||||||
import type { Pet, MedicalAlert, CoatType, AlertSeverity } from "@groombook/types";
|
import type { Pet, MedicalAlert, CoatType, AlertSeverity } from "@groombook/types";
|
||||||
|
|
||||||
const COAT_TYPES: CoatType[] = ["double", "wire", "curly", "smooth", "long", "hairless"];
|
const COAT_TYPES: CoatType[] = ["double", "wire", "curly", "smooth", "long", "hairless"];
|
||||||
@@ -9,15 +9,17 @@ type SizeOption = typeof SIZE_OPTIONS[number];
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pet?: Pet;
|
pet?: Pet;
|
||||||
onSave: (pet: Pet) => void;
|
onSave: (pet: Pet) => void | Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
saving?: boolean;
|
||||||
|
saveError?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function newAlert(): Omit<MedicalAlert, "id"> {
|
function newAlert(): Omit<MedicalAlert, "id"> {
|
||||||
return { type: "", description: "", severity: "low" };
|
return { type: "", description: "", severity: "low" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PetForm({ pet, onSave, onCancel }: Props) {
|
export function PetForm({ pet, onSave, onCancel, saving, saveError }: Props) {
|
||||||
const [name, setName] = useState(pet?.name ?? "");
|
const [name, setName] = useState(pet?.name ?? "");
|
||||||
const [breed, setBreed] = useState(pet?.breed ?? "");
|
const [breed, setBreed] = useState(pet?.breed ?? "");
|
||||||
const [weight, setWeight] = useState(pet?.weightKg ?? 0);
|
const [weight, setWeight] = useState(pet?.weightKg ?? 0);
|
||||||
@@ -305,18 +307,23 @@ export function PetForm({ pet, onSave, onCancel }: Props) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="flex-1 px-4 py-2 border border-stone-200 rounded-lg text-sm text-stone-600 hover:bg-stone-50"
|
disabled={saving}
|
||||||
|
className="flex-1 px-4 py-2 border border-stone-200 rounded-lg text-sm text-stone-600 hover:bg-stone-50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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)"
|
disabled={saving}
|
||||||
|
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) disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Save size={14} />
|
{saving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
|
||||||
Save
|
{saving ? "Saving…" : "Save"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{saveError && (
|
||||||
|
<p className="text-sm text-red-500 text-center">{saveError}</p>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -83,9 +83,27 @@ export function PetProfiles({ sessionId, readOnly }: Props) {
|
|||||||
const petHistory = appointments.appointments.filter(a => a.pet?.id === selectedPetId && new Date(a.startTime) <= new Date());
|
const petHistory = appointments.appointments.filter(a => a.pet?.id === selectedPetId && new Date(a.startTime) <= new Date());
|
||||||
const editingPet = editingPetId ? pets.find(p => p.id === editingPetId) ?? null : null;
|
const editingPet = editingPetId ? pets.find(p => p.id === editingPetId) ?? null : null;
|
||||||
|
|
||||||
function handlePetSave(updatedPet: Pet) {
|
const [saving, setSaving] = useState(false);
|
||||||
setPets(prev => prev.map(p => p.id === updatedPet.id ? updatedPet : p));
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
setEditingPetId(null);
|
|
||||||
|
async function handlePetSave(updatedPet: Pet) {
|
||||||
|
setSaving(true);
|
||||||
|
setSaveError(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/portal/pets/${updatedPet.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json", ...buildHeaders(sessionId) },
|
||||||
|
body: JSON.stringify(updatedPet),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error("Failed to save pet");
|
||||||
|
const saved: Pet = await res.json();
|
||||||
|
setPets(prev => prev.map(p => p.id === saved.id ? saved : p));
|
||||||
|
setEditingPetId(null);
|
||||||
|
} catch (e) {
|
||||||
|
setSaveError(e instanceof Error ? e.message : "Failed to save pet");
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editingPet) {
|
if (editingPet) {
|
||||||
@@ -94,6 +112,8 @@ export function PetProfiles({ sessionId, readOnly }: Props) {
|
|||||||
pet={editingPet}
|
pet={editingPet}
|
||||||
onSave={handlePetSave}
|
onSave={handlePetSave}
|
||||||
onCancel={() => setEditingPetId(null)}
|
onCancel={() => setEditingPetId(null)}
|
||||||
|
saving={saving}
|
||||||
|
saveError={saveError}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user