diff --git a/YodaDefault.png b/YodaDefault.png new file mode 100644 index 0000000..4298f9f Binary files /dev/null and b/YodaDefault.png differ diff --git a/backend/assets/yoda.png b/backend/assets/yoda.png index 00bc6c5..4298f9f 100644 Binary files a/backend/assets/yoda.png and b/backend/assets/yoda.png differ diff --git a/backend/src/app.ts b/backend/src/app.ts index 2516bcc..c3a948f 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1914,9 +1914,11 @@ app.get('/api/admin/summary', requireAuth, requireSessionAuth, requireAdmin, asy res.json({ summary: { totalBirds: Number(summary?.total_birds ?? 0), + memorializedBirds: Number(summary?.memorialized_birds ?? 0), totalUsers: Number(summary?.total_users ?? 0), totalWorkspaces: Number(summary?.total_workspaces ?? 0), rescueWorkspaces: Number(summary?.rescue_workspaces ?? 0), + rescueBirds: Number(summary?.rescue_birds ?? 0), pendingRescues: Number(summary?.pending_rescues ?? 0), dailyUsers: Number(summary?.daily_users ?? 0), }, diff --git a/backend/src/repositories/workspaceRepository.test.ts b/backend/src/repositories/workspaceRepository.test.ts index e9ae239..e2e0ce4 100644 --- a/backend/src/repositories/workspaceRepository.test.ts +++ b/backend/src/repositories/workspaceRepository.test.ts @@ -6,6 +6,7 @@ import { deleteWorkspaceIfEmpty, ensurePersonalWorkspaceForUser, findAlternateWorkspaceForUser, + getPlatformAdminSummary, listOwnedWorkspacesByOwnerEmail, updateWorkspace, } from './workspaceRepository.js'; @@ -179,3 +180,30 @@ test('listOwnedWorkspacesByOwnerEmail resolves accepted owner flocks by email', assert.match(calls[0].text, /accepted_at IS NOT NULL/); assert.match(calls[0].text, /workspaces\.id <> \$2/); }); + +test('getPlatformAdminSummary counts memorialized birds separately', async () => { + const { calls } = mockDb({ + rowCount: 1, + rows: [ + { + total_birds: 8, + memorialized_birds: 3, + total_users: 4, + total_workspaces: 2, + rescue_workspaces: 1, + rescue_birds: 5, + pending_rescues: 1, + daily_users: 2, + }, + ], + }); + + const summary = await getPlatformAdminSummary(); + + assert.equal(summary?.memorialized_birds, 3); + assert.equal(calls.length, 1); + assert.match(calls[0].text, /memorialized_birds/); + assert.match(calls[0].text, /memorialized_at IS NOT NULL/); + assert.match(calls[0].text, /rescue_birds/); + assert.match(calls[0].text, /workspaces\.workspace_type = 'rescue'/); +}); diff --git a/backend/src/repositories/workspaceRepository.ts b/backend/src/repositories/workspaceRepository.ts index cd472b5..c06183a 100644 --- a/backend/src/repositories/workspaceRepository.ts +++ b/backend/src/repositories/workspaceRepository.ts @@ -549,17 +549,21 @@ export const setWorkspaceSubscriptionStatusByStripeSubscriptionId = async ( export const getPlatformAdminSummary = async () => { const result = await db.query<{ total_birds: number; + memorialized_birds: number; total_users: number; total_workspaces: number; rescue_workspaces: number; + rescue_birds: number; pending_rescues: number; daily_users: number; }>( `SELECT (SELECT COUNT(*)::int FROM birds) AS total_birds, + (SELECT COUNT(*)::int FROM birds WHERE memorialized_at IS NOT NULL) AS memorialized_birds, (SELECT COUNT(*)::int FROM users) AS total_users, (SELECT COUNT(*)::int FROM workspaces) AS total_workspaces, (SELECT COUNT(*)::int FROM workspaces WHERE workspace_type = 'rescue') AS rescue_workspaces, + (SELECT COUNT(*)::int FROM birds INNER JOIN workspaces ON workspaces.id = birds.workspace_id WHERE workspaces.workspace_type = 'rescue') AS rescue_birds, (SELECT COUNT(*)::int FROM workspaces WHERE workspace_type = 'rescue' AND rescue_verification_status = 'pending') AS pending_rescues, (SELECT COUNT(DISTINCT user_id)::int FROM auth_sessions WHERE created_at >= CURRENT_DATE) AS daily_users`, ); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b9289c7..7a2184f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -140,9 +140,11 @@ type AuthSessionPayload = { type AdminSummary = { totalBirds: number; + memorializedBirds: number; totalUsers: number; totalWorkspaces: number; rescueWorkspaces: number; + rescueBirds: number; pendingRescues: number; dailyUsers: number; }; @@ -3945,6 +3947,10 @@ function App() { Total birds {adminSummary?.totalBirds ?? '-'} +
+ Memorialized birds + {adminSummary?.memorializedBirds ?? '-'} +
Daily users {adminSummary?.dailyUsers ?? '-'} @@ -3961,6 +3967,10 @@ function App() { Rescue flocks {adminSummary?.rescueWorkspaces ?? '-'}
+
+ Total rescue birds + {adminSummary?.rescueBirds ?? '-'} +
Pending rescues {adminSummary?.pendingRescues ?? '-'} diff --git a/frontend/src/assets/yoda.png b/frontend/src/assets/yoda.png index 00bc6c5..4298f9f 100644 Binary files a/frontend/src/assets/yoda.png and b/frontend/src/assets/yoda.png differ