@@ -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 (
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , padding : '12px 0' , borderBottom : '1px solid var(--border)' , gap : 12 } } >
< span className = "muted" style = { { fontSize : 13 } } > { label } < / span >
< span style = { { fontSize : 13.5 , fontWeight : 500 , textAlign : 'left' } } > { value } < / span >
< / div >
) ;
}
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 < HTMLInputElement > ( null ) ;
const { data , isLoading } = useQuery ( {
queryKey : [ 'settlement-detail' , uuid ] ,
queryFn : ( ) = > api . get < ApiResponse < SettlementDetail > > ( ` /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 < ApiResponse < null > > ( ` /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 < ApiResponse < null > > ( ` /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 < ApiResponse < null > > ( ` /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 (
< div className = "fade-in" >
< div className = "card card-pad" > < div className = "skeleton" style = { { height : 300 , borderRadius : 'var(--r)' } } / > < / div >
< / div >
) ;
}
if ( ! s ) {
return (
< div className = "fade-in" >
< div className = "card card-pad" style = { { textAlign : 'center' , padding : 40 } } >
< p className = "muted" > د ر خ و ا س ت ت س و ی ه ی ا ف ت ن ش د < / p >
< button className = "btn ghost sm" style = { { marginTop : 12 } } onClick = { ( ) = > navigate ( '/admin/settlements' ) } >
ب ا ز گ ش ت ب ه ل ی س ت
< / button >
< / div >
< / div >
) ;
}
return (
< div className = "fade-in" >
< div className = "card-title-row" style = { { marginBottom : 'var(--gap)' } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 10 } } >
< button className = "btn ghost sm" onClick = { ( ) = > navigate ( '/admin/settlements' ) } style = { { padding : '6px 10px' } } >
< ArrowRightIcon style = { { width : 15 , height : 15 } } / > ت س و ی ه ح س ا ب ه ا
< / button >
< div >
< h1 className = "section-title" > ج ز ئ ی ا ت ت س و ی ه < / h1 >
< div className = "muted" style = { { fontSize : 13 } } > { s . representation_name } < / div >
< / div >
< / div >
{ s . status === 'pending' && (
< div style = { { display : 'flex' , gap : 8 } } >
< button className = "btn primary sm" onClick = { ( ) = > setApproveOpen ( true ) } >
< CheckIcon style = { { width : 15 , height : 15 } } / > ت أ ی ی د
< / button >
< button className = "btn danger sm" onClick = { ( ) = > setRejectOpen ( true ) } >
< XMarkIcon style = { { width : 15 , height : 15 } } / > ر د
< / button >
< / div >
) }
< / div >
< div className = "card card-pad" style = { { maxWidth : 560 } } >
< div style = { { display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , marginBottom : 8 } } >
< span style = { { fontSize : 20 , fontWeight : 700 , color : 'var(--primary)' } } > { formatRial ( s . amount ) } < / span >
< StatusBadge type = "settlement" value = { s . status } / >
< / div >
< Row label = "نماینده" value = { s . representation_name } / >
< Row label = "موبایل" value = { s . representation_mobile ? < span dir = "ltr" > { s . representation_mobile } < / span > : '—' } / >
< Row label = "صاحب حساب" value = { s . bank_owner ? ? '—' } / >
< Row label = "بانک" value = { s . bank_name ? ? '—' } / >
< Row label = "شماره کارت" value = { s . bank_card ? < span dir = "ltr" style = { { fontFamily : 'monospace' } } > { s . bank_card } < / span > : '—' } / >
< Row label = "شبا" value = { s . bank_iban ? < span dir = "ltr" style = { { fontFamily : 'monospace' , fontSize : 12 } } > { s . bank_iban } < / span > : '—' } / >
< Row label = "تاریخ درخواست" value = { formatDateTime ( s . requested_at ) } / >
< Row label = "تاریخ بررسی" value = { s . processed_at ? formatDateTime ( s . processed_at ) : '—' } / >
{ s . reject_reason && < Row label = "یادداشت/دلیل رد" value = { s . reject_reason } / > }
< / div >
{ /* رسید پرداخت */ }
{ ( s . status === 'approved' || s . status === 'paid' || s . receipt ) && (
< div className = "card card-pad" style = { { maxWidth : 560 , marginTop : 'var(--gap)' } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , marginBottom : 12 } } >
< DocumentTextIcon style = { { width : 18 , height : 18 , color : 'var(--text-3)' } } / >
< b style = { { fontSize : 14 } } > ر س ی د پ ر د ا خ ت < / b >
< / div >
{ s . receipt ? (
< a href = { s . receipt } target = "_blank" rel = "noopener noreferrer"
style = { { display : 'block' , maxWidth : 280 } } >
< img src = { s . receipt } alt = "رسید"
style = { { width : '100%' , borderRadius : 'var(--r-sm)' , border : '1px solid var(--border)' } } / >
< span className = "muted" style = { { fontSize : 12 , display : 'block' , marginTop : 6 , color : 'var(--primary)' } } >
م ش ا ه د ه ی ا ن د ا ز ه ی ک ا م ل ↗
< / span >
< / a >
) : (
< p className = "muted" style = { { fontSize : 13 } } > ه ن و ز ر س ی د ی آ پ ل و د ن ش د ه ا س ت . < / p >
) }
{ s . status === 'approved' && (
< div style = { { marginTop : 14 } } >
< input ref = { receiptInputRef } type = "file" accept = "image/*" style = { { display : 'none' } }
onChange = { e = > e . target . files ? . [ 0 ] && handleReceiptUpload ( e . target . files [ 0 ] ) } / >
< button className = "btn primary sm" disabled = { paying }
onClick = { ( ) = > receiptInputRef . current ? . click ( ) } >
< ArrowUpTrayIcon style = { { width : 15 , height : 15 } } / >
{ paying ? 'در حال ثبت...' : 'آپلود رسید و ثبت پرداخت' }
< / button >
< p className = "muted" style = { { fontSize : 12 , marginTop : 8 } } >
ب ا آ پ ل و د ر س ی د ، و ض ع ی ت ب ه « پ ر د ا خ ت ش د ه » ت غ ی ی ر م ی ک ن د . م ب ل غ ه ن گ ا م ث ب ت د ر خ و ا س ت ا ز ک ی ف پ و ل ک س ر ش د ه ا س ت .
< / p >
< / div >
) }
< / div >
) }
< ConfirmDialog
open = { approveOpen }
title = "تأیید تسویه"
message = { ` تسویه ${ s . representation_name } به مبلغ ${ formatRial ( s . amount ) } را تأیید میکنید؟ ` }
confirmLabel = "تأیید"
loading = { approveMut . isPending }
onConfirm = { ( ) = > approveMut . mutate ( ) }
onCancel = { ( ) = > setApproveOpen ( false ) }
/ >
< Modal
open = { rejectOpen }
title = "رد درخواست تسویه"
onClose = { ( ) = > { setRejectOpen ( false ) ; setRejectReason ( '' ) ; } }
footer = {
< >
< button onClick = { ( ) = > { setRejectOpen ( false ) ; setRejectReason ( '' ) ; } } className = "btn ghost sm" > ل غ و < / button >
< button onClick = { ( ) = > rejectMut . mutate ( ) } disabled = { ! rejectReason || rejectMut . isPending } className = "btn danger sm" >
{ rejectMut . isPending ? 'در حال ارسال...' : 'رد کردن' }
< / button >
< / >
}
>
< div className = "form-row" >
< label > د ل ی ل ر د < / label >
< textarea value = { rejectReason } onChange = { ( e ) = > setRejectReason ( e . target . value ) } rows = { 4 }
placeholder = "دلیل رد درخواست را بنویسید..." className = "input" style = { { resize : 'none' , height : 'auto' } } / >
< / div >
< / Modal >
< / div >
) ;
}