canina/frontend/admin-panel/src/pages/ZibalPortalPage.tsx

1154 lines
54 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 { useState, useEffect, useCallback } from 'react';
import {
RotateCcw,
Search,
Download,
RefreshCw,
Filter,
Calendar,
Layers,
FileSpreadsheet,
CheckCircle2,
Clock,
AlertCircle,
XCircle,
Eye,
CreditCard,
Building2,
ListOrdered,
Plus,
Send,
SlidersHorizontal,
ChevronLeft,
ChevronRight,
TrendingUp,
} from 'lucide-react';
import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
export default function ZibalPortalPage() {
const [activeMainTab, setActiveMainTab] = useState<'refunds' | 'checkouts' | 'queue' | 'gateway_transactions'>('refunds');
// ==========================================
// TAB 1: REFUNDS (استرداد وجه)
// ==========================================
const [refunds, setRefunds] = useState<any[]>([]);
const [loadingRefunds, setLoadingRefunds] = useState(false);
const [refundSearchOpen, setRefundSearchOpen] = useState(false);
const [refundFilters, setRefundFilters] = useState({
status: '',
psp: '',
fromDate: '',
toDate: '',
trackId: '',
refundId: '',
orderId: '',
description: '',
name: '',
destination: '',
refNumber: '',
page: 1,
size: 20,
});
// New Refund Modal
const [newRefundModal, setNewRefundModal] = useState(false);
const [refundForm, setRefundForm] = useState({
trackId: '',
amount: '',
tryReverse: true,
cardNumber: '',
description: '',
});
const [isSubmittingRefund, setIsSubmittingRefund] = useState(false);
// ==========================================
// TAB 2: CHECKOUTS / PAYOUTS (گزارش واریز / تسویه)
// ==========================================
const [checkouts, setCheckouts] = useState<any[]>([]);
const [loadingCheckouts, setLoadingCheckouts] = useState(false);
const [checkoutSearchOpen, setCheckoutSearchOpen] = useState(false);
const [checkoutFilters, setCheckoutFilters] = useState({
fromDate: '',
toDate: '',
status: '',
type: '',
refNumber: '',
destinationAccount: '',
merchantName: '',
page: 1,
size: 20,
});
// ==========================================
// TAB 3: CHECKOUT QUEUE (صف تسویه)
// ==========================================
const [queueItems, setQueueItems] = useState<any[]>([]);
const [loadingQueue, setLoadingQueue] = useState(false);
// ==========================================
// TAB 4: DIRECT GATEWAY TRANSACTIONS (تراکنش‌های درگاه)
// ==========================================
const [gatewayTxList, setGatewayTxList] = useState<any[]>([]);
const [loadingGatewayTx, setLoadingGatewayTx] = useState(false);
const [gatewaySearchOpen, setGatewaySearchOpen] = useState(false);
const [gatewayFilters, setGatewayFilters] = useState({
fromDate: '',
toDate: '',
status: '',
trackId: '',
orderId: '',
mobile: '',
cardNumber: '',
amount: '',
page: 1,
size: 20,
});
// Details Modal
const [detailModalItem, setDetailModalItem] = useState<{ title: string; data: any } | null>(null);
// -------------------------------------------------------------
// Data Fetching Handlers
// -------------------------------------------------------------
const fetchRefunds = useCallback(async () => {
try {
setLoadingRefunds(true);
const res = await api.post('/payment/admin/refund/list', {
page: refundFilters.page,
size: refundFilters.size,
fromDate: refundFilters.fromDate || undefined,
toDate: refundFilters.toDate || undefined,
status: refundFilters.status ? [Number(refundFilters.status)] : undefined,
});
let items = [];
if (Array.isArray(res.data)) items = res.data;
else if (res.data?.data && Array.isArray(res.data.data)) items = res.data.data;
else if (res.data?.refundList && Array.isArray(res.data.refundList)) items = res.data.refundList;
// Client-side quick filter matching user screenshot search fields
if (refundFilters.trackId) items = items.filter((x: any) => String(x.trackId || x.transactionTrackId || '').includes(refundFilters.trackId));
if (refundFilters.refundId) items = items.filter((x: any) => String(x.id || x.refundId || '').includes(refundFilters.refundId));
if (refundFilters.orderId) items = items.filter((x: any) => String(x.orderId || '').includes(refundFilters.orderId));
if (refundFilters.refNumber) items = items.filter((x: any) => String(x.refNumber || '').includes(refundFilters.refNumber));
if (refundFilters.name) items = items.filter((x: any) => String(x.name || x.cardHolder || '').includes(refundFilters.name));
if (refundFilters.destination) items = items.filter((x: any) => String(x.cardNumber || x.bankAccount || x.iban || '').includes(refundFilters.destination));
setRefunds(items);
} catch (e: any) {
toast.error(e?.response?.data?.message || 'خطا در دریافت لیست استردادها از زیبال');
} finally {
setLoadingRefunds(false);
}
}, [refundFilters]);
const fetchCheckouts = useCallback(async () => {
try {
setLoadingCheckouts(true);
const res = await api.post('/payment/admin/checkout-report', {
page: checkoutFilters.page,
size: checkoutFilters.size,
fromDate: checkoutFilters.fromDate || undefined,
toDate: checkoutFilters.toDate || undefined,
});
let items = [];
if (Array.isArray(res.data)) items = res.data;
else if (res.data?.data && Array.isArray(res.data.data)) items = res.data.data;
else if (res.data?.checkoutList && Array.isArray(res.data.checkoutList)) items = res.data.checkoutList;
if (checkoutFilters.refNumber) items = items.filter((x: any) => String(x.refNumber || x.referenceId || '').includes(checkoutFilters.refNumber));
if (checkoutFilters.destinationAccount) items = items.filter((x: any) => String(x.accountIban || x.iban || x.destination || '').includes(checkoutFilters.destinationAccount));
setCheckouts(items);
} catch (e: any) {
toast.error(e?.response?.data?.message || 'خطا در دریافت گزارش تسویه‌ها از زیبال');
} finally {
setLoadingCheckouts(false);
}
}, [checkoutFilters]);
const fetchQueue = async () => {
try {
setLoadingQueue(true);
const res = await api.get('/payment/admin/checkout-queue');
const items = Array.isArray(res.data) ? res.data : (res.data?.data || []);
setQueueItems(items);
} catch (e: any) {
toast.error('خطا در دریافت صف تسویه‌های در انتظار');
} finally {
setLoadingQueue(false);
}
};
const fetchGatewayTx = useCallback(async () => {
try {
setLoadingGatewayTx(true);
const res = await api.post('/payment/admin/gateway-report', {
page: gatewayFilters.page,
size: gatewayFilters.size,
fromDate: gatewayFilters.fromDate || undefined,
toDate: gatewayFilters.toDate || undefined,
trackId: gatewayFilters.trackId ? Number(gatewayFilters.trackId) : undefined,
orderId: gatewayFilters.orderId || undefined,
mobile: gatewayFilters.mobile || undefined,
cardNumber: gatewayFilters.cardNumber || undefined,
status: gatewayFilters.status ? Number(gatewayFilters.status) : undefined,
});
let items = [];
if (Array.isArray(res.data)) items = res.data;
else if (res.data?.data && Array.isArray(res.data.data)) items = res.data.data;
else if (res.data?.transactions && Array.isArray(res.data.transactions)) items = res.data.transactions;
setGatewayTxList(items);
} catch (e: any) {
toast.error('خطا در دریافت لیست تراکنش‌های زیبال');
} finally {
setLoadingGatewayTx(false);
}
}, [gatewayFilters]);
useEffect(() => {
if (activeMainTab === 'refunds') fetchRefunds();
if (activeMainTab === 'checkouts') fetchCheckouts();
if (activeMainTab === 'queue') fetchQueue();
if (activeMainTab === 'gateway_transactions') fetchGatewayTx();
}, [activeMainTab, fetchRefunds, fetchCheckouts, fetchGatewayTx]);
// -------------------------------------------------------------
// Actions
// -------------------------------------------------------------
const handleCreateRefundSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!refundForm.trackId) {
toast.error('شناسه تراکنش (Track ID) الزامی است');
return;
}
try {
setIsSubmittingRefund(true);
const res = await api.post('/payment/admin/refund', {
trackId: refundForm.trackId,
amount: refundForm.amount ? Number(refundForm.amount) * 10 : undefined, // Convert to Rials
tryReverse: refundForm.tryReverse,
cardNumber: refundForm.cardNumber || undefined,
description: refundForm.description || undefined,
});
if (res.data?.result === 1 || res.data?.status === 1 || res.data?.success) {
toast.success(res.data?.message || 'درخواست استرداد با موفقیت به زیبال ارسال شد.');
setNewRefundModal(false);
setRefundForm({ trackId: '', amount: '', tryReverse: true, cardNumber: '', description: '' });
fetchRefunds();
} else {
toast.error(res.data?.message || 'خطا در ثبت استرداد وجه');
}
} catch (e: any) {
toast.error(e?.response?.data?.message || 'خطا در برقراری ارتباط با سرویس استرداد زیبال');
} finally {
setIsSubmittingRefund(false);
}
};
const handleInquireRefundStatus = async (item: any) => {
try {
const res = await api.post('/payment/admin/refund/inquiry', {
refundId: item.id || item.refundId,
transactionTrackId: item.trackId || item.transactionTrackId,
});
setDetailModalItem({
title: `استعلام استرداد #${item.id || item.trackId}`,
data: res.data,
});
toast.success('استعلام با موفقیت انجام شد');
} catch (e: any) {
toast.error(e?.response?.data?.message || 'خطا در استعلام');
}
};
const downloadCSV = (data: any[], filename: string) => {
if (!data || data.length === 0) {
toast.error('داده‌ای برای خروجی اکسل یافت نشد');
return;
}
const headers = Object.keys(data[0]).join(',');
const rows = data.map((row) =>
Object.values(row)
.map((v) => `"${String(v ?? '').replace(/"/g, '""')}"`)
.join(',')
);
const csvContent = '\uFEFF' + [headers, ...rows].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `${filename}-${new Date().toISOString().split('T')[0]}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success('فایل گزارش با موفقیت دانلود شد');
};
return (
<div className="space-y-6 font-vazir" dir="rtl">
{/* Top 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">
<RotateCcw className="w-7 h-7 text-indigo-600" />
پرتال و داشبورد خدمات مالی زیبال
</h2>
<p className="text-gray-500 font-medium text-xs sm:text-sm mt-1">
مشاهده گزارشات استرداد وجه (Refund/Reverse)، تسویهها و واریزها، صف تسویه و تراکنشهای رسمی شاپرک
</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setNewRefundModal(true)}
className="bg-rose-600 hover:bg-rose-700 text-white font-bold text-xs sm:text-sm px-4 py-2.5 rounded-xl transition-colors shadow-md shadow-rose-200 flex items-center gap-2 cursor-pointer"
>
<Plus className="w-4 h-4" />
<span>ثبت استرداد وجه جدید</span>
</button>
</div>
</div>
{/* Main Feature Tabs (Matching Zibal Panel layout) */}
<div className="bg-white p-2 rounded-2xl shadow-sm border border-gray-200 flex items-center justify-between overflow-x-auto gap-2 no-scrollbar">
<div className="flex items-center gap-2 shrink-0">
<button
onClick={() => setActiveMainTab('refunds')}
className={`px-5 py-2.5 rounded-xl font-black text-xs sm:text-sm transition-all flex items-center gap-2 cursor-pointer whitespace-nowrap ${
activeMainTab === 'refunds'
? 'bg-indigo-600 text-white shadow-md shadow-indigo-200'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<RotateCcw className="w-4 h-4 shrink-0" />
<span>استرداد وجه</span>
</button>
<button
onClick={() => setActiveMainTab('checkouts')}
className={`px-5 py-2.5 rounded-xl font-black text-xs sm:text-sm transition-all flex items-center gap-2 cursor-pointer whitespace-nowrap ${
activeMainTab === 'checkouts'
? 'bg-indigo-600 text-white shadow-md shadow-indigo-200'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<CreditCard className="w-4 h-4 shrink-0" />
<span>گزارش واریز و تسویه</span>
</button>
<button
onClick={() => setActiveMainTab('queue')}
className={`px-5 py-2.5 rounded-xl font-black text-xs sm:text-sm transition-all flex items-center gap-2 cursor-pointer whitespace-nowrap ${
activeMainTab === 'queue'
? 'bg-indigo-600 text-white shadow-md shadow-indigo-200'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<ListOrdered className="w-4 h-4 shrink-0" />
<span>صف تسویه</span>
</button>
<button
onClick={() => setActiveMainTab('gateway_transactions')}
className={`px-5 py-2.5 rounded-xl font-black text-xs sm:text-sm transition-all flex items-center gap-2 cursor-pointer whitespace-nowrap ${
activeMainTab === 'gateway_transactions'
? 'bg-indigo-600 text-white shadow-md shadow-indigo-200'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<TrendingUp className="w-4 h-4 shrink-0" />
<span>تراکنشهای درگاه (IPG)</span>
</button>
</div>
</div>
{/* ======================================================== */}
{/* 1. TAB CONTENT: REFUNDS (استرداد وجه) */}
{/* ======================================================== */}
{activeMainTab === 'refunds' && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
{/* Top Bar with Filter & Search Controls */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
<div className="flex items-center gap-2.5">
<Button
variant="primary"
size="sm"
startIcon={Download}
onClick={() => downloadCSV(refunds, 'zibal-refunds')}
>
دانلود گزارش
</Button>
<Button
variant="outline"
size="sm"
onClick={fetchRefunds}
isLoading={loadingRefunds}
startIcon={RefreshCw}
title="بروزرسانی"
/>
</div>
{/* Filter Inputs Bar */}
<div className="flex flex-wrap items-center gap-3">
{/* Date Filter */}
<div className="flex items-center gap-1.5 bg-gray-50 border border-gray-200 px-3 py-1.5 rounded-xl text-xs font-bold">
<Calendar className="w-3.5 h-3.5 text-indigo-600" />
<span>تاریخ از:</span>
<input
type="date"
value={refundFilters.fromDate}
onChange={(e) => setRefundFilters({ ...refundFilters, fromDate: e.target.value })}
className="bg-transparent outline-none font-mono text-[11px]"
/>
<span>تا:</span>
<input
type="date"
value={refundFilters.toDate}
onChange={(e) => setRefundFilters({ ...refundFilters, toDate: e.target.value })}
className="bg-transparent outline-none font-mono text-[11px]"
/>
</div>
{/* Status Selector */}
<select
value={refundFilters.status}
onChange={(e) => setRefundFilters({ ...refundFilters, status: e.target.value })}
className="bg-gray-50 border border-gray-200 px-3 py-2 rounded-xl text-xs font-bold outline-none"
>
<option value="">همه وضعیتها</option>
<option value="1">موفق (تایید شده)</option>
<option value="0">در انتظار انجام</option>
<option value="-1">ناموفق / لغو شده</option>
</select>
{/* Advanced Search Popup Toggle */}
<div className="relative">
<button
onClick={() => setRefundSearchOpen(!refundSearchOpen)}
className="border border-gray-200 bg-gray-50 hover:bg-gray-100 px-4 py-2 rounded-xl text-xs font-bold flex items-center gap-2 cursor-pointer"
>
<Search className="w-3.5 h-3.5 text-gray-500" />
<span>جستجوی پیشرفته</span>
</button>
{/* Advanced Search Dropdown Window (Matching Exact User Screenshot) */}
{refundSearchOpen && (
<div className="absolute left-0 mt-2 w-72 bg-white rounded-2xl shadow-2xl border border-gray-200 p-4 space-y-3 z-30 font-vazir text-xs">
<div className="space-y-2.5">
<div className="relative">
<input
type="text"
placeholder="جستجوی شماره تراکنش (Track ID)"
value={refundFilters.trackId}
onChange={(e) => setRefundFilters({ ...refundFilters, trackId: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-2.5 pr-3 pl-8 text-xs outline-none focus:ring-1 focus:ring-indigo-500"
/>
<Search className="w-3.5 h-3.5 text-gray-400 absolute left-2.5 top-3" />
</div>
<div className="relative">
<input
type="text"
placeholder="جستجوی شناسه استرداد (Refund ID)"
value={refundFilters.refundId}
onChange={(e) => setRefundFilters({ ...refundFilters, refundId: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-2.5 pr-3 pl-8 text-xs outline-none focus:ring-1 focus:ring-indigo-500"
/>
<Search className="w-3.5 h-3.5 text-gray-400 absolute left-2.5 top-3" />
</div>
<div className="relative">
<input
type="text"
placeholder="جستجوی شناسه سفارش (Order ID)"
value={refundFilters.orderId}
onChange={(e) => setRefundFilters({ ...refundFilters, orderId: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-2.5 pr-3 pl-8 text-xs outline-none focus:ring-1 focus:ring-indigo-500"
/>
<Search className="w-3.5 h-3.5 text-gray-400 absolute left-2.5 top-3" />
</div>
<div className="relative">
<input
type="text"
placeholder="جستجوی نام صاحب حساب"
value={refundFilters.name}
onChange={(e) => setRefundFilters({ ...refundFilters, name: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-2.5 pr-3 pl-8 text-xs outline-none focus:ring-1 focus:ring-indigo-500"
/>
<Search className="w-3.5 h-3.5 text-gray-400 absolute left-2.5 top-3" />
</div>
<div className="relative">
<input
type="text"
placeholder="جستجوی مقصد (شماره کارت / شبا)"
value={refundFilters.destination}
onChange={(e) => setRefundFilters({ ...refundFilters, destination: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-2.5 pr-3 pl-8 text-xs outline-none focus:ring-1 focus:ring-indigo-500"
/>
<Search className="w-3.5 h-3.5 text-gray-400 absolute left-2.5 top-3" />
</div>
<div className="relative">
<input
type="text"
placeholder="جستجوی شماره مرجع بانکی (RRN)"
value={refundFilters.refNumber}
onChange={(e) => setRefundFilters({ ...refundFilters, refNumber: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-2.5 pr-3 pl-8 text-xs outline-none focus:ring-1 focus:ring-indigo-500"
/>
<Search className="w-3.5 h-3.5 text-gray-400 absolute left-2.5 top-3" />
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
<button
onClick={() => {
setRefundFilters({
...refundFilters,
trackId: '',
refundId: '',
orderId: '',
description: '',
name: '',
destination: '',
refNumber: '',
});
}}
className="text-indigo-600 hover:text-indigo-800 font-bold text-xs cursor-pointer"
>
پاک کردن فرم
</button>
<button
onClick={() => {
setRefundSearchOpen(false);
fetchRefunds();
}}
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold px-4 py-2 rounded-xl flex items-center gap-1.5 cursor-pointer shadow-md shadow-indigo-100"
>
<Search className="w-3 h-3" />
<span>جستجو</span>
</button>
</div>
</div>
)}
</div>
</div>
</div>
{/* Refund Table */}
{loadingRefunds ? (
<div className="py-20 flex justify-center">
<Spinner size="lg" className="text-indigo-600" />
</div>
) : refunds.length === 0 ? (
<div className="py-16 text-center text-gray-400 space-y-3">
<div className="w-14 h-14 bg-gray-50 border border-gray-100 rounded-2xl flex items-center justify-center mx-auto text-gray-300">
<RotateCcw className="w-6 h-6" />
</div>
<p className="text-xs font-bold">نتیجهای یافت نشد.</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-right border-collapse text-xs">
<thead>
<tr className="bg-gray-50/80 border-b border-gray-200 text-[11px] font-black text-gray-600">
<th className="p-3.5">شناسه استرداد</th>
<th className="p-3.5">شماره تراکنش</th>
<th className="p-3.5">شناسه سفارش</th>
<th className="p-3.5">شماره مرجع</th>
<th className="p-3.5">نام / دارنده</th>
<th className="p-3.5">مقصد</th>
<th className="p-3.5">مبلغ استرداد</th>
<th className="p-3.5">نوع / کارمزد</th>
<th className="p-3.5">وضعیت</th>
<th className="p-3.5">تاریخ ایجاد</th>
<th className="p-3.5 text-center">عملیات</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 font-bold">
{refunds.map((rf: any, i: number) => (
<tr key={rf.id || i} className="hover:bg-gray-50/70 transition-colors">
<td className="p-3.5 font-mono text-[11px] text-gray-800">{rf.id || rf.refundId || '-'}</td>
<td className="p-3.5 font-mono text-[11px] text-indigo-700">{rf.trackId || rf.transactionTrackId || '-'}</td>
<td className="p-3.5 font-mono text-[11px] text-gray-600">{rf.orderId || '-'}</td>
<td className="p-3.5 font-mono text-[11px] text-emerald-700">{rf.refNumber || '-'}</td>
<td className="p-3.5 text-gray-900">{rf.name || rf.cardHolder || '-'}</td>
<td className="p-3.5 font-mono text-[11px] text-gray-700">{rf.cardNumber || rf.bankAccount || '-'}</td>
<td className="p-3.5 font-black text-gray-900 font-mono">
{(Number(rf.amount || 0) / 10).toLocaleString()} تومان
</td>
<td className="p-3.5 text-[11px] text-gray-500">
<span className="bg-slate-100 text-slate-700 px-2 py-0.5 rounded">
{rf.tryReverse ? 'Reverse' : 'Refund'}
</span>
</td>
<td className="p-3.5">
<span
className={`px-2.5 py-1 rounded-full text-[10px] font-black inline-flex items-center gap-1 ${
rf.status === 1 || rf.status === 'SUCCESS'
? 'bg-emerald-50 text-emerald-700 border border-emerald-200'
: rf.status === 0 || rf.status === 'PENDING'
? 'bg-amber-50 text-amber-700 border border-amber-200'
: 'bg-rose-50 text-rose-700 border border-rose-200'
}`}
>
{rf.status === 1 || rf.status === 'SUCCESS' ? 'موفق' : rf.status === 0 || rf.status === 'PENDING' ? 'در صف' : 'ناموفق'}
</span>
</td>
<td className="p-3.5 text-[11px] text-gray-500 font-mono">
{rf.createdAt ? new Date(rf.createdAt).toLocaleDateString('fa-IR') : '-'}
</td>
<td className="p-3.5 text-center">
<button
onClick={() => handleInquireRefundStatus(rf)}
className="p-1.5 bg-indigo-50 hover:bg-indigo-100 text-indigo-600 rounded-lg transition-colors cursor-pointer"
title="استعلام آخرین وضعیت از زیبال"
>
<Eye className="w-3.5 h-3.5" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* ======================================================== */}
{/* 2. TAB CONTENT: CHECKOUTS / PAYOUTS (گزارش واریز) */}
{/* ======================================================== */}
{activeMainTab === 'checkouts' && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
<div className="flex items-center gap-2.5">
<Button
variant="primary"
size="sm"
startIcon={Download}
onClick={() => downloadCSV(checkouts, 'zibal-checkouts')}
>
دانلود گزارش
</Button>
<Button
variant="outline"
size="sm"
onClick={fetchCheckouts}
isLoading={loadingCheckouts}
startIcon={RefreshCw}
title="بروزرسانی"
/>
</div>
{/* Filter Inputs Bar */}
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-1.5 bg-gray-50 border border-gray-200 px-3 py-1.5 rounded-xl text-xs font-bold">
<Calendar className="w-3.5 h-3.5 text-indigo-600" />
<span>از:</span>
<input
type="date"
value={checkoutFilters.fromDate}
onChange={(e) => setCheckoutFilters({ ...checkoutFilters, fromDate: e.target.value })}
className="bg-transparent outline-none font-mono text-[11px]"
/>
<span>تا:</span>
<input
type="date"
value={checkoutFilters.toDate}
onChange={(e) => setCheckoutFilters({ ...checkoutFilters, toDate: e.target.value })}
className="bg-transparent outline-none font-mono text-[11px]"
/>
</div>
{/* Advanced Checkout Search Popup */}
<div className="relative">
<button
onClick={() => setCheckoutSearchOpen(!checkoutSearchOpen)}
className="border border-gray-200 bg-gray-50 hover:bg-gray-100 px-4 py-2 rounded-xl text-xs font-bold flex items-center gap-2 cursor-pointer"
>
<Search className="w-3.5 h-3.5 text-gray-500" />
<span>جستجو</span>
</button>
{checkoutSearchOpen && (
<div className="absolute left-0 mt-2 w-72 bg-white rounded-2xl shadow-2xl border border-gray-200 p-4 space-y-3 z-30 font-vazir text-xs">
<div className="space-y-2.5">
<input
type="text"
placeholder="جستجوی شناسه مرجع"
value={checkoutFilters.refNumber}
onChange={(e) => setCheckoutFilters({ ...checkoutFilters, refNumber: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-2.5 text-xs outline-none focus:ring-1 focus:ring-indigo-500"
/>
<input
type="text"
placeholder="جستجوی حساب مقصد (شبا)"
value={checkoutFilters.destinationAccount}
onChange={(e) => setCheckoutFilters({ ...checkoutFilters, destinationAccount: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-2.5 text-xs outline-none focus:ring-1 focus:ring-indigo-500"
/>
</div>
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
<button
onClick={() => setCheckoutFilters({ ...checkoutFilters, refNumber: '', destinationAccount: '' })}
className="text-indigo-600 hover:text-indigo-800 font-bold text-xs cursor-pointer"
>
پاک کردن فرم
</button>
<button
onClick={() => {
setCheckoutSearchOpen(false);
fetchCheckouts();
}}
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold px-4 py-2 rounded-xl flex items-center gap-1.5 cursor-pointer"
>
<Search className="w-3 h-3" />
<span>جستجو</span>
</button>
</div>
</div>
)}
</div>
</div>
</div>
{/* Table */}
{loadingCheckouts ? (
<div className="py-20 flex justify-center">
<Spinner size="lg" className="text-indigo-600" />
</div>
) : checkouts.length === 0 ? (
<div className="py-16 text-center text-gray-400 space-y-3">
<div className="w-14 h-14 bg-gray-50 border border-gray-100 rounded-2xl flex items-center justify-center mx-auto text-gray-300">
<CreditCard className="w-6 h-6" />
</div>
<p className="text-xs font-bold">نتیجهای یافت نشد.</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-right border-collapse text-xs">
<thead>
<tr className="bg-gray-50/80 border-b border-gray-200 text-[11px] font-black text-gray-600">
<th className="p-3.5">حساب مقصد</th>
<th className="p-3.5">نام صاحب حساب</th>
<th className="p-3.5">مبلغ کل (تومان)</th>
<th className="p-3.5">نوع تسویه</th>
<th className="p-3.5">تاریخ واریز</th>
<th className="p-3.5">شناسه مرجع</th>
<th className="p-3.5">وضعیت</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 font-bold">
{checkouts.map((chk: any, i: number) => (
<tr key={chk.id || i} className="hover:bg-gray-50/70 transition-colors">
<td className="p-3.5 font-mono text-[11px] text-gray-900">{chk.accountIban || chk.iban || chk.destination || '-'}</td>
<td className="p-3.5 text-gray-900">{chk.accountName || chk.name || '-'}</td>
<td className="p-3.5 font-black text-emerald-700 font-mono">
{(Number(chk.amount || 0) / 10).toLocaleString()} تومان
</td>
<td className="p-3.5 text-[11px] text-gray-500">{chk.type || 'پایا شاپرکی'}</td>
<td className="p-3.5 font-mono text-[11px] text-gray-500">
{chk.paidAt || chk.createdAt ? new Date(chk.paidAt || chk.createdAt).toLocaleDateString('fa-IR') : '-'}
</td>
<td className="p-3.5 font-mono text-[11px] text-indigo-700">{chk.refNumber || chk.referenceId || '-'}</td>
<td className="p-3.5">
<span className="bg-emerald-50 text-emerald-700 border border-emerald-200 px-2.5 py-1 rounded-full text-[10px] font-black">
واریز شده
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* ======================================================== */}
{/* 3. TAB CONTENT: QUEUE (صف تسویه) */}
{/* ======================================================== */}
{activeMainTab === 'queue' && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-4">
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
<h3 className="font-black text-sm text-gray-900 flex items-center gap-2">
<ListOrdered className="w-4 h-4 text-indigo-600" />
<span>لیست تسویهها و واریزیهای در انتظار تایید شاپرک</span>
</h3>
<button
onClick={fetchQueue}
disabled={loadingQueue}
className="text-xs font-bold text-indigo-600 hover:text-indigo-800 flex items-center gap-1 cursor-pointer"
>
<RefreshCw className={`w-3.5 h-3.5 ${loadingQueue ? 'animate-spin' : ''}`} />
<span>بروزرسانی صف</span>
</button>
</div>
{loadingQueue ? (
<div className="py-20 flex justify-center">
<Spinner size="lg" className="text-indigo-600" />
</div>
) : queueItems.length === 0 ? (
<div className="py-12 text-center text-gray-400 space-y-2">
<CheckCircle2 className="w-10 h-10 text-emerald-500 mx-auto" />
<p className="text-xs font-bold text-gray-700">صف تسویه خالی است. کلیه مبالغ تسویه شدهاند.</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-right border-collapse text-xs">
<thead>
<tr className="bg-gray-50/80 border-b border-gray-200 text-[11px] font-black text-gray-600">
<th className="p-3.5">شناسه درخواست</th>
<th className="p-3.5">حساب مقصد (شبا)</th>
<th className="p-3.5">مبلغ در صف</th>
<th className="p-3.5">زمان ثبت در صف</th>
<th className="p-3.5">وضعیت</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 font-bold">
{queueItems.map((q: any, i: number) => (
<tr key={q.id || i} className="hover:bg-gray-50/70">
<td className="p-3.5 font-mono text-[11px]">{q.id || q.requestId || '-'}</td>
<td className="p-3.5 font-mono text-[11px] text-gray-800">{q.iban || q.accountIban || '-'}</td>
<td className="p-3.5 font-black text-indigo-700 font-mono">
{(Number(q.amount || 0) / 10).toLocaleString()} تومان
</td>
<td className="p-3.5 font-mono text-[11px] text-gray-500">
{q.createdAt ? new Date(q.createdAt).toLocaleDateString('fa-IR') : '-'}
</td>
<td className="p-3.5">
<span className="bg-amber-50 text-amber-700 border border-amber-200 px-2.5 py-1 rounded-full text-[10px] font-black">
در انتظار سیکل پایا
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* ======================================================== */}
{/* 4. TAB CONTENT: GATEWAY TRANSACTIONS (تراکنش‌های درگاه) */}
{/* ======================================================== */}
{activeMainTab === 'gateway_transactions' && (
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 p-6 space-y-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-100 pb-5">
<div className="flex items-center gap-2.5">
<Button
variant="primary"
size="sm"
startIcon={Download}
onClick={() => downloadCSV(gatewayTxList, 'zibal-ipg-transactions')}
>
دانلود گزارش درگاه
</Button>
<Button
variant="outline"
size="sm"
onClick={fetchGatewayTx}
isLoading={loadingGatewayTx}
startIcon={RefreshCw}
title="بروزرسانی"
/>
</div>
{/* Filter Inputs */}
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-1.5 bg-gray-50 border border-gray-200 px-3 py-1.5 rounded-xl text-xs font-bold">
<Calendar className="w-3.5 h-3.5 text-indigo-600" />
<span>از:</span>
<input
type="date"
value={gatewayFilters.fromDate}
onChange={(e) => setGatewayFilters({ ...gatewayFilters, fromDate: e.target.value })}
className="bg-transparent outline-none font-mono text-[11px]"
/>
<span>تا:</span>
<input
type="date"
value={gatewayFilters.toDate}
onChange={(e) => setGatewayFilters({ ...gatewayFilters, toDate: e.target.value })}
className="bg-transparent outline-none font-mono text-[11px]"
/>
</div>
{/* Advanced Gateway Search Popup */}
<div className="relative">
<button
onClick={() => setGatewaySearchOpen(!gatewaySearchOpen)}
className="border border-gray-200 bg-gray-50 hover:bg-gray-100 px-4 py-2 rounded-xl text-xs font-bold flex items-center gap-2 cursor-pointer"
>
<Search className="w-3.5 h-3.5 text-gray-500" />
<span>جستجو و فیلتر تراکنش</span>
</button>
{gatewaySearchOpen && (
<div className="absolute left-0 mt-2 w-72 bg-white rounded-2xl shadow-2xl border border-gray-200 p-4 space-y-3 z-30 font-vazir text-xs">
<div className="space-y-2.5">
<input
type="text"
placeholder="شماره تراکنش (Track ID)"
value={gatewayFilters.trackId}
onChange={(e) => setGatewayFilters({ ...gatewayFilters, trackId: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-2.5 text-xs outline-none focus:ring-1 focus:ring-indigo-500 font-mono"
dir="ltr"
/>
<input
type="text"
placeholder="شماره کارت پرداخت‌کننده"
value={gatewayFilters.cardNumber}
onChange={(e) => setGatewayFilters({ ...gatewayFilters, cardNumber: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-2.5 text-xs outline-none focus:ring-1 focus:ring-indigo-500 font-mono"
dir="ltr"
/>
<input
type="text"
placeholder="شماره موبایل"
value={gatewayFilters.mobile}
onChange={(e) => setGatewayFilters({ ...gatewayFilters, mobile: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-2.5 text-xs outline-none focus:ring-1 focus:ring-indigo-500 font-mono"
dir="ltr"
/>
</div>
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
<button
onClick={() => setGatewayFilters({ ...gatewayFilters, trackId: '', cardNumber: '', mobile: '' })}
className="text-indigo-600 hover:text-indigo-800 font-bold text-xs cursor-pointer"
>
پاک کردن فرم
</button>
<button
onClick={() => {
setGatewaySearchOpen(false);
fetchGatewayTx();
}}
className="bg-indigo-600 hover:bg-indigo-700 text-white font-bold px-4 py-2 rounded-xl flex items-center gap-1.5 cursor-pointer"
>
<Search className="w-3 h-3" />
<span>جستجو</span>
</button>
</div>
</div>
)}
</div>
</div>
</div>
{/* Table */}
{loadingGatewayTx ? (
<div className="py-20 flex justify-center">
<Spinner size="lg" className="text-indigo-600" />
</div>
) : gatewayTxList.length === 0 ? (
<div className="py-16 text-center text-gray-400 space-y-3">
<div className="w-14 h-14 bg-gray-50 border border-gray-100 rounded-2xl flex items-center justify-center mx-auto text-gray-300">
<TrendingUp className="w-6 h-6" />
</div>
<p className="text-xs font-bold">تراکنشی در درگاه پرداخت اینترنتی یافت نشد.</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-right border-collapse text-xs">
<thead>
<tr className="bg-gray-50/80 border-b border-gray-200 text-[11px] font-black text-gray-600">
<th className="p-3.5">شماره تراکنش (Track ID)</th>
<th className="p-3.5">شناسه سفارش</th>
<th className="p-3.5">شماره کارت</th>
<th className="p-3.5">مبلغ پرداخت</th>
<th className="p-3.5">شماره موبایل</th>
<th className="p-3.5">تاریخ پرداخت</th>
<th className="p-3.5">وضعیت تراکنش</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 font-bold">
{gatewayTxList.map((tx: any, i: number) => (
<tr key={tx.id || tx.trackId || i} className="hover:bg-gray-50/70 transition-colors">
<td className="p-3.5 font-mono text-[11px] text-indigo-700">{tx.trackId || '-'}</td>
<td className="p-3.5 font-mono text-[11px] text-gray-800">{tx.orderId || '-'}</td>
<td className="p-3.5 font-mono text-[11px] text-gray-700 dir-ltr text-right">{tx.cardNumber || '-'}</td>
<td className="p-3.5 font-black text-gray-900 font-mono">
{(Number(tx.amount || 0) / 10).toLocaleString()} تومان
</td>
<td className="p-3.5 font-mono text-[11px] text-gray-600 dir-ltr text-right">{tx.mobile || '-'}</td>
<td className="p-3.5 font-mono text-[11px] text-gray-500">
{tx.paidAt ? new Date(tx.paidAt).toLocaleDateString('fa-IR') : '-'}
</td>
<td className="p-3.5">
<span
className={`px-2.5 py-1 rounded-full text-[10px] font-black ${
tx.status === 1 || tx.status === 2
? 'bg-emerald-50 text-emerald-700 border border-emerald-200'
: 'bg-rose-50 text-rose-700 border border-rose-200'
}`}
>
{tx.status === 1 || tx.status === 2 ? 'پرداخت موفق' : 'ناموفق'}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* ======================================================== */}
{/* MODAL: NEW REFUND SUBMIT */}
{/* ======================================================== */}
{newRefundModal && (
<Modal
isOpen={newRefundModal}
onClose={() => setNewRefundModal(false)}
title="ثبت و ارسال استرداد وجه جدید (Refund / Reverse)"
icon={RotateCcw}
maxWidth="lg"
footer={
<div className="flex justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
type="button"
onClick={() => setNewRefundModal(false)}
>
انصراف
</Button>
<Button
variant="danger"
size="sm"
type="submit"
form="zibalRefundForm"
startIcon={Send}
isLoading={isSubmittingRefund}
>
ارسال به زیبال
</Button>
</div>
}
>
<div className="space-y-4 text-xs font-vazir text-right">
<form id="zibalRefundForm" onSubmit={handleCreateRefundSubmit} className="space-y-4 text-xs">
<div>
<label className="block font-bold text-gray-700 mb-1">شماره تراکنش شاپرک (Track ID) *</label>
<input
type="text"
required
placeholder="مثال: 2808993485"
value={refundForm.trackId}
onChange={(e) => setRefundForm({ ...refundForm, trackId: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-3 font-mono outline-none focus:ring-2 focus:ring-rose-500"
dir="ltr"
/>
</div>
<div>
<label className="block font-bold text-gray-700 mb-1">مبلغ استرداد به تومان (اختیاری - خالی برای کل مبلغ)</label>
<input
type="number"
placeholder="کل مبلغ تراکنش"
value={refundForm.amount}
onChange={(e) => setRefundForm({ ...refundForm, amount: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-3 font-mono outline-none focus:ring-2 focus:ring-rose-500"
dir="ltr"
/>
</div>
<div>
<label className="block font-bold text-gray-700 mb-1">شماره کارت مقصد (اختیاری جهت واریز به کارت خاص)</label>
<input
type="text"
placeholder="شماره کارت ۱۶ رقمی"
value={refundForm.cardNumber}
onChange={(e) => setRefundForm({ ...refundForm, cardNumber: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-3 font-mono outline-none focus:ring-2 focus:ring-rose-500"
dir="ltr"
/>
</div>
<div>
<label className="block font-bold text-gray-700 mb-1">توضیحات و علت استرداد</label>
<textarea
rows={2}
placeholder="علت لغو سفارش یا مرجوعی کالا..."
value={refundForm.description}
onChange={(e) => setRefundForm({ ...refundForm, description: e.target.value })}
className="w-full border border-gray-200 rounded-xl p-3 outline-none focus:ring-2 focus:ring-rose-500"
/>
</div>
<div className="flex items-center gap-2 pt-1">
<input
type="checkbox"
id="modalTryReverse"
checked={refundForm.tryReverse}
onChange={(e) => setRefundForm({ ...refundForm, tryReverse: e.target.checked })}
className="rounded text-rose-600 focus:ring-rose-500 cursor-pointer"
/>
<label htmlFor="modalTryReverse" className="font-bold text-gray-700 cursor-pointer">
اولویت با Reverse باشد (برگشت آنی قبل از تسویه شاپرک)
</label>
</div>
</form>
</div>
</Modal>
)}
{/* ======================================================== */}
{/* MODAL: DETAIL / INQUIRY VIEWER */}
{/* ======================================================== */}
{detailModalItem && (
<Modal
isOpen={!!detailModalItem}
onClose={() => setDetailModalItem(null)}
title={detailModalItem.title}
maxWidth="lg"
footer={
<div className="flex justify-end w-full">
<Button
variant="secondary"
size="sm"
onClick={() => setDetailModalItem(null)}
>
بستن
</Button>
</div>
}
>
<pre className="bg-slate-900 text-emerald-400 p-4 rounded-xl text-[11px] font-mono overflow-x-auto dir-ltr">
{JSON.stringify(detailModalItem.data, null, 2)}
</pre>
</Modal>
)}
</div>
);
}