398 lines
22 KiB
TypeScript
398 lines
22 KiB
TypeScript
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { AppLayout } from '@/components/layout/AppLayout';
|
|
import { mockPartners, mockDealTerms, mockLedger, mockKYCDocuments, mockPartnerEvents } from '@/data/mockPartnerData';
|
|
import { getRiskLevel } from '@/types/partner';
|
|
import { StatusBadge, TypeBadge } from './components/PartnerBadges';
|
|
import { KYCVaultPanel } from './components/KYCVaultPanel';
|
|
import { EventApprovalQueue } from './components/EventApprovalQueue';
|
|
import { ImpersonationDialog } from './components/ImpersonationDialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionTrigger,
|
|
} from '@/components/ui/accordion';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from '@/components/ui/alert-dialog';
|
|
import {
|
|
Mail,
|
|
Phone,
|
|
ArrowLeft,
|
|
LogIn,
|
|
KeyRound,
|
|
ShieldOff,
|
|
Ban,
|
|
UserCheck,
|
|
Calendar,
|
|
Wallet,
|
|
TrendingUp,
|
|
AlertTriangle,
|
|
ExternalLink,
|
|
FileSignature,
|
|
DollarSign,
|
|
} from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { cn } from '@/lib/utils';
|
|
import {
|
|
resetPartner2FA,
|
|
resetPartnerPassword,
|
|
suspendPartner,
|
|
unsuspendPartner,
|
|
} from '@/lib/actions/partner-governance';
|
|
import { useState } from 'react';
|
|
|
|
export default function PartnerProfile() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const partner = mockPartners.find(p => p.id === id);
|
|
|
|
const [partnerStatus, setPartnerStatus] = useState(partner?.status || 'Active');
|
|
|
|
if (!partner) {
|
|
return (
|
|
<AppLayout title="Partner Not Found">
|
|
<div className="flex flex-col items-center justify-center py-20">
|
|
<p className="text-lg text-muted-foreground mb-4">Partner not found.</p>
|
|
<Button onClick={() => navigate('/partners')}>
|
|
<ArrowLeft className="h-4 w-4 mr-2" /> Back to Partners
|
|
</Button>
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
}
|
|
|
|
const kycDocs = mockKYCDocuments.filter(d => d.partnerId === partner.id);
|
|
const partnerEvents = mockPartnerEvents.filter(e => e.partnerId === partner.id);
|
|
const dealTerms = mockDealTerms.filter(d => d.partnerId === partner.id);
|
|
const ledger = mockLedger.filter(l => l.partnerId === partner.id);
|
|
const riskLevel = getRiskLevel(partner.riskScore);
|
|
|
|
const handleReset2FA = async () => {
|
|
const result = await resetPartner2FA(partner.id);
|
|
if (result.success) toast.success(result.message);
|
|
else toast.error(result.message);
|
|
};
|
|
|
|
const handleResetPassword = async () => {
|
|
const result = await resetPartnerPassword(partner.id);
|
|
if (result.success) toast.success(result.message);
|
|
else toast.error(result.message);
|
|
};
|
|
|
|
const handleSuspend = async () => {
|
|
const result = await suspendPartner(partner.id, 'Suspended by admin from profile page');
|
|
if (result.success) {
|
|
toast.success(result.message);
|
|
setPartnerStatus('Suspended');
|
|
} else {
|
|
toast.error(result.message);
|
|
}
|
|
};
|
|
|
|
const handleUnsuspend = async () => {
|
|
const result = await unsuspendPartner(partner.id);
|
|
if (result.success) {
|
|
toast.success(result.message);
|
|
setPartnerStatus('Active');
|
|
} else {
|
|
toast.error(result.message);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AppLayout title={partner.name}>
|
|
{/* Header */}
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<Button variant="ghost" size="icon" onClick={() => navigate('/partners')} className="shrink-0">
|
|
<ArrowLeft className="h-4 w-4" />
|
|
</Button>
|
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
<div className="h-12 w-12 rounded-xl bg-secondary flex items-center justify-center overflow-hidden border border-border/50 shrink-0">
|
|
{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 className="min-w-0">
|
|
<h1 className="text-xl font-bold truncate">{partner.name}</h1>
|
|
<div className="flex items-center gap-2 mt-0.5">
|
|
<TypeBadge type={partner.type} />
|
|
<StatusBadge status={partnerStatus as any} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 3-Column Layout */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
|
{/* ── Column 1: Identity & Stats ───────────────────────────── */}
|
|
<div className="space-y-4">
|
|
{/* Contact Card */}
|
|
<div className="neu-card p-5 space-y-4">
|
|
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Contact</h3>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<div className="h-8 w-8 rounded-full bg-primary/10 flex items-center justify-center">
|
|
<span className="text-xs font-bold text-primary">{partner.primaryContact.name.substring(0, 2)}</span>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm font-medium">{partner.primaryContact.name}</p>
|
|
<p className="text-xs text-muted-foreground">{partner.primaryContact.role || 'Contact'}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Mail className="h-3.5 w-3.5" /> {partner.primaryContact.email}
|
|
</div>
|
|
{partner.primaryContact.phone && (
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Phone className="h-3.5 w-3.5" /> {partner.primaryContact.phone}
|
|
</div>
|
|
)}
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Calendar className="h-3.5 w-3.5" /> Joined {new Date(partner.joinedAt).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Stats */}
|
|
<div className="neu-card p-5">
|
|
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">Stats</h3>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="p-3 rounded-lg bg-secondary/30 border border-border/30">
|
|
<p className="text-xs text-muted-foreground flex items-center gap-1"><Wallet className="h-3 w-3" /> Revenue</p>
|
|
<p className="font-bold text-lg mt-1">₹{partner.metrics.totalRevenue.toLocaleString()}</p>
|
|
</div>
|
|
<div className="p-3 rounded-lg bg-secondary/30 border border-border/30">
|
|
<p className="text-xs text-muted-foreground flex items-center gap-1"><TrendingUp className="h-3 w-3" /> Events</p>
|
|
<p className="font-bold text-lg mt-1">{partner.metrics.eventsCount}</p>
|
|
</div>
|
|
<div className="p-3 rounded-lg bg-secondary/30 border border-border/30">
|
|
<p className="text-xs text-muted-foreground flex items-center gap-1"><DollarSign className="h-3 w-3" /> Open Bal.</p>
|
|
<p className="font-bold text-lg mt-1">₹{partner.metrics.openBalance.toLocaleString()}</p>
|
|
</div>
|
|
<div className={cn(
|
|
'p-3 rounded-lg border',
|
|
riskLevel === 'low' ? 'bg-success/5 border-success/20' :
|
|
riskLevel === 'medium' ? 'bg-warning/5 border-warning/20' :
|
|
'bg-destructive/5 border-destructive/20'
|
|
)}>
|
|
<p className="text-xs text-muted-foreground flex items-center gap-1"><AlertTriangle className="h-3 w-3" /> Risk</p>
|
|
<p className={cn(
|
|
'font-bold text-lg mt-1',
|
|
riskLevel === 'low' ? 'text-success' :
|
|
riskLevel === 'medium' ? 'text-warning' :
|
|
'text-destructive'
|
|
)}>{partner.riskScore}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Admin Actions */}
|
|
<div className="neu-card p-5 space-y-3">
|
|
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">Admin Actions</h3>
|
|
|
|
<ImpersonationDialog partnerId={partner.id} partnerName={partner.name}>
|
|
<Button variant="outline" className="w-full justify-start gap-2 h-9 text-sm">
|
|
<LogIn className="h-4 w-4 text-warning" /> Login as Partner
|
|
<ExternalLink className="h-3 w-3 ml-auto text-muted-foreground" />
|
|
</Button>
|
|
</ImpersonationDialog>
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button variant="outline" className="w-full justify-start gap-2 h-9 text-sm">
|
|
<KeyRound className="h-4 w-4 text-blue-400" /> Reset Password
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Reset Password</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
This will send a password reset email to {partner.primaryContact.email}. This action is logged.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleResetPassword}>Send Reset Email</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button variant="outline" className="w-full justify-start gap-2 h-9 text-sm">
|
|
<ShieldOff className="h-4 w-4 text-orange-400" /> Reset 2FA
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Reset Two-Factor Authentication</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
This will remove {partner.name}'s 2FA setup. They will be required to re-enroll on their next login. This action is logged.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleReset2FA}>Reset 2FA</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
{partnerStatus === 'Suspended' ? (
|
|
<Button
|
|
variant="outline"
|
|
className="w-full justify-start gap-2 h-9 text-sm text-success border-success/30 hover:bg-success/10"
|
|
onClick={handleUnsuspend}
|
|
>
|
|
<UserCheck className="h-4 w-4" /> Revoke Suspension
|
|
</Button>
|
|
) : (
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button variant="outline" className="w-full justify-start gap-2 h-9 text-sm text-destructive border-destructive/30 hover:bg-destructive/10">
|
|
<Ban className="h-4 w-4" /> Suspend Partner
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Suspend Partner</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
This will suspend {partner.name}'s account. They will be unable to access their dashboard or manage events. This action is logged.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction onClick={handleSuspend} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
|
|
Suspend
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
)}
|
|
</div>
|
|
|
|
{/* Deal Terms & Finance Accordion */}
|
|
<Accordion type="multiple" className="neu-card overflow-hidden">
|
|
<AccordionItem value="deals" className="border-b-0 px-5">
|
|
<AccordionTrigger className="text-sm font-semibold hover:no-underline">
|
|
<span className="flex items-center gap-2">
|
|
<FileSignature className="h-4 w-4 text-muted-foreground" /> Deal Terms
|
|
<Badge variant="secondary" className="text-[10px] h-4 px-1">{dealTerms.length}</Badge>
|
|
</span>
|
|
</AccordionTrigger>
|
|
<AccordionContent>
|
|
{dealTerms.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{dealTerms.map(dt => (
|
|
<div key={dt.id} className="p-3 rounded-lg bg-secondary/20 border border-border/30">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-sm font-medium">{dt.name}</p>
|
|
<Badge variant="outline" className={cn('text-[10px]',
|
|
dt.status === 'Active' ? 'bg-success/10 text-success border-success/20' :
|
|
dt.status === 'Draft' ? 'bg-muted text-muted-foreground' :
|
|
'bg-warning/10 text-warning border-warning/20'
|
|
)}>
|
|
{dt.status}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{dt.type} • {dt.params.percentage ? `${dt.params.percentage}%` : `₹${dt.params.amount}`}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-xs text-muted-foreground text-center py-3">No deals configured</p>
|
|
)}
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
<AccordionItem value="finance" className="border-b-0 px-5">
|
|
<AccordionTrigger className="text-sm font-semibold hover:no-underline">
|
|
<span className="flex items-center gap-2">
|
|
<Wallet className="h-4 w-4 text-muted-foreground" /> Finance Ledger
|
|
<Badge variant="secondary" className="text-[10px] h-4 px-1">{ledger.length}</Badge>
|
|
</span>
|
|
</AccordionTrigger>
|
|
<AccordionContent>
|
|
{ledger.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{ledger.map(entry => (
|
|
<div key={entry.id} className="p-3 rounded-lg bg-secondary/20 border border-border/30 flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm font-medium">{entry.description}</p>
|
|
<p className="text-xs text-muted-foreground">{new Date(entry.createdAt).toLocaleDateString()} • {entry.type}</p>
|
|
</div>
|
|
<p className={cn('font-semibold text-sm', entry.amount >= 0 ? 'text-success' : 'text-destructive')}>
|
|
{entry.amount >= 0 ? '+' : ''}₹{Math.abs(entry.amount).toLocaleString()}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-xs text-muted-foreground text-center py-3">No ledger entries</p>
|
|
)}
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
</Accordion>
|
|
</div>
|
|
|
|
{/* ── Column 2: KYC Vault ──────────────────────────────────── */}
|
|
<div className="neu-card p-5">
|
|
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-4">
|
|
KYC & Compliance
|
|
</h3>
|
|
<KYCVaultPanel
|
|
partnerId={partner.id}
|
|
partnerName={partner.name}
|
|
verificationStatus={partner.verificationStatus}
|
|
documents={kycDocs}
|
|
/>
|
|
</div>
|
|
|
|
{/* ── Column 3: Event Governance ──────────────────────────── */}
|
|
<div className="neu-card p-5">
|
|
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-4">
|
|
Event Governance
|
|
</h3>
|
|
<EventApprovalQueue
|
|
partnerId={partner.id}
|
|
events={partnerEvents}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tags */}
|
|
{partner.tags && partner.tags.length > 0 && (
|
|
<div className="mt-6 flex items-center gap-2 flex-wrap">
|
|
{partner.tags.map(tag => (
|
|
<Badge key={tag} variant="secondary" className="text-xs">{tag}</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Notes */}
|
|
{partner.notes && (
|
|
<div className="mt-4 p-4 bg-warning/5 border border-warning/20 rounded-lg">
|
|
<p className="text-sm text-warning font-medium flex items-center gap-2">
|
|
<AlertTriangle className="h-4 w-4" /> Notes
|
|
</p>
|
|
<p className="text-sm text-muted-foreground mt-1">{partner.notes}</p>
|
|
</div>
|
|
)}
|
|
</AppLayout>
|
|
);
|
|
}
|