diff --git a/assets/admin/App.tsx b/assets/admin/App.tsx index 20566886..0f689e33 100644 --- a/assets/admin/App.tsx +++ b/assets/admin/App.tsx @@ -17,6 +17,7 @@ import AppointmentDetailPage from './pages/AppointmentDetailPage'; import PaymentsPage from './pages/PaymentsPage'; import PaymentDetailPage from './pages/PaymentDetailPage'; import SettlementsPage from './pages/SettlementsPage'; +import SettlementDetailPage from './pages/SettlementDetailPage'; import RepresentationsPage from './pages/RepresentationsPage'; import RepresentationDetailPage from './pages/RepresentationDetailPage'; import CommentsPage from './pages/CommentsPage'; @@ -147,6 +148,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/assets/admin/pages/SettlementDetailPage.tsx b/assets/admin/pages/SettlementDetailPage.tsx new file mode 100644 index 00000000..eea223b0 --- /dev/null +++ b/assets/admin/pages/SettlementDetailPage.tsx @@ -0,0 +1,236 @@ +import React, { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useParams, useNavigate } from 'react-router-dom'; +import { ArrowRightIcon, CheckIcon, XMarkIcon, ArrowUpTrayIcon, DocumentTextIcon } from '@heroicons/react/24/outline'; +import { toast } from 'sonner'; +import { api } from '../lib/api'; +import type { ApiResponse } from '../lib/api'; +import { useAuthStore } from '../stores/authStore'; +import { formatDateTime, formatRial } from '../lib/utils'; +import StatusBadge from '../components/ui/StatusBadge'; +import ConfirmDialog from '../components/ui/ConfirmDialog'; +import Modal from '../components/ui/Modal'; + +interface SettlementDetail { + uuid: string; + representation_name: string; + representation_mobile: string | null; + amount: number; + status: 'pending' | 'approved' | 'rejected' | 'paid'; + bank_card: string | null; + bank_name: string | null; + bank_iban: string | null; + bank_owner: string | null; + reject_reason: string | null; + receipt: string | null; + requested_at: string; + processed_at: string | null; +} + +function Row({ label, value }: { label: string; value: React.ReactNode }) { + return ( +
+ {label} + {value} +
+ ); +} + +export default function SettlementDetailPage() { + const { uuid } = useParams<{ uuid: string }>(); + const navigate = useNavigate(); + const qc = useQueryClient(); + const authToken = useAuthStore(s => s.token); + const [approveOpen, setApproveOpen] = useState(false); + const [rejectOpen, setRejectOpen] = useState(false); + const [rejectReason, setRejectReason] = useState(''); + const [paying, setPaying] = useState(false); + const receiptInputRef = React.useRef(null); + + const { data, isLoading } = useQuery({ + queryKey: ['settlement-detail', uuid], + queryFn: () => api.get>(`/api/v1/admin/settlement/${uuid}`), + enabled: !!uuid, + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const s: SettlementDetail | undefined = (data?.data as any)?.data ?? data?.data; + + const approveMut = useMutation({ + mutationFn: () => api.post>(`/api/v1/settlement/${uuid}/approve`, {}), + onSuccess: () => { + toast.success('تسویه تأیید شد'); + setApproveOpen(false); + qc.invalidateQueries({ queryKey: ['settlement-detail', uuid] }); + qc.invalidateQueries({ queryKey: ['settlements'] }); + }, + onError: (e: Error) => toast.error(e.message), + }); + + const rejectMut = useMutation({ + mutationFn: () => api.post>(`/api/v1/settlement/${uuid}/reject`, { reason: rejectReason }), + onSuccess: () => { + toast.success('تسویه رد شد'); + setRejectOpen(false); + setRejectReason(''); + qc.invalidateQueries({ queryKey: ['settlement-detail', uuid] }); + qc.invalidateQueries({ queryKey: ['settlements'] }); + }, + onError: (e: Error) => toast.error(e.message), + }); + + // آپلود رسید → سپس ثبت پرداخت نهایی (paid). مبلغ قبلاً هنگام درخواست از کیف‌پول کسر شده. + const handleReceiptUpload = async (file: File) => { + setPaying(true); + try { + const res = await fetch('/file/upload/clinic_pro/settlement/receipt', { + method: 'POST', + headers: { + 'Content-Disposition': `filename="${file.name}"`, + 'Content-Type': file.type || 'application/octet-stream', + Authorization: `Bearer ${authToken ?? ''}`, + }, + body: file, + }); + const json = await res.json(); + const url = json?.data?.url; + if (!url) throw new Error('آپلود رسید ناموفق بود'); + await api.post>(`/api/v1/settlement/${uuid}/paid`, { receipt: url }); + toast.success('پرداخت ثبت شد'); + qc.invalidateQueries({ queryKey: ['settlement-detail', uuid] }); + qc.invalidateQueries({ queryKey: ['settlements'] }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { toast.error(e?.message ?? 'خطا در ثبت پرداخت'); } + finally { setPaying(false); } + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!s) { + return ( +
+
+

درخواست تسویه یافت نشد

+ +
+
+ ); + } + + return ( +
+
+
+ +
+

جزئیات تسویه

+
{s.representation_name}
+
+
+ {s.status === 'pending' && ( +
+ + +
+ )} +
+ +
+
+ {formatRial(s.amount)} + +
+ + {s.representation_mobile} : '—'} /> + + + {s.bank_card} : '—'} /> + {s.bank_iban} : '—'} /> + + + {s.reject_reason && } +
+ + {/* رسید پرداخت */} + {(s.status === 'approved' || s.status === 'paid' || s.receipt) && ( +
+
+ + رسید پرداخت +
+ + {s.receipt ? ( + + رسید + + مشاهده‌ی اندازه‌ی کامل ↗ + + + ) : ( +

هنوز رسیدی آپلود نشده است.

+ )} + + {s.status === 'approved' && ( +
+ e.target.files?.[0] && handleReceiptUpload(e.target.files[0])} /> + +

+ با آپلود رسید، وضعیت به «پرداخت شده» تغییر می‌کند. مبلغ هنگام ثبت درخواست از کیف‌پول کسر شده است. +

+
+ )} +
+ )} + + approveMut.mutate()} + onCancel={() => setApproveOpen(false)} + /> + + { setRejectOpen(false); setRejectReason(''); }} + footer={ + <> + + + + } + > +
+ +