feat(task-37): admin Analytics & System — analytics, traffic, moderation, foster dashboard, fosters, system

Key metrics dashboard, page view traffic analysis, foster submission moderation,
foster assignment management, active foster dashboard with supply requests, and
system settings/org config editor with full audit log.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 12:45:42 -05:00
co-authored by Claude Opus 4.6
parent 1ae4c1be2c
commit 6ca598b45c
12 changed files with 975 additions and 0 deletions
@@ -0,0 +1,47 @@
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/db';
import { pets, applications, donations, fosterAssignments, users, pageViews } from '$lib/server/schema';
import { eq, gte, count, sum, and } from 'drizzle-orm';
import { error } from '@sveltejs/kit';
import { hasPermission } from '$lib/roles';
import type { Role } from '$lib/roles';
export const load: PageServerLoad = async ({ locals }) => {
if (!hasPermission(locals.user!.role as Role, 'dashboard')) error(403, 'Access denied');
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const [totalPetsRow] = await db.select({ total: count() }).from(pets);
const [availablePetsRow] = await db.select({ total: count() }).from(pets).where(eq(pets.status, 'available'));
const [fosterPetsRow] = await db.select({ total: count() }).from(pets).where(eq(pets.status, 'foster'));
const [adoptedThisMonthRow] = await db.select({ total: count() }).from(pets).where(
and(eq(pets.status, 'adopted'), gte(pets.adoptedDate, thirtyDaysAgo))
);
const [pendingAppsRow] = await db.select({ total: count() }).from(applications).where(eq(applications.status, 'pending'));
const [newAppsRow] = await db.select({ total: count() }).from(applications).where(gte(applications.createdAt, thirtyDaysAgo));
const [donationsRow] = await db.select({ total: sum(donations.amount) }).from(donations).where(gte(donations.date, thirtyDaysAgo));
const [totalUsersRow] = await db.select({ total: count() }).from(users).where(eq(users.active, true));
const [activeFostersRow] = await db.select({ total: count() }).from(fosterAssignments).where(eq(fosterAssignments.isCurrent, true));
const [pageViewsRow] = await db.select({ total: count() }).from(pageViews).where(
and(gte(pageViews.createdAt, thirtyDaysAgo), eq(pageViews.isBot, false))
);
return {
stats: {
totalPets: totalPetsRow?.total ?? 0,
availablePets: availablePetsRow?.total ?? 0,
fosterPets: fosterPetsRow?.total ?? 0,
adoptedThisMonth: adoptedThisMonthRow?.total ?? 0,
pendingApplications: pendingAppsRow?.total ?? 0,
newApplications: newAppsRow?.total ?? 0,
donationsThisMonth: (donationsRow?.total ?? 0) as number,
activeUsers: totalUsersRow?.total ?? 0,
activeFosters: activeFostersRow?.total ?? 0,
pageViewsThisMonth: pageViewsRow?.total ?? 0
}
};
};
+58
View File
@@ -0,0 +1,58 @@
<script lang="ts">
let { data } = $props();
const s = $derived(data.stats);
const fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
</script>
<svelte:head><title>Analytics — Admin</title></svelte:head>
<div>
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900">Analytics</h1>
<p class="text-sm text-gray-500 mt-1">Last 30 days unless otherwise noted</p>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-8">
<div class="bg-white rounded-xl shadow-sm border border-gray-soft p-5">
<p class="text-xs text-gray-400 uppercase tracking-wide">Total Pets</p>
<p class="text-3xl font-bold text-gray-900 mt-1">{s.totalPets}</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-soft p-5">
<p class="text-xs text-gray-400 uppercase tracking-wide">Available</p>
<p class="text-3xl font-bold text-teal-brand mt-1">{s.availablePets}</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-soft p-5">
<p class="text-xs text-gray-400 uppercase tracking-wide">In Foster</p>
<p class="text-3xl font-bold text-gray-900 mt-1">{s.fosterPets}</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-soft p-5">
<p class="text-xs text-gray-400 uppercase tracking-wide">Adopted (30d)</p>
<p class="text-3xl font-bold text-green-600 mt-1">{s.adoptedThisMonth}</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-soft p-5">
<p class="text-xs text-gray-400 uppercase tracking-wide">Active Fosters</p>
<p class="text-3xl font-bold text-gray-900 mt-1">{s.activeFosters}</p>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div class="bg-white rounded-xl shadow-sm border border-gray-soft p-5">
<p class="text-xs text-gray-400 uppercase tracking-wide">Pending Applications</p>
<p class="text-3xl font-bold {s.pendingApplications > 0 ? 'text-amber-600' : 'text-gray-900'} mt-1">{s.pendingApplications}</p>
<p class="text-xs text-gray-400 mt-1">{s.newApplications} new this month</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-soft p-5">
<p class="text-xs text-gray-400 uppercase tracking-wide">Donations (30d)</p>
<p class="text-3xl font-bold text-green-600 mt-1">{fmt.format(s.donationsThisMonth / 100)}</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-soft p-5">
<p class="text-xs text-gray-400 uppercase tracking-wide">Active Users</p>
<p class="text-3xl font-bold text-gray-900 mt-1">{s.activeUsers}</p>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-soft p-5">
<p class="text-xs text-gray-400 uppercase tracking-wide">Page Views (30d)</p>
<p class="text-3xl font-bold text-gray-900 mt-1">{s.pageViewsThisMonth.toLocaleString()}</p>
</div>
</div>
</div>