diff --git a/src/routes/admin/+layout.server.ts b/src/routes/admin/+layout.server.ts
new file mode 100644
index 0000000..e69a77f
--- /dev/null
+++ b/src/routes/admin/+layout.server.ts
@@ -0,0 +1,40 @@
+import type { LayoutServerLoad } from './$types';
+import { hasPermission, canViewPets } from '$lib/roles';
+import { db } from '$lib/server/db';
+import { fosterSubmissions, supplyRequests } from '$lib/server/schema';
+import { eq, count } from 'drizzle-orm';
+
+// Note: auth redirect is handled by hooks.server.ts โ no need to duplicate here.
+
+export const load: LayoutServerLoad = async ({ locals }) => {
+ const hasPets = hasPermission(locals.user!.role, 'pets');
+
+ let pendingModeration = 0;
+ if (hasPets) {
+ const [[subRow], [reqRow]] = await Promise.all([
+ db.select({ total: count() }).from(fosterSubmissions).where(eq(fosterSubmissions.status, 'pending')),
+ db.select({ total: count() }).from(supplyRequests).where(eq(supplyRequests.status, 'pending'))
+ ]);
+ pendingModeration = (subRow?.total ?? 0) + (reqRow?.total ?? 0);
+ }
+
+ return {
+ user: locals.user!,
+ pendingModeration,
+ canEditPets: hasPets,
+ permissions: {
+ pets: hasPets || canViewPets(locals.user!.role),
+ applications: hasPermission(locals.user!.role, 'applications'),
+ donations: hasPermission(locals.user!.role, 'donations'),
+ expenses: hasPermission(locals.user!.role, 'expenses'),
+ vets: hasPermission(locals.user!.role, 'vets'),
+ medical: hasPermission(locals.user!.role, 'medical'),
+ events: hasPermission(locals.user!.role, 'events'),
+ content: hasPermission(locals.user!.role, 'content'),
+ volunteers: hasPermission(locals.user!.role, 'volunteers'),
+ users: hasPermission(locals.user!.role, 'users'),
+ system: hasPermission(locals.user!.role, 'system'),
+ import: hasPermission(locals.user!.role, 'import')
+ }
+ };
+};
diff --git a/src/routes/admin/+layout.svelte b/src/routes/admin/+layout.svelte
new file mode 100644
index 0000000..e9c7652
--- /dev/null
+++ b/src/routes/admin/+layout.svelte
@@ -0,0 +1,92 @@
+
+
+
+
+
+
{data.orgConfig.orgInfo.name} Admin
+
+
+
+
+
+
+
+
+ {#if sidebarOpen}
+
sidebarOpen = false}
+ role="button"
+ tabindex="-1"
+ onkeydown={() => {}}
+ >
+ {/if}
+
+
+
+ {@render children()}
+
+
+
diff --git a/src/routes/admin/+page.server.ts b/src/routes/admin/+page.server.ts
new file mode 100644
index 0000000..3a9796b
--- /dev/null
+++ b/src/routes/admin/+page.server.ts
@@ -0,0 +1,78 @@
+import type { PageServerLoad } from './$types';
+import { db } from '$lib/server/db';
+import { pets, applications, fosterAssignments, donations, auditLog, users, fosterSubmissions, supplyRequests } from '$lib/server/schema';
+import { eq, gte, sql, count, sum } from 'drizzle-orm';
+import { hasPermission } from '$lib/roles';
+import { error } from '@sveltejs/kit';
+
+export const load: PageServerLoad = async ({ locals }) => {
+ if (!hasPermission(locals.user!.role, 'dashboard')) error(403, 'Access denied');
+
+ // Available pets count
+ const [availableRow] = await db
+ .select({ total: count() })
+ .from(pets)
+ .where(eq(pets.status, 'available'));
+ const availablePets = availableRow?.total ?? 0;
+
+ // Pending applications count
+ const [pendingRow] = await db
+ .select({ total: count() })
+ .from(applications)
+ .where(eq(applications.status, 'pending'));
+ const pendingApplications = pendingRow?.total ?? 0;
+
+ // Active fosters count
+ const [fosterRow] = await db
+ .select({ total: count() })
+ .from(fosterAssignments)
+ .where(eq(fosterAssignments.isCurrent, true));
+ const activeFosters = fosterRow?.total ?? 0;
+
+ // This month's donations sum (amount stored as int cents)
+ const now = new Date();
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
+ const [donationRow] = await db
+ .select({ total: sum(donations.amount) })
+ .from(donations)
+ .where(gte(donations.date, monthStart));
+ const monthlyDonationsCents = parseInt(donationRow?.total ?? '0', 10);
+ const monthlyDonations = monthlyDonationsCents / 100;
+
+ // Pending moderation counts (for users with 'pets' permission)
+ let pendingSubmissions = 0;
+ let pendingRequests = 0;
+ if (hasPermission(locals.user!.role, 'pets')) {
+ const [[subRow], [reqRow]] = await Promise.all([
+ db.select({ total: count() }).from(fosterSubmissions).where(eq(fosterSubmissions.status, 'pending')),
+ db.select({ total: count() }).from(supplyRequests).where(eq(supplyRequests.status, 'pending'))
+ ]);
+ pendingSubmissions = subRow?.total ?? 0;
+ pendingRequests = reqRow?.total ?? 0;
+ }
+
+ // Last 10 audit log entries with user name
+ const recentActivity = await db
+ .select({
+ id: auditLog.id,
+ action: auditLog.action,
+ entity: auditLog.entity,
+ entityId: auditLog.entityId,
+ createdAt: auditLog.createdAt,
+ userName: users.name
+ })
+ .from(auditLog)
+ .leftJoin(users, eq(auditLog.userId, users.id))
+ .orderBy(sql`${auditLog.createdAt} DESC`)
+ .limit(10);
+
+ return {
+ availablePets,
+ pendingApplications,
+ activeFosters,
+ monthlyDonations,
+ pendingSubmissions,
+ pendingRequests,
+ recentActivity
+ };
+};
diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte
new file mode 100644
index 0000000..6e4d4c5
--- /dev/null
+++ b/src/routes/admin/+page.svelte
@@ -0,0 +1,115 @@
+
+
+
+ Dashboard โ {data.orgConfig.orgInfo.name} Admin
+
+
+Dashboard
+
+{#if data.pendingSubmissions > 0 || data.pendingRequests > 0}
+
+
+
Moderation Queue
+
+ You have
+ {#if data.pendingSubmissions > 0}
+ {data.pendingSubmissions} foster submission{data.pendingSubmissions === 1 ? '' : 's'}
+ {/if}
+ {#if data.pendingSubmissions > 0 && data.pendingRequests > 0}
+ and
+ {/if}
+ {#if data.pendingRequests > 0}
+ {data.pendingRequests} supply request{data.pendingRequests === 1 ? '' : 's'}
+ {/if}
+ pending review.
+
+
+
+ Review Now
+
+
+{/if}
+
+
+
+
Available Pets
+
{data.availablePets}
+
+
+
Pending Applications
+
{data.pendingApplications}
+
+
+
Active Fosters
+
{data.activeFosters}
+
+
+
This Month's Donations
+
{fmt.format(data.monthlyDonations)}
+
+
+
+
+
+
Recent Activity
+
+ {#if data.recentActivity.length === 0}
+
+ No recent activity recorded.
+
+ {:else}
+
+ {#each data.recentActivity as entry}
+ -
+
+ {formatAction(entry.action)}
+
+
+
+ {entry.userName ?? 'System'}
+ {entry.action === 'delete' ? 'deleted' : entry.action === 'create' ? 'created' : 'updated'}
+ {formatEntity(entry.entity)}
+ {#if entry.entityId}
+ #{entry.entityId}
+ {/if}
+
+
+ {formatDate(entry.createdAt)}
+
+ {/each}
+
+ {/if}
+
+
+
+Welcome, {data.user.name}.