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'; export default function ZibalPortalPage() { const [activeMainTab, setActiveMainTab] = useState<'refunds' | 'checkouts' | 'queue' | 'gateway_transactions'>('refunds'); // ========================================== // TAB 1: REFUNDS (استرداد وجه) // ========================================== const [refunds, setRefunds] = useState([]); 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([]); 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([]); const [loadingQueue, setLoadingQueue] = useState(false); // ========================================== // TAB 4: DIRECT GATEWAY TRANSACTIONS (تراکنش‌های درگاه) // ========================================== const [gatewayTxList, setGatewayTxList] = useState([]); 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 (
{/* Top Header */}

پرتال و داشبورد خدمات مالی زیبال

مشاهده گزارشات استرداد وجه (Refund/Reverse)، تسویه‌ها و واریزها، صف تسویه و تراکنش‌های رسمی شاپرک

{/* Main Feature Tabs (Matching Zibal Panel layout) */}
{/* ======================================================== */} {/* 1. TAB CONTENT: REFUNDS (استرداد وجه) */} {/* ======================================================== */} {activeMainTab === 'refunds' && (
{/* Top Bar with Filter & Search Controls */}
{/* Filter Inputs Bar */}
{/* Date Filter */}
تاریخ از: setRefundFilters({ ...refundFilters, fromDate: e.target.value })} className="bg-transparent outline-none font-mono text-[11px]" /> تا: setRefundFilters({ ...refundFilters, toDate: e.target.value })} className="bg-transparent outline-none font-mono text-[11px]" />
{/* Status Selector */} {/* Advanced Search Popup Toggle */}
{/* Advanced Search Dropdown Window (Matching Exact User Screenshot) */} {refundSearchOpen && (
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" />
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" />
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" />
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" />
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" />
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" />
)}
{/* Refund Table */} {loadingRefunds ? (
) : refunds.length === 0 ? (

نتیجه‌ای یافت نشد.

) : (
{refunds.map((rf: any, i: number) => ( ))}
شناسه استرداد شماره تراکنش شناسه سفارش شماره مرجع نام / دارنده مقصد مبلغ استرداد نوع / کارمزد وضعیت تاریخ ایجاد عملیات
{rf.id || rf.refundId || '-'} {rf.trackId || rf.transactionTrackId || '-'} {rf.orderId || '-'} {rf.refNumber || '-'} {rf.name || rf.cardHolder || '-'} {rf.cardNumber || rf.bankAccount || '-'} {(Number(rf.amount || 0) / 10).toLocaleString()} تومان {rf.tryReverse ? 'Reverse' : 'Refund'} {rf.status === 1 || rf.status === 'SUCCESS' ? 'موفق' : rf.status === 0 || rf.status === 'PENDING' ? 'در صف' : 'ناموفق'} {rf.createdAt ? new Date(rf.createdAt).toLocaleDateString('fa-IR') : '-'}
)}
)} {/* ======================================================== */} {/* 2. TAB CONTENT: CHECKOUTS / PAYOUTS (گزارش واریز) */} {/* ======================================================== */} {activeMainTab === 'checkouts' && (
{/* Filter Inputs Bar */}
از: setCheckoutFilters({ ...checkoutFilters, fromDate: e.target.value })} className="bg-transparent outline-none font-mono text-[11px]" /> تا: setCheckoutFilters({ ...checkoutFilters, toDate: e.target.value })} className="bg-transparent outline-none font-mono text-[11px]" />
{/* Advanced Checkout Search Popup */}
{checkoutSearchOpen && (
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" /> 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" />
)}
{/* Table */} {loadingCheckouts ? (
) : checkouts.length === 0 ? (

نتیجه‌ای یافت نشد.

) : (
{checkouts.map((chk: any, i: number) => ( ))}
حساب مقصد نام صاحب حساب مبلغ کل (تومان) نوع تسویه تاریخ واریز شناسه مرجع وضعیت
{chk.accountIban || chk.iban || chk.destination || '-'} {chk.accountName || chk.name || '-'} {(Number(chk.amount || 0) / 10).toLocaleString()} تومان {chk.type || 'پایا شاپرکی'} {chk.paidAt || chk.createdAt ? new Date(chk.paidAt || chk.createdAt).toLocaleDateString('fa-IR') : '-'} {chk.refNumber || chk.referenceId || '-'} واریز شده
)}
)} {/* ======================================================== */} {/* 3. TAB CONTENT: QUEUE (صف تسویه) */} {/* ======================================================== */} {activeMainTab === 'queue' && (

لیست تسویه‌ها و واریزی‌های در انتظار تایید شاپرک

{loadingQueue ? (
) : queueItems.length === 0 ? (

صف تسویه خالی است. کلیه مبالغ تسویه شده‌اند.

) : (
{queueItems.map((q: any, i: number) => ( ))}
شناسه درخواست حساب مقصد (شبا) مبلغ در صف زمان ثبت در صف وضعیت
{q.id || q.requestId || '-'} {q.iban || q.accountIban || '-'} {(Number(q.amount || 0) / 10).toLocaleString()} تومان {q.createdAt ? new Date(q.createdAt).toLocaleDateString('fa-IR') : '-'} در انتظار سیکل پایا
)}
)} {/* ======================================================== */} {/* 4. TAB CONTENT: GATEWAY TRANSACTIONS (تراکنش‌های درگاه) */} {/* ======================================================== */} {activeMainTab === 'gateway_transactions' && (
{/* Filter Inputs */}
از: setGatewayFilters({ ...gatewayFilters, fromDate: e.target.value })} className="bg-transparent outline-none font-mono text-[11px]" /> تا: setGatewayFilters({ ...gatewayFilters, toDate: e.target.value })} className="bg-transparent outline-none font-mono text-[11px]" />
{/* Advanced Gateway Search Popup */}
{gatewaySearchOpen && (
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" /> 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" /> 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" />
)}
{/* Table */} {loadingGatewayTx ? (
) : gatewayTxList.length === 0 ? (

تراکنشی در درگاه پرداخت اینترنتی یافت نشد.

) : (
{gatewayTxList.map((tx: any, i: number) => ( ))}
شماره تراکنش (Track ID) شناسه سفارش شماره کارت مبلغ پرداخت شماره موبایل تاریخ پرداخت وضعیت تراکنش
{tx.trackId || '-'} {tx.orderId || '-'} {tx.cardNumber || '-'} {(Number(tx.amount || 0) / 10).toLocaleString()} تومان {tx.mobile || '-'} {tx.paidAt ? new Date(tx.paidAt).toLocaleDateString('fa-IR') : '-'} {tx.status === 1 || tx.status === 2 ? 'پرداخت موفق' : 'ناموفق'}
)}
)} {/* ======================================================== */} {/* MODAL: SUBMIT NEW REFUND / REVERSE */} {/* ======================================================== */} {newRefundModal && (

ثبت درخواست استرداد وجه (Refund / Reverse)

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" />
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" />
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" />