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:
@@ -167,6 +167,35 @@ export class BillingService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate wallet movement for a user, grouped by transaction type. Used by
|
||||
* the admin user-detail dashboard. "revenue" is what the user actually spent
|
||||
* on services (deductions + direct gateway payments) — i.e. platform income —
|
||||
* as opposed to "charged" which is just money topped up into the wallet.
|
||||
*/
|
||||
async getRevenueSummary(
|
||||
userId: string,
|
||||
): Promise<{ revenue: number; charged: number; refunded: number }> {
|
||||
const wallet = await this.getOrCreateWallet(userId);
|
||||
const rows = await this.txRepo
|
||||
.createQueryBuilder('tx')
|
||||
.select('tx.type', 'type')
|
||||
.addSelect('COALESCE(SUM(tx.amount), 0)', 'total')
|
||||
.where('tx.walletId = :walletId', { walletId: wallet.id })
|
||||
.groupBy('tx.type')
|
||||
.getRawMany<{ type: TransactionType; total: string }>();
|
||||
|
||||
const totals = new Map(rows.map((r) => [r.type, Number(r.total)]));
|
||||
const revenue =
|
||||
(totals.get(TransactionType.DEDUCTION) ?? 0) +
|
||||
(totals.get(TransactionType.GATEWAY_PAYMENT) ?? 0);
|
||||
return {
|
||||
revenue,
|
||||
charged: totals.get(TransactionType.CHARGE) ?? 0,
|
||||
refunded: totals.get(TransactionType.REFUND) ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
async recordGatewayPayment(
|
||||
userId: string,
|
||||
amount: number,
|
||||
|
||||
Reference in New Issue
Block a user