Files
goodgo-platform/apps/web/app/[locale]/(dashboard)/dashboard/kyc/page.tsx
Ho Ngoc Hai 7195064f12 feat(web): add i18n locale routes and language switcher component
Add locale-prefixed routes for admin, auth, dashboard, and public pages.
Add error, loading, and not-found pages for locale context. Add language
switcher UI component for Vietnamese/English toggle.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 09:44:18 +07:00

316 lines
13 KiB
TypeScript

'use client';
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select } from '@/components/ui/select';
import { apiClient } from '@/lib/api-client';
import { useAuthStore } from '@/lib/auth-store';
const KYC_STATUS_MAP: Record<string, { label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline'; description: string }> = {
NONE: { label: 'Chưa xác minh', variant: 'outline', description: 'Bạn chưa gửi hồ sơ xác minh danh tính. Hoàn tất KYC để mở khóa đầy đủ tính năng.' },
PENDING: { label: 'Đang chờ duyệt', variant: 'secondary', description: 'Hồ sơ của bạn đã được gửi và đang chờ đội ngũ quản trị xem xét. Vui lòng chờ 1-3 ngày làm việc.' },
VERIFIED: { label: 'Đã xác minh', variant: 'default', description: 'Danh tính của bạn đã được xác minh thành công. Bạn có thể sử dụng đầy đủ tính năng.' },
REJECTED: { label: 'Bị từ chối', variant: 'destructive', description: 'Hồ sơ xác minh bị từ chối. Vui lòng kiểm tra lại và gửi lại hồ sơ.' },
};
const DOCUMENT_TYPES = [
{ value: 'CCCD', label: 'Căn cước công dân (CCCD)' },
{ value: 'CMND', label: 'Chứng minh nhân dân (CMND)' },
{ value: 'PASSPORT', label: 'Hộ chiếu' },
{ value: 'BUSINESS_LICENSE', label: 'Giấy phép kinh doanh' },
];
const KYC_STEPS = [
{ step: 1, title: 'Loại giấy tờ', description: 'Chọn loại giấy tờ tùy thân' },
{ step: 2, title: 'Tải ảnh', description: 'Tải ảnh mặt trước, mặt sau và ảnh selfie' },
{ step: 3, title: 'Xác nhận', description: 'Kiểm tra và gửi hồ sơ' },
];
export default function KycPage() {
const { user, fetchProfile } = useAuthStore();
const [currentStep, setCurrentStep] = useState(1);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [documentType, setDocumentType] = useState('CCCD');
const [documentNumber, setDocumentNumber] = useState('');
const [frontImage, setFrontImage] = useState<File | null>(null);
const [backImage, setBackImage] = useState<File | null>(null);
const [selfieImage, setSelfieImage] = useState<File | null>(null);
const kycStatus = user?.kycStatus ?? 'NONE';
const kycInfo = KYC_STATUS_MAP[kycStatus] ?? { label: 'Chưa xác minh', variant: 'outline' as const, description: 'Bạn chưa gửi hồ sơ xác minh danh tính.' };
const canSubmit = kycStatus === 'NONE' || kycStatus === 'REJECTED';
const handleSubmit = async () => {
if (!documentNumber.trim()) {
setError('Vui lòng nhập số giấy tờ');
return;
}
if (!frontImage) {
setError('Vui lòng tải ảnh mặt trước');
return;
}
setSubmitting(true);
setError(null);
try {
await apiClient.patch('/auth/profile', {
kycData: {
documentType,
documentNumber: documentNumber.trim(),
submittedAt: new Date().toISOString(),
},
});
await fetchProfile();
setSuccess(true);
} catch (e) {
setError(e instanceof Error ? e.message : 'Gửi hồ sơ thất bại');
} finally {
setSubmitting(false);
}
};
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Xác minh danh tính (KYC)</h1>
<p className="mt-2 text-muted-foreground">
Xác minh danh tính đ sử dụng đy đ tính năng của GoodGo
</p>
</div>
{/* KYC Status */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">Trạng thái xác minh</CardTitle>
<Badge variant={kycInfo.variant}>{kycInfo.label}</Badge>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{kycInfo.description}</p>
</CardContent>
</Card>
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700">
{error}
<button onClick={() => setError(null)} className="ml-2 font-medium underline">
Đóng
</button>
</div>
)}
{success && (
<div className="rounded-lg border border-green-200 bg-green-50 p-4 text-sm text-green-700">
Hồ KYC đã đưc gửi thành công. Vui lòng chờ 1-3 ngày làm việc đ đưc xem xét.
</div>
)}
{/* KYC Form */}
{canSubmit && !success && (
<>
{/* Step indicator */}
<div className="flex items-center justify-center gap-2">
{KYC_STEPS.map((s, i) => (
<div key={s.step} className="flex items-center">
<div
className={`flex h-8 w-8 items-center justify-center rounded-full text-sm font-semibold ${
currentStep >= s.step
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground'
}`}
>
{s.step}
</div>
<span className="ml-2 hidden text-sm sm:inline">{s.title}</span>
{i < KYC_STEPS.length - 1 && (
<div className="mx-3 h-px w-8 bg-border sm:w-16" />
)}
</div>
))}
</div>
<Card>
<CardHeader>
<CardTitle className="text-lg">
{KYC_STEPS[currentStep - 1]?.title}
</CardTitle>
<CardDescription>
{KYC_STEPS[currentStep - 1]?.description}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Step 1: Document type */}
{currentStep === 1 && (
<>
<div className="space-y-2">
<Label htmlFor="docType">Loại giấy tờ</Label>
<Select
id="docType"
value={documentType}
onChange={(e) => setDocumentType(e.target.value)}
>
{DOCUMENT_TYPES.map((dt) => (
<option key={dt.value} value={dt.value}>
{dt.label}
</option>
))}
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="docNumber">Số giấy tờ</Label>
<Input
id="docNumber"
value={documentNumber}
onChange={(e) => setDocumentNumber(e.target.value)}
placeholder="Nhập số CCCD/CMND/Hộ chiếu"
/>
</div>
</>
)}
{/* Step 2: Upload images */}
{currentStep === 2 && (
<>
<div className="space-y-2">
<Label htmlFor="frontImg">nh mặt trước *</Label>
<Input
id="frontImg"
type="file"
accept="image/*"
onChange={(e) => setFrontImage(e.target.files?.[0] ?? null)}
/>
{frontImage && (
<p className="text-xs text-muted-foreground">{frontImage.name}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="backImg">nh mặt sau</Label>
<Input
id="backImg"
type="file"
accept="image/*"
onChange={(e) => setBackImage(e.target.files?.[0] ?? null)}
/>
{backImage && (
<p className="text-xs text-muted-foreground">{backImage.name}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="selfieImg">nh selfie cầm giấy tờ</Label>
<Input
id="selfieImg"
type="file"
accept="image/*"
onChange={(e) => setSelfieImage(e.target.files?.[0] ?? null)}
/>
{selfieImage && (
<p className="text-xs text-muted-foreground">{selfieImage.name}</p>
)}
</div>
</>
)}
{/* Step 3: Confirm */}
{currentStep === 3 && (
<div className="space-y-3 rounded-lg border bg-muted/50 p-4">
<h3 className="font-semibold">Kiểm tra thông tin</h3>
<div className="grid gap-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Loại giấy tờ</span>
<span>{DOCUMENT_TYPES.find((d) => d.value === documentType)?.label}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Số giấy tờ</span>
<span>{documentNumber}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">nh mặt trước</span>
<span>{frontImage ? frontImage.name : 'Chưa tải'}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">nh mặt sau</span>
<span>{backImage ? backImage.name : 'Không có'}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">nh selfie</span>
<span>{selfieImage ? selfieImage.name : 'Không có'}</span>
</div>
</div>
</div>
)}
{/* Navigation buttons */}
<div className="flex justify-between pt-2">
{currentStep > 1 ? (
<Button variant="outline" onClick={() => setCurrentStep((s) => s - 1)}>
Quay lại
</Button>
) : (
<div />
)}
{currentStep < 3 ? (
<Button
onClick={() => {
if (currentStep === 1 && !documentNumber.trim()) {
setError('Vui lòng nhập số giấy tờ');
return;
}
setError(null);
setCurrentStep((s) => s + 1);
}}
>
Tiếp tục
</Button>
) : (
<Button onClick={handleSubmit} disabled={submitting}>
{submitting ? 'Đang gửi...' : 'Gửi hồ sơ xác minh'}
</Button>
)}
</div>
</CardContent>
</Card>
</>
)}
{/* Already verified */}
{kycStatus === 'VERIFIED' && (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-green-100 text-3xl">
</div>
<h2 className="text-xl font-semibold">Danh tính đã đưc xác minh</h2>
<p className="mt-2 text-center text-sm text-muted-foreground">
Tài khoản của bạn đã đưc xác minh đy đ. Bạn thể sử dụng tất cả tính năng của
GoodGo.
</p>
</CardContent>
</Card>
)}
{/* Pending status */}
{kycStatus === 'PENDING' && !success && (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-yellow-100 text-3xl">
</div>
<h2 className="text-xl font-semibold">Đang xem xét hồ </h2>
<p className="mt-2 text-center text-sm text-muted-foreground">
Đi ngũ quản trị đang xem xét hồ của bạn. Thời gian dự kiến: 1-3 ngày làm việc.
</p>
</CardContent>
</Card>
)}
</div>
);
}