feat(task-35): admin People section — vets, sponsors, mentors, volunteers, users

Full CRUD for vets, sponsors, mentors, volunteer applications, and users.
Includes approve/deny flows for pending users, role management, and password reset.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 12:38:48 -05:00
co-authored by Claude Opus 4.6
parent 63f127dfca
commit 416b25fca9
26 changed files with 1788 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
<script lang="ts">
let { data } = $props();
const vetList = $derived(data.vets);
const inputClass = 'w-full px-3 py-2.5 rounded-lg border border-gray-soft bg-white text-gray-brand placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-brand focus:border-teal-brand text-sm';
</script>
<svelte:head><title>Vets — Admin</title></svelte:head>
<div>
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-900">Vet Management</h1>
<a href="/admin/vets/new" class="px-4 py-2.5 text-sm font-medium rounded-lg bg-teal-brand text-white hover:bg-teal-dark transition-colors">+ Add Vet</a>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-soft p-4 mb-6">
<form method="GET" class="flex gap-3">
<input type="text" name="q" value={data.q} placeholder="Search by name or clinic..." class={inputClass} />
<button type="submit" class="px-5 py-2.5 text-sm font-medium rounded-lg bg-teal-brand text-white hover:bg-teal-dark transition-colors whitespace-nowrap cursor-pointer">Search</button>
{#if data.q}
<a href="/admin/vets" class="px-5 py-2.5 text-sm font-medium rounded-lg border border-gray-soft text-gray-brand hover:bg-gray-100 transition-colors whitespace-nowrap">Clear</a>
{/if}
</form>
</div>
{#if vetList.length === 0}
<div class="text-center py-12 text-gray-400">
<svg class="mx-auto h-12 w-12 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<p class="text-sm font-medium">{data.q ? 'No vets match your search' : 'No vets yet'}</p>
</div>
{:else}
<div class="bg-white rounded-xl shadow-sm border border-gray-soft overflow-hidden">
<table class="w-full text-sm text-left">
<thead>
<tr class="border-b border-gray-soft bg-gray-50">
<th class="px-6 py-3 font-semibold text-gray-600">Name</th>
<th class="px-6 py-3 font-semibold text-gray-600">Clinic</th>
<th class="px-6 py-3 font-semibold text-gray-600">Phone</th>
<th class="px-6 py-3 font-semibold text-gray-600">Email</th>
<th class="px-6 py-3 font-semibold text-gray-600 text-right">Actions</th>
</tr>
</thead>
<tbody>
{#each vetList as vet}
<tr class="border-b border-gray-soft last:border-0 hover:bg-gray-50 transition-colors">
<td class="px-6 py-4"><a href="/admin/vets/{vet.id}" class="font-medium text-teal-brand hover:text-teal-dark">{vet.name}</a></td>
<td class="px-6 py-4 text-gray-600">{vet.clinic ?? '—'}</td>
<td class="px-6 py-4 text-gray-600">{#if vet.phone}<a href="tel:{vet.phone}" class="hover:text-teal-brand">{vet.phone}</a>{:else}—{/if}</td>
<td class="px-6 py-4 text-gray-600">{#if vet.email}<a href="mailto:{vet.email}" class="hover:text-teal-brand">{vet.email}</a>{:else}—{/if}</td>
<td class="px-6 py-4 text-right"><a href="/admin/vets/{vet.id}" class="text-sm font-medium text-teal-brand hover:text-teal-dark">Edit</a></td>
</tr>
{/each}
</tbody>
</table>
</div>
<p class="mt-4 text-sm text-gray-400">{vetList.length} vet{vetList.length !== 1 ? 's' : ''} total</p>
{/if}
</div>