Files
eventify_command_center/src/features/partners/PartnerProfile.tsx

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>
);
}