Add admin dashboard filtering and sorting
Provide stable server-side list queries and focused chart controls so operators can inspect large datasets without misleading partial-page ordering.
This commit is contained in:
+41
-19
@@ -4,16 +4,21 @@ import type {
|
||||
AdminOperatorProvisioning,
|
||||
AdminSecuritySummary,
|
||||
AdminLoginResponse,
|
||||
AuditQuery,
|
||||
AuditLogEntry,
|
||||
CreditGrantRequest,
|
||||
CreditGrantResponse,
|
||||
LedgerQuery,
|
||||
LedgerEntry,
|
||||
OperatorsQuery,
|
||||
Overview,
|
||||
PageResult,
|
||||
ProductAnalyticsOverview,
|
||||
ReferralsQuery,
|
||||
ReferralOverview,
|
||||
SessionResponse,
|
||||
UserDetail,
|
||||
UsersQuery,
|
||||
UserSummary,
|
||||
} from "./types";
|
||||
|
||||
@@ -140,15 +145,21 @@ async function request<T>(
|
||||
}
|
||||
}
|
||||
|
||||
function query(params: Record<string, string | undefined>): string {
|
||||
export function encodeQuery<T extends object>(params: T): string {
|
||||
const search = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value) search.set(key, value);
|
||||
if (value !== undefined && value !== "") search.set(key, String(value));
|
||||
});
|
||||
const result = search.toString();
|
||||
return result ? `?${result}` : "";
|
||||
}
|
||||
|
||||
function cursorQuery<T extends CursorQuery>(value?: string | T): CursorQuery | T {
|
||||
return typeof value === "string" ? { cursor: value } : (value ?? {});
|
||||
}
|
||||
|
||||
type CursorQuery = { cursor?: string };
|
||||
|
||||
export const adminApi = {
|
||||
session: () => request<SessionResponse>("/auth/session"),
|
||||
|
||||
@@ -161,28 +172,35 @@ export const adminApi = {
|
||||
logout: () => request<void>("/auth/logout", { method: "POST" }),
|
||||
|
||||
overview: (range: string) =>
|
||||
request<Overview>(`/overview${query({ range })}`),
|
||||
request<Overview>(`/overview${encodeQuery({ range })}`),
|
||||
|
||||
referrals: (range: string) =>
|
||||
request<ReferralOverview>(`/referrals${query({ range })}`),
|
||||
referrals: (value: string | ReferralsQuery) => {
|
||||
const params = typeof value === "string" ? { range: value } : value;
|
||||
return request<ReferralOverview>(`/referrals${encodeQuery(params)}`);
|
||||
},
|
||||
|
||||
productAnalytics: (range: string) =>
|
||||
request<ProductAnalyticsOverview>(`/analytics${query({ range })}`),
|
||||
request<ProductAnalyticsOverview>(`/analytics${encodeQuery({ range })}`),
|
||||
|
||||
users: (search = "", cursor?: string) =>
|
||||
request<PageResult<UserSummary>>(
|
||||
`/users${query({ q: search.trim(), cursor })}`,
|
||||
),
|
||||
users: (value: string | UsersQuery = "", legacyCursor?: string) => {
|
||||
const params =
|
||||
typeof value === "string"
|
||||
? { q: value.trim(), cursor: legacyCursor }
|
||||
: { ...value, q: value.q?.trim() };
|
||||
return request<PageResult<UserSummary>>(`/users${encodeQuery(params)}`);
|
||||
},
|
||||
|
||||
user: (userId: string) =>
|
||||
request<UserDetail>(`/users/${encodeURIComponent(userId)}`),
|
||||
|
||||
latestLedger: (cursor?: string) =>
|
||||
request<PageResult<LedgerEntry>>(`/credits/ledger${query({ cursor })}`),
|
||||
|
||||
ledger: (userId: string, cursor?: string) =>
|
||||
latestLedger: (value?: string | LedgerQuery) =>
|
||||
request<PageResult<LedgerEntry>>(
|
||||
`/users/${encodeURIComponent(userId)}/ledger${query({ cursor })}`,
|
||||
`/credits/ledger${encodeQuery(cursorQuery(value))}`,
|
||||
),
|
||||
|
||||
ledger: (userId: string, value?: string | LedgerQuery) =>
|
||||
request<PageResult<LedgerEntry>>(
|
||||
`/users/${encodeURIComponent(userId)}/ledger${encodeQuery(cursorQuery(value))}`,
|
||||
),
|
||||
|
||||
grantCredits: (payload: CreditGrantRequest) =>
|
||||
@@ -196,11 +214,15 @@ export const adminApi = {
|
||||
headers: { "Idempotency-Key": payload.idempotencyKey },
|
||||
}),
|
||||
|
||||
auditLogs: (cursor?: string) =>
|
||||
request<PageResult<AuditLogEntry>>(`/audit${query({ cursor })}`),
|
||||
auditLogs: (value?: string | AuditQuery) =>
|
||||
request<PageResult<AuditLogEntry>>(
|
||||
`/audit${encodeQuery(cursorQuery(value))}`,
|
||||
),
|
||||
|
||||
operators: (cursor?: string) =>
|
||||
request<PageResult<AdminOperator>>(`/operators${query({ cursor })}`),
|
||||
operators: (value?: string | OperatorsQuery) =>
|
||||
request<PageResult<AdminOperator>>(
|
||||
`/operators${encodeQuery(cursorQuery(value))}`,
|
||||
),
|
||||
|
||||
operatorSummary: () =>
|
||||
request<AdminSecuritySummary>("/operators/summary"),
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
export type AdminRole = "SUPER_ADMIN" | "SUPPORT" | "ANALYST";
|
||||
|
||||
export type SortOrder = "asc" | "desc";
|
||||
export type AdminAuditAction =
|
||||
| "LOGIN_SUCCEEDED"
|
||||
| "LOGIN_FAILED"
|
||||
| "SESSION_REVOKED"
|
||||
| "OPERATOR_CREATED"
|
||||
| "OPERATOR_ENABLED"
|
||||
| "OPERATOR_DISABLED"
|
||||
| "OPERATOR_UNLOCKED"
|
||||
| "OPERATOR_CREDENTIALS_RESET"
|
||||
| "OPERATOR_SESSIONS_REVOKED"
|
||||
| "MANUAL_CREDIT_GRANTED";
|
||||
|
||||
export interface CursorPageQuery {
|
||||
cursor?: string;
|
||||
limit?: number;
|
||||
from?: string;
|
||||
until?: string;
|
||||
sort?: "createdAt";
|
||||
order?: SortOrder;
|
||||
}
|
||||
|
||||
export interface UsersQuery extends CursorPageQuery {
|
||||
q?: string;
|
||||
status?: UserSummary["status"];
|
||||
}
|
||||
|
||||
export interface LedgerQuery extends CursorPageQuery {
|
||||
type?: LedgerEntryType;
|
||||
}
|
||||
|
||||
export interface AuditQuery extends CursorPageQuery {
|
||||
action?: AdminAuditAction;
|
||||
result?: AuditLogEntry["result"];
|
||||
}
|
||||
|
||||
export interface OperatorsQuery extends Omit<CursorPageQuery, "sort"> {
|
||||
role?: AdminRole;
|
||||
enabled?: boolean;
|
||||
locked?: boolean;
|
||||
sort?: "createdAt" | "username" | "lastLoginAt";
|
||||
}
|
||||
|
||||
export interface ReferralsQuery {
|
||||
range: string;
|
||||
sort?: "invited" | "qualified" | "creditsEarned";
|
||||
order?: SortOrder;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export type AuthState =
|
||||
| { status: "anonymous" }
|
||||
| { status: "authenticated"; operatorName: string; role: AdminRole };
|
||||
|
||||
Reference in New Issue
Block a user