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.
 
 
 

272 lines
12 KiB

import React, { useState, useEffect } from 'react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Ic } from '@/icons'
import type { FactorItem } from '../types'
import { BillingStatusBadge } from './BillingStatusBadge'
import { resolveMediaUrl, getMediaFallbackUrl, toFa } from '@/lib/utils'
interface Props {
factor: FactorItem | null
open: boolean
onClose: () => void
onClearReceipt?: (factorId: number) => Promise<void>
}
export function ReceiptLightboxDialog({
factor,
open,
onClose,
onClearReceipt,
}: Props) {
const [zoom, setZoom] = useState<number>(1)
const [rotation, setRotation] = useState<number>(0)
const [imageError, setImageError] = useState(false)
const [imageLoading, setImageLoading] = useState(true)
const [imgSrc, setImgSrc] = useState<string>('')
const [fallbackTried, setFallbackTried] = useState(false)
useEffect(() => {
if (open && factor) {
setZoom(1)
setRotation(0)
setImageError(false)
setImageLoading(true)
setFallbackTried(false)
setImgSrc(resolveMediaUrl(factor.transaction_receipt))
}
}, [open, factor?.id, factor?.transaction_receipt])
if (!factor) return null
const receiptUrl = imgSrc || resolveMediaUrl(factor.transaction_receipt)
const hasReceipt = Boolean(receiptUrl)
const handleZoomIn = () => setZoom((prev) => Math.min(4, +(prev + 0.25).toFixed(2)))
const handleZoomOut = () => setZoom((prev) => Math.max(0.5, +(prev - 0.25).toFixed(2)))
const handleReset = () => {
setZoom(1)
setRotation(0)
}
const handleRotate = () => setRotation((prev) => (prev + 90) % 360)
const handleToggleZoom = () => {
setZoom((prev) => (prev > 1 ? 1 : 2))
}
return (
<Dialog open={open} onOpenChange={(val) => !val && onClose()}>
<DialogContent className="max-w-3xl sm:max-w-4xl max-h-[92vh] overflow-hidden p-0 border border-border/60 bg-background/95 backdrop-blur-md flex flex-col">
{/* هدر مدال پیش‌نمایش فیش */}
<DialogHeader className="p-4 px-5 border-b border-border/40 flex flex-row items-center justify-between shrink-0 bg-background/95">
<div className="flex items-center gap-2.5">
<div className="size-9 rounded-xl bg-primary/15 text-primary flex items-center justify-center font-bold shrink-0">
<Ic name="receipt" className="size-4.5" />
</div>
<div>
<DialogTitle className="text-base font-bold flex items-center gap-2">
<span>تصویر فیش پرداختی صورت‌حساب #{factor.id}</span>
</DialogTitle>
<DialogDescription className="text-xs text-muted-foreground mt-0.5">
مشتری: {factor.user_info?.fullname || factor.user_info?.email || 'نامشخص'} • مبلغ:{' '}
<span className="font-mono font-bold text-foreground">
${Number(factor.amount).toLocaleString()}
</span>
</DialogDescription>
</div>
</div>
<div className="flex items-center gap-2 pl-6">
<BillingStatusBadge status={factor.status} />
</div>
</DialogHeader>
{/* نوار ابزار بزرگنمایی و کنترل تصویر */}
{hasReceipt && !imageError && (
<div className="px-5 py-2 border-b border-border-soft bg-muted/40 flex items-center justify-between text-xs shrink-0">
<div className="flex items-center gap-1.5">
<Button
variant="outline"
size="sm"
className="h-7.5 px-2.5 rounded-lg text-xs gap-1 border-border-soft bg-card shadow-2xs hover:bg-muted/60"
onClick={handleZoomIn}
title="بزرگنمایی (+)"
>
<Ic name="plus" className="size-3.5" />
<span>بزرگ‌نمایی</span>
</Button>
<Button
variant="outline"
size="sm"
className="h-7.5 px-2.5 rounded-lg text-xs gap-1 border-border-soft bg-card shadow-2xs hover:bg-muted/60"
onClick={handleZoomOut}
disabled={zoom <= 0.5}
title="کوچک‌نمایی (-)"
>
<span className="font-bold text-base leading-none">−</span>
<span>کوچک‌نمایی</span>
</Button>
<Button
variant="outline"
size="sm"
className="h-7.5 px-2.5 rounded-lg text-xs gap-1 border-border-soft bg-card shadow-2xs hover:bg-muted/60"
onClick={handleRotate}
title="چرخش 90 درجه"
>
<Ic name="refresh" className="size-3.5" />
<span>چرخش</span>
</Button>
{(zoom !== 1 || rotation !== 0) && (
<Button
variant="ghost"
size="sm"
className="h-7.5 px-2 rounded-lg text-xs text-muted-foreground hover:text-foreground"
onClick={handleReset}
title="بازنشانی اندازه"
>
<span>اندازه اولیه (100%)</span>
</Button>
)}
<span className="text-[11px] font-mono text-muted-foreground ms-1">
{toFa(Math.round(zoom * 100))}%
</span>
</div>
<div className="flex items-center gap-1.5">
<a
href={receiptUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 h-7.5 px-3 rounded-lg text-xs font-medium border border-border-soft bg-card hover:bg-muted/60 text-foreground transition-colors shadow-2xs"
title="مشاهده فایل اصلی در تب جدید"
>
<Ic name="external" className="size-3.5" />
<span>اندازه اصلی</span>
</a>
</div>
</div>
)}
{/* کادر نمایش تصویر فیش */}
<div className="relative flex-1 min-h-[380px] max-h-[62vh] overflow-auto p-4 flex items-center justify-center bg-slate-950/20 dark:bg-black/40">
{hasReceipt && !imageError ? (
<div className="relative flex items-center justify-center min-w-full min-h-full">
{imageLoading && (
<div className="absolute inset-0 flex flex-col items-center justify-center text-muted-foreground gap-2">
<span className="size-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
<span className="text-xs font-medium">در حال بارگذاری تصویر فیش...</span>
</div>
)}
<img
src={receiptUrl}
alt={`فیش پرداختی فاکتور #${factor.id}`}
className={`rounded-xl object-contain shadow-lg border border-border/40 transition-transform duration-150 select-none max-h-[56vh] max-w-full ${
zoom > 1 ? 'cursor-zoom-out' : 'cursor-zoom-in'
}`}
style={{
transform: `scale(${zoom}) rotate(${rotation}deg)`,
transformOrigin: 'center center',
}}
onDoubleClick={handleToggleZoom}
title={zoom > 1 ? 'دوبار کلیک برای بازنشانی اندازه' : 'دوبار کلیک برای بزرگ‌نمایی'}
onLoad={() => setImageLoading(false)}
onError={() => {
if (!fallbackTried && factor.transaction_receipt) {
setFallbackTried(true)
const fallback = getMediaFallbackUrl(factor.transaction_receipt)
if (fallback && imgSrc !== fallback) {
setImgSrc(fallback)
return
}
}
setImageLoading(false)
setImageError(true)
}}
/>
</div>
) : (
<div className="flex flex-col items-center justify-center text-center p-8 text-muted-foreground">
<div className="size-14 rounded-full bg-amber-500/10 flex items-center justify-center text-amber-500 mb-3">
<Ic name="alert" className="size-7" />
</div>
<p className="font-bold text-sm text-foreground">
{imageError
? 'خطا در بارگذاری تصویر فیش پرداختی.'
: 'هنوز فیش واریزی برای این صورت‌حساب ثبت نشده است.'}
</p>
<p className="text-xs mt-1 text-muted-foreground">
{imageError
? 'آدرس فایل در سرور در دسترس نیست یا فرمت تصویر نامعتبر می‌باشد.'
: 'وضعیت فاکتور در انتظار پرداخت توسط مشتری می‌باشد.'}
</p>
{imageError && onClearReceipt && (
<Button
variant="outline"
size="sm"
onClick={async () => {
await onClearReceipt(factor.id)
onClose()
}}
className="mt-4 rounded-xl h-8 px-4 text-xs font-semibold border-destructive/40 text-destructive hover:bg-destructive/10 gap-1.5 shadow-2xs"
>
<Ic name="trash" className="size-3.5" />
<span>پاکسازی و حذف فیش نامعتبر</span>
</Button>
)}
</div>
)}
</div>
{/* اطلاعات شماره کارت و توضیحات جانبی */}
{factor.card_number && (
<div className="px-5 py-2.5 border-t border-border-soft bg-card/50 flex items-center justify-between text-xs shrink-0">
<span className="text-muted-foreground">شماره کارت مقصد:</span>
<span className="font-mono font-bold tracking-wider dir-ltr text-foreground">
{factor.card_number}
</span>
</div>
)}
{/* فوتر مودال: دکمه بستن و دکمه حذف فیش (در صورت وجود) */}
<DialogFooter className="p-4 px-5 border-t border-border-soft bg-card/80 flex flex-row items-center justify-between w-full shrink-0">
<div>
{onClearReceipt && factor.transaction_receipt && (
<Button
variant="outline"
size="sm"
onClick={async () => {
await onClearReceipt(factor.id)
onClose()
}}
className="rounded-xl h-9 px-3.5 text-xs font-semibold border-destructive/30 text-destructive hover:bg-destructive/10 gap-1.5"
>
<Ic name="trash" className="size-3.5" />
<span>حذف فیش</span>
</Button>
)}
</div>
<Button
variant="outline"
size="sm"
onClick={onClose}
className="rounded-xl h-9 px-6 text-xs font-bold border-border-soft hover:bg-muted/50"
>
بستن
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}