Implement KYC Review Sheet with document list and verify/reject actions
This commit is contained in:
@@ -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(),
|
||||
}
|
||||
];
|
||||
|
||||
130
src/features/partners/components/KYCReviewSheet.tsx
Normal file
130
src/features/partners/components/KYCReviewSheet.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user