You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

783 lines
39 KiB

import React, { useState, useEffect, useRef } from 'react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Avatar } from '@/components/ui/avatar'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Textarea } from '@/components/ui/textarea'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Ic } from '@/icons'
import type { FactorItem, FactorStatus } from '../types'
import { BillingServiceBadge } from './BillingServiceBadge'
import { fetchFactorDetail, reviewFactor } from '../services/billing-api'
import { ShimmerLine, ShimmerBox } from '@/components/shared/shimmer_frontend'
import { toast } from '@/stores/toast'
import { useTranslation } from '@/i18n'
import { formatDateByLanguage, formatDateTimeByLanguage } from '@/lib/date-utils'
import { cn, resolveMediaUrl, getMediaFallbackUrl } from '@/lib/utils'
interface Props {
factor: FactorItem | null
open: boolean
onClose: () => void
onFactorUpdated?: (updatedFactor: FactorItem) => void
onApprove?: (factorId: number) => Promise<void>
onOpenRejectDialog?: (factor: FactorItem) => void
onOpenReceiptLightbox: (factor: FactorItem) => void
onClearReceipt?: (factorId: number) => Promise<void>
isUpdating?: boolean
}
const STATUS_OPTIONS: {
value: FactorStatus
label: string
dotClass: string
}[] = [
{
value: 'awaiting_payment',
label: 'در انتظار پرداخت',
dotClass: 'bg-amber-500',
},
{
value: 'pending',
label: 'در انتظار بررسی فیش',
dotClass: 'bg-blue-500',
},
{
value: 'approved',
label: 'تایید شده / پرداخت شده',
dotClass: 'bg-emerald-500',
},
{
value: 'rejected',
label: 'رد شده',
dotClass: 'bg-rose-500',
},
{
value: 'expired',
label: 'منقضی شده',
dotClass: 'bg-slate-400',
},
]
export function BillingDetailModal({
factor,
open,
onClose,
onFactorUpdated,
onOpenReceiptLightbox,
onClearReceipt,
isUpdating,
}: Props) {
const { t, language } = useTranslation()
const [copiedCard, setCopiedCard] = useState(false)
const [currentFactor, setCurrentFactor] = useState<FactorItem | null>(factor)
const [selectedStatus, setSelectedStatus] = useState<FactorStatus>(factor?.status || 'awaiting_payment')
const [rejectReason, setRejectReason] = useState<string>(factor?.rejected_description || '')
const [isLoadingDetail, setIsLoadingDetail] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const [hasChanges, setHasChanges] = useState(false)
const [receiptError, setReceiptError] = useState(false)
const [isClearingReceipt, setIsClearingReceipt] = useState(false)
const lastLoadedFactorIdRef = useRef<number | null>(null)
useEffect(() => {
if (!open || !factor) {
lastLoadedFactorIdRef.current = null
return
}
// مقداردهی اولیه وضعیت و فیلدها منحصراً در زمان باز شدن مودال یا تغییر شناسه صورت‌حساب
if (lastLoadedFactorIdRef.current !== factor.id) {
lastLoadedFactorIdRef.current = factor.id
setCurrentFactor(factor)
setSelectedStatus(factor.status)
setRejectReason(factor.rejected_description || '')
setHasChanges(false)
setReceiptError(false)
if (!factor.detail_service) {
setIsLoadingDetail(true)
fetchFactorDetail(factor.id)
.then((res) => {
// بروزرسانی جزئیات بدون بازنویسی وضعیت تغییریافته توسط کاربر (جلوگیری قطعی از برگشت به حالت رد شده)
setCurrentFactor((prev) => (prev ? { ...prev, ...res } : res))
})
.catch((err) => {
console.error('Failed to load factor details:', err)
})
.finally(() => {
setIsLoadingDetail(false)
})
}
}
}, [factor?.id, open])
const activeFactor = currentFactor || factor
if (!factor || !activeFactor) return null
const handleCopyCard = (cardNumber: string) => {
navigator.clipboard.writeText(cardNumber)
setCopiedCard(true)
setTimeout(() => setCopiedCard(false), 2000)
}
const handleSaveStatus = async () => {
if (!activeFactor) return
if (selectedStatus === 'rejected' && !rejectReason.trim()) {
toast.error('وارد کردن دلیل رد صورت‌حساب الزامی است.', 'دلیل رد الزامی است')
return
}
setIsSaving(true)
try {
const payload = {
status: selectedStatus,
rejected_description: selectedStatus === 'rejected' ? rejectReason.trim() : undefined,
}
const updated = await reviewFactor(activeFactor.id, payload)
setCurrentFactor(updated)
setSelectedStatus(updated.status)
setRejectReason(updated.rejected_description || '')
setHasChanges(false)
if (onFactorUpdated) {
onFactorUpdated(updated)
}
const statusLabel =
STATUS_OPTIONS.find((s) => s.value === selectedStatus)?.label || selectedStatus
toast.success(
`وضعیت صورت‌حساب #${activeFactor.id} با موفقیت به «${statusLabel}» تغییر یافت.`,
'بروزرسانی وضعیت'
)
onClose()
} catch (err: any) {
toast.error(err?.message || 'خطا در ذخیره‌سازی وضعیت صورت‌حساب.', 'خطا در عملیات')
} finally {
setIsSaving(false)
}
}
const detail = activeFactor.detail_service
const user = activeFactor.user_info
return (
<Dialog open={open} onOpenChange={(val) => !val && onClose()}>
<DialogContent className="max-w-3xl max-h-[90vh] flex flex-col border border-border/60 bg-card p-0 overflow-hidden shadow-2xl rounded-2xl">
{/* هدر مودال ثابت با پس‌زمینه سالید بدون تداخل اسکرول */}
<DialogHeader className="p-4 px-5 border-b border-border/40 bg-card shrink-0 z-10">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2.5">
<div className="size-10 rounded-xl bg-primary/15 text-primary flex items-center justify-center font-bold shrink-0">
<Ic name="receipt" className="size-5" />
</div>
<div>
<DialogTitle className="text-lg font-bold flex items-center gap-2">
<span>صورت‌حساب شماره #{activeFactor.id}</span>
</DialogTitle>
<DialogDescription className="text-xs text-muted-foreground mt-0.5">
ثبت شده در: {formatDateTimeByLanguage(activeFactor.created_at, language as any)}
</DialogDescription>
</div>
</div>
<div className="flex items-center gap-2.5">
<BillingServiceBadge service={activeFactor.service} />
{/* دراپ‌داون تعاملی انتخاب وضعیت صورت‌حساب و دکمه ذخیره سریع در هدر */}
<div className="flex items-center gap-2">
<div className="relative">
<Select
value={selectedStatus}
onValueChange={(val) => {
const newStatus = val as FactorStatus
setSelectedStatus(newStatus)
setHasChanges(newStatus !== activeFactor.status)
}}
>
<SelectTrigger className="h-8.5 px-3 min-w-[175px] text-xs font-bold rounded-xl border-border-soft bg-card shadow-2xs hover:bg-muted/40 transition-colors">
<SelectValue placeholder="انتخاب وضعیت...">
{(() => {
const curOpt = STATUS_OPTIONS.find((o) => o.value === selectedStatus)
if (!curOpt) return selectedStatus
return (
<div className="flex items-center gap-2">
<span className={cn('size-2 rounded-full shrink-0', curOpt.dotClass)} />
<span className="truncate">{curOpt.label}</span>
</div>
)
})()}
</SelectValue>
</SelectTrigger>
<SelectContent className="rounded-xl border-border-soft bg-card shadow-xl p-1 z-50">
{STATUS_OPTIONS.map((opt) => (
<SelectItem
key={opt.value}
value={opt.value}
className="rounded-lg text-xs font-medium py-2 gap-2 cursor-pointer"
>
<div className="flex items-center gap-2">
<span className={cn('size-2 rounded-full shrink-0', opt.dotClass)} />
<span>{opt.label}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* دکمه ذخیره وضعیت سریع در کنار منو برای جلوگیری از سردرگمی کاربر */}
{hasChanges && (
<Button
size="sm"
disabled={isSaving || isUpdating || (selectedStatus === 'rejected' && !rejectReason.trim())}
isLoading={isSaving || isUpdating}
onClick={handleSaveStatus}
className="h-8.5 px-3.5 text-xs font-bold gap-1 rounded-xl bg-primary text-primary-foreground shadow-xs hover:brightness-110 animate-in fade-in zoom-in-95 duration-150 shrink-0"
title="ذخیره وضعیت انتخاب‌شده"
>
<Ic name="check" className="size-3.5" />
<span>ذخیره وضعیت</span>
</Button>
)}
</div>
</div>
</div>
</DialogHeader>
{/* بدنه محتوا با اسکرول داخلی مجزا */}
<div className="p-5 space-y-5 flex-1 overflow-y-auto">
{/* کادر ویرایش/مشاهده دلیل رد صورت‌حساب در صورت انتخاب وضعیت رد شده */}
{selectedStatus === 'rejected' && (
<div className="bg-rose-500/10 border border-rose-500/25 rounded-2xl p-4 space-y-2.5 text-foreground">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="size-6 rounded-lg bg-rose-500/20 text-rose-600 dark:text-rose-400 flex items-center justify-center shrink-0">
<Ic name="xCircle" className="size-3.5" />
</div>
<h4 className="font-bold text-xs text-rose-700 dark:text-rose-300 flex items-center gap-1.5">
<span>دلیل رد صورت‌حساب:</span>
<span className="text-[11px] font-bold text-rose-600 dark:text-rose-400 bg-rose-500/15 px-2 py-0.5 rounded-md">
(الزامی)
</span>
</h4>
</div>
<span className="text-[10px] text-rose-600/80 font-medium">نمایش برای مسافر در اپلیکیشن</span>
</div>
<Textarea
value={rejectReason}
onChange={(e) => {
setRejectReason(e.target.value)
setHasChanges(true)
}}
placeholder="علت رد فاکتور را حتماً وارد نمایید (مثال: فیش ناخوانا است، مبلغ واریزی مغایرت دارد، تصویر جعلی است و...)"
className={cn(
'text-xs bg-background/90 rounded-xl resize-none min-h-[76px] transition-colors',
!rejectReason.trim()
? 'border-rose-500/50 focus:border-rose-500 focus:ring-1 focus:ring-rose-500/30'
: 'border-rose-500/25 focus:border-rose-500/50'
)}
/>
{!rejectReason.trim() && (
<p className="text-[11px] text-rose-600 dark:text-rose-400 font-medium flex items-center gap-1.5 pt-0.5">
<Ic name="alert" className="size-3.5 shrink-0" />
<span>برای ثبت وضعیت «رد شده»، وارد کردن توضیح و دلیل الزامی است.</span>
</p>
)}
</div>
)}
{/* کارت وضعیت انقضای قبلی صورت‌حساب */}
{activeFactor.status === 'expired' && selectedStatus === 'expired' && (
<div className="bg-slate-100/80 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/60 rounded-2xl p-4 flex items-start gap-3.5">
<div className="size-8 rounded-xl bg-slate-200 dark:bg-slate-700 text-slate-800 dark:text-slate-100 flex items-center justify-center shrink-0 mt-0.5">
<Ic name="clock" className="size-4" />
</div>
<div className="space-y-1 flex-1">
<h4 className="font-bold text-xs text-slate-900 dark:text-slate-100">دلیل انقضای صورت‌حساب:</h4>
<p className="text-xs text-slate-600 dark:text-slate-300 leading-relaxed font-medium">
{activeFactor.rejected_description || 'انقضای مهلت پرداخت (سپری شدن مهلت ۷۲ ساعته)'}
</p>
</div>
</div>
)}
{/* اطلاع‌رسانی تمدید خودکار مهلت ۳ روزه در صورت فعال‌سازی مجدد فاکتور منقضی شده یا رد شده */}
{(activeFactor.status === 'expired' || activeFactor.status === 'rejected') && (selectedStatus === 'awaiting_payment' || selectedStatus === 'pending') && (
<div className="bg-slate-800 border border-amber-500/40 rounded-2xl p-4 flex items-start gap-3.5 shadow-md">
<div className="size-8 rounded-xl bg-amber-500/20 text-amber-400 flex items-center justify-center shrink-0 mt-0.5">
<Ic name="clock" className="size-4" />
</div>
<div className="space-y-1 flex-1 text-xs">
<div className="flex items-center gap-2 flex-wrap">
<h4 className="font-bold text-white">
تمدید مجدد مهلت پرداخت صورت‌حساب:
</h4>
<span className="text-[11px] font-bold text-amber-300 bg-amber-500/20 border border-amber-500/30 px-2 py-0.5 rounded-md">
+۳ روز مهلت جدید
</span>
</div>
<p className="text-slate-200 leading-relaxed font-normal">
با ثبت وضعیت <strong className="font-bold text-amber-300">«{selectedStatus === 'awaiting_payment' ? 'در انتظار پرداخت' : 'در انتظار بررسی فیش'}»</strong>، این صورت‌حساب مجدداً فعال شده و یک مهلت پرداخت ۳ روزهٔ جدید (۷۲ ساعت از تاریخ امروز) برای مسافر در نظر گرفته می‌شود.
</p>
</div>
</div>
)}
{activeFactor.expiration_date && (
<div className="bg-muted/50 border border-border-soft rounded-xl p-3 flex items-center justify-between text-xs">
<span className="text-foreground/80 font-medium flex items-center gap-1.5">
<Ic name="clock" className="size-4 text-amber-600 dark:text-amber-400 shrink-0" />
<span>مهلت پرداخت (تاریخ انقضا):</span>
</span>
<span className="font-bold text-foreground font-mono">
{formatDateByLanguage(activeFactor.expiration_date, language as any)}
</span>
</div>
)}
{/* اطلاعات خریدار و اطلاعات مالی در ۲ ستون */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* کارت مشخصات خریدار */}
<Card className="border-border/50 shadow-xs">
<CardHeader className="py-3 px-4 border-b border-border/40">
<CardTitle className="text-xs font-bold text-foreground flex items-center gap-2">
<Ic name="users" className="size-4 text-primary" />
<span>اطلاعات مشتری</span>
</CardTitle>
</CardHeader>
<CardContent className="p-4 space-y-3">
<div className="flex items-center gap-3">
<Avatar className="size-11 shrink-0" iconClassName="size-5.5" />
<div className="min-w-0">
<p className="font-bold text-sm truncate text-foreground">
{user?.fullname || 'نام ثبت نشده'}
</p>
<p className="text-xs text-muted-foreground truncate font-mono dir-ltr text-start">
{user?.email || '—'}
</p>
</div>
</div>
<div className="pt-2 border-t border-border/40 space-y-1.5 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">شماره تماس:</span>
<span className="font-mono dir-ltr font-medium text-foreground">
{user?.phone_number || '—'}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">شناسه کاربری:</span>
<span className="font-mono font-medium">{user?.id ?? '—'}</span>
</div>
</div>
</CardContent>
</Card>
{/* کارت مشخصات مالی فاکتور */}
<Card className="border-border/50 shadow-xs">
<CardHeader className="py-3 px-4 border-b border-border/40">
<CardTitle className="text-xs font-bold text-foreground flex items-center gap-2">
<Ic name="creditCard" className="size-4 text-primary" />
<span>مشخصات مالی و واریز</span>
</CardTitle>
</CardHeader>
<CardContent className="p-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">مبلغ کل فاکتور:</span>
<span className="text-lg font-black text-primary font-mono">
${Number(activeFactor.amount).toLocaleString()}
</span>
</div>
{activeFactor.card_number && (
<div className="bg-muted/40 p-2.5 rounded-lg border border-border/40 flex items-center justify-between text-xs">
<div className="flex flex-col">
<span className="text-[10px] text-muted-foreground">شماره کارت مقصد:</span>
<span className="font-mono font-bold tracking-wider dir-ltr text-foreground">
{activeFactor.card_number}
</span>
</div>
<Button
variant="ghost"
size="icon"
className="size-7 text-muted-foreground hover:text-foreground"
onClick={() => handleCopyCard(activeFactor.card_number!)}
title="کپی شماره کارت"
>
<Ic name={copiedCard ? 'check' : 'clipboardCheck'} className="size-3.5" />
</Button>
</div>
)}
<div className="pt-2 border-t border-border/40 space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-muted-foreground">شناسه سفارش مرجع:</span>
<span className="font-mono font-medium">
{activeFactor.order_id || activeFactor.object_id}
</span>
</div>
{activeFactor.description && (
<div className="flex justify-between gap-2">
<span className="text-muted-foreground shrink-0">توضیحات:</span>
<span className="text-foreground text-end truncate">{activeFactor.description}</span>
</div>
)}
</div>
</CardContent>
</Card>
</div>
{/* جزئیات سرویس چندشکلی (Polymorphic Details) */}
<Card className="border-border/50 shadow-xs">
<CardHeader className="py-3 px-4 border-b border-border/40 bg-muted/20">
<CardTitle className="text-xs font-bold flex items-center gap-2">
<Ic name="list" className="size-4 text-primary" />
<span>ریز اقلام و مشخصات سرویس ({activeFactor.service.toUpperCase()})</span>
</CardTitle>
</CardHeader>
<CardContent className="p-4 text-xs space-y-4">
{isLoadingDetail ? (
<div className="space-y-3 py-2">
<ShimmerLine width="50%" height={16} rounded="md" />
<ShimmerBox width="100%" height={44} rounded="xl" />
<ShimmerBox width="100%" height={44} rounded="xl" />
</div>
) : (
<>
{/* ۱. تور */}
{activeFactor.service === 'tour' && (
<div className="space-y-3">
<div className="flex items-center justify-between pb-2 border-b border-border/40">
<span className="text-muted-foreground">عنوان تور:</span>
<span className="font-bold text-sm text-foreground">{detail?.tour || '—'}</span>
</div>
{detail?.passenger_data ? (
<div className="space-y-3">
<p className="font-bold text-xs text-muted-foreground">اسامی و رده سنی مسافران:</p>
{/* بزرگسالان */}
{(detail.passenger_data.adults?.count ?? 0) > 0 && (
<div className="bg-muted/40 p-2.5 rounded-xl border border-border-soft">
<span className="font-bold text-foreground">
بزرگسال ({detail.passenger_data.adults?.count} نفر):
</span>
<span className="ms-2 text-muted-foreground">
{detail.passenger_data.adults?.names.join('، ')}
</span>
</div>
)}
{/* کودکان */}
{(detail.passenger_data.children?.count ?? 0) > 0 && (
<div className="bg-muted/40 p-2.5 rounded-xl border border-border-soft">
<span className="font-bold text-foreground">
کودک ({detail.passenger_data.children?.count} نفر):
</span>
<span className="ms-2 text-muted-foreground">
{detail.passenger_data.children?.names.join('، ')}
</span>
</div>
)}
{/* نوزادان */}
{(detail.passenger_data.infants?.count ?? 0) > 0 && (
<div className="bg-muted/40 p-2.5 rounded-xl border border-border-soft">
<span className="font-bold text-foreground">
نوزاد ({detail.passenger_data.infants?.count} نفر):
</span>
<span className="ms-2 text-muted-foreground">
{detail.passenger_data.infants?.names.join('، ')}
</span>
</div>
)}
</div>
) : (
<p className="text-muted-foreground">اطلاعات مسافران ثبت نشده است.</p>
)}
</div>
)}
{/* ۲. سیم‌کارت */}
{activeFactor.service === 'simcard' && (
<div className="space-y-3">
<div className="divide-y divide-border/40">
{detail?.items?.map((item, idx) => (
<div key={idx} className="py-2 flex items-center justify-between">
<div>
<span className="font-medium text-foreground">
{item.simcard_title || `سیم‌کارت کد ${item.simcard_id}`}
</span>
<span className="ms-2 text-muted-foreground">
(تعداد: {item.quantity})
</span>
</div>
<span className="font-bold font-mono">
${Number(item.cost).toLocaleString()}
</span>
</div>
))}
</div>
{detail?.total_cost && (
<div className="flex justify-between pt-2 border-t border-border/40 font-bold">
<span>هزینه کل اقلام:</span>
<span className="text-primary font-mono">${detail.total_cost}</span>
</div>
)}
</div>
)}
{/* ۳. فروشگاه */}
{activeFactor.service === 'shop' && (
<div className="space-y-3">
<div className="divide-y divide-border/40">
{detail?.items?.map((item, idx) => (
<div key={idx} className="py-2 flex items-center justify-between">
<div>
<span className="font-medium text-foreground">
{item.product_slug || `محصول #${item.product_id}`}
</span>
<span className="ms-2 text-muted-foreground">
({item.quantity} عدد × ${item.price})
</span>
</div>
<span className="font-bold font-mono">
${Number(item.cost).toLocaleString()}
</span>
</div>
))}
</div>
{detail?.total_cost && (
<div className="flex justify-between pt-2 border-t border-border/40 font-bold">
<span>جمع اقلام سبد خرید:</span>
<span className="text-primary font-mono">${detail.total_cost}</span>
</div>
)}
</div>
)}
{/* ۴. صرافی / تسریف */}
{activeFactor.service === 'tasrif' && (
<div className="space-y-2 bg-muted/30 p-3 rounded-lg border border-border/40">
<div className="flex justify-between">
<span className="text-muted-foreground">تبدیل ارز:</span>
<span className="font-bold text-foreground">
از {detail?.from_currency} به {detail?.to_currency}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">مبلغ درخواستی:</span>
<span className="font-mono font-medium">{detail?.amount}</span>
</div>
<div className="flex justify-between pt-2 border-border/40 font-bold">
<span>مبلغ نهایی معادل:</span>
<span className="text-emerald-600 dark:text-emerald-400 font-mono text-sm">
${detail?.total_price}
</span>
</div>
</div>
)}
{/* ۵. سفر اختصاصی (Custom Trip) */}
{activeFactor.service === 'custom_trip' && (
<div className="space-y-3">
{detail?.destinations && detail.destinations.length > 0 && (
<div>
<span className="text-muted-foreground block mb-1">مسیر سفر:</span>
<div className="flex flex-wrap gap-2">
{detail.destinations.map((d, i) => (
<span
key={i}
className="bg-primary/10 text-primary px-2.5 py-1 rounded-md text-xs font-semibold"
>
{d}
</span>
))}
</div>
</div>
)}
{detail?.detail && (
<div className="bg-muted/30 p-3 rounded-lg border border-border/40">
<span className="text-muted-foreground block mb-1">خلاصه برنامه سفر:</span>
<p className="text-xs leading-relaxed text-foreground whitespace-pre-wrap">
{typeof detail.detail === 'string' ? detail.detail : JSON.stringify(detail.detail)}
</p>
</div>
)}
</div>
)}
</>
)}
</CardContent>
</Card>
{/* کارت تصویر فیش پرداختی آپلود شده توسط کاربر */}
<Card className="border-border/50 shadow-xs">
<CardHeader className="py-3 px-4 border-b border-border/40">
<CardTitle className="text-xs font-bold flex items-center gap-2">
<Ic name="receipt" className="size-4 text-primary" />
<span>فیش واریزی و رسید بانکی</span>
</CardTitle>
</CardHeader>
<CardContent className="p-4">
{activeFactor.transaction_receipt ? (
<div className="flex flex-col sm:flex-row items-center gap-4 bg-muted/20 p-3 rounded-xl border border-border/40">
<div
onClick={() => onOpenReceiptLightbox(activeFactor)}
className="relative cursor-pointer group overflow-hidden rounded-xl border border-border/60 size-28 sm:size-32 shrink-0 bg-muted/40 shadow-2xs flex items-center justify-center hover:border-primary/50 transition-colors"
title="مشاهده در اندازه بزرگ"
>
<img
src={resolveMediaUrl(activeFactor.transaction_receipt)}
alt={`فیش صورت‌حساب #${activeFactor.id}`}
className="size-full object-cover group-hover:scale-105 transition-transform"
onError={(e) => {
const el = e.currentTarget
if (!el.dataset.fallbackApplied && activeFactor.transaction_receipt) {
el.dataset.fallbackApplied = 'true'
const fallback = getMediaFallbackUrl(activeFactor.transaction_receipt)
if (fallback && el.src !== fallback) {
el.src = fallback
return
}
}
el.style.display = 'none'
setReceiptError(true)
const fallback = el.nextElementSibling as HTMLElement
if (fallback) fallback.style.display = 'flex'
}}
/>
<div
style={{ display: 'none' }}
className="size-full flex flex-col items-center justify-center text-muted-foreground p-2 text-center"
>
<Ic name="alert" className="size-6 text-amber-500 mb-1" />
<span className="text-[10px] font-semibold text-amber-600 dark:text-amber-400">عدم لود تصویر</span>
</div>
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 flex items-center justify-center text-white transition-opacity text-xs font-medium">
<Ic name="eye" className="size-5" />
</div>
</div>
<div className="space-y-2 text-xs flex-1">
{receiptError ? (
<>
<p className="font-bold text-destructive flex items-center gap-1.5">
<Ic name="alert" className="size-3.5" />
<span>فایل تصویر رسید در سرور یافت نشد یا آسیب دیده است</span>
</p>
<p className="text-muted-foreground text-[11px] leading-relaxed">
آدرس فیش در پایگاه داده ثبت شده، اما فایل فیزیکی در سرور در دسترس نیست. جهت جلوگیری از سردرگمی می‌توانید فیش نامعتبر را پاکسازی کنید.
</p>
</>
) : (
<>
<p className="font-bold text-foreground">فیش بانکی توسط مشتری آپلود شده است.</p>
<p className="text-muted-foreground text-[11px] leading-relaxed">
جهت بررسی صحت اطلاعات و انطباق با واریز حساب، می‌توانید تصویر فیش را در سایز
کامل باز کرده و جزئیات آن را بررسی نمایید.
</p>
</>
)}
<div className="flex flex-wrap items-center gap-2 pt-1">
<Button
variant="outline"
size="sm"
className="h-8 gap-1.5 text-xs rounded-xl shadow-2xs border-border-soft hover:bg-muted/60"
onClick={() => onOpenReceiptLightbox(activeFactor)}
>
<Ic name="eye" className="size-3.5 text-primary" />
<span>مشاهده در اندازه بزرگ</span>
</Button>
{onClearReceipt && (
<Button
variant="outline"
size="sm"
disabled={isClearingReceipt}
className="h-8 gap-1.5 text-xs rounded-xl shadow-2xs border-destructive/30 text-destructive hover:bg-destructive/10"
onClick={async () => {
setIsClearingReceipt(true)
try {
await onClearReceipt(activeFactor.id)
setCurrentFactor((prev) => (prev ? { ...prev, transaction_receipt: null } : null))
setReceiptError(false)
} finally {
setIsClearingReceipt(false)
}
}}
>
<Ic name="trash" className="size-3.5" />
<span>{isClearingReceipt ? 'در حال پاکسازی...' : receiptError ? 'پاکسازی فیش نامعتبر' : 'حذف فیش'}</span>
</Button>
)}
</div>
</div>
</div>
) : (
<div className="flex items-center gap-3.5 p-4 rounded-xl border border-amber-500/30 bg-amber-500/10 text-foreground">
<div className="size-9 rounded-xl bg-amber-500/20 text-amber-700 dark:text-amber-400 flex items-center justify-center shrink-0">
<Ic name="alert" className="size-5" />
</div>
<div className="space-y-0.5">
<p className="text-xs font-bold text-amber-950 dark:text-amber-200">
عدم بارگذاری فیش واریزی
</p>
<p className="text-xs text-foreground/80 font-medium leading-relaxed">
هنوز تصویر فیش واریزی توسط مشتری در سامانه بارگذاری نشده است (صورت‌حساب در انتظار پرداخت می‌باشد).
</p>
</div>
</div>
)}
</CardContent>
</Card>
</div>
{/* فوتر مودال و دکمه‌های عملیاتی: ثابت و بدون تداخل با اسکرول */}
<DialogFooter className="p-4 px-5 border-t border-border-soft bg-card shrink-0 flex flex-row items-center justify-between w-full z-10">
<Button
variant="outline"
size="sm"
onClick={onClose}
className="rounded-xl h-9 px-4 text-xs font-bold border-border-soft hover:bg-muted/50"
>
{t('common.close')}
</Button>
<div className="flex items-center gap-2">
<Button
variant="default"
size="sm"
disabled={isSaving || isUpdating || (selectedStatus === 'rejected' && !rejectReason.trim())}
className={cn(
'rounded-xl h-9 px-5 text-xs font-bold gap-1.5 shadow-xs transition-all',
hasChanges && !(selectedStatus === 'rejected' && !rejectReason.trim())
? 'bg-primary text-primary-foreground hover:brightness-110 ring-2 ring-primary/30'
: 'bg-primary text-primary-foreground hover:brightness-105'
)}
isLoading={isSaving || isUpdating}
loadingText="در حال ذخیره..."
onClick={handleSaveStatus}
>
<Ic name="check" className="size-4" />
<span>{hasChanges ? 'ذخیره تغییرات' : 'ثبت وضعیت'}</span>
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
)
}