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:
@@ -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
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user