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:
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user