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:
59
apps/web/lib/payment-api.ts
Normal file
59
apps/web/lib/payment-api.ts
Normal 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}` : ''}`);
|
||||
},
|
||||
};
|
||||
34
apps/web/lib/profile-api.ts
Normal file
34
apps/web/lib/profile-api.ts
Normal 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),
|
||||
};
|
||||
80
apps/web/lib/subscription-api.ts
Normal file
80
apps/web/lib/subscription-api.ts
Normal 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'),
|
||||
};
|
||||
Reference in New Issue
Block a user