Implement KYC Review Sheet with document list and verify/reject actions

This commit is contained in:
CycroftX
2026-02-03 22:43:48 +05:30
parent 69c6db945a
commit 532ab07961
3 changed files with 242 additions and 64 deletions

View File

@@ -200,5 +200,35 @@ export const mockDocuments: PartnerDocument[] = [
status: 'Verified',
uploadedBy: 'Alex Rivera',
uploadedAt: subMonths(new Date(), 6).toISOString(),
},
{
id: 'doc3',
partnerId: 'p5',
type: 'Compliance',
name: 'Company Registration (Pending)',
url: '#',
status: 'Pending',
uploadedBy: 'John Doe',
uploadedAt: subDays(new Date(), 1).toISOString(),
},
{
id: 'doc4',
partnerId: 'p5',
type: 'Tax',
name: 'PAN Card Copy',
url: '#',
status: 'Pending',
uploadedBy: 'John Doe',
uploadedAt: subDays(new Date(), 1).toISOString(),
},
{
id: 'doc5',
partnerId: 'p5',
type: 'Other',
name: 'Cancelled Cheque',
url: '#',
status: 'Pending',
uploadedBy: 'John Doe',
uploadedAt: subDays(new Date(), 1).toISOString(),
}
];

View File

@@ -0,0 +1,130 @@
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetFooter,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";
import { Partner, PartnerDocument } from "@/types/partner";
import { mockDocuments } from "@/data/mockPartnerData";
import { FileText, CheckCircle2, XCircle, Clock, Eye, Download } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import { useState } from "react";
interface KYCReviewSheetProps {
partner: Partner;
open: boolean;
onOpenChange: (open: boolean) => void;
}
export function KYCReviewSheet({ partner, open, onOpenChange }: KYCReviewSheetProps) {
// Filter documents for this partner or use defaults if none found (for demo)
const partnerDocs = mockDocuments.filter(d => d.partnerId === partner.id);
const documentsToDisplay = partnerDocs.length > 0 ? partnerDocs : mockDocuments.slice(0, 3);
const [status, setStatus] = useState<'pending' | 'approved' | 'rejected'>('pending');
const handleApprove = () => {
setStatus('approved');
toast.success(`KYC documents for ${partner.name} verified successfully.`);
setTimeout(() => onOpenChange(false), 1500);
};
const handleReject = () => {
setStatus('rejected');
toast.error(`KYC documents for ${partner.name} rejected. Partner notified.`);
setTimeout(() => onOpenChange(false), 1500);
};
const getStatusIcon = (status: PartnerDocument['status']) => {
switch (status) {
case 'Verified': return <CheckCircle2 className="h-4 w-4 text-success" />;
case 'Rejected': return <XCircle className="h-4 w-4 text-error" />;
case 'Signed': return <CheckCircle2 className="h-4 w-4 text-blue-500" />;
default: return <Clock className="h-4 w-4 text-warning" />;
}
};
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="w-[400px] sm:w-[540px] overflow-y-auto">
<SheetHeader className="mb-6">
<div className="flex items-center gap-3 mb-2">
<div className="h-10 w-10 rounded-lg bg-orange-500/10 flex items-center justify-center text-orange-600 font-bold">
KYC
</div>
<div>
<SheetTitle>Verification Review</SheetTitle>
<SheetDescription>Review submitted documents for {partner.name}</SheetDescription>
</div>
</div>
</SheetHeader>
<div className="space-y-6">
<div className="rounded-lg border border-border bg-card p-4">
<h4 className="text-sm font-semibold mb-3">Submitted Documents</h4>
<div className="space-y-3">
{documentsToDisplay.map((doc) => (
<div key={doc.id} className="flex items-start gap-3 p-3 rounded-lg bg-secondary/20 hover:bg-secondary/40 transition-colors border border-transparent hover:border-border/50 group">
<div className="mt-1">
<FileText className="h-5 w-5 text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<p className="font-medium text-sm truncate pr-2">{doc.name}</p>
{getStatusIcon(doc.status)}
</div>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>{doc.type} {new Date(doc.uploadedAt).toLocaleDateString()}</span>
<Badge variant="outline" className="text-[10px] h-5 px-1.5 font-normal">
{doc.status}
</Badge>
</div>
</div>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-foreground">
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-foreground">
<Download className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</div>
<div className="space-y-3">
<h4 className="text-sm font-semibold">Verification Decision</h4>
<div className="flex gap-3">
<Button
className="flex-1 bg-success hover:bg-success/90 text-white gap-2"
onClick={handleApprove}
disabled={status !== 'pending'}
>
<CheckCircle2 className="h-4 w-4" />
Approve & Verify
</Button>
<Button
variant="destructive"
className="flex-1 gap-2"
onClick={handleReject}
disabled={status !== 'pending'}
>
<XCircle className="h-4 w-4" />
Reject & Request Changes
</Button>
</div>
<p className="text-xs text-muted-foreground text-center pt-2">
Actions are logged for audit purposes.
</p>
</div>
</div>
</SheetContent>
</Sheet>
);
}

View File

@@ -9,6 +9,8 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { useNavigate } from 'react-router-dom';
import { useState } from 'react';
import { KYCReviewSheet } from './KYCReviewSheet';
interface PartnerCardProps {
partner: Partner;
@@ -16,78 +18,94 @@ interface PartnerCardProps {
export function PartnerCard({ partner }: PartnerCardProps) {
const navigate = useNavigate();
const [showKYCSheet, setShowKYCSheet] = useState(false);
return (
<div
className="group relative neu-card p-5 hover:border-accent/50 transition-all duration-300 cursor-pointer"
onClick={() => navigate(`/partners/${partner.id}`)}
>
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-3">
<div className="h-12 w-12 rounded-xl bg-secondary flex items-center justify-center overflow-hidden border border-border/50">
{partner.logo ? (
<img src={partner.logo} alt={partner.name} className="h-full w-full object-cover" />
) : (
<span className="text-lg font-bold text-muted-foreground">{partner.name.substring(0, 2)}</span>
)}
</div>
<div>
<h3 className="font-bold text-foreground group-hover:text-accent transition-colors">{partner.name}</h3>
<div className="flex gap-2 mt-1">
<TypeBadge type={partner.type} />
<StatusBadge status={partner.status} />
<>
<div
className="group relative neu-card p-5 hover:border-accent/50 transition-all duration-300 cursor-pointer"
onClick={() => navigate(`/partners/${partner.id}`)}
>
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-3">
<div className="h-12 w-12 rounded-xl bg-secondary flex items-center justify-center overflow-hidden border border-border/50">
{partner.logo ? (
<img src={partner.logo} alt={partner.name} className="h-full w-full object-cover" />
) : (
<span className="text-lg font-bold text-muted-foreground">{partner.name.substring(0, 2)}</span>
)}
</div>
<div>
<h3 className="font-bold text-foreground group-hover:text-accent transition-colors">{partner.name}</h3>
<div className="flex gap-2 mt-1">
<TypeBadge type={partner.type} />
<StatusBadge status={partner.status} />
</div>
</div>
</div>
<div onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => navigate(`/partners/${partner.id}`)}>View Details</DropdownMenuItem>
<DropdownMenuItem>Assign to Event</DropdownMenuItem>
{partner.status === 'Suspended' ? (
<DropdownMenuItem className="text-success font-medium">Revoke Suspension</DropdownMenuItem>
) : (
<DropdownMenuItem className="text-error">Suspend Partner</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => navigate(`/partners/${partner.id}`)}>View Details</DropdownMenuItem>
<DropdownMenuItem>Assign to Event</DropdownMenuItem>
{partner.status === 'Suspended' ? (
<DropdownMenuItem className="text-success font-medium">Revoke Suspension</DropdownMenuItem>
) : (
<DropdownMenuItem className="text-error">Suspend Partner</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
{partner.verificationStatus === 'Pending' && (
<div className="mb-4 bg-warning/10 text-warning px-3 py-1.5 rounded-md text-xs font-medium flex items-center justify-between" onClick={(e) => e.stopPropagation()}>
<span>KYC Verification Pending</span>
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-warning hover:text-warning hover:bg-warning/20"
onClick={() => setShowKYCSheet(true)}
>
Review
</Button>
</div>
)}
<div className="grid grid-cols-2 gap-4 mt-6 py-4 border-t border-border/30">
<div>
<p className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
<Calendar className="h-3 w-3" /> Active Deals
</p>
<p className="font-semibold text-foreground">{partner.metrics.activeDeals}</p>
</div>
<div>
<p className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
<Wallet className="h-3 w-3" /> Open Balance
</p>
<p className="font-semibold text-foreground">{partner.metrics.openBalance.toLocaleString()}</p>
</div>
</div>
<div className="mt-4 flex items-center justify-between text-xs text-muted-foreground">
<span>{partner.primaryContact.name}</span>
<span className="flex items-center gap-1 hover:text-accent">
View Portal <ExternalLink className="h-3 w-3" />
</span>
</div>
</div>
{partner.verificationStatus === 'Pending' && (
<div className="mb-4 bg-warning/10 text-warning px-3 py-1.5 rounded-md text-xs font-medium flex items-center justify-between">
<span>KYC Verification Pending</span>
<Button variant="ghost" size="sm" className="h-6 px-2 text-warning hover:text-warning hover:bg-warning/20">Review</Button>
</div>
)}
<div className="grid grid-cols-2 gap-4 mt-6 py-4 border-t border-border/30">
<div>
<p className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
<Calendar className="h-3 w-3" /> Active Deals
</p>
<p className="font-semibold text-foreground">{partner.metrics.activeDeals}</p>
</div>
<div>
<p className="text-xs text-muted-foreground mb-1 flex items-center gap-1">
<Wallet className="h-3 w-3" /> Open Balance
</p>
<p className="font-semibold text-foreground">{partner.metrics.openBalance.toLocaleString()}</p>
</div>
</div>
<div className="mt-4 flex items-center justify-between text-xs text-muted-foreground">
<span>{partner.primaryContact.name}</span>
<span className="flex items-center gap-1 hover:text-accent">
View Portal <ExternalLink className="h-3 w-3" />
</span>
</div>
</div>
<KYCReviewSheet
partner={partner}
open={showKYCSheet}
onOpenChange={setShowKYCSheet}
/>
</>
);
}