Show top apps and most-active users on the admin dashboard (Task #21)
Added two leaderboard panels to the admin dashboard that surface which
apps are most popular and which users drive the most activity in the
selected time range.
Backend (artifacts/api-server/src/routes/stats.ts):
- Extended GET /api/stats/admin to also return:
- topApps: top 5 apps by app_opens count, with id, slug, names,
iconName, color, count
- mostActiveUsers: top 5 users by app_opens count, with id, username,
displayNames, avatarUrl, count
- Both lists honor the existing `range` query param (7d/30d/90d) so
they stay in sync with the trend charts. Task wording said "last 7
days" because 7d is the default; using the selected range is a small
intentional improvement that matches the rest of the dashboard.
API spec (lib/api-spec/openapi.yaml):
- Added TopAppItem and TopUserItem schemas.
- Added topApps and mostActiveUsers to AdminStats and made them
required. Regenerated api-client-react and api-zod via codegen.
Frontend (artifacts/teaboy-os/src/pages/admin.tsx):
- Added two new panels to DashboardSection rendered in a 2-column grid
between the trend charts and the recent activity card.
- Each row shows rank, color/initial, name (i18n), count, and a
proportional progress bar. Empty state when no activity yet.
i18n (artifacts/teaboy-os/src/locales/{ar,en}.json):
- Added admin.dashboard.topApps, mostActiveUsers, *Subtitle,
leaderboardEmpty, openCount keys.
Verified with end-to-end test: admin login, dashboard renders both
panels with seeded data, range switch updates subtitles to "Last 30
days".
This commit is contained in:
@@ -215,6 +215,71 @@ router.get("/stats/admin", requireAuth, requireAdmin, async (req, res): Promise<
|
||||
.from(servicesTable)
|
||||
.where(sql`${servicesTable.createdAt} >= ${rangeStart.toISOString()}`);
|
||||
|
||||
const topAppsRows = await db
|
||||
.select({
|
||||
appId: appsTable.id,
|
||||
slug: appsTable.slug,
|
||||
nameAr: appsTable.nameAr,
|
||||
nameEn: appsTable.nameEn,
|
||||
iconName: appsTable.iconName,
|
||||
color: appsTable.color,
|
||||
count: sql<number>`count(${appOpensTable.id})`,
|
||||
})
|
||||
.from(appOpensTable)
|
||||
.innerJoin(appsTable, eq(appOpensTable.appId, appsTable.id))
|
||||
.where(sql`${appOpensTable.createdAt} >= ${rangeStart.toISOString()}`)
|
||||
.groupBy(
|
||||
appsTable.id,
|
||||
appsTable.slug,
|
||||
appsTable.nameAr,
|
||||
appsTable.nameEn,
|
||||
appsTable.iconName,
|
||||
appsTable.color,
|
||||
)
|
||||
.orderBy(sql`count(${appOpensTable.id}) desc`)
|
||||
.limit(5);
|
||||
|
||||
const topApps = topAppsRows.map((r) => ({
|
||||
appId: r.appId,
|
||||
slug: r.slug,
|
||||
nameAr: r.nameAr,
|
||||
nameEn: r.nameEn,
|
||||
iconName: r.iconName,
|
||||
color: r.color,
|
||||
count: Number(r.count),
|
||||
}));
|
||||
|
||||
const mostActiveUsersRows = await db
|
||||
.select({
|
||||
userId: usersTable.id,
|
||||
username: usersTable.username,
|
||||
displayNameAr: usersTable.displayNameAr,
|
||||
displayNameEn: usersTable.displayNameEn,
|
||||
avatarUrl: usersTable.avatarUrl,
|
||||
count: sql<number>`count(${appOpensTable.id})`,
|
||||
})
|
||||
.from(appOpensTable)
|
||||
.innerJoin(usersTable, eq(appOpensTable.userId, usersTable.id))
|
||||
.where(sql`${appOpensTable.createdAt} >= ${rangeStart.toISOString()}`)
|
||||
.groupBy(
|
||||
usersTable.id,
|
||||
usersTable.username,
|
||||
usersTable.displayNameAr,
|
||||
usersTable.displayNameEn,
|
||||
usersTable.avatarUrl,
|
||||
)
|
||||
.orderBy(sql`count(${appOpensTable.id}) desc`)
|
||||
.limit(5);
|
||||
|
||||
const mostActiveUsers = mostActiveUsersRows.map((r) => ({
|
||||
userId: r.userId,
|
||||
username: r.username,
|
||||
displayNameAr: r.displayNameAr,
|
||||
displayNameEn: r.displayNameEn,
|
||||
avatarUrl: r.avatarUrl,
|
||||
count: Number(r.count),
|
||||
}));
|
||||
|
||||
res.json({
|
||||
range,
|
||||
rangeDays,
|
||||
@@ -228,6 +293,8 @@ router.get("/stats/admin", requireAuth, requireAdmin, async (req, res): Promise<
|
||||
appOpensPrevRange: Number(appOpensPrevRangeRow?.count ?? 0),
|
||||
servicesCreatedByDay,
|
||||
servicesCreatedInRange: Number(servicesCreatedInRangeRow?.count ?? 0),
|
||||
topApps,
|
||||
mostActiveUsers,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user