import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { supabase } from '@/lib/supabase'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog'; import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { ScrollArea } from '@/components/ui/scroll-area'; import { toast } from 'sonner'; import { formatAddress, formatDate } from '@pezkuwi/utils/formatting'; import { AlertTriangle, CheckCircle2, Clock, Eye, FileText, Gavel, Image as ImageIcon, Loader2, RefreshCw, Scale, Shield, User, XCircle, ExternalLink, Download } from 'lucide-react'; // Types interface Dispute { id: string; trade_id: string; opened_by: string; reason: string; category: string; evidence_urls: string[]; status: 'open' | 'under_review' | 'resolved' | 'escalated' | 'closed'; decision?: string; decision_reasoning?: string; assigned_moderator_id?: string; assigned_at?: string; resolved_at?: string; created_at: string; updated_at: string; // Joined data trade?: Trade; opener?: UserProfile; evidence?: Evidence[]; } interface Trade { id: string; offer_id: string; seller_id: string; buyer_id: string; crypto_amount: number; fiat_amount: number; status: string; created_at: string; seller?: UserProfile; buyer?: UserProfile; } interface UserProfile { id: string; username?: string; wallet_address?: string; } interface Evidence { id: string; dispute_id: string; uploaded_by: string; evidence_type: string; file_url: string; file_name?: string; description?: string; created_at: string; is_valid?: boolean; review_notes?: string; } // Decision option values - labels are translated via t() in the component const DECISION_OPTION_KEYS = [ { value: 'release_to_buyer', labelKey: 'dispute.releaseToBuyer' }, { value: 'refund_to_seller', labelKey: 'dispute.refundToSeller' }, { value: 'split', labelKey: 'dispute.split' }, { value: 'escalate', labelKey: 'dispute.escalate' }, ]; // Status badge colors const STATUS_COLORS: Record = { open: 'bg-yellow-500/20 text-yellow-500 border-yellow-500/30', under_review: 'bg-blue-500/20 text-blue-500 border-blue-500/30', resolved: 'bg-green-500/20 text-green-500 border-green-500/30', escalated: 'bg-purple-500/20 text-purple-500 border-purple-500/30', closed: 'bg-gray-500/20 text-gray-400 border-gray-500/30' }; // Category translation keys const CATEGORY_KEYS: Record = { payment_not_received: 'dispute.categoryPaymentNotReceived', wrong_amount: 'dispute.categoryWrongAmount', fake_payment_proof: 'dispute.categoryFakePaymentProof', seller_not_responding: 'dispute.categorySellerNotResponding', buyer_not_responding: 'dispute.categoryBuyerNotResponding', fraudulent_behavior: 'dispute.categoryFraudulentBehavior', other: 'dispute.categoryOther' }; export function DisputeResolutionPanel() { const { t } = useTranslation(); const [disputes, setDisputes] = useState([]); const [loading, setLoading] = useState(true); const [selectedDispute, setSelectedDispute] = useState(null); const [detailsOpen, setDetailsOpen] = useState(false); const [resolveOpen, setResolveOpen] = useState(false); const [activeTab, setActiveTab] = useState('open'); const [decision, setDecision] = useState(''); const [reasoning, setReasoning] = useState(''); const [submitting, setSubmitting] = useState(false); const [lightboxImage, setLightboxImage] = useState(null); // Fetch disputes const fetchDisputes = async () => { setLoading(true); try { const { data, error } = await supabase .from('p2p_fiat_disputes') .select(` *, trade:p2p_fiat_trades( id, offer_id, seller_id, buyer_id, crypto_amount, fiat_amount, status, created_at ) `) .order('created_at', { ascending: false }); if (error) throw error; // Fetch evidence for each dispute const disputesWithEvidence = await Promise.all( (data || []).map(async (dispute) => { const { data: evidence } = await supabase .from('p2p_dispute_evidence') .select('*') .eq('dispute_id', dispute.id) .order('created_at', { ascending: true }); return { ...dispute, evidence: evidence || [] }; }) ); setDisputes(disputesWithEvidence); } catch (error) { console.error('Error fetching disputes:', error); toast.error(t('dispute.loadFailed')); } finally { setLoading(false); } }; useEffect(() => { fetchDisputes(); // Subscribe to real-time updates const channel = supabase .channel('admin-disputes') .on('postgres_changes', { event: '*', schema: 'public', table: 'p2p_fiat_disputes' }, () => { fetchDisputes(); }) .subscribe(); return () => { supabase.removeChannel(channel); }; }, []); // Filter disputes by status const filteredDisputes = disputes.filter(d => { if (activeTab === 'open') return d.status === 'open'; if (activeTab === 'under_review') return d.status === 'under_review'; if (activeTab === 'resolved') return ['resolved', 'closed'].includes(d.status); if (activeTab === 'escalated') return d.status === 'escalated'; return true; }); // Claim dispute for review const claimDispute = async (disputeId: string) => { try { const { data: { user } } = await supabase.auth.getUser(); if (!user) throw new Error('Not authenticated'); const { error } = await supabase .from('p2p_fiat_disputes') .update({ status: 'under_review', assigned_moderator_id: user.id, assigned_at: new Date().toISOString() }) .eq('id', disputeId); if (error) throw error; toast.success(t('dispute.claimedToast')); fetchDisputes(); } catch (error) { console.error('Error claiming dispute:', error); toast.error(t('dispute.claimFailed')); } }; // Resolve dispute const resolveDispute = async () => { if (!selectedDispute || !decision || !reasoning) { toast.error(t('dispute.noDecision')); return; } setSubmitting(true); try { const { data: { user } } = await supabase.auth.getUser(); if (!user) throw new Error('Not authenticated'); // Update dispute const { error: disputeError } = await supabase .from('p2p_fiat_disputes') .update({ status: decision === 'escalate' ? 'escalated' : 'resolved', decision, decision_reasoning: reasoning, resolved_at: new Date().toISOString() }) .eq('id', selectedDispute.id); if (disputeError) throw disputeError; // Update trade status based on decision if (decision !== 'escalate' && selectedDispute.trade) { const tradeStatus = decision === 'release_to_buyer' ? 'completed' : 'refunded'; await supabase .from('p2p_fiat_trades') .update({ status: tradeStatus }) .eq('id', selectedDispute.trade_id); } // Create notifications for both parties if (selectedDispute.trade) { const notificationPromises = [ supabase.rpc('create_p2p_notification', { p_user_id: selectedDispute.trade.seller_id, p_type: 'dispute_resolved', p_title: 'Dispute Resolved', p_message: `The dispute has been resolved: ${t(DECISION_OPTION_KEYS.find(o => o.value === decision)?.labelKey || '')}`, p_reference_type: 'dispute', p_reference_id: selectedDispute.id }), supabase.rpc('create_p2p_notification', { p_user_id: selectedDispute.trade.buyer_id, p_type: 'dispute_resolved', p_title: 'Dispute Resolved', p_message: `The dispute has been resolved: ${t(DECISION_OPTION_KEYS.find(o => o.value === decision)?.labelKey || '')}`, p_reference_type: 'dispute', p_reference_id: selectedDispute.id }) ]; await Promise.all(notificationPromises); } toast.success(t('dispute.resolvedToast')); setResolveOpen(false); setSelectedDispute(null); setDecision(''); setReasoning(''); fetchDisputes(); } catch (error) { console.error('Error resolving dispute:', error); toast.error(t('dispute.resolveFailed')); } finally { setSubmitting(false); } }; // Open details modal const openDetails = (dispute: Dispute) => { setSelectedDispute(dispute); setDetailsOpen(true); }; // Open resolve modal const openResolve = (dispute: Dispute) => { setSelectedDispute(dispute); setResolveOpen(true); }; // Stats const stats = { open: disputes.filter(d => d.status === 'open').length, under_review: disputes.filter(d => d.status === 'under_review').length, resolved: disputes.filter(d => ['resolved', 'closed'].includes(d.status)).length, escalated: disputes.filter(d => d.status === 'escalated').length }; return (
{/* Header */}

{t('dispute.title')}

{t('dispute.subtitle')}

{/* Stats Cards */}

{t('dispute.statsOpen')}

{stats.open}

{t('dispute.statsUnderReview')}

{stats.under_review}

{t('dispute.statsResolved')}

{stats.resolved}

{t('dispute.statsEscalated')}

{stats.escalated}

{/* Disputes Tabs */} {t('dispute.statsOpen')} {stats.open > 0 && ( {stats.open} )} {t('dispute.tabInReview')} {t('dispute.statsResolved')} {t('dispute.statsEscalated')} {loading ? ( ) : filteredDisputes.length === 0 ? (

{t('dispute.empty')}

) : (
{filteredDisputes.map((dispute) => (
{dispute.status.replace('_', ' ').toUpperCase()} {t(CATEGORY_KEYS[dispute.category] || dispute.category)} {dispute.evidence && dispute.evidence.length > 0 && ( {t('dispute.evidence', { count: dispute.evidence.length })} )}

{dispute.reason}

Trade: {formatAddress(dispute.trade_id)} {formatDate(dispute.created_at)} {dispute.trade && ( {dispute.trade.crypto_amount} crypto )}
{dispute.status === 'open' && ( )} {dispute.status === 'under_review' && ( )}
))}
)}
{/* Details Modal */} {t('dispute.detailsTitle')} {t('dispute.detailsDesc')} {selectedDispute && (
{/* Status & Category */}
{selectedDispute.status.replace('_', ' ').toUpperCase()} {t(CATEGORY_KEYS[selectedDispute.category] || selectedDispute.category)}
{/* Reason */}

{t('dispute.reason')}

{selectedDispute.reason}

{/* Trade Info */} {selectedDispute.trade && (

{t('dispute.tradeInfo')}

{t('dispute.tradeId')}: {formatAddress(selectedDispute.trade_id)}
{t('dispute.amount')}: {selectedDispute.trade.crypto_amount} crypto
{t('dispute.fiat')}: {selectedDispute.trade.fiat_amount}
{t('dispute.tradeStatus')}: {selectedDispute.trade.status}
)} {/* Parties */} {selectedDispute.trade && (

{t('dispute.parties')}

{t('dispute.seller')}

{formatAddress(selectedDispute.trade.seller_id)}

{t('dispute.buyer')}

{formatAddress(selectedDispute.trade.buyer_id)}

)} {/* Evidence */}

{t('dispute.evidence', { count: selectedDispute.evidence?.length || 0 })}

{selectedDispute.evidence && selectedDispute.evidence.length > 0 ? (
{selectedDispute.evidence.map((ev) => (
setLightboxImage(ev.file_url)} >
{ev.evidence_type === 'screenshot' || ev.file_url.match(/\.(jpg|jpeg|png|gif|webp)$/i) ? ( {ev.description ) : (
)}
{ev.evidence_type} {ev.description && (

{ev.description}

)}
))}
) : (

{t('dispute.noEvidence')}

)}
{/* Timeline */}

{t('dispute.timeline')}

{t('dispute.opened')}: {formatDate(selectedDispute.created_at)}
{selectedDispute.assigned_at && (
{t('dispute.claimed')}: {formatDate(selectedDispute.assigned_at)}
)} {selectedDispute.resolved_at && (
{t('dispute.resolved')}: {formatDate(selectedDispute.resolved_at)}
)}
{/* Resolution (if resolved) */} {selectedDispute.decision && (

{t('dispute.resolution')}

{t(DECISION_OPTION_KEYS.find(o => o.value === selectedDispute.decision)?.labelKey || '')} {selectedDispute.decision_reasoning && (

{selectedDispute.decision_reasoning}

)}
)}
)} {selectedDispute?.status === 'under_review' && ( )}
{/* Resolve Modal */} {t('dispute.resolveTitle')} {t('dispute.resolveDesc')}
{/* Decision */}
{/* Reasoning */}