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.

Replit-Task-Id: dd11d7f7-5569-47b4-878e-ad63043eda31
This commit is contained in:
riyadhafraa
2026-04-20 12:16:55 +00:00
parent dd9153ea4f
commit ada3ef6264
8 changed files with 330 additions and 5 deletions
@@ -305,3 +305,16 @@ export interface HomeStats {
unreadMessages: number;
totalUsers: number;
}
export type AdminStatsSignupsByDayItem = {
date: string;
count: number;
};
export interface AdminStats {
newUsersLast7Days: number;
newUsersPrev7Days: number;
activeServices: number;
inactiveServices: number;
signupsByDay: AdminStatsSignupsByDayItem[];
}
+76
View File
@@ -17,6 +17,7 @@ import type {
} from "@tanstack/react-query";
import type {
AdminStats,
App,
AppSettings,
AuthUser,
@@ -2977,3 +2978,78 @@ export function useGetHomeStats<
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get admin dashboard trend stats
*/
export const getGetAdminStatsUrl = () => {
return `/api/stats/admin`;
};
export const getAdminStats = async (
options?: RequestInit,
): Promise<AdminStats> => {
return customFetch<AdminStats>(getGetAdminStatsUrl(), {
...options,
method: "GET",
});
};
export const getGetAdminStatsQueryKey = () => {
return [`/api/stats/admin`] as const;
};
export const getGetAdminStatsQueryOptions = <
TData = Awaited<ReturnType<typeof getAdminStats>>,
TError = ErrorType<unknown>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAdminStats>>,
TError,
TData
>;
request?: SecondParameter<typeof customFetch>;
}) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetAdminStatsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getAdminStats>>> = ({
signal,
}) => getAdminStats({ signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getAdminStats>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetAdminStatsQueryResult = NonNullable<
Awaited<ReturnType<typeof getAdminStats>>
>;
export type GetAdminStatsQueryError = ErrorType<unknown>;
/**
* @summary Get admin dashboard trend stats
*/
export function useGetAdminStats<
TData = Awaited<ReturnType<typeof getAdminStats>>,
TError = ErrorType<unknown>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getAdminStats>>,
TError,
TData
>;
request?: SecondParameter<typeof customFetch>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetAdminStatsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
+43
View File
@@ -704,6 +704,19 @@ paths:
schema:
$ref: "#/components/schemas/HomeStats"
/stats/admin:
get:
operationId: getAdminStats
tags: [stats]
summary: Get admin dashboard trend stats
responses:
"200":
description: Admin stats
content:
application/json:
schema:
$ref: "#/components/schemas/AdminStats"
components:
schemas:
HealthStatus:
@@ -1287,3 +1300,33 @@ components:
- unreadNotifications
- unreadMessages
- totalUsers
AdminStats:
type: object
properties:
newUsersLast7Days:
type: integer
newUsersPrev7Days:
type: integer
activeServices:
type: integer
inactiveServices:
type: integer
signupsByDay:
type: array
items:
type: object
properties:
date:
type: string
count:
type: integer
required:
- date
- count
required:
- newUsersLast7Days
- newUsersPrev7Days
- activeServices
- inactiveServices
- signupsByDay
+16
View File
@@ -688,3 +688,19 @@ export const GetHomeStatsResponse = zod.object({
unreadMessages: zod.number(),
totalUsers: zod.number(),
});
/**
* @summary Get admin dashboard trend stats
*/
export const GetAdminStatsResponse = zod.object({
newUsersLast7Days: zod.number(),
newUsersPrev7Days: zod.number(),
activeServices: zod.number(),
inactiveServices: zod.number(),
signupsByDay: zod.array(
zod.object({
date: zod.string(),
count: zod.number(),
}),
),
});