diff --git a/actions/user.ts b/actions/user.ts index be9442f5..6d50a291 100644 --- a/actions/user.ts +++ b/actions/user.ts @@ -368,8 +368,12 @@ export async function updateUserKycStatus( user.isKycVerified = false user.kycVerified = false } else if (physicalMeetingStatus === "approved") { - if (oldPhysicalMeetingStatus !== "scheduled") { - return { success: false, message: "Only scheduled meetings can be approved." } + if ( + oldPhysicalMeetingStatus !== "scheduled" && + oldPhysicalMeetingStatus !== "none" && + oldPhysicalMeetingStatus !== "rescheduled" + ) { + return { success: false, message: "Only scheduled, none, or rescheduled meetings can be approved." } } if (!user.physicalMeetingDate && !normalizedMeetingDate) { diff --git a/app/api/admin/kyc-requests/[id]/audit/route.ts b/app/api/admin/kyc-requests/[id]/audit/route.ts new file mode 100644 index 00000000..816d8cf8 --- /dev/null +++ b/app/api/admin/kyc-requests/[id]/audit/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server" + +import { finalizeAuthenticatedResponse, requireAuthenticatedUser } from "@/lib/api/route-guard" +import dbConnect from "@/lib/dbConnect" +import AuditLog from "@/models/AuditLog" + +interface RouteContext { + params: Promise<{ id: string }> +} + +export async function GET(request: Request, context: RouteContext) { + try { + await dbConnect() + + const authContext = await requireAuthenticatedUser(request, ["admin"], { + forbiddenMessage: "Admin access required", + }) + if ("response" in authContext) return authContext.response + + const { id } = await context.params + if (!id) { + return NextResponse.json({ error: "User ID is required" }, { status: 400 }) + } + + // Retrieve all audit logs related to this user target ID + const auditLogs = await AuditLog.find({ targetId: id }) + .sort({ createdAt: -1 }) + .limit(100) + .lean() + + const response = NextResponse.json(auditLogs, { status: 200 }) + return finalizeAuthenticatedResponse(response, authContext) + } catch (error) { + console.error("KYC_USER_AUDIT_LOG_GET_ERROR", error) + return NextResponse.json({ error: "Internal server error" }, { status: 500 }) + } +} diff --git a/app/dashboard/admin/kyc-management/page.tsx b/app/dashboard/admin/kyc-management/page.tsx index fb57e83a..cee08a9e 100644 --- a/app/dashboard/admin/kyc-management/page.tsx +++ b/app/dashboard/admin/kyc-management/page.tsx @@ -6,7 +6,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Skeleton } from "@/components/ui/skeleton" -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Separator } from "@/components/ui/separator" +import { ScrollArea } from "@/components/ui/scroll-area" import { Dialog, DialogContent, @@ -16,16 +17,35 @@ import { DialogFooter, } from "@/components/ui/dialog" import { useToast } from "@/hooks/use-toast" -import { Loader2, CheckCircle, XCircle, FileText, Eye, RefreshCw, AlertTriangle, Calendar } from "lucide-react" +import { + Loader2, + CheckCircle, + XCircle, + FileText, + Eye, + RefreshCw, + AlertTriangle, + Calendar, + Search, + User, + History, + Clock, + ArrowRight, + ShieldAlert, + Check, + CalendarClock, + MapPin, + Mail, + Phone, + Wallet, +} from "lucide-react" import { useAuth } from "@/hooks/use-auth" import { Label } from "@/components/ui/label" import { Textarea } from "@/components/ui/textarea" -import { Input } from "@/components/ui/input" // Import Input for date picker +import { Input } from "@/components/ui/input" -// Reusing the updateUserKycStatus Server Action import { updateUserKycStatus } from "@/actions/user" -// Update the KycRequest interface to include new fields interface KycRequest { _id: string role: "driver" | "investor" @@ -39,40 +59,23 @@ interface KycRequest { updatedAt: string kycRejectionReason?: string | null physicalMeetingDate?: string | null - physicalMeetingStatus?: "none" | "scheduled" | "approved" | "rescheduled" | "completed" | "rejected_stage2" // Updated enum -} - -function getRequestDisplayName(request: KycRequest) { - return request.fullName?.trim() || request.name || request.email || "Unknown user" + physicalMeetingStatus?: "none" | "scheduled" | "approved" | "rescheduled" | "completed" | "rejected_stage2" } -function getActionLabel(actionType: AdminKycManagementPageAction | null, request: KycRequest | null) { - if (!actionType) return "Action" - if (request?.role === "investor") { - if (actionType === "approve_stage1") return "Approve KYC" - if (actionType === "reject_stage1") return "Reject KYC" - } - - return actionType.replace(/_/g, " ") +interface AuditLogEntry { + _id: string + actorId?: string + actorRole?: string + action: string + targetType: string + targetId?: string + status: string + createdAt: string + metadata?: Record } -type AdminKycManagementPageAction = - | "approve_stage1" - | "reject_stage1" - | "approve_meeting_date" - | "reschedule_meeting" - | "complete_stage2" - | "reject_stage2" - -// Add this helper function at the top of the component, before the `export default function AdminKycManagementPage()` -// It checks if the given date string is today or in the past. -const isMeetingDatePassedOrToday = (dateString: string | null | undefined) => { - if (!dateString) return false - const meetingDate = new Date(dateString) - meetingDate.setHours(0, 0, 0, 0) // Normalize to start of day - const today = new Date() - today.setHours(0, 0, 0, 0) // Normalize to start of day - return meetingDate <= today +function getRequestDisplayName(request: KycRequest) { + return request.fullName?.trim() || request.name || request.email || "Unknown user" } export default function AdminKycManagementPage() { @@ -84,24 +87,33 @@ export default function AdminKycManagementPage() { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) - // State for confirmation dialog (approve/reject stage 1, complete/reject stage 2) - const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false) + // Filtering states + const [searchQuery, setSearchQuery] = useState("") + const [roleFilter, setRoleFilter] = useState<"all" | "driver" | "investor">("all") + const [statusFilter, setStatusFilter] = useState("all") + const [stageFilter, setStageFilter] = useState<"all" | "stage1" | "stage2">("all") + const [sortOrder, setSortOrder] = useState<"newest" | "oldest">("newest") + + // Selected User review panel const [selectedRequest, setSelectedRequest] = useState(null) - const [actionType, setActionType] = useState(null) + const [auditTrail, setAuditTrail] = useState([]) + const [loadingAudit, setLoadingAudit] = useState(false) + + // Actions and confirmations + const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false) + const [actionType, setActionType] = useState< + "approve_stage1" | "reject_stage1" | "approve_meeting_date" | "reschedule_meeting" | "complete_stage2" | "reject_stage2" | "schedule_initial_meeting" | null + >(null) const [isSubmitting, setIsSubmitting] = useState(false) const [rejectionReasonInput, setRejectionReasonInput] = useState("") + const [meetingDateInput, setMeetingDateInput] = useState("") - // State for document viewer dialog + // Document Viewer Dialog const [isDocumentViewerOpen, setIsDocumentViewerOpen] = useState(false) const [currentDocumentUrl, setCurrentDocumentUrl] = useState(null) const [currentDocumentLabel, setCurrentDocumentLabel] = useState("Selected document") - // State for reschedule meeting dialog - const [isRescheduleDialogOpen, setIsRescheduleDialogOpen] = useState(false) - const [rescheduleDateInput, setRescheduleDateInput] = useState("") - const [rescheduleReasonInput, setRescheduleReasonInput] = useState("") - - const fetchKycRequests = useCallback(async () => { + const fetchKycRequests = useCallback(async (autoSelectId?: string) => { setLoading(true) setError(null) try { @@ -112,6 +124,19 @@ export default function AdminKycManagementPage() { } const data: KycRequest[] = await res.json() setKycRequests(data) + + // Retain or select user + if (autoSelectId) { + const found = data.find((req) => req._id === autoSelectId) + if (found) { + setSelectedRequest(found) + fetchAuditTrail(found._id) + } + } else if (data.length > 0) { + // Don't auto-select to keep layout clean, or auto-select first one. Let's auto-select first one. + setSelectedRequest(data[0]) + fetchAuditTrail(data[0]._id) + } } catch (err: any) { console.error("Error fetching KYC requests:", err) setError(err.message || "An unexpected error occurred.") @@ -120,6 +145,24 @@ export default function AdminKycManagementPage() { } }, []) + const fetchAuditTrail = async (userId: string) => { + setLoadingAudit(true) + try { + const res = await fetch(`/api/admin/kyc-requests/${userId}/audit`) + if (res.ok) { + const data = await res.json() + setAuditTrail(data) + } else { + setAuditTrail([]) + } + } catch (error) { + console.error("Error fetching audit trail:", error) + setAuditTrail([]) + } finally { + setLoadingAudit(false) + } + } + useEffect(() => { if (!authLoading) { if (!authUser || authUser.role !== "admin") { @@ -128,38 +171,45 @@ export default function AdminKycManagementPage() { description: "You must be an admin to view this page.", variant: "destructive", }) - router.replace("/signin") // Redirect non-admins + router.replace("/signin") } else { fetchKycRequests() } } }, [authLoading, authUser, router, toast, fetchKycRequests]) - const handleAction = ( - request: KycRequest, - type: AdminKycManagementPageAction, + const selectRequest = (req: KycRequest) => { + setSelectedRequest(req) + fetchAuditTrail(req._id) + } + + const triggerAction = ( + type: "approve_stage1" | "reject_stage1" | "approve_meeting_date" | "reschedule_meeting" | "complete_stage2" | "reject_stage2" | "schedule_initial_meeting", ) => { - setSelectedRequest(request) setActionType(type) - setRejectionReasonInput("") // Reset rejection reason input - setRescheduleDateInput("") // Reset reschedule date input - setRescheduleReasonInput("") // Reset reschedule reason input - - if (type === "reschedule_meeting") { - setIsRescheduleDialogOpen(true) - } else { - setIsConfirmDialogOpen(true) - } + setRejectionReasonInput("") + setMeetingDateInput("") + setIsConfirmDialogOpen(true) } - const confirmAction = async () => { + const handleConfirmAction = async () => { if (!selectedRequest || !actionType) return - // If rejecting, ensure a reason is provided + // Rejection reason validation if ((actionType === "reject_stage1" || actionType === "reject_stage2") && !rejectionReasonInput.trim()) { toast({ title: "Rejection Reason Required", - description: "Please provide a reason for rejecting the KYC.", + description: "Please provide a reason for rejecting this KYC stage.", + variant: "destructive", + }) + return + } + + // Meeting date validation + if ((actionType === "schedule_initial_meeting" || actionType === "reschedule_meeting") && !meetingDateInput) { + toast({ + title: "Meeting Date Required", + description: "Please select a date for the physical meeting.", variant: "destructive", }) return @@ -167,9 +217,9 @@ export default function AdminKycManagementPage() { setIsSubmitting(true) try { - let newKycStatus: KycRequest["kycStatus"] = selectedRequest.kycStatus - let newPhysicalMeetingStatus: KycRequest["physicalMeetingStatus"] = selectedRequest.physicalMeetingStatus - const newPhysicalMeetingDate: Date | null = selectedRequest.physicalMeetingDate + let newKycStatus = selectedRequest.kycStatus + let newPhysicalMeetingStatus = selectedRequest.physicalMeetingStatus + let newPhysicalMeetingDate = selectedRequest.physicalMeetingDate ? new Date(selectedRequest.physicalMeetingDate) : null let rejectionReason: string | null = null @@ -184,6 +234,7 @@ export default function AdminKycManagementPage() { rejectionReason = rejectionReasonInput.trim() } } else { + // Driver flow switch (actionType) { case "approve_stage1": newKycStatus = "approved_stage1" @@ -192,10 +243,17 @@ export default function AdminKycManagementPage() { newKycStatus = "rejected" rejectionReason = rejectionReasonInput.trim() break + case "schedule_initial_meeting": + newPhysicalMeetingStatus = "approved" + newPhysicalMeetingDate = new Date(meetingDateInput) + break case "approve_meeting_date": newPhysicalMeetingStatus = "approved" break case "reschedule_meeting": + newPhysicalMeetingStatus = "rescheduled" + newPhysicalMeetingDate = new Date(meetingDateInput) + rejectionReason = rejectionReasonInput.trim() || "Rescheduled by admin" break case "complete_stage2": newKycStatus = "approved_stage2" @@ -216,87 +274,33 @@ export default function AdminKycManagementPage() { newKycStatus, selectedRequest.kycDocuments, rejectionReason, - newPhysicalMeetingDate, // Pass existing or updated date - newPhysicalMeetingStatus, // Pass updated physical meeting status + newPhysicalMeetingDate, + newPhysicalMeetingStatus, ) if (res.success) { toast({ - title: "Success", - description: `KYC for ${getRequestDisplayName(selectedRequest)} has been ${getActionLabel(actionType, selectedRequest).toLowerCase()}.`, + title: "Action Successful", + description: `KYC for ${getRequestDisplayName(selectedRequest)} has been updated.`, }) - fetchKycRequests() // Re-fetch to update the list + setIsConfirmDialogOpen(false) + await fetchKycRequests(selectedRequest._id) } else { toast({ title: "Action Failed", - description: res.message || `Failed to ${getActionLabel(actionType, selectedRequest).toLowerCase()}.`, + description: res.message || "Could not update user status.", variant: "destructive", }) } } catch (err) { - console.error(`Error during ${getActionLabel(actionType, selectedRequest)} KYC:`, err) + console.error("Action error:", err) toast({ title: "Error", - description: `An unexpected error occurred during ${getActionLabel(actionType, selectedRequest).toLowerCase()} action.`, + description: "An unexpected error occurred.", variant: "destructive", }) } finally { setIsSubmitting(false) - setIsConfirmDialogOpen(false) - setSelectedRequest(null) - setActionType(null) - setRejectionReasonInput("") // Clear input after action - } - } - - const handleRescheduleSubmit = async () => { - if (!selectedRequest || !rescheduleDateInput || !rescheduleReasonInput.trim()) { - toast({ - title: "Missing Information", - description: "Please select a new date and provide a reason for rescheduling.", - variant: "destructive", - }) - return - } - - setIsSubmitting(true) - try { - const res = await updateUserKycStatus( - selectedRequest._id, - selectedRequest.kycStatus, // Keep current KYC status - selectedRequest.kycDocuments, - rescheduleReasonInput.trim(), - new Date(rescheduleDateInput), // New rescheduled date - "rescheduled", // Set physical meeting status to rescheduled - ) - - if (res.success) { - toast({ - title: "Success", - description: `Physical meeting for ${getRequestDisplayName(selectedRequest)} has been rescheduled to ${new Date(rescheduleDateInput).toLocaleDateString()}.`, - }) - fetchKycRequests() // Re-fetch to update the list - } else { - toast({ - title: "Reschedule Failed", - description: res.message || "Failed to reschedule the meeting.", - variant: "destructive", - }) - } - } catch (err) { - console.error("Error during reschedule:", err) - toast({ - title: "Error", - description: "An unexpected error occurred during reschedule action.", - variant: "destructive", - }) - } finally { - setIsSubmitting(false) - setIsRescheduleDialogOpen(false) - setSelectedRequest(null) - setActionType(null) - setRescheduleDateInput("") - setRescheduleReasonInput("") } } @@ -306,343 +310,678 @@ export default function AdminKycManagementPage() { setIsDocumentViewerOpen(true) } - const getStatusBadgeVariant = (status: KycRequest["kycStatus"]) => { + // Filter queue + const filteredRequests = kycRequests + .filter((req) => { + const search = searchQuery.toLowerCase().trim() + const matchesSearch = + req.name.toLowerCase().includes(search) || + (req.fullName && req.fullName.toLowerCase().includes(search)) || + (req.email && req.email.toLowerCase().includes(search)) || + (req.phoneNumber && req.phoneNumber.includes(search)) + + const matchesRole = roleFilter === "all" || req.role === roleFilter + + const matchesStatus = statusFilter === "all" || req.kycStatus === statusFilter + + let matchesStage = true + if (stageFilter === "stage1") { + matchesStage = ["pending", "approved_stage1", "rejected"].includes(req.kycStatus) && (!req.physicalMeetingStatus || req.physicalMeetingStatus === "none") + } else if (stageFilter === "stage2") { + matchesStage = ["pending_stage2", "approved_stage2", "rejected"].includes(req.kycStatus) && (req.physicalMeetingStatus && req.physicalMeetingStatus !== "none") + } + + return matchesSearch && matchesRole && matchesStatus && matchesStage + }) + .sort((a, b) => { + const timeA = new Date(a.updatedAt).getTime() + const timeB = new Date(b.updatedAt).getTime() + return sortOrder === "newest" ? timeB - timeA : timeA - timeB + }) + + const getStatusBadge = (status: KycRequest["kycStatus"]) => { switch (status) { case "pending": - return "yellow" + return Pending Stage 1 case "approved_stage1": - return "purple" + return Stage 1 Approved case "pending_stage2": - return "blue" + return Pending Stage 2 case "approved_stage2": - return "green" + return Fully Verified case "rejected": - return "red" + return Rejected default: - return "secondary" + return None } } - const getPhysicalMeetingStatusBadgeVariant = (status: KycRequest["physicalMeetingStatus"]) => { + const getMeetingBadge = (status: KycRequest["physicalMeetingStatus"]) => { + if (!status || status === "none") return null switch (status) { case "scheduled": - return "yellow" + return Meeting Scheduled case "approved": - return "green" + return Meeting Date Set case "rescheduled": - return "blue" + return Rescheduled case "completed": - return "green" + return Meeting Completed case "rejected_stage2": - return "red" + return Meeting Rejected default: - return "secondary" + return null } } if (authLoading || (authUser && authUser.role !== "admin")) { return ( -
+
- -

Loading...

-

Please wait while we verify your access.

+ +

Verifying access...

) } return ( - <> - - - KYC Management - - - - Review and manage investor and driver KYC verification requests. - {error && ( -
- - Error -
{error}
-
- )} - {loading ? ( -
- {Array.from({ length: 5 }).map((_, i) => ( - - ))} -
- ) : kycRequests.length === 0 ? ( -
- -

No KYC requests found.

-
- ) : ( -
- - - - User - Role - Email - Phone - KYC Status - Meeting Status - Meeting Date - Documents - Rejection Reason - Actions - - - - {kycRequests.map((request) => ( - - {getRequestDisplayName(request)} - {request.role} - {request.email || "N/A"} - {request.phoneNumber || "N/A"} - - - {request.kycStatus.charAt(0).toUpperCase() + request.kycStatus.slice(1).replace(/_/g, " ")} +
+ {/* Header */} +
+
+

KYC Review Center

+

Review driver & investor profile verifications, stage gates, and audit event logs.

+
+ +
+ +
+ {/* Left column - Queue and Filters */} +
+ + + + Review Queue + + {filteredRequests.length} Active + + + Search and filter incoming verification requests. + + + {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="pl-9 bg-background/50 border-border focus-visible:ring-[#E57700]" + /> +
+ + {/* Filters grid */} +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ + {/* Queue List */} + + +
+ {loading && kycRequests.length === 0 ? ( + Array.from({ length: 4 }).map((_, i) => ( +
+
+
+
+ )) + ) : filteredRequests.length === 0 ? ( +
+ +

No verification requests match your filters.

+
+ ) : ( + filteredRequests.map((req) => { + const isSelected = selectedRequest?._id === req._id + return ( + + ) + }) + )} +
+ + +
+ + {/* Right column - User Review Panel & Timeline */} +
+ {selectedRequest ? ( +
+ {/* Detailed User Review Panel */} + + +
+
+
+ + {getRequestDisplayName(selectedRequest)} + + + {selectedRequest.role} - - - {request.role === "driver" && request.physicalMeetingStatus && request.physicalMeetingStatus !== "none" ? ( - + + Review submissions, verify documents, and record approvals. + +
+
+ {getStatusBadge(selectedRequest.kycStatus)} + {getMeetingBadge(selectedRequest.physicalMeetingStatus)} +
+
+ + + {/* Profile Info Summary */} +
+
+ +
+ Email Address + + {selectedRequest.email || "N/A"} + +
+
+
+ +
+ Phone Number + + {selectedRequest.phoneNumber || "N/A"} + +
+
+
+ +
+ Registered Date + + {new Date(selectedRequest.createdAt).toLocaleString()} + +
+
+
+ +
+ Last State Update + + {new Date(selectedRequest.updatedAt).toLocaleString()} + +
+
+
+ + {/* Documents Section */} +
+

+ + Uploaded Documents +

+ {selectedRequest.kycDocuments && selectedRequest.kycDocuments.length > 0 ? ( +
+ {selectedRequest.kycDocuments.map((doc, idx) => ( +
- {request.physicalMeetingStatus.replace(/_/g, " ")} - - ) : ( - N/A - )} - - - {request.role === "driver" && request.physicalMeetingDate ? ( -
- - {new Date(request.physicalMeetingDate).toLocaleDateString()} +
+
+ +
+ + Document {idx + 1} + +
+
- ) : ( - N/A - )} -
- - {request.kycDocuments && request.kycDocuments.length > 0 ? ( -
- {request.kycDocuments.map((doc, index) => ( - - ))} + ))} +
+ ) : ( +
+ + No documents uploaded yet. +
+ )} +
+ + + + {/* KYC Stage Stepper / State Info */} +
+

+ + Verification Actions +

+ + {/* Pending review (Stage 1) Actions */} + {selectedRequest.kycStatus === "pending" && ( +
+

+ Review documents to approve profile Stage 1. Once approved, {selectedRequest.role === "driver" ? "physical meeting scheduling is unlocked" : "verification is completed"}. +

+
+ + +
+
+ )} + + {/* Approved Stage 1 / Driver physical meeting actions */} + {selectedRequest.role === "driver" && + (selectedRequest.kycStatus === "approved_stage1" || selectedRequest.kycStatus === "pending_stage2") && + (selectedRequest.physicalMeetingStatus === "none" || selectedRequest.physicalMeetingStatus === "rescheduled") && ( +
+

+ Stage 1 has been approved. Please select a physical meeting date to unlock final Stage 2 review. +

+ +
+ )} + + {/* Scheduled meeting review */} + {selectedRequest.role === "driver" && + (selectedRequest.kycStatus === "approved_stage1" || selectedRequest.kycStatus === "pending_stage2") && + selectedRequest.physicalMeetingStatus === "scheduled" && ( +
+
+ +
+ + Requested Physical Meeting Date + + + {selectedRequest.physicalMeetingDate + ? new Date(selectedRequest.physicalMeetingDate).toLocaleDateString("en-NG", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }) + : "No date set"} + +
- ) : ( - N/A - )} - - - {request.kycRejectionReason ? ( - {request.kycRejectionReason} - ) : ( - N/A - )} - - - {request.kycStatus === "pending" && ( -
+
- )} - {request.role === "driver" && - (request.kycStatus === "approved_stage1" || request.kycStatus === "pending_stage2") && - request.physicalMeetingStatus === "scheduled" && ( -
- - +
+ )} + + {/* Stage 2 validation */} + {selectedRequest.role === "driver" && + selectedRequest.kycStatus === "pending_stage2" && + selectedRequest.physicalMeetingStatus === "approved" && ( +
+
+
+ + Physical Meeting Approved
- )} - {request.role === "driver" && request.kycStatus === "pending_stage2" && request.physicalMeetingStatus === "approved" && ( -
+

+ Scheduled Date:{" "} + + {selectedRequest.physicalMeetingDate + ? new Date(selectedRequest.physicalMeetingDate).toLocaleDateString() + : "N/A"} + +

+
+

+ Verify the driver's background check and completed meeting files. Approve to verify their profile, or reject with reason. +

+
+
+ )} + + {/* Rejection State banner */} + {selectedRequest.kycStatus === "rejected" && ( +
+
+ + KYC Rejected +
+ {selectedRequest.kycRejectionReason && ( +
+ Reason given: +

+ {selectedRequest.kycRejectionReason} +

+
)} - {/* Display "No action needed" for other statuses */} - {!( - request.kycStatus === "pending" || - (request.role === "driver" && - (request.kycStatus === "approved_stage1" || request.kycStatus === "pending_stage2") && - request.physicalMeetingStatus === "scheduled") || - (request.role === "driver" && - request.kycStatus === "pending_stage2" && - request.physicalMeetingStatus === "approved" && - isMeetingDatePassedOrToday(request.physicalMeetingDate)) - ) && No action needed} - - - ))} - -
+ + The user has been notified. They can resubmit updated documents from their dashboard. + +
+ )} + + {/* Fully Approved banner */} + {selectedRequest.kycStatus === "approved_stage2" && ( +
+
+ + Profile Fully Verified +
+ + KYC verification completed successfully. The user now has full functional access to the platform. + +
+ )} +
+ + + + {/* Dynamic timeline / audit trail */} + + +
+ + + Decision History & Audit Trail + + + Verifiable log of admin decisions and system events for this profile. + +
+ +
+ + {loadingAudit ? ( +
+ + +
+ ) : auditTrail.length === 0 ? ( +
+ +

No admin decisions or audit logs found for this user.

+
+ ) : ( +
+ {auditTrail.map((log) => ( +
+ {/* Indicator bullet */} + + + + +
+ + {log.action.replace(/\./g, " ").toUpperCase()} + + + {new Date(log.createdAt).toLocaleString()} + +
+ +
+
+ + Actor: {log.actorRole || "System"} + + + Status: {log.status} + +
+ {log.metadata && ( +
+                                {JSON.stringify(log.metadata, null, 2)}
+                              
+ )} +
+
+ ))} +
+ )} +
+
+ ) : ( + + +

Select a user to review

+

+ Click on any user in the left queue to view document links, make actions, and examine the decision trail. +

+
)} - - + + - {/* Confirmation Dialog (for approve/reject stage 1, approve/complete/reject stage 2) */} + {/* Confirmation & Inputs Dialog */} - + - Confirm {getActionLabel(actionType, selectedRequest)} + + {actionType?.replace(/_/g, " ")} + - Are you sure you want to {getActionLabel(actionType, selectedRequest).toLowerCase()} for{" "} - {selectedRequest ? getRequestDisplayName(selectedRequest) : "this user"}? This action cannot be undone. + Confirm this KYC change for{" "} + + {selectedRequest ? getRequestDisplayName(selectedRequest) : "the user"} + + . - {(actionType === "reject_stage1" || actionType === "reject_stage2") && ( -
- + + {/* Date Picker Input */} + {(actionType === "schedule_initial_meeting" || actionType === "reschedule_meeting") && ( +
+ + setMeetingDateInput(e.target.value)} + className="bg-background border-border text-foreground text-xs" + /> +
+ )} + + {/* Rejection / Reschedule Reason Input */} + {(actionType === "reject_stage1" || + actionType === "reject_stage2" || + actionType === "reschedule_meeting") && ( +
+