Add admin trend stats and sign-up sparkline
Task #13: Show admin trends like new sign-ups this week. Backend - Added GET /api/stats/admin (admin-only) in artifacts/api-server/src/routes/stats.ts. - Returns: newUsersLast7Days, newUsersPrev7Days, activeServices, inactiveServices, signupsByDay (7-day series with zero-filled days, computed via date_trunc on usersTable.createdAt). API spec / codegen - Added /stats/admin path and AdminStats schema in lib/api-spec/openapi.yaml. - Re-ran @workspace/api-spec codegen, regenerating react-query hooks (useGetAdminStats) and zod schemas. Frontend (artifacts/teaboy-os/src/pages/admin.tsx) - Wired useGetAdminStats in AdminPage (enabled only for admins). - Extended DashboardSection with two trend cards (new users 7d w/ delta vs previous 7d, active services with inactive count) and a small bar chart of sign-ups over the last 7 days. - Added matching i18n keys (en/ar) under admin.dashboard. Notes - Avoided sending all users to the client for trend computation, as recommended in the task brief. - Verified pnpm typecheck passes and the new endpoint returns 401 to unauthenticated callers.
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
messagesTable,
|
||||
messageReadsTable,
|
||||
} from "@workspace/db";
|
||||
import { requireAuth } from "../middlewares/auth";
|
||||
import { requireAuth, requireAdmin } from "../middlewares/auth";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
@@ -111,4 +111,67 @@ router.get("/stats/home", requireAuth, async (req, res): Promise<void> => {
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/stats/admin", requireAuth, requireAdmin, async (_req, res): Promise<void> => {
|
||||
const now = new Date();
|
||||
const startOfTodayUtc = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
||||
const sevenDaysAgo = new Date(startOfTodayUtc.getTime() - 6 * 24 * 60 * 60 * 1000);
|
||||
const fourteenDaysAgo = new Date(startOfTodayUtc.getTime() - 13 * 24 * 60 * 60 * 1000);
|
||||
const last7Start = sevenDaysAgo;
|
||||
const prev7Start = fourteenDaysAgo;
|
||||
|
||||
const [newUsersLast7Row] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(usersTable)
|
||||
.where(sql`${usersTable.createdAt} >= ${last7Start.toISOString()}`);
|
||||
|
||||
const [newUsersPrev7Row] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(usersTable)
|
||||
.where(
|
||||
and(
|
||||
sql`${usersTable.createdAt} >= ${prev7Start.toISOString()}`,
|
||||
sql`${usersTable.createdAt} < ${last7Start.toISOString()}`,
|
||||
),
|
||||
);
|
||||
|
||||
const [activeServicesRow] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(servicesTable)
|
||||
.where(eq(servicesTable.isAvailable, true));
|
||||
|
||||
const [inactiveServicesRow] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(servicesTable)
|
||||
.where(eq(servicesTable.isAvailable, false));
|
||||
|
||||
const signupRows = await db
|
||||
.select({
|
||||
day: sql<string>`to_char(date_trunc('day', ${usersTable.createdAt} AT TIME ZONE 'UTC'), 'YYYY-MM-DD')`,
|
||||
count: sql<number>`count(*)`,
|
||||
})
|
||||
.from(usersTable)
|
||||
.where(sql`${usersTable.createdAt} >= ${last7Start.toISOString()}`)
|
||||
.groupBy(sql`date_trunc('day', ${usersTable.createdAt} AT TIME ZONE 'UTC')`);
|
||||
|
||||
const countsByDay = new Map<string, number>();
|
||||
for (const row of signupRows) {
|
||||
countsByDay.set(row.day, Number(row.count));
|
||||
}
|
||||
|
||||
const signupsByDay: { date: string; count: number }[] = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const d = new Date(startOfTodayUtc.getTime() - i * 24 * 60 * 60 * 1000);
|
||||
const key = d.toISOString().slice(0, 10);
|
||||
signupsByDay.push({ date: key, count: countsByDay.get(key) ?? 0 });
|
||||
}
|
||||
|
||||
res.json({
|
||||
newUsersLast7Days: Number(newUsersLast7Row?.count ?? 0),
|
||||
newUsersPrev7Days: Number(newUsersPrev7Row?.count ?? 0),
|
||||
activeServices: Number(activeServicesRow?.count ?? 0),
|
||||
inactiveServices: Number(inactiveServicesRow?.count ?? 0),
|
||||
signupsByDay,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user