diff --git a/artifacts/api-server/src/routes/stats.ts b/artifacts/api-server/src/routes/stats.ts index 0a21094e..32f75beb 100644 --- a/artifacts/api-server/src/routes/stats.ts +++ b/artifacts/api-server/src/routes/stats.ts @@ -112,26 +112,32 @@ router.get("/stats/home", requireAuth, async (req, res): Promise => { }); }); -router.get("/stats/admin", requireAuth, requireAdmin, async (_req, res): Promise => { +router.get("/stats/admin", requireAuth, requireAdmin, async (req, res): Promise => { + const rangeParam = typeof req.query.range === "string" ? req.query.range : "7d"; + const allowedRanges = ["7d", "30d", "90d"] as const; + type RangeKey = typeof allowedRanges[number]; + const range: RangeKey = (allowedRanges as readonly string[]).includes(rangeParam) + ? (rangeParam as RangeKey) + : "7d"; + const rangeDays = range === "90d" ? 90 : range === "30d" ? 30 : 7; + 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 rangeStart = new Date(startOfTodayUtc.getTime() - (rangeDays - 1) * 24 * 60 * 60 * 1000); + const prevRangeStart = new Date(startOfTodayUtc.getTime() - (2 * rangeDays - 1) * 24 * 60 * 60 * 1000); - const [newUsersLast7Row] = await db + const [newUsersInRangeRow] = await db .select({ count: sql`count(*)` }) .from(usersTable) - .where(sql`${usersTable.createdAt} >= ${last7Start.toISOString()}`); + .where(sql`${usersTable.createdAt} >= ${rangeStart.toISOString()}`); - const [newUsersPrev7Row] = await db + const [newUsersPrevRangeRow] = await db .select({ count: sql`count(*)` }) .from(usersTable) .where( and( - sql`${usersTable.createdAt} >= ${prev7Start.toISOString()}`, - sql`${usersTable.createdAt} < ${last7Start.toISOString()}`, + sql`${usersTable.createdAt} >= ${prevRangeStart.toISOString()}`, + sql`${usersTable.createdAt} < ${rangeStart.toISOString()}`, ), ); @@ -145,34 +151,13 @@ router.get("/stats/admin", requireAuth, requireAdmin, async (_req, res): Promise .from(servicesTable) .where(eq(servicesTable.isAvailable, false)); - const signupRows = await db - .select({ - day: sql`to_char(date_trunc('day', ${usersTable.createdAt} AT TIME ZONE 'UTC'), 'YYYY-MM-DD')`, - count: sql`count(*)`, - }) - .from(usersTable) - .where(sql`${usersTable.createdAt} >= ${last7Start.toISOString()}`) - .groupBy(sql`date_trunc('day', ${usersTable.createdAt} AT TIME ZONE 'UTC')`); - - const countsByDay = new Map(); - 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 }); - } - const buildSeries = ( rows: { day: string; count: number }[], ): { date: string; count: number }[] => { const map = new Map(); for (const r of rows) map.set(r.day, Number(r.count)); const out: { date: string; count: number }[] = []; - for (let i = 6; i >= 0; i--) { + for (let i = rangeDays - 1; i >= 0; i--) { const d = new Date(startOfTodayUtc.getTime() - i * 24 * 60 * 60 * 1000); const key = d.toISOString().slice(0, 10); out.push({ date: key, count: map.get(key) ?? 0 }); @@ -180,28 +165,38 @@ router.get("/stats/admin", requireAuth, requireAdmin, async (_req, res): Promise return out; }; + const signupRows = await db + .select({ + day: sql`to_char(date_trunc('day', ${usersTable.createdAt} AT TIME ZONE 'UTC'), 'YYYY-MM-DD')`, + count: sql`count(*)`, + }) + .from(usersTable) + .where(sql`${usersTable.createdAt} >= ${rangeStart.toISOString()}`) + .groupBy(sql`date_trunc('day', ${usersTable.createdAt} AT TIME ZONE 'UTC')`); + const signupsByDay = buildSeries(signupRows); + const appOpenRows = await db .select({ day: sql`to_char(date_trunc('day', ${appOpensTable.createdAt} AT TIME ZONE 'UTC'), 'YYYY-MM-DD')`, count: sql`count(*)`, }) .from(appOpensTable) - .where(sql`${appOpensTable.createdAt} >= ${last7Start.toISOString()}`) + .where(sql`${appOpensTable.createdAt} >= ${rangeStart.toISOString()}`) .groupBy(sql`date_trunc('day', ${appOpensTable.createdAt} AT TIME ZONE 'UTC')`); const appOpensByDay = buildSeries(appOpenRows); - const [appOpensLast7Row] = await db + const [appOpensInRangeRow] = await db .select({ count: sql`count(*)` }) .from(appOpensTable) - .where(sql`${appOpensTable.createdAt} >= ${last7Start.toISOString()}`); + .where(sql`${appOpensTable.createdAt} >= ${rangeStart.toISOString()}`); - const [appOpensPrev7Row] = await db + const [appOpensPrevRangeRow] = await db .select({ count: sql`count(*)` }) .from(appOpensTable) .where( and( - sql`${appOpensTable.createdAt} >= ${prev7Start.toISOString()}`, - sql`${appOpensTable.createdAt} < ${last7Start.toISOString()}`, + sql`${appOpensTable.createdAt} >= ${prevRangeStart.toISOString()}`, + sql`${appOpensTable.createdAt} < ${rangeStart.toISOString()}`, ), ); @@ -211,26 +206,28 @@ router.get("/stats/admin", requireAuth, requireAdmin, async (_req, res): Promise count: sql`count(*)`, }) .from(servicesTable) - .where(sql`${servicesTable.createdAt} >= ${last7Start.toISOString()}`) + .where(sql`${servicesTable.createdAt} >= ${rangeStart.toISOString()}`) .groupBy(sql`date_trunc('day', ${servicesTable.createdAt} AT TIME ZONE 'UTC')`); const servicesCreatedByDay = buildSeries(servicesCreatedRows); - const [servicesCreatedLast7Row] = await db + const [servicesCreatedInRangeRow] = await db .select({ count: sql`count(*)` }) .from(servicesTable) - .where(sql`${servicesTable.createdAt} >= ${last7Start.toISOString()}`); + .where(sql`${servicesTable.createdAt} >= ${rangeStart.toISOString()}`); res.json({ - newUsersLast7Days: Number(newUsersLast7Row?.count ?? 0), - newUsersPrev7Days: Number(newUsersPrev7Row?.count ?? 0), + range, + rangeDays, + newUsersInRange: Number(newUsersInRangeRow?.count ?? 0), + newUsersPrevRange: Number(newUsersPrevRangeRow?.count ?? 0), activeServices: Number(activeServicesRow?.count ?? 0), inactiveServices: Number(inactiveServicesRow?.count ?? 0), signupsByDay, appOpensByDay, - appOpensLast7Days: Number(appOpensLast7Row?.count ?? 0), - appOpensPrev7Days: Number(appOpensPrev7Row?.count ?? 0), + appOpensInRange: Number(appOpensInRangeRow?.count ?? 0), + appOpensPrevRange: Number(appOpensPrevRangeRow?.count ?? 0), servicesCreatedByDay, - servicesCreatedLast7Days: Number(servicesCreatedLast7Row?.count ?? 0), + servicesCreatedInRange: Number(servicesCreatedInRangeRow?.count ?? 0), }); }); diff --git a/artifacts/teaboy-os/public/opengraph.jpg b/artifacts/teaboy-os/public/opengraph.jpg index f0e9e16a..502ced98 100644 Binary files a/artifacts/teaboy-os/public/opengraph.jpg and b/artifacts/teaboy-os/public/opengraph.jpg differ diff --git a/artifacts/teaboy-os/src/locales/ar.json b/artifacts/teaboy-os/src/locales/ar.json index 39a7aab2..8f9544b1 100644 --- a/artifacts/teaboy-os/src/locales/ar.json +++ b/artifacts/teaboy-os/src/locales/ar.json @@ -147,14 +147,30 @@ "none": "—", "newUsers7d": "مستخدمون جدد (٧ أيام)", "vsPrev7d": "الأسبوع السابق: {{count}}", + "newUsersInRange": "مستخدمون جدد · {{range}}", + "vsPrevRange": "{{range}}: {{count}}", "activeServices": "الخدمات النشطة", "inactiveServices": "غير نشطة: {{count}}", "signupsTrend": "التسجيلات", "last7Days": "آخر ٧ أيام", "appOpensTrend": "فتح التطبيقات", "appOpensSummary": "{{count}} إجمالاً · {{delta}}", + "appOpensSummaryRanged": "{{count}} في {{range}} · {{delta}}", "servicesCreatedTrend": "خدمات جديدة", - "servicesCreatedSummary": "{{count}} مضافة" + "servicesCreatedSummary": "{{count}} مضافة", + "servicesCreatedSummaryRanged": "{{count}} في {{range}}", + "trends": "الاتجاهات", + "rangeSelector": "المدى الزمني", + "range": { + "7d": "آخر ٧ أيام", + "30d": "آخر ٣٠ يومًا", + "90d": "آخر ٩٠ يومًا" + }, + "prevRange": { + "7d": "الأيام ٧ السابقة", + "30d": "الأيام ٣٠ السابقة", + "90d": "الأيام ٩٠ السابقة" + } } }, "notFound": { diff --git a/artifacts/teaboy-os/src/locales/en.json b/artifacts/teaboy-os/src/locales/en.json index 35189ecc..be2ed6e6 100644 --- a/artifacts/teaboy-os/src/locales/en.json +++ b/artifacts/teaboy-os/src/locales/en.json @@ -147,14 +147,30 @@ "none": "—", "newUsers7d": "New users (7d)", "vsPrev7d": "Previous 7d: {{count}}", + "newUsersInRange": "New users · {{range}}", + "vsPrevRange": "{{range}}: {{count}}", "activeServices": "Active services", "inactiveServices": "Inactive: {{count}}", "signupsTrend": "Sign-ups", "last7Days": "Last 7 days", "appOpensTrend": "App opens", "appOpensSummary": "{{count}} total · {{delta}}", + "appOpensSummaryRanged": "{{count}} in {{range}} · {{delta}}", "servicesCreatedTrend": "Services added", - "servicesCreatedSummary": "{{count}} added" + "servicesCreatedSummary": "{{count}} added", + "servicesCreatedSummaryRanged": "{{count}} in {{range}}", + "trends": "Trends", + "rangeSelector": "Time range", + "range": { + "7d": "Last 7 days", + "30d": "Last 30 days", + "90d": "Last 90 days" + }, + "prevRange": { + "7d": "Previous 7 days", + "30d": "Previous 30 days", + "90d": "Previous 90 days" + } } }, "notFound": { diff --git a/artifacts/teaboy-os/src/pages/admin.tsx b/artifacts/teaboy-os/src/pages/admin.tsx index 2411a938..436dd981 100644 --- a/artifacts/teaboy-os/src/pages/admin.tsx +++ b/artifacts/teaboy-os/src/pages/admin.tsx @@ -82,9 +82,11 @@ export default function AdminPage() { const { data: services } = useListServices({ query: { queryKey: getListServicesQueryKey() } }); const { data: users } = useListUsers({ query: { queryKey: getListUsersQueryKey() } }); const { data: appSettings } = useGetAppSettings({ query: { queryKey: getGetAppSettingsQueryKey() } }); - const { data: adminStats } = useGetAdminStats({ - query: { queryKey: getGetAdminStatsQueryKey(), enabled: isAdmin }, - }); + const [statsRange, setStatsRange] = useState<"7d" | "30d" | "90d">("7d"); + const { data: adminStats } = useGetAdminStats( + { range: statsRange }, + { query: { queryKey: getGetAdminStatsQueryKey({ range: statsRange }), enabled: isAdmin } }, + ); const createApp = useCreateApp(); const updateApp = useUpdateApp(); @@ -392,6 +394,8 @@ export default function AdminPage() { appSettings={appSettings} adminStats={adminStats} lang={lang} + statsRange={statsRange} + onStatsRangeChange={setStatsRange} /> )} @@ -757,6 +761,8 @@ function DashboardSection({ appSettings, adminStats, lang, + statsRange, + onStatsRangeChange, }: { apps: App[] | undefined; services: Service[] | undefined; @@ -764,6 +770,8 @@ function DashboardSection({ appSettings: AppSettings | undefined; adminStats: AdminStats | undefined; lang: string; + statsRange: "7d" | "30d" | "90d"; + onStatsRangeChange: (range: "7d" | "30d" | "90d") => void; }) { const { t } = useTranslation(); @@ -800,8 +808,8 @@ function DashboardSection({ }, ]; - const newUsers = adminStats?.newUsersLast7Days ?? 0; - const prevUsers = adminStats?.newUsersPrev7Days ?? 0; + const newUsers = adminStats?.newUsersInRange ?? 0; + const prevUsers = adminStats?.newUsersPrevRange ?? 0; const userDelta = newUsers - prevUsers; const userDeltaPct = prevUsers > 0 ? Math.round((userDelta / prevUsers) * 100) : null; const activeServices = adminStats?.activeServices ?? 0; @@ -810,13 +818,16 @@ function DashboardSection({ const maxSignup = Math.max(1, ...signups.map((s) => s.count)); const opens = adminStats?.appOpensByDay ?? []; const maxOpens = Math.max(1, ...opens.map((s) => s.count)); - const opensTotal = adminStats?.appOpensLast7Days ?? 0; - const opensPrev = adminStats?.appOpensPrev7Days ?? 0; + const opensTotal = adminStats?.appOpensInRange ?? 0; + const opensPrev = adminStats?.appOpensPrevRange ?? 0; const opensDelta = opensTotal - opensPrev; const opensDeltaPct = opensPrev > 0 ? Math.round((opensDelta / opensPrev) * 100) : null; const servicesCreated = adminStats?.servicesCreatedByDay ?? []; const maxServicesCreated = Math.max(1, ...servicesCreated.map((s) => s.count)); - const servicesCreatedTotal = adminStats?.servicesCreatedLast7Days ?? 0; + const servicesCreatedTotal = adminStats?.servicesCreatedInRange ?? 0; + const rangeDays = adminStats?.rangeDays ?? (statsRange === "90d" ? 90 : statsRange === "30d" ? 30 : 7); + const rangeLabel = t(`admin.dashboard.range.${statsRange}`); + const prevRangeLabel = t(`admin.dashboard.prevRange.${statsRange}`); const dayLabel = (iso: string) => { try { return formatWeekdayUtil(new Date(iso + "T00:00:00Z"), lang, "short"); @@ -825,6 +836,17 @@ function DashboardSection({ } }; + const showCounts = rangeDays <= 14; + const labelStride = rangeDays <= 7 ? 1 : rangeDays <= 30 ? 5 : 15; + const gapClass = rangeDays <= 7 ? "gap-1.5" : rangeDays <= 30 ? "gap-1" : "gap-px"; + const monthDayLabel = (iso: string) => { + try { + return formatDateUtil(iso + "T00:00:00Z", lang, { month: "short", day: "numeric" }); + } catch { + return iso.slice(5); + } + }; + const renderChart = ( title: string, series: { date: string; count: number }[], @@ -835,14 +857,20 @@ function DashboardSection({
{title}
-
{subtitle ?? t("admin.dashboard.last7Days")}
+
{subtitle ?? rangeLabel}
-
- {series.map((s) => { +
+ {series.map((s, idx) => { const heightPct = (s.count / max) * 100; + const showLabel = + rangeDays <= 7 + ? true + : idx === series.length - 1 || (series.length - 1 - idx) % labelStride === 0; return (
-
{s.count}
+ {showCounts && ( +
{s.count}
+ )}
- {dayLabel(s.date)} + {showLabel ? (rangeDays <= 14 ? dayLabel(s.date) : monthDayLabel(s.date)) : ""}
); @@ -859,6 +887,12 @@ function DashboardSection({
); + const rangeOptions: { value: "7d" | "30d" | "90d"; label: string }[] = [ + { value: "7d", label: t("admin.dashboard.range.7d") }, + { value: "30d", label: t("admin.dashboard.range.30d") }, + { value: "90d", label: t("admin.dashboard.range.90d") }, + ]; + return (
@@ -875,13 +909,42 @@ function DashboardSection({ ))}
+
+
+ {t("admin.dashboard.trends")} +
+
+ {rangeOptions.map((opt) => { + const active = statsRange === opt.value; + return ( + + ); + })} +
+
+
-
{t("admin.dashboard.newUsers7d")}
+
+ {t("admin.dashboard.newUsersInRange", { range: rangeLabel })} +
{newUsers}
{userDelta !== 0 && ( @@ -899,7 +962,7 @@ function DashboardSection({ )}
- {t("admin.dashboard.vsPrev7d", { count: prevUsers })} + {t("admin.dashboard.vsPrevRange", { range: prevRangeLabel, count: prevUsers })}
@@ -936,8 +999,9 @@ function DashboardSection({ opens, maxOpens, "from-sky-300/40 to-sky-500", - t("admin.dashboard.appOpensSummary", { + t("admin.dashboard.appOpensSummaryRanged", { count: opensTotal, + range: rangeLabel, delta: opensDelta === 0 ? "±0" @@ -951,7 +1015,7 @@ function DashboardSection({ servicesCreated, maxServicesCreated, "from-amber-300/40 to-amber-500", - t("admin.dashboard.servicesCreatedSummary", { count: servicesCreatedTotal }), + t("admin.dashboard.servicesCreatedSummaryRanged", { count: servicesCreatedTotal, range: rangeLabel }), )}
diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts index ddb92f63..1e97cdd7 100644 --- a/lib/api-client-react/src/generated/api.schemas.ts +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -318,6 +318,15 @@ export interface HomeStats { totalUsers: number; } +export type AdminStatsRange = + (typeof AdminStatsRange)[keyof typeof AdminStatsRange]; + +export const AdminStatsRange = { + "7d": "7d", + "30d": "30d", + "90d": "90d", +} as const; + export type AdminStatsSignupsByDayItem = { date: string; count: number; @@ -334,14 +343,32 @@ export type AdminStatsServicesCreatedByDayItem = { }; export interface AdminStats { - newUsersLast7Days: number; - newUsersPrev7Days: number; + range: AdminStatsRange; + rangeDays: number; + newUsersInRange: number; + newUsersPrevRange: number; activeServices: number; inactiveServices: number; signupsByDay: AdminStatsSignupsByDayItem[]; appOpensByDay: AdminStatsAppOpensByDayItem[]; - appOpensLast7Days: number; - appOpensPrev7Days: number; + appOpensInRange: number; + appOpensPrevRange: number; servicesCreatedByDay: AdminStatsServicesCreatedByDayItem[]; - servicesCreatedLast7Days: number; + servicesCreatedInRange: number; } + +export type GetAdminStatsParams = { + /** + * Time range for trend stats + */ + range?: GetAdminStatsRange; +}; + +export type GetAdminStatsRange = + (typeof GetAdminStatsRange)[keyof typeof GetAdminStatsRange]; + +export const GetAdminStatsRange = { + "7d": "7d", + "30d": "30d", + "90d": "90d", +} as const; diff --git a/lib/api-client-react/src/generated/api.ts b/lib/api-client-react/src/generated/api.ts index cfcdcf61..036c0000 100644 --- a/lib/api-client-react/src/generated/api.ts +++ b/lib/api-client-react/src/generated/api.ts @@ -26,6 +26,7 @@ import type { CreateConversationBody, CreateServiceBody, ErrorResponse, + GetAdminStatsParams, HealthStatus, HomeStats, LoginBody, @@ -3066,41 +3067,57 @@ export function useGetHomeStats< /** * @summary Get admin dashboard trend stats */ -export const getGetAdminStatsUrl = () => { - return `/api/stats/admin`; +export const getGetAdminStatsUrl = (params?: GetAdminStatsParams) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/stats/admin?${stringifiedParams}` + : `/api/stats/admin`; }; export const getAdminStats = async ( + params?: GetAdminStatsParams, options?: RequestInit, ): Promise => { - return customFetch(getGetAdminStatsUrl(), { + return customFetch(getGetAdminStatsUrl(params), { ...options, method: "GET", }); }; -export const getGetAdminStatsQueryKey = () => { - return [`/api/stats/admin`] as const; +export const getGetAdminStatsQueryKey = (params?: GetAdminStatsParams) => { + return [`/api/stats/admin`, ...(params ? [params] : [])] as const; }; export const getGetAdminStatsQueryOptions = < TData = Awaited>, TError = ErrorType, ->(options?: { - query?: UseQueryOptions< - Awaited>, - TError, - TData - >; - request?: SecondParameter; -}) => { +>( + params?: GetAdminStatsParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +) => { const { query: queryOptions, request: requestOptions } = options ?? {}; - const queryKey = queryOptions?.queryKey ?? getGetAdminStatsQueryKey(); + const queryKey = queryOptions?.queryKey ?? getGetAdminStatsQueryKey(params); const queryFn: QueryFunction>> = ({ signal, - }) => getAdminStats({ signal, ...requestOptions }); + }) => getAdminStats(params, { signal, ...requestOptions }); return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< Awaited>, @@ -3121,15 +3138,18 @@ export type GetAdminStatsQueryError = ErrorType; export function useGetAdminStats< TData = Awaited>, TError = ErrorType, ->(options?: { - query?: UseQueryOptions< - Awaited>, - TError, - TData - >; - request?: SecondParameter; -}): UseQueryResult & { queryKey: QueryKey } { - const queryOptions = getGetAdminStatsQueryOptions(options); +>( + params?: GetAdminStatsParams, + options?: { + query?: UseQueryOptions< + Awaited>, + TError, + TData + >; + request?: SecondParameter; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = getGetAdminStatsQueryOptions(params, options); const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey; diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index 9d0bdae0..ac37bfd6 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -730,6 +730,15 @@ paths: operationId: getAdminStats tags: [stats] summary: Get admin dashboard trend stats + parameters: + - in: query + name: range + required: false + schema: + type: string + enum: ["7d", "30d", "90d"] + default: "7d" + description: Time range for trend stats responses: "200": description: Admin stats @@ -1337,9 +1346,14 @@ components: AdminStats: type: object properties: - newUsersLast7Days: + range: + type: string + enum: ["7d", "30d", "90d"] + rangeDays: type: integer - newUsersPrev7Days: + newUsersInRange: + type: integer + newUsersPrevRange: type: integer activeServices: type: integer @@ -1369,9 +1383,9 @@ components: required: - date - count - appOpensLast7Days: + appOpensInRange: type: integer - appOpensPrev7Days: + appOpensPrevRange: type: integer servicesCreatedByDay: type: array @@ -1385,16 +1399,18 @@ components: required: - date - count - servicesCreatedLast7Days: + servicesCreatedInRange: type: integer required: - - newUsersLast7Days - - newUsersPrev7Days + - range + - rangeDays + - newUsersInRange + - newUsersPrevRange - activeServices - inactiveServices - signupsByDay - appOpensByDay - - appOpensLast7Days - - appOpensPrev7Days + - appOpensInRange + - appOpensPrevRange - servicesCreatedByDay - - servicesCreatedLast7Days + - servicesCreatedInRange diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index df67688b..2b2c96e8 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -717,9 +717,20 @@ export const GetHomeStatsResponse = zod.object({ /** * @summary Get admin dashboard trend stats */ +export const getAdminStatsQueryRangeDefault = `7d`; + +export const GetAdminStatsQueryParams = zod.object({ + range: zod + .enum(["7d", "30d", "90d"]) + .default(getAdminStatsQueryRangeDefault) + .describe("Time range for trend stats"), +}); + export const GetAdminStatsResponse = zod.object({ - newUsersLast7Days: zod.number(), - newUsersPrev7Days: zod.number(), + range: zod.enum(["7d", "30d", "90d"]), + rangeDays: zod.number(), + newUsersInRange: zod.number(), + newUsersPrevRange: zod.number(), activeServices: zod.number(), inactiveServices: zod.number(), signupsByDay: zod.array( @@ -734,13 +745,13 @@ export const GetAdminStatsResponse = zod.object({ count: zod.number(), }), ), - appOpensLast7Days: zod.number(), - appOpensPrev7Days: zod.number(), + appOpensInRange: zod.number(), + appOpensPrevRange: zod.number(), servicesCreatedByDay: zod.array( zod.object({ date: zod.string(), count: zod.number(), }), ), - servicesCreatedLast7Days: zod.number(), + servicesCreatedInRange: zod.number(), });