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:
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import StatusBadge from '$lib/components/ui/StatusBadge.svelte';
|
||||
import Pagination from '$lib/components/ui/Pagination.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const statusOptions = [
|
||||
{ value: '', label: 'All Statuses' },
|
||||
{ value: 'pending', label: 'Pending' },
|
||||
{ value: 'reviewing', label: 'Reviewing' },
|
||||
{ value: 'approved', label: 'Approved' },
|
||||
{ value: 'denied', label: 'Denied' }
|
||||
];
|
||||
|
||||
function handleFilterChange(value: string) {
|
||||
const url = new URL(page.url);
|
||||
if (value) { url.searchParams.set('status', value); } else { url.searchParams.delete('status'); }
|
||||
url.searchParams.delete('page');
|
||||
goto(url.pathname + url.search);
|
||||
}
|
||||
|
||||
function buildBaseUrl(): string {
|
||||
const url = new URL(page.url);
|
||||
return url.pathname + '?' + url.searchParams.toString();
|
||||
}
|
||||
|
||||
function formatDate(d: string | Date): string {
|
||||
return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Volunteer Applications — Admin</title></svelte:head>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Volunteer Applications</h1>
|
||||
<span class="text-sm text-gray-500">{data.total} total</span>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-soft p-4 mb-6">
|
||||
<select
|
||||
value={data.status}
|
||||
onchange={(e) => handleFilterChange(e.currentTarget.value)}
|
||||
class="px-3 py-2.5 rounded-lg border border-gray-soft bg-white text-sm focus:outline-none focus:ring-2 focus:ring-teal-brand"
|
||||
>
|
||||
{#each statusOptions as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{#if data.volunteers.length === 0}
|
||||
<div class="text-center py-12 text-gray-400">
|
||||
<p class="text-sm font-medium">No volunteer applications found.</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">Email</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">Status</th>
|
||||
<th class="px-6 py-3 font-semibold text-gray-600">Date</th>
|
||||
<th class="px-6 py-3 font-semibold text-gray-600 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.volunteers as vol}
|
||||
<tr class="border-b border-gray-soft last:border-0 hover:bg-gray-50 transition-colors">
|
||||
<td class="px-6 py-4 font-medium text-gray-900">{vol.applicantName}</td>
|
||||
<td class="px-6 py-4 text-gray-600"><a href="mailto:{vol.email}" class="hover:text-teal-brand">{vol.email}</a></td>
|
||||
<td class="px-6 py-4 text-gray-600">{vol.phone ?? '—'}</td>
|
||||
<td class="px-6 py-4"><StatusBadge status={vol.status} /></td>
|
||||
<td class="px-6 py-4 text-gray-500 text-xs">{formatDate(vol.createdAt)}</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<a href="/admin/volunteers/{vol.id}" class="text-sm font-medium text-teal-brand hover:text-teal-dark">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination page={data.page} totalPages={data.totalPages} baseUrl={buildBaseUrl()} />
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user