- Added BackButton component to standardize back navigation across pages. - Integrated BackButton into various pages, replacing custom back buttons for consistency. - Updated PageHeader to accept backTo prop for displaying BackButton when navigating from subpages. - Created useGoBack hook to handle navigation logic, determining whether to go back in history or redirect to a fallback page. - Added tests for BackButton and its integration with PageHeader to ensure expected behavior.
164 lines
7.0 KiB
TypeScript
164 lines
7.0 KiB
TypeScript
import React from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { ArrowRightIcon } from '@heroicons/react/24/outline';
|
|
import { toast } from 'sonner';
|
|
import { api } from '../lib/api';
|
|
import type { ApiResponse } from '../lib/api';
|
|
import type { Payment } from '../types';
|
|
import { formatDateTime, formatRial } from '../lib/utils';
|
|
import PageHeader from '../components/ui/PageHeader';
|
|
import StatusBadge from '../components/ui/StatusBadge';
|
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
|
|
|
const PAYMENT_TYPE_LABELS: Record<string, string> = {
|
|
appointment: 'نوبت',
|
|
subscription: 'اشتراک',
|
|
sms_wallet: 'کیف پول پیامک',
|
|
};
|
|
|
|
function InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
|
|
return (
|
|
<div className="cp-info-row">
|
|
<span className="cp-info-label text-sm">{label}</span>
|
|
<span className="cp-info-value">{value ?? '—'}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function PaymentDetailPage() {
|
|
const { uuid } = useParams<{ uuid: string }>();
|
|
const navigate = useNavigate();
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['payment', uuid],
|
|
queryFn: () => api.get<ApiResponse<Payment>>(`/api/v1/admin/payments/${uuid}`),
|
|
enabled: !!uuid,
|
|
});
|
|
|
|
const payment = data?.data;
|
|
const queryClient = useQueryClient();
|
|
const [confirm, setConfirm] = React.useState<null | 'refund' | 'reverse'>(null);
|
|
|
|
const action = useMutation({
|
|
mutationFn: (kind: 'refund' | 'reverse') =>
|
|
api.post<ApiResponse<{ status: string }>>(`/api/v1/admin/payments/${uuid}/${kind}`, {}),
|
|
onSuccess: (_res, kind) => {
|
|
toast.success(kind === 'refund' ? 'درخواست استرداد وجه ثبت شد' : 'برگشت وجه انجام شد');
|
|
queryClient.invalidateQueries({ queryKey: ['payment', uuid] });
|
|
setConfirm(null);
|
|
},
|
|
onError: (e: any) => {
|
|
toast.error(e?.message || 'عملیات ناموفق بود');
|
|
setConfirm(null);
|
|
},
|
|
});
|
|
|
|
const canRefund = payment?.status === 'success' && payment?.gateway === 'mellat';
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader
|
|
backTo="/admin/payments"
|
|
title="جزئیات پرداخت"
|
|
breadcrumbs={[
|
|
{ label: 'داشبورد', to: '/admin/dashboard' },
|
|
{ label: 'پرداختها', to: '/admin/payments' },
|
|
{ label: 'جزئیات' },
|
|
]}
|
|
action={
|
|
<button onClick={() => navigate('/admin/payments')}
|
|
className="flex items-center gap-2 text-sm text-[var(--text-2)] hover:text-[var(--text)] transition-colors">
|
|
<ArrowRightIcon className="w-4 h-4" />
|
|
بازگشت
|
|
</button>
|
|
}
|
|
/>
|
|
|
|
{isLoading ? (
|
|
<div className="cp-card p-6 space-y-3">
|
|
{Array.from({ length: 6 }).map((_, i) => (
|
|
<div key={i} className="h-8 rounded-lg skeleton" />
|
|
))}
|
|
</div>
|
|
) : payment ? (
|
|
<div className="max-w-lg">
|
|
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6">
|
|
<InfoRow label="شناسه" value={<span dir="ltr" className="font-mono text-xs">{payment.uuid}</span>} />
|
|
<InfoRow label="شماره سفارش" value={payment.order_id ? <span dir="ltr" className="font-mono text-xs">{payment.order_id}</span> : null} />
|
|
<InfoRow label="بیمار" value={payment.patient_name} />
|
|
<InfoRow label="موبایل" value={<span dir="ltr">{payment.patient_mobile}</span>} />
|
|
<InfoRow label="نوع" value={PAYMENT_TYPE_LABELS[payment.type ?? ''] ?? payment.type} />
|
|
<InfoRow label="مبلغ" value={formatRial(payment.amount)} />
|
|
<InfoRow label="وضعیت" value={<StatusBadge type="payment" value={payment.status} />} />
|
|
<InfoRow label="درگاه" value={<span className="uppercase">{payment.gateway}</span>} />
|
|
<InfoRow label="شماره مرجع" value={payment.ref_id ? <span dir="ltr" className="font-mono text-xs">{payment.ref_id}</span> : null} />
|
|
<InfoRow label="شماره کارت" value={payment.card_pan ? <span dir="ltr" className="font-mono text-xs">{payment.card_pan}</span> : null} />
|
|
<InfoRow label="تاریخ پرداخت" value={formatDateTime(payment.paid_at)} />
|
|
<InfoRow label="تاریخ ثبت" value={formatDateTime(payment.created_at)} />
|
|
{payment.appointment_uuid && (
|
|
<InfoRow
|
|
label="نوبت مرتبط"
|
|
value={
|
|
<a href={`/admin/appointments/${payment.appointment_uuid}`}
|
|
className="text-[var(--primary)] hover:underline text-xs font-mono">
|
|
مشاهده نوبت
|
|
</a>
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{payment.refunds && payment.refunds.length > 0 && (
|
|
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] shadow-sm p-6 mt-4">
|
|
<h3 className="text-sm font-semibold mb-3">استردادها</h3>
|
|
{payment.refunds.map((r, i) => (
|
|
<InfoRow
|
|
key={i}
|
|
label={formatDateTime(r.at)}
|
|
value={
|
|
<span>
|
|
{formatRial(r.amount)}
|
|
{r.ref && <span dir="ltr" className="font-mono text-xs text-[var(--text-3)]"> ({r.ref})</span>}
|
|
</span>
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{canRefund && (
|
|
<div className="flex gap-3 mt-4">
|
|
<button onClick={() => setConfirm('refund')} className="btn danger">
|
|
استرداد وجه
|
|
</button>
|
|
<button onClick={() => setConfirm('reverse')} className="btn">
|
|
برگشت وجه
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<ConfirmDialog
|
|
open={confirm !== null}
|
|
danger
|
|
loading={action.isPending}
|
|
title={confirm === 'reverse' ? 'برگشت وجه' : 'استرداد وجه'}
|
|
message={
|
|
confirm === 'reverse'
|
|
? 'کل مبلغ این تراکنش برگشت داده میشود. ادامه میدهید؟'
|
|
: 'درخواست استرداد کل مبلغ به کارت پرداختکننده ثبت میشود (عودت نهایی ممکن است چند روز طول بکشد). ادامه میدهید؟'
|
|
}
|
|
confirmLabel={confirm === 'reverse' ? 'برگشت وجه' : 'استرداد وجه'}
|
|
onConfirm={() => confirm && action.mutate(confirm)}
|
|
onCancel={() => setConfirm(null)}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="bg-[var(--surface)] rounded-2xl border border-[var(--border)] p-16 text-center text-[var(--text-3)]">
|
|
پرداختی یافت نشد
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|