feat(web): add Agent Profile, KYC, Subscription & Payment dashboard pages

Implement four new dashboard pages with full UI:
- /dashboard/profile: view/edit profile, agent details, KYC status
- /dashboard/kyc: multi-step KYC document submission flow
- /dashboard/subscription: plan comparison, quota usage, billing history
- /dashboard/payments: transaction history with filters and pagination

Also adds API client modules (profile-api, subscription-api, payment-api)
and updates dashboard navigation with new page links.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 16:33:50 +07:00
parent a2e87c34e4
commit 238c27c47a
8 changed files with 1404 additions and 1 deletions

View File

@@ -0,0 +1,59 @@
import { apiClient } from './api-client';
export interface CreatePaymentPayload {
provider: 'VNPAY' | 'MOMO' | 'ZALOPAY' | 'BANK_TRANSFER';
type: 'SUBSCRIPTION' | 'LISTING_FEE' | 'DEPOSIT' | 'FEATURED_LISTING';
amountVND: number;
description: string;
returnUrl: string;
idempotencyKey?: string;
}
export interface CreatePaymentResult {
paymentId: string;
paymentUrl: string;
providerTxId: string;
}
export interface PaymentStatusDto {
id: string;
provider: string;
type: string;
amountVND: string;
status: string;
providerTxId: string | null;
createdAt: string;
updatedAt: string;
}
export interface TransactionListDto {
items: Array<{
id: string;
provider: string;
type: string;
amountVND: string;
status: string;
providerTxId: string | null;
createdAt: string;
}>;
total: number;
limit: number;
offset: number;
}
export const paymentApi = {
createPayment: (data: CreatePaymentPayload) =>
apiClient.post<CreatePaymentResult>('/payments', data),
getPaymentStatus: (id: string) =>
apiClient.get<PaymentStatusDto>(`/payments/${id}`),
getTransactions: (params: { status?: string; limit?: number; offset?: number } = {}) => {
const query = new URLSearchParams();
if (params.status) query.set('status', params.status);
if (params.limit) query.set('limit', String(params.limit));
if (params.offset) query.set('offset', String(params.offset));
const qs = query.toString();
return apiClient.get<TransactionListDto>(`/payments${qs ? `?${qs}` : ''}`);
},
};

View File

@@ -0,0 +1,34 @@
import { apiClient } from './api-client';
import type { UserProfile } from './auth-api';
export interface AgentProfile {
id: string;
email: string | null;
phone: string;
fullName: string;
avatarUrl: string | null;
role: string;
kycStatus: string;
isActive: boolean;
createdAt: string;
licenseNumber: string | null;
agency: string | null;
qualityScore: number | null;
serviceAreas: string[];
isVerified: boolean;
}
export interface UpdateProfilePayload {
fullName?: string;
email?: string;
phone?: string;
}
export const profileApi = {
getProfile: () => apiClient.get<UserProfile>('/auth/profile'),
getAgentProfile: () => apiClient.get<AgentProfile | null>('/auth/profile/agent'),
updateProfile: (data: UpdateProfilePayload) =>
apiClient.patch<{ message: string }>('/auth/profile', data),
};

View File

@@ -0,0 +1,80 @@
import { apiClient } from './api-client';
export interface PlanDto {
id: string;
tier: string;
name: string;
priceMonthlyVND: string;
priceYearlyVND: string;
maxListings: number;
maxSavedSearches: number;
features: Record<string, boolean | number | string>;
isActive: boolean;
}
export interface SubscriptionInfo {
id: string;
planTier: string;
status: string;
currentPeriodStart: string;
currentPeriodEnd: string;
cancelledAt: string | null;
createdAt: string;
}
export interface BillingHistoryDto {
subscription: SubscriptionInfo | null;
payments: Array<{
id: string;
provider: string;
type: string;
amountVND: string;
status: string;
createdAt: string;
}>;
total: number;
}
export interface QuotaCheckResult {
metric: string;
used: number;
limit: number;
remaining: number;
}
export interface CreateSubscriptionResult {
subscriptionId: string;
planTier: string;
status: string;
currentPeriodStart: string;
currentPeriodEnd: string;
}
export const subscriptionApi = {
getPlans: () => apiClient.get<PlanDto[]>('/subscriptions/plans'),
getPlanByTier: (tier: string) =>
apiClient.get<PlanDto>(`/subscriptions/plans/${tier}`),
getBillingHistory: (limit = 20, offset = 0) =>
apiClient.get<BillingHistoryDto>(
`/subscriptions/billing?limit=${limit}&offset=${offset}`,
),
checkQuota: (metric: string) =>
apiClient.get<QuotaCheckResult>(`/subscriptions/quota/${metric}`),
createSubscription: (planTier: string, billingCycle: 'monthly' | 'yearly') =>
apiClient.post<CreateSubscriptionResult>('/subscriptions', {
planTier,
billingCycle,
}),
upgradeSubscription: (newPlanTier: string) =>
apiClient.post<{ message: string }>('/subscriptions/upgrade', {
newPlanTier,
}),
cancelSubscription: (_reason: string) =>
apiClient.delete<{ message: string }>('/subscriptions'),
};