Files
pwap/web/src/components/admin/DisputeResolutionPanel.tsx
T
pezkuwichain 4f683538d3 feat: complete i18n support for all components (6 languages)
Add full internationalization across 127+ components and pages.
790+ translation keys in en, tr, kmr, ckb, ar, fa locales.
Remove duplicate keys and delete unused .json locale files.
2026-02-22 04:48:20 +03:00

828 lines
31 KiB
TypeScript

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<string, string> = {
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<string, string> = {
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<Dispute[]>([]);
const [loading, setLoading] = useState(true);
const [selectedDispute, setSelectedDispute] = useState<Dispute | null>(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<string | null>(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 (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<Gavel className="h-6 w-6 text-kurdish-green" />
{t('dispute.title')}
</h2>
<p className="text-muted-foreground text-sm mt-1">
{t('dispute.subtitle')}
</p>
</div>
<Button variant="outline" onClick={fetchDisputes} disabled={loading}>
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
{t('dispute.refresh')}
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="bg-yellow-500/10 border-yellow-500/20">
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">{t('dispute.statsOpen')}</p>
<p className="text-2xl font-bold text-yellow-500">{stats.open}</p>
</div>
<AlertTriangle className="h-8 w-8 text-yellow-500/50" />
</div>
</CardContent>
</Card>
<Card className="bg-blue-500/10 border-blue-500/20">
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">{t('dispute.statsUnderReview')}</p>
<p className="text-2xl font-bold text-blue-500">{stats.under_review}</p>
</div>
<Clock className="h-8 w-8 text-blue-500/50" />
</div>
</CardContent>
</Card>
<Card className="bg-green-500/10 border-green-500/20">
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">{t('dispute.statsResolved')}</p>
<p className="text-2xl font-bold text-green-500">{stats.resolved}</p>
</div>
<CheckCircle2 className="h-8 w-8 text-green-500/50" />
</div>
</CardContent>
</Card>
<Card className="bg-purple-500/10 border-purple-500/20">
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">{t('dispute.statsEscalated')}</p>
<p className="text-2xl font-bold text-purple-500">{stats.escalated}</p>
</div>
<Scale className="h-8 w-8 text-purple-500/50" />
</div>
</CardContent>
</Card>
</div>
{/* Disputes Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-4 w-full max-w-md">
<TabsTrigger value="open" className="gap-1">
{t('dispute.statsOpen')}
{stats.open > 0 && (
<Badge variant="secondary" className="ml-1 h-5 px-1.5">
{stats.open}
</Badge>
)}
</TabsTrigger>
<TabsTrigger value="under_review">{t('dispute.tabInReview')}</TabsTrigger>
<TabsTrigger value="resolved">{t('dispute.statsResolved')}</TabsTrigger>
<TabsTrigger value="escalated">{t('dispute.statsEscalated')}</TabsTrigger>
</TabsList>
<TabsContent value={activeTab} className="mt-4">
{loading ? (
<Card>
<CardContent className="py-12 flex items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</CardContent>
</Card>
) : filteredDisputes.length === 0 ? (
<Card>
<CardContent className="py-12 text-center">
<Shield className="h-12 w-12 mx-auto text-muted-foreground/50 mb-4" />
<p className="text-muted-foreground">{t('dispute.empty')}</p>
</CardContent>
</Card>
) : (
<div className="space-y-3">
{filteredDisputes.map((dispute) => (
<Card key={dispute.id} className="hover:border-kurdish-green/50 transition-colors">
<CardContent className="py-4">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<Badge className={STATUS_COLORS[dispute.status]}>
{dispute.status.replace('_', ' ').toUpperCase()}
</Badge>
<Badge variant="outline">
{t(CATEGORY_KEYS[dispute.category] || dispute.category)}
</Badge>
{dispute.evidence && dispute.evidence.length > 0 && (
<Badge variant="secondary" className="gap-1">
<ImageIcon className="h-3 w-3" />
{t('dispute.evidence', { count: dispute.evidence.length })}
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground line-clamp-2 mb-2">
{dispute.reason}
</p>
<div className="flex flex-wrap items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<FileText className="h-3 w-3" />
Trade: {formatAddress(dispute.trade_id)}
</span>
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{formatDate(dispute.created_at)}
</span>
{dispute.trade && (
<span className="flex items-center gap-1">
<Scale className="h-3 w-3" />
{dispute.trade.crypto_amount} crypto
</span>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => openDetails(dispute)}
>
<Eye className="h-4 w-4 mr-1" />
{t('dispute.view')}
</Button>
{dispute.status === 'open' && (
<Button
size="sm"
onClick={() => claimDispute(dispute.id)}
>
{t('dispute.claim')}
</Button>
)}
{dispute.status === 'under_review' && (
<Button
size="sm"
className="bg-kurdish-green hover:bg-kurdish-green-dark"
onClick={() => openResolve(dispute)}
>
<Gavel className="h-4 w-4 mr-1" />
{t('dispute.resolve')}
</Button>
)}
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
</Tabs>
{/* Details Modal */}
<Dialog open={detailsOpen} onOpenChange={setDetailsOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Scale className="h-5 w-5" />
{t('dispute.detailsTitle')}
</DialogTitle>
<DialogDescription>
{t('dispute.detailsDesc')}
</DialogDescription>
</DialogHeader>
{selectedDispute && (
<ScrollArea className="flex-1 pr-4">
<div className="space-y-6">
{/* Status & Category */}
<div className="flex items-center gap-2">
<Badge className={STATUS_COLORS[selectedDispute.status]}>
{selectedDispute.status.replace('_', ' ').toUpperCase()}
</Badge>
<Badge variant="outline">
{t(CATEGORY_KEYS[selectedDispute.category] || selectedDispute.category)}
</Badge>
</div>
{/* Reason */}
<div>
<h4 className="font-medium mb-2">{t('dispute.reason')}</h4>
<p className="text-sm text-muted-foreground bg-muted p-3 rounded-lg">
{selectedDispute.reason}
</p>
</div>
{/* Trade Info */}
{selectedDispute.trade && (
<div>
<h4 className="font-medium mb-2">{t('dispute.tradeInfo')}</h4>
<div className="bg-muted p-3 rounded-lg space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">{t('dispute.tradeId')}:</span>
<span className="font-mono">{formatAddress(selectedDispute.trade_id)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">{t('dispute.amount')}:</span>
<span>{selectedDispute.trade.crypto_amount} crypto</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">{t('dispute.fiat')}:</span>
<span>{selectedDispute.trade.fiat_amount}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">{t('dispute.tradeStatus')}:</span>
<Badge variant="secondary">{selectedDispute.trade.status}</Badge>
</div>
</div>
</div>
)}
{/* Parties */}
{selectedDispute.trade && (
<div>
<h4 className="font-medium mb-2">{t('dispute.parties')}</h4>
<div className="grid grid-cols-2 gap-3">
<div className="bg-muted p-3 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<User className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">{t('dispute.seller')}</span>
</div>
<p className="text-xs font-mono text-muted-foreground">
{formatAddress(selectedDispute.trade.seller_id)}
</p>
</div>
<div className="bg-muted p-3 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<User className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">{t('dispute.buyer')}</span>
</div>
<p className="text-xs font-mono text-muted-foreground">
{formatAddress(selectedDispute.trade.buyer_id)}
</p>
</div>
</div>
</div>
)}
{/* Evidence */}
<div>
<h4 className="font-medium mb-2">
{t('dispute.evidence', { count: selectedDispute.evidence?.length || 0 })}
</h4>
{selectedDispute.evidence && selectedDispute.evidence.length > 0 ? (
<div className="grid grid-cols-2 gap-3">
{selectedDispute.evidence.map((ev) => (
<div
key={ev.id}
className="bg-muted p-3 rounded-lg cursor-pointer hover:bg-muted/80 transition-colors"
onClick={() => setLightboxImage(ev.file_url)}
>
<div className="aspect-video relative mb-2 rounded overflow-hidden bg-black/20">
{ev.evidence_type === 'screenshot' || ev.file_url.match(/\.(jpg|jpeg|png|gif|webp)$/i) ? (
<img
src={ev.file_url}
alt={ev.description || 'Evidence'}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<FileText className="h-8 w-8 text-muted-foreground" />
</div>
)}
</div>
<div className="text-xs">
<Badge variant="outline" className="mb-1">
{ev.evidence_type}
</Badge>
{ev.description && (
<p className="text-muted-foreground line-clamp-2 mt-1">
{ev.description}
</p>
)}
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-muted-foreground">{t('dispute.noEvidence')}</p>
)}
</div>
{/* Timeline */}
<div>
<h4 className="font-medium mb-2">{t('dispute.timeline')}</h4>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-yellow-500" />
<span className="text-muted-foreground">{t('dispute.opened')}:</span>
<span>{formatDate(selectedDispute.created_at)}</span>
</div>
{selectedDispute.assigned_at && (
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-blue-500" />
<span className="text-muted-foreground">{t('dispute.claimed')}:</span>
<span>{formatDate(selectedDispute.assigned_at)}</span>
</div>
)}
{selectedDispute.resolved_at && (
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500" />
<span className="text-muted-foreground">{t('dispute.resolved')}:</span>
<span>{formatDate(selectedDispute.resolved_at)}</span>
</div>
)}
</div>
</div>
{/* Resolution (if resolved) */}
{selectedDispute.decision && (
<div>
<h4 className="font-medium mb-2">{t('dispute.resolution')}</h4>
<div className="bg-green-500/10 border border-green-500/20 p-3 rounded-lg">
<Badge className="bg-green-500/20 text-green-500 mb-2">
{t(DECISION_OPTION_KEYS.find(o => o.value === selectedDispute.decision)?.labelKey || '')}
</Badge>
{selectedDispute.decision_reasoning && (
<p className="text-sm text-muted-foreground">
{selectedDispute.decision_reasoning}
</p>
)}
</div>
</div>
)}
</div>
</ScrollArea>
)}
<DialogFooter className="mt-4">
<Button variant="outline" onClick={() => setDetailsOpen(false)}>
{t('dispute.close')}
</Button>
{selectedDispute?.status === 'under_review' && (
<Button
className="bg-kurdish-green hover:bg-kurdish-green-dark"
onClick={() => {
setDetailsOpen(false);
openResolve(selectedDispute);
}}
>
<Gavel className="h-4 w-4 mr-2" />
{t('dispute.resolve')}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
{/* Resolve Modal */}
<Dialog open={resolveOpen} onOpenChange={setResolveOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Gavel className="h-5 w-5 text-kurdish-green" />
{t('dispute.resolveTitle')}
</DialogTitle>
<DialogDescription>
{t('dispute.resolveDesc')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* Decision */}
<div>
<label className="text-sm font-medium mb-2 block">{t('dispute.decision')}</label>
<Select value={decision} onValueChange={setDecision}>
<SelectTrigger>
<SelectValue placeholder={t('dispute.decisionPlaceholder')} />
</SelectTrigger>
<SelectContent>
{DECISION_OPTION_KEYS.map((option) => (
<SelectItem key={option.value} value={option.value}>
<div className="flex flex-col">
<span>{t(option.labelKey)}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Reasoning */}
<div>
<label className="text-sm font-medium mb-2 block">
{t('dispute.reasoning')} <span className="text-muted-foreground">({t('dispute.required')})</span>
</label>
<Textarea
value={reasoning}
onChange={(e) => setReasoning(e.target.value)}
placeholder={t('dispute.reasoningPlaceholder')}
rows={4}
/>
<p className="text-xs text-muted-foreground mt-1">
{t('dispute.reasoningHint')}
</p>
</div>
{/* Warning */}
<div className="flex items-start gap-2 p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
<AlertTriangle className="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" />
<div className="text-sm">
<p className="font-medium text-yellow-500">{t('dispute.warningTitle')}</p>
<p className="text-muted-foreground">
{t('dispute.warningText')}
</p>
</div>
</div>
</div>
<DialogFooter className="mt-4">
<Button
variant="outline"
onClick={() => setResolveOpen(false)}
disabled={submitting}
>
{t('dispute.cancel')}
</Button>
<Button
className="bg-kurdish-green hover:bg-kurdish-green-dark"
onClick={resolveDispute}
disabled={submitting || !decision || !reasoning}
>
{submitting ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<CheckCircle2 className="h-4 w-4 mr-2" />
)}
{t('dispute.confirmResolution')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Image Lightbox */}
<Dialog open={!!lightboxImage} onOpenChange={() => setLightboxImage(null)}>
<DialogContent className="max-w-4xl p-0 bg-black/90">
{lightboxImage && (
<div className="relative">
<img
src={lightboxImage}
alt="Evidence"
className="w-full h-auto max-h-[80vh] object-contain"
/>
<div className="absolute top-4 right-4 flex gap-2">
<Button
size="icon"
variant="secondary"
onClick={() => window.open(lightboxImage, '_blank')}
>
<ExternalLink className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="secondary"
asChild
>
<a href={lightboxImage} download>
<Download className="h-4 w-4" />
</a>
</Button>
<Button
size="icon"
variant="secondary"
onClick={() => setLightboxImage(null)}
>
<XCircle className="h-4 w-4" />
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
</div>
);
}
export default DisputeResolutionPanel;