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>
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user