canina/frontend/admin-panel/src/pages/Transactions.tsx
2026-08-16 18:01:43 +03:30

870 lines
37 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect, useCallback } from 'react';
import {
CreditCard,
Search,
RefreshCw,
Eye,
Copy,
CheckCircle2,
XCircle,
Clock,
Filter,
DollarSign,
TrendingUp,
AlertTriangle,
Receipt,
User,
ShoppingBag,
ArrowUpDown,
ExternalLink,
ShieldCheck,
RotateCcw,
Activity,
Globe,
Monitor,
Code
} from 'lucide-react';
import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination';
interface Transaction {
id: string;
userId: string;
orderId?: string | null;
amount: number | string;
amountRials?: string | number | null;
gateway: string;
trackId?: string | null;
refNumber?: string | null;
cardNumber?: string | null;
status: string;
resultCode?: number | null;
message?: string | null;
description?: string | null;
type: string;
ipAddress?: string | null;
userAgent?: string | null;
rawRequest?: any;
rawResponse?: any;
paidAt?: string | null;
createdAt: string;
user?: {
id: string;
firstName: string;
lastName: string;
mobile: string;
email?: string;
} | null;
order?: {
id: string;
trackingNumber?: string;
totalAmount: number | string;
status: string;
paymentMethod?: string;
} | null;
}
interface Stats {
totalCount: number;
verifiedCount: number;
pendingCount: number;
failedCount: number;
totalVolume: number;
todayVolume: number;
successRate: number;
}
interface GatewayHealth {
gatewayName: string;
status: 'ONLINE' | 'DEGRADED' | 'OFFLINE';
latencyMs: number;
merchantConfigured: boolean;
activeMerchant: string;
message: string;
checkedAt: string;
}
export default function Transactions() {
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [stats, setStats] = useState<Stats | null>(null);
const [health, setHealth] = useState<GatewayHealth | null>(null);
const [healthLoading, setHealthLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isReconciling, setIsReconciling] = useState(false);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalItems, setTotalItems] = useState(0);
// Filters
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [gatewayFilter, setGatewayFilter] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [sortBy, setSortBy] = useState('createdAt');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
// Details Modal
const [selectedTx, setSelectedTx] = useState<Transaction | null>(null);
const [liveInquiryLoading, setLiveInquiryLoading] = useState(false);
const [liveInquiryData, setLiveInquiryData] = useState<any>(null);
const [showRawLogs, setShowRawLogs] = useState(false);
const fetchStats = async () => {
try {
const res = await api.get('/payment/admin/stats');
if (res.data) {
setStats(res.data);
}
} catch (e) {
console.error('Failed to fetch stats', e);
}
};
const fetchHealth = async (silent = false) => {
try {
if (!silent) setHealthLoading(true);
const res = await api.get('/payment/admin/health');
if (res.data) {
setHealth(res.data);
if (!silent) toast.success(`وضعیت درگاه زیبال: ${res.data.status} (${res.data.latencyMs}ms)`);
}
} catch (e) {
console.error('Failed to fetch gateway health', e);
if (!silent) toast.error('عدم دسترسی به سرویس Health Check درگاه');
} finally {
if (!silent) setHealthLoading(false);
}
};
const fetchTransactions = useCallback(async () => {
try {
setIsLoading(true);
const params = new URLSearchParams();
params.append('page', String(page));
params.append('limit', '15');
if (search) params.append('search', search);
if (statusFilter) params.append('status', statusFilter);
if (gatewayFilter) params.append('gateway', gatewayFilter);
if (typeFilter) params.append('type', typeFilter);
params.append('sortBy', sortBy);
params.append('sortOrder', sortOrder);
const res = await api.get(`/payment/admin/transactions?${params.toString()}`);
if (res.data) {
setTransactions(res.data.data || []);
setTotalPages(res.data.meta?.lastPage || 1);
setTotalItems(res.data.meta?.total || 0);
}
} catch (e) {
console.error('Failed to fetch transactions', e);
toast.error('خطا در بارگذاری لیست تراکنش‌ها');
} finally {
setIsLoading(false);
}
}, [page, search, statusFilter, gatewayFilter, typeFilter, sortBy, sortOrder]);
useEffect(() => {
fetchTransactions();
fetchStats();
fetchHealth(true);
}, [fetchTransactions]);
const handleCopy = (text: string, title: string) => {
navigator.clipboard.writeText(text);
toast.success(`${title} کپی شد!`);
};
const handleReconcile = async () => {
try {
setIsReconciling(true);
const res = await api.post('/payment/admin/reconcile');
toast.success(
`انطباق انجام شد: ${res.data.verified} تایید شده، ${res.data.failed} ناموفق`,
);
fetchTransactions();
fetchStats();
} catch (e) {
console.error('Reconcile error', e);
toast.error('خطا در اجرای استعلام خودکار');
} finally {
setIsReconciling(false);
}
};
const handleLiveInquiry = async (txId: string) => {
try {
setLiveInquiryLoading(true);
const res = await api.get(`/payment/admin/live-inquiry/${txId}`);
setLiveInquiryData(res.data);
toast.success('استعلام زنده زیبال با موفقیت دریافت شد');
fetchTransactions();
fetchStats();
} catch (e: any) {
const msg = e.response?.data?.message || 'خطا در استعلام از درگاه زیبال';
toast.error(msg);
} finally {
setLiveInquiryLoading(false);
}
};
const handleManualVerify = async (txId: string) => {
if (!window.confirm('آیا از تایید دستی این تراکنش (افزایش موجودی کیف پول یا تایید سفارش) اطمینان دارید؟')) return;
try {
const res = await api.post(`/payment/admin/manual-verify/${txId}`, {
adminNote: 'تایید دستی فیش واریزی توسط مدیر سیستم',
});
toast.success(res.data?.message || 'تراکنش با موفقیت تایید شد');
fetchTransactions();
fetchStats();
if (selectedTx) setSelectedTx(null);
} catch (e: any) {
toast.error(e.response?.data?.message || 'خطا در تایید دستی تراکنش');
}
};
const handleManualReject = async (txId: string) => {
const reason = window.prompt('لطفاً دلیل رد تراکنش را وارد کنید:', 'فیش واریزی نامعتبر است');
if (reason === null) return;
try {
const res = await api.post(`/payment/admin/manual-reject/${txId}`, { reason });
toast.success(res.data?.message || 'تراکنش رد شد');
fetchTransactions();
fetchStats();
if (selectedTx) setSelectedTx(null);
} catch (e: any) {
toast.error(e.response?.data?.message || 'خطا در رد تراکنش');
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-black text-gray-900 flex items-center gap-2">
<Receipt className="w-7 h-7 text-purple-600" />
لاگها و مدیریت تراکنشهای مالی
</h2>
<p className="text-gray-500 font-medium mt-1">
رهگیری لحظهای پرداختهای آنلاین زیبال، بررسی لاگهای خام و عیبیابی تراکنشها
</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
<button
onClick={() => fetchHealth(false)}
disabled={healthLoading}
className="bg-purple-50 text-purple-700 hover:bg-purple-100 border border-purple-200 text-xs font-bold px-3.5 py-2.5 rounded-xl transition-all flex items-center gap-2 shadow-sm disabled:opacity-50"
>
<Activity className={`w-4 h-4 text-purple-600 ${healthLoading ? 'animate-spin' : ''}`} />
<span>پایش سلامت درگاه (Ping)</span>
</button>
<button
onClick={handleReconcile}
disabled={isReconciling}
className="bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 text-xs font-bold px-3.5 py-2.5 rounded-xl transition-all flex items-center gap-2 shadow-sm disabled:opacity-50"
>
<RotateCcw className={`w-4 h-4 ${isReconciling ? 'animate-spin' : ''}`} />
<span>{isReconciling ? 'در حال استعلام...' : 'انطباق خودکار تراکنش‌ها'}</span>
</button>
</div>
</div>
{/* Gateway Health Monitor Banner */}
{health && (
<div className="bg-gradient-to-r from-slate-900 via-indigo-950 to-purple-950 p-4 rounded-2xl text-white shadow-md flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 border border-indigo-800/40">
<div className="flex items-center gap-3">
<div className="relative flex items-center justify-center">
<span className={`w-3.5 h-3.5 rounded-full ${health.status === 'ONLINE' ? 'bg-emerald-500 animate-ping' : health.status === 'DEGRADED' ? 'bg-amber-500' : 'bg-rose-500'}`} />
<span className={`absolute w-2.5 h-2.5 rounded-full ${health.status === 'ONLINE' ? 'bg-emerald-400' : health.status === 'DEGRADED' ? 'bg-amber-400' : 'bg-rose-400'}`} />
</div>
<div>
<div className="flex items-center gap-2">
<span className="font-black text-sm">{health.gatewayName}</span>
<span className={`text-[10px] font-black px-2 py-0.5 rounded-full ${health.status === 'ONLINE' ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30' : 'bg-amber-500/20 text-amber-300 border border-amber-500/30'}`}>
{health.status}
</span>
</div>
<p className="text-[11px] text-gray-300 mt-0.5">{health.message}</p>
</div>
</div>
<div className="flex items-center gap-4 text-xs font-mono text-gray-300 mr-auto sm:mr-0">
<div className="bg-white/10 px-3 py-1.5 rounded-xl border border-white/10 flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5 text-purple-300" />
<span>تاخیر: <strong>{health.latencyMs} ms</strong></span>
</div>
<div className="bg-white/10 px-3 py-1.5 rounded-xl border border-white/10 flex items-center gap-1.5">
<ShieldCheck className="w-3.5 h-3.5 text-emerald-300" />
<span>مرچنت: <strong>{health.activeMerchant}</strong></span>
</div>
</div>
</div>
)}
{/* Summary Stat Cards */}
{stats && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm flex items-center justify-between">
<div>
<p className="text-xs font-bold text-gray-500">حجم کل تراکنشهای موفق</p>
<p className="text-xl font-black text-gray-900 mt-1">
{Number(stats.totalVolume).toLocaleString('fa-IR')}{' '}
<span className="text-xs font-normal text-gray-400">تومان</span>
</p>
</div>
<div className="w-12 h-12 bg-purple-50 text-purple-600 rounded-2xl flex items-center justify-center">
<DollarSign className="w-6 h-6" />
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm flex items-center justify-between">
<div>
<p className="text-xs font-bold text-gray-500">پرداختهای موفق امروز</p>
<p className="text-xl font-black text-emerald-600 mt-1">
{Number(stats.todayVolume).toLocaleString('fa-IR')}{' '}
<span className="text-xs font-normal text-gray-400">تومان</span>
</p>
</div>
<div className="w-12 h-12 bg-emerald-50 text-emerald-600 rounded-2xl flex items-center justify-center">
<TrendingUp className="w-6 h-6" />
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm flex items-center justify-between">
<div>
<p className="text-xs font-bold text-gray-500">نرخ موفقیت پرداختها</p>
<p className="text-xl font-black text-indigo-600 mt-1">
٪{Number(stats.successRate).toLocaleString('fa-IR')}
</p>
</div>
<div className="w-12 h-12 bg-indigo-50 text-indigo-600 rounded-2xl flex items-center justify-center">
<ShieldCheck className="w-6 h-6" />
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm flex items-center justify-between">
<div>
<p className="text-xs font-bold text-gray-500">وضعیت تراکنشها</p>
<div className="flex items-center gap-3 mt-1 text-xs font-bold">
<span className="text-emerald-600"> {stats.verifiedCount}</span>
<span className="text-amber-500"> {stats.pendingCount}</span>
<span className="text-rose-600"> {stats.failedCount}</span>
</div>
</div>
<div className="w-12 h-12 bg-gray-50 text-gray-600 rounded-2xl flex items-center justify-center">
<Receipt className="w-6 h-6" />
</div>
</div>
</div>
)}
{/* Filter and Search Bar */}
<div className="bg-white p-5 rounded-2xl border border-gray-200 shadow-sm space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3">
{/* Search Input */}
<div className="lg:col-span-2 relative">
<Search className="w-4 h-4 text-gray-400 absolute right-3.5 top-3.5" />
<input
type="text"
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPage(1);
}}
placeholder="جستجو بر اساس نام، موبایل، Track ID، شماره مرجع..."
className="w-full pl-3 pr-10 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
{/* Status Filter */}
<div>
<select
value={statusFilter}
onChange={(e) => {
setStatusFilter(e.target.value);
setPage(1);
}}
className="w-full py-2.5 px-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">همه وضعیتها</option>
<option value="VERIFIED">موفق (VERIFIED)</option>
<option value="PENDING">در انتظار (PENDING)</option>
<option value="FAILED">ناموفق (FAILED)</option>
</select>
</div>
{/* Gateway Filter */}
<div>
<select
value={gatewayFilter}
onChange={(e) => {
setGatewayFilter(e.target.value);
setPage(1);
}}
className="w-full py-2.5 px-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">همه درگاهها</option>
<option value="zibal">درگاه زیبال (Zibal)</option>
<option value="wallet">کیف پول (Wallet)</option>
<option value="card">کارت به کارت</option>
</select>
</div>
{/* Type Filter */}
<div>
<select
value={typeFilter}
onChange={(e) => {
setTypeFilter(e.target.value);
setPage(1);
}}
className="w-full py-2.5 px-3 bg-gray-50 border border-gray-200 rounded-xl text-xs font-bold outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">همه انواع</option>
<option value="ORDER">پرداخت سفارش (ORDER)</option>
<option value="WALLET_TOPUP">شارژ کیف پول (TOPUP)</option>
</select>
</div>
</div>
{/* Total records found */}
<div className="flex items-center justify-between text-xs text-gray-500 font-bold pt-2 border-t border-gray-100">
<span>مجموع {totalItems.toLocaleString('fa-IR')} تراکنش ثبت شده</span>
<button
onClick={() => {
setSearch('');
setStatusFilter('');
setGatewayFilter('');
setTypeFilter('');
setPage(1);
}}
className="text-purple-600 hover:underline"
>
پاک کردن فیلترها
</button>
</div>
</div>
{/* Transactions Table */}
<div className="bg-white rounded-2xl border border-gray-200 shadow-sm overflow-hidden">
{isLoading ? (
<div className="p-12 flex justify-center">
<Spinner size="lg" className="text-purple-600" />
</div>
) : transactions.length === 0 ? (
<div className="p-12 text-center text-gray-500 font-bold">
هیچ تراکنشی با فیلترهای مشخص شده یافت نشد.
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-right border-collapse">
<thead>
<tr className="bg-gray-50/80 border-b border-gray-200 text-[11px] font-black text-gray-500">
<th className="p-4">کاربر خریدار</th>
<th className="p-4">سفارش / نوع</th>
<th className="p-4">مبلغ (تومان)</th>
<th className="p-4">درگاه</th>
<th className="p-4">شناسههای پرداخت</th>
<th className="p-4">وضعیت</th>
<th className="p-4">تاریخ و زمان</th>
<th className="p-4 text-center">عملیات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 text-xs font-bold">
{transactions.map((tx) => {
const isVerified = tx.status === 'VERIFIED';
const isPending = tx.status === 'PENDING';
const isFailed = tx.status === 'FAILED';
return (
<tr key={tx.id} className="hover:bg-gray-50/60 transition-colors">
{/* User Info */}
<td className="p-4">
{tx.user ? (
<div>
<p className="font-black text-gray-900">
{tx.user.firstName} {tx.user.lastName}
</p>
<p className="text-[11px] text-gray-400 font-mono mt-0.5 dir-ltr text-right">
{tx.user.mobile}
</p>
</div>
) : (
<span className="text-gray-400">کاربر مهمان</span>
)}
</td>
{/* Order / Type */}
<td className="p-4">
{tx.type === 'WALLET_TOPUP' ? (
<span className="bg-blue-50 text-blue-700 px-2.5 py-1 rounded-lg text-[10px] font-black">
شارژ کیف پول
</span>
) : (
<div>
<span className="text-purple-700 font-mono text-[11px] font-black dir-ltr block">
{tx.order?.trackingNumber || 'سفارش آنلاین'}
</span>
<span className="text-[10px] text-gray-400 font-medium">
خرید محصول
</span>
</div>
)}
</td>
{/* Amount */}
<td className="p-4 font-black text-gray-900 font-mono text-sm">
{Number(tx.amount).toLocaleString('fa-IR')}
</td>
{/* Gateway */}
<td className="p-4">
<span className="bg-indigo-50 text-indigo-700 px-2.5 py-1 rounded-lg text-[10px] font-black">
{tx.gateway === 'zibal' ? 'زیبال (Zibal)' : tx.gateway}
</span>
</td>
{/* Tracking Numbers */}
<td className="p-4 font-mono text-[11px]">
{tx.trackId && (
<div className="flex items-center gap-1 text-gray-800">
<span className="text-gray-400 text-[10px]">Track:</span>
<span className="dir-ltr">{tx.trackId}</span>
<button
type="button"
onClick={() => handleCopy(tx.trackId!, 'Track ID')}
className="text-gray-400 hover:text-purple-600"
>
<Copy className="w-3 h-3" />
</button>
</div>
)}
{tx.refNumber && (
<div className="flex items-center gap-1 text-emerald-700 mt-0.5">
<span className="text-gray-400 text-[10px]">Ref:</span>
<span className="dir-ltr">{tx.refNumber}</span>
<button
type="button"
onClick={() => handleCopy(tx.refNumber!, 'شماره مرجع')}
className="text-gray-400 hover:text-purple-600"
>
<Copy className="w-3 h-3" />
</button>
</div>
)}
</td>
{/* Status */}
<td className="p-4">
{isVerified && (
<span className="inline-flex items-center gap-1 bg-emerald-50 text-emerald-700 border border-emerald-200 px-2.5 py-1 rounded-full text-[10px] font-black">
<CheckCircle2 className="w-3 h-3" />
موفق
</span>
)}
{isPending && (
<span className="inline-flex items-center gap-1 bg-amber-50 text-amber-700 border border-amber-200 px-2.5 py-1 rounded-full text-[10px] font-black">
<Clock className="w-3 h-3" />
در انتظار
</span>
)}
{isFailed && (
<span className="inline-flex items-center gap-1 bg-rose-50 text-rose-700 border border-rose-200 px-2.5 py-1 rounded-full text-[10px] font-black">
<XCircle className="w-3 h-3" />
ناموفق
</span>
)}
</td>
{/* Date */}
<td className="p-4 text-[11px] text-gray-500">
{new Date(tx.createdAt).toLocaleDateString('fa-IR', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</td>
{/* Actions */}
<td className="p-4 text-center">
<div className="flex items-center justify-center gap-1.5">
<button
onClick={() => {
setSelectedTx(tx);
setLiveInquiryData(null);
setShowRawLogs(false);
}}
className="p-2 bg-gray-100 hover:bg-purple-50 text-gray-600 hover:text-purple-600 rounded-xl transition-all"
title="مشاهده جزئیات و عیب‌یابی"
>
<Eye className="w-4 h-4" />
</button>
{tx.gateway === 'zibal' && tx.trackId && (
<button
onClick={() => {
setSelectedTx(tx);
handleLiveInquiry(tx.id);
}}
className="p-2 bg-indigo-50 hover:bg-indigo-100 text-indigo-600 rounded-xl transition-all"
title="استعلام زنده از زیبال"
>
<RotateCcw className="w-4 h-4" />
</button>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{/* Pagination */}
<div className="p-4 border-t border-gray-100">
<Pagination
currentPage={page}
totalPages={totalPages}
onPageChange={(p) => setPage(p)}
/>
</div>
</div>
{/* Diagnostics and Details Modal */}
{selectedTx && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
<div className="bg-white rounded-3xl w-full max-w-2xl max-h-[90vh] overflow-y-auto border border-gray-200 shadow-2xl p-6 sm:p-8 space-y-6">
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
<h3 className="text-lg font-black text-gray-900 flex items-center gap-2">
<Receipt className="w-5 h-5 text-purple-600" />
جزئیات تراکنش و ریشهیابی خطا
</h3>
<button
onClick={() => {
setSelectedTx(null);
setLiveInquiryData(null);
}}
className="text-gray-400 hover:text-gray-700 text-xl font-bold"
>
</button>
</div>
{/* Quick Grid Info */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 text-xs bg-gray-50 p-4 rounded-2xl border border-gray-200">
<div>
<span className="text-gray-400 block font-medium">وضعیت تراکنش:</span>
<span
className={`font-black mt-0.5 inline-block ${
selectedTx.status === 'VERIFIED'
? 'text-emerald-600'
: selectedTx.status === 'PENDING'
? 'text-amber-500'
: 'text-rose-600'
}`}
>
{selectedTx.status}
</span>
</div>
<div>
<span className="text-gray-400 block font-medium">مبلغ تراکنش:</span>
<span className="font-black text-gray-900 mt-0.5 inline-block">
{Number(selectedTx.amount).toLocaleString('fa-IR')} تومان
</span>
</div>
<div>
<span className="text-gray-400 block font-medium">درگاه پرداخت:</span>
<span className="font-black text-gray-900 mt-0.5 inline-block">
{selectedTx.gateway}
</span>
</div>
<div>
<span className="text-gray-400 block font-medium">شناسه پیگیری (Track ID):</span>
<span className="font-mono font-bold text-gray-900 dir-ltr inline-block">
{selectedTx.trackId || 'ثبت نشده'}
</span>
</div>
<div>
<span className="text-gray-400 block font-medium">شماره مرجع بانکی:</span>
<span className="font-mono font-bold text-gray-900 dir-ltr inline-block">
{selectedTx.refNumber || 'ثبت نشده'}
</span>
</div>
<div>
<span className="text-gray-400 block font-medium">شماره کارت ماسک شده:</span>
<span className="font-mono font-bold text-gray-900 dir-ltr inline-block">
{selectedTx.cardNumber || 'ثبت نشده'}
</span>
</div>
{selectedTx.ipAddress && (
<div>
<span className="text-gray-400 block font-medium">IP کلاینت:</span>
<span className="font-mono font-bold text-gray-700 dir-ltr inline-block">
{selectedTx.ipAddress}
</span>
</div>
)}
{selectedTx.userAgent && (
<div className="sm:col-span-2">
<span className="text-gray-400 block font-medium">User-Agent دستگاه:</span>
<span className="text-[10px] font-mono text-gray-600 line-clamp-1 dir-ltr inline-block">
{selectedTx.userAgent}
</span>
</div>
)}
</div>
{/* Error Message / Reason */}
{selectedTx.message && (
<div
className={`p-4 rounded-2xl border ${
selectedTx.status === 'VERIFIED'
? 'bg-emerald-50 border-emerald-200 text-emerald-900'
: 'bg-rose-50 border-rose-200 text-rose-900'
}`}
>
<div className="flex items-center gap-2 font-black text-xs mb-1">
{selectedTx.status === 'VERIFIED' ? (
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
) : (
<AlertTriangle className="w-4 h-4 text-rose-600" />
)}
<span>پیام و نتیجه گزارش درگاه:</span>
</div>
<p className="text-xs font-bold leading-relaxed">{selectedTx.message}</p>
{selectedTx.resultCode && (
<p className="text-[11px] font-mono mt-1 text-gray-500">
کد نتیجه زیبال: {selectedTx.resultCode}
</p>
)}
</div>
)}
{/* Live Inquiry Section */}
<div className="border border-indigo-100 bg-indigo-50/50 p-5 rounded-2xl space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<RotateCcw className="w-4 h-4 text-indigo-600" />
<h4 className="font-black text-xs text-indigo-900">
استعلام زنده از سرور زیبال (Live Gateway Inquiry)
</h4>
</div>
<button
onClick={() => handleLiveInquiry(selectedTx.id)}
disabled={liveInquiryLoading}
className="bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-bold px-3 py-1.5 rounded-xl transition-all flex items-center gap-1.5 disabled:opacity-50"
>
<RefreshCw
className={`w-3.5 h-3.5 ${liveInquiryLoading ? 'animate-spin' : ''}`}
/>
<span>استعلام زنده لحظهای</span>
</button>
</div>
{liveInquiryData && (
<div className="bg-white p-4 rounded-xl border border-indigo-200 space-y-2 text-xs font-mono">
<div className="flex justify-between text-gray-700 font-bold font-vazir">
<span>وضعیت زیبال:</span>
<span className="text-indigo-600">{liveInquiryData.statusMessage}</span>
</div>
<pre className="text-[11px] bg-slate-900 text-emerald-400 p-3 rounded-lg overflow-x-auto dir-ltr">
{JSON.stringify(liveInquiryData.gatewayResponse, null, 2)}
</pre>
</div>
)}
</div>
{/* Raw Request & Response Inspector Toggle */}
<div className="border border-gray-200 rounded-2xl overflow-hidden">
<button
type="button"
onClick={() => setShowRawLogs(!showRawLogs)}
className="w-full bg-gray-50 hover:bg-gray-100 px-4 py-3 text-xs font-bold text-gray-700 flex items-center justify-between transition-colors"
>
<span className="flex items-center gap-2">
<Code className="w-4 h-4 text-purple-600" />
مشاهده لاگ خام درخواست و پاسخ (Raw JSON Payload)
</span>
<span>{showRawLogs ? '▲ بستن' : '▼ باز کردن'}</span>
</button>
{showRawLogs && (
<div className="p-4 bg-slate-950 text-emerald-400 text-[11px] font-mono space-y-4 dir-ltr overflow-x-auto">
{selectedTx.rawRequest && (
<div>
<p className="text-purple-400 font-bold mb-1">// Raw Request:</p>
<pre className="p-2 bg-slate-900 rounded-lg">{JSON.stringify(selectedTx.rawRequest, null, 2)}</pre>
</div>
)}
{selectedTx.rawResponse && (
<div>
<p className="text-cyan-400 font-bold mb-1">// Raw Response:</p>
<pre className="p-2 bg-slate-900 rounded-lg">{JSON.stringify(selectedTx.rawResponse, null, 2)}</pre>
</div>
)}
{!selectedTx.rawRequest && !selectedTx.rawResponse && (
<p className="text-gray-500">// لاگ خامی برای این تراکنش ثبت نشده است.</p>
)}
</div>
)}
</div>
{/* Modal Actions */}
<div className="flex items-center justify-between gap-3 pt-2">
<div className="flex items-center gap-2">
{selectedTx.status !== 'VERIFIED' && (
<button
type="button"
onClick={() => handleManualVerify(selectedTx.id)}
className="bg-emerald-600 hover:bg-emerald-700 text-white font-black px-4 py-2.5 rounded-xl text-xs flex items-center gap-1.5 shadow-md shadow-emerald-600/20 transition-all cursor-pointer"
>
<CheckCircle2 className="w-4 h-4" />
<span>تایید دستی و اعمال مالی</span>
</button>
)}
{selectedTx.status === 'PENDING' && (
<button
type="button"
onClick={() => handleManualReject(selectedTx.id)}
className="bg-rose-50 hover:bg-rose-100 text-rose-700 border border-rose-200 font-bold px-4 py-2.5 rounded-xl text-xs flex items-center gap-1.5 transition-all cursor-pointer"
>
<XCircle className="w-4 h-4" />
<span>رد کردن تراکنش</span>
</button>
)}
</div>
<button
type="button"
onClick={() => {
setSelectedTx(null);
setLiveInquiryData(null);
setShowRawLogs(false);
}}
className="bg-gray-100 hover:bg-gray-200 text-gray-800 font-bold px-6 py-2.5 rounded-xl text-xs transition-colors cursor-pointer"
>
بستن
</button>
</div>
</div>
</div>
)}
</div>
);
}