Files
fosterflow/src/routes/admin/pets/[id]/profile/+page.svelte
T
justinandClaude Opus 4.6 63f127dfca Add admin pet detail pages: profile, photos, medical, fosters (Task 32b)
Pet layout: tabbed nav with counts for photos/medical/fosters, status
badge in header. Detail overview: summary cards and compatibility grid.
Profile: full edit form including adoption info conditional on status,
audit logged. Photos: upload, set primary (star toggle), delete, reorder
with prev/next arrows. Medical: add/delete records grouped by date,
cost stored as cents. Fosters: assign by user account (linked to
foster-role users), set current, delete, audit logged throughout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:33:24 -05:00

285 lines
12 KiB
Svelte

<script lang="ts">
import { enhance } from '$app/forms';
let { data, form } = $props();
const pet = $derived(data.pet);
const canEdit = $derived(data.canEditPets);
const allStatuses = [
'available', 'adopted', 'foster', 'medical_hold', 'behavioral_hold',
'pending', 'trial', 'hospice', 'nursing', 'whelping',
'deceased', 'transferred'
];
const showAdoption = $derived(['adopted', 'pending', 'trial'].includes(pet.status));
let submitting = $state(false);
let showSuccess = $state(false);
function handleEnhance() {
submitting = true;
return async ({ update, result }: { update: (opts?: { reset?: boolean }) => Promise<void>; result: { type: string } }) => {
submitting = false;
await update({ reset: false });
if (result.type === 'success') {
showSuccess = true;
setTimeout(() => (showSuccess = false), 3000);
}
};
}
function dateVal(d: Date | string | null | undefined): string {
if (!d) return '';
const date = d instanceof Date ? d : new Date(d);
if (isNaN(date.getTime())) return '';
return date.toISOString().split('T')[0];
}
const inputClass = 'w-full px-3 py-2.5 rounded-lg border border-gray-200 bg-white text-gray-900 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-600 focus:border-teal-600 text-sm';
const selectClass = inputClass;
const labelClass = 'block text-sm font-medium text-gray-700 mb-1';
</script>
{#if showSuccess}
<div class="mb-6 p-4 bg-green-50 border border-green-200 rounded-xl text-sm text-green-700 flex items-center gap-2">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
Pet updated successfully.
</div>
{/if}
{#if form?.error}
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-xl text-sm text-red-700">
{form.error}
</div>
{/if}
<form method="POST" use:enhance={handleEnhance} class="space-y-6">
<fieldset disabled={!canEdit} class={!canEdit ? 'opacity-75' : ''}>
<!-- Basic Info -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Basic Information</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="md:col-span-2">
<label for="name" class={labelClass}>Name <span class="text-red-500">*</span></label>
<input type="text" id="name" name="name" required value={pet.name} class={inputClass} />
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-2">Species <span class="text-red-500">*</span></label>
<div class="flex gap-6">
{#each ['dog', 'cat', 'other'] as s}
<label class="inline-flex items-center gap-2 cursor-pointer">
<input type="radio" name="species" value={s} checked={pet.species === s} class="w-4 h-4 text-teal-600 focus:ring-teal-600 border-gray-200" />
<span class="text-sm text-gray-700 capitalize">{s}</span>
</label>
{/each}
</div>
</div>
<div>
<label for="breed" class={labelClass}>Breed</label>
<input type="text" id="breed" name="breed" value={pet.breed ?? ''} class={inputClass} placeholder="e.g., Labrador Mix" />
</div>
<div>
<label for="sex" class={labelClass}>Sex</label>
<select id="sex" name="sex" class={selectClass}>
<option value="unknown" selected={pet.sex === 'unknown'}>Unknown</option>
<option value="male" selected={pet.sex === 'male'}>Male</option>
<option value="female" selected={pet.sex === 'female'}>Female</option>
</select>
</div>
<div>
<label for="age" class={labelClass}>Age Estimate</label>
<input type="text" id="age" name="age" value={pet.age ?? ''} class={inputClass} placeholder="e.g., 2 years" />
</div>
<div>
<label for="birthday" class={labelClass}>Birthday</label>
<input type="date" id="birthday" name="birthday" value={dateVal(pet.birthday)} class={inputClass} />
</div>
<div>
<label for="weight" class={labelClass}>Weight</label>
<input type="text" id="weight" name="weight" value={pet.weight ?? ''} class={inputClass} placeholder="e.g., 45 lbs" />
</div>
<div>
<label for="color" class={labelClass}>Color / Markings</label>
<input type="text" id="color" name="color" value={pet.color ?? ''} class={inputClass} />
</div>
<div class="md:col-span-2">
<label for="description" class={labelClass}>Description</label>
<textarea id="description" name="description" rows="4" class="{inputClass} resize-y">{pet.description ?? ''}</textarea>
</div>
</div>
</div>
<!-- Compatibility & Care -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mt-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Compatibility & Care</h2>
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
{#each [
{ name: 'goodWithDogs', label: 'Good with dogs', value: pet.goodWithDogs },
{ name: 'goodWithCats', label: 'Good with cats', value: pet.goodWithCats },
{ name: 'goodWithKids', label: 'Good with kids', value: pet.goodWithKids },
{ name: 'houseTrained', label: 'House trained', value: pet.houseTrained },
{ name: 'spayedNeutered', label: 'Spayed/Neutered', value: pet.spayedNeutered },
{ name: 'microchipped', label: 'Microchipped', value: pet.microchipped }
] as item}
<label class="inline-flex items-center gap-2 cursor-pointer">
<input type="checkbox" name={item.name} checked={item.value ?? false} class="w-4 h-4 rounded border-gray-200 text-teal-600 focus:ring-teal-600" />
<span class="text-sm text-gray-700">{item.label}</span>
</label>
{/each}
</div>
<div class="mt-4">
<label for="microchipId" class={labelClass}>Microchip ID</label>
<input type="text" id="microchipId" name="microchipId" value={pet.microchipId ?? ''} class="{inputClass} max-w-xs" />
</div>
</div>
<!-- Notes -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mt-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Notes</h2>
<div class="space-y-4">
<div>
<label for="medicalNotes" class={labelClass}>Medical Notes</label>
<textarea id="medicalNotes" name="medicalNotes" rows="3" class="{inputClass} resize-y">{pet.medicalNotes ?? ''}</textarea>
</div>
<div>
<label for="behavioralNotes" class={labelClass}>Behavioral Notes</label>
<textarea id="behavioralNotes" name="behavioralNotes" rows="3" class="{inputClass} resize-y">{pet.behavioralNotes ?? ''}</textarea>
</div>
<div>
<label for="specialNeeds" class={labelClass}>Special Needs</label>
<textarea id="specialNeeds" name="specialNeeds" rows="2" class="{inputClass} resize-y">{pet.specialNeeds ?? ''}</textarea>
</div>
</div>
</div>
<!-- Intake Information -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mt-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Intake Information</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="intakeDate" class={labelClass}>Intake Date</label>
<input type="date" id="intakeDate" name="intakeDate" value={dateVal(pet.intakeDate)} class={inputClass} />
</div>
<div>
<label for="intakeSourceId" class={labelClass}>Intake Source</label>
<select id="intakeSourceId" name="intakeSourceId" class={selectClass}>
<option value="">Select source</option>
{#each data.intakeSources as source}
<option value={source.id} selected={pet.intakeSourceId === source.id}>{source.name}</option>
{/each}
</select>
</div>
<div>
<label for="vetId" class={labelClass}>Vet Clinic</label>
<select id="vetId" name="vetId" class={selectClass}>
<option value="">Select vet</option>
{#each data.vets as vet}
<option value={vet.id} selected={pet.vetId === vet.id}>{vet.name}{vet.clinic ? ` — ${vet.clinic}` : ''}</option>
{/each}
</select>
</div>
</div>
</div>
<!-- Settings -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mt-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Settings</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="status" class={labelClass}>Status</label>
<select id="status" name="status" class={selectClass}>
{#each allStatuses as s}
<option value={s} selected={pet.status === s}>{s.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())}</option>
{/each}
</select>
</div>
<div class="flex flex-col gap-3 pt-1">
<label class="inline-flex items-center gap-3 cursor-pointer">
<input type="checkbox" name="featured" checked={pet.featured} class="w-5 h-5 rounded border-gray-200 text-teal-600 focus:ring-teal-600" />
<span class="text-sm font-medium text-gray-700">Featured on homepage</span>
</label>
<label class="inline-flex items-center gap-3 cursor-pointer">
<input type="checkbox" name="published" checked={pet.published} class="w-5 h-5 rounded border-gray-200 text-teal-600 focus:ring-teal-600" />
<span class="text-sm font-medium text-gray-700">Published (visible publicly)</span>
</label>
</div>
</div>
</div>
<!-- Adoption Info (conditional) -->
{#if showAdoption}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mt-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">Adoption Information</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="adoptedDate" class={labelClass}>Adoption Date</label>
<input type="date" id="adoptedDate" name="adoptedDate" value={dateVal(pet.adoptedDate)} class={inputClass} />
</div>
<div>
<label for="adoptionFee" class={labelClass}>Adoption Fee ($)</label>
<input type="text" id="adoptionFee" name="adoptionFee" value={pet.adoptionFee != null ? (pet.adoptionFee / 100).toFixed(2) : ''} class={inputClass} placeholder="0.00" />
</div>
<div class="flex items-center">
<label class="inline-flex items-center gap-3 cursor-pointer">
<input type="checkbox" name="adoptionFeeCollected" checked={pet.adoptionFeeCollected ?? false} class="w-5 h-5 rounded border-gray-200 text-teal-600 focus:ring-teal-600" />
<span class="text-sm font-medium text-gray-700">Fee collected</span>
</label>
</div>
<div>
<label for="adopterName" class={labelClass}>Adopter Name</label>
<input type="text" id="adopterName" name="adopterName" value={pet.adopterName ?? ''} class={inputClass} />
</div>
<div>
<label for="adopterEmail" class={labelClass}>Adopter Email</label>
<input type="email" id="adopterEmail" name="adopterEmail" value={pet.adopterEmail ?? ''} class={inputClass} />
</div>
<div>
<label for="adopterPhone" class={labelClass}>Adopter Phone</label>
<input type="tel" id="adopterPhone" name="adopterPhone" value={pet.adopterPhone ?? ''} class={inputClass} />
</div>
</div>
</div>
{/if}
</fieldset>
{#if canEdit}
<div class="flex justify-end gap-3 mt-6">
<a href="/admin/pets" class="px-6 py-2.5 text-sm font-medium rounded-lg border border-gray-200 text-gray-900 hover:bg-gray-100 transition-colors">
Cancel
</a>
<button
type="submit"
disabled={submitting}
class="px-6 py-2.5 text-sm font-medium rounded-lg bg-teal-600 text-white hover:bg-teal-700 focus:outline-none focus:ring-2 focus:ring-teal-600 focus:ring-offset-2 transition-colors disabled:opacity-50 cursor-pointer"
>
{submitting ? 'Saving...' : 'Save Changes'}
</button>
</div>
{:else}
<div class="flex justify-end mt-6">
<a href="/admin/pets" class="px-6 py-2.5 text-sm font-medium rounded-lg border border-gray-200 text-gray-900 hover:bg-gray-100 transition-colors">
Back to Pets
</a>
</div>
{/if}
</form>