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>
285 lines
12 KiB
Svelte
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>
|