feat(admin): super-admin user detail dashboard

Add a read-only User Detail dashboard for super admins, reachable by
clicking a user name in the admin users list.

Backend: new `admin` module aggregating existing domain services
(no new entities). ADMIN-only endpoints under /api/v1/admin:
overview (profile, account status, wallet balance, revenue, summary
counts), wallet transactions, applications (incl. deleted/docked with
restore eligibility), build/deploy errors, tickets with conversation,
and a composite activity timeline. Adds BillingService.getRevenueSummary
and guards against a wallet get-or-create race in the overview reads.

Frontend: tabbed detail page (overview/applications/activity/errors/
tickets) with lazy per-tab queries; user names in the admin list link to
it (admin only); fa/en i18n keys and response types.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-17 23:52:21 +03:30
parent 87e2224d38
commit 7958d2fa72
10 changed files with 1409 additions and 3 deletions
+137 -1
View File
@@ -172,7 +172,7 @@ export type DeploymentStatus =
| 'stopped'
| 'deleting';
export type AppLifecycleStatus = 'active' | 'suspended' | 'pending_deletion' | 'deleted';
export type AppLifecycleStatus = 'active' | 'suspended' | 'pending_deletion' | 'docked' | 'deleted';
export interface ResourceCredit {
id: string;
@@ -432,6 +432,142 @@ export interface AdminUser extends User {
appCount?: number;
}
// ─── Admin User Detail dashboard ────────────────────
export interface AdminUserProfile {
id: string;
phone?: string | null;
email: string | null;
firstName: string;
lastName: string;
role: 'user' | 'admin' | 'technical' | 'sales';
isActive: boolean;
phoneVerified?: boolean;
namespace?: string;
createdAt: string;
updatedAt?: string;
}
export interface AdminUserCounts {
appsTotal: number;
appsActive: number;
appsDeleted: number;
managedServices: number;
optionalServices: number;
deployments: number;
ticketsTotal: number;
ticketsOpen: number;
}
export interface AdminRevenueSummary {
/** What the user actually spent on services (platform income). */
revenue: number;
/** Total topped up into the wallet. */
charged: number;
refunded: number;
}
export interface AdminUserTransaction {
id: string;
type: TransactionType;
amount: number;
balanceAfter: number;
description?: string | null;
applicationId?: string | null;
invoiceId?: string | null;
createdAt: string;
}
export interface AdminUserOverview {
profile: AdminUserProfile;
wallet: { balance: number };
revenue: AdminRevenueSummary;
counts: AdminUserCounts;
recentTransactions: AdminUserTransaction[];
}
export type RestoreEligibility = 'restorable' | 'recoverable' | 'none';
export interface AdminUserApplication {
id: string;
name: string;
productType?: ProductType;
runtime: string;
databaseType: string;
lifecycleStatus?: AppLifecycleStatus;
billingCycle?: BillingCycle | null;
subdomain?: string | null;
customDomain?: string | null;
enableRedis: boolean;
enableRabbitmq: boolean;
enableElasticsearch: boolean;
cpuLimit: string;
memoryLimit: string;
replicas: number;
createdAt: string;
planExpiresAt?: string | null;
scheduledDeletionAt?: string | null;
dockedAt?: string | null;
restorable: RestoreEligibility;
latestDeployment?: { id: string; status: DeploymentStatus; createdAt: string } | null;
}
export interface AdminUserError {
deploymentId: string;
applicationId: string;
applicationName: string;
status: DeploymentStatus;
errorMessage?: string | null;
createdAt: string;
finishedAt?: string | null;
resolved: boolean;
}
export interface AdminDeploymentLogs {
id: string;
status: DeploymentStatus;
errorMessage?: string | null;
buildLog?: string | null;
deployLog?: string | null;
createdAt: string;
finishedAt?: string | null;
}
export interface AdminUserTicketMessage {
id: string;
message: string;
senderRole: 'user' | 'admin' | 'technical' | 'sales';
senderName?: string | null;
createdAt: string;
}
export interface AdminUserTicket {
id: string;
subject: string;
department: TicketDepartment;
status: TicketStatus;
priority: TicketPriority;
createdAt: string;
updatedAt: string;
closedAt?: string | null;
messageCount: number;
messages: AdminUserTicketMessage[];
}
export interface AdminActivityEvent {
type:
| 'app_created'
| 'app_docked'
| 'app_deleted'
| 'deployment'
| 'transaction'
| 'ticket'
| string;
title: string;
at: string;
meta: Record<string, any>;
}
export interface ClusterNode {
name: string;
status: string;