diff --git a/frontend/src/app/admin/kyc/page.tsx b/frontend/src/app/admin/kyc/page.tsx new file mode 100644 index 0000000..1c490aa --- /dev/null +++ b/frontend/src/app/admin/kyc/page.tsx @@ -0,0 +1,268 @@ +"use client"; + +import React, { useEffect, useState, useCallback } from "react"; +import Image from "next/image"; +import { api } from "@/lib/api"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/Card"; +import { Button } from "@/components/ui/Button"; +import { Input } from "@/components/ui/Input"; + +type KycStatus = "PENDING" | "APPROVED" | "REJECTED" | "ESCALATED"; + +interface KycReview { + id: string; + userId: string; + status: KycStatus; + documents: any[]; + notes?: string; + user: { + username: string; + email: string; + }; + createdAt: string; +} + +export default function KycDashboard() { + const [reviews, setReviews] = useState([]); + const [loading, setLoading] = useState(true); + const [filterStatus, setFilterStatus] = useState("ALL"); + const [selectedReview, setSelectedReview] = useState(null); + const [decisionNotes, setDecisionNotes] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + const fetchReviews = useCallback(async () => { + setLoading(true); + try { + const params = filterStatus !== "ALL" ? { status: filterStatus } : {}; + const data = await api.getKycReviews(params); + setReviews((data as any).reviews || []); + } catch (error) { + console.error("Failed to fetch KYC reviews:", error); + } finally { + setLoading(false); + } + }, [filterStatus]); + + useEffect(() => { + fetchReviews(); + }, [fetchReviews]); + + const handleProcess = async (id: string, status: KycStatus) => { + if (status === "REJECTED" && !decisionNotes) { + alert("Please provide a reason for rejection."); + return; + } + + setIsSubmitting(true); + + // Optimistic Update + const previousReviews = [...reviews]; + setReviews(reviews.map((r: KycReview) => r.id === id ? { ...r, status } : r)); + if (selectedReview?.id === id) { + setSelectedReview({ ...selectedReview, status }); + } + + try { + await api.processKycReview(id, { + status, + notes: decisionNotes, + }); + setDecisionNotes(""); + setSelectedReview(null); + // Re-fetch to ensure sync + fetchReviews(); + } catch (error) { + // Rollback on failure + setReviews(previousReviews); + alert("Failed to process KYC review: " + (error as Error).message); + } finally { + setIsSubmitting(false); + } + }; + + if (loading && reviews.length === 0) { + return
Loading KYC reviews...
; + } + + return ( +
+
+
+

+ KYC Review Queue +

+

+ Verify user identities and manage account risk. +

+
+
+ + +
+
+ +
+ {/* Review List */} +
+ {reviews.length === 0 ? ( + + +

No reviews found for this status.

+
+
+ ) : ( + reviews.map((review: KycReview) => ( + setSelectedReview(review)} + > + +
+ {review.user.username} + + {review.status} + +
+ {new Date(review.createdAt).toLocaleDateString()} +
+
+ )) + )} +
+ + {/* Detail Pane */} +
+ {selectedReview ? ( + + +
+
+ {selectedReview.user.username} + {selectedReview.user.email} +
+
+

Review ID

+

{selectedReview.id.substring(0, 8)}...

+
+
+
+ +
+

+ + Identity Documents +

+
+ {Array.isArray(selectedReview.documents) && selectedReview.documents.length > 0 ? ( + selectedReview.documents.map((doc: any, i: number) => ( +
+ {`Document +
+ +
+
+ )) + ) : ( +
+

No documents uploaded for this review.

+
+ )} +
+
+ +
+

Final Decision

+
+
+ +