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
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>
|
|
)
|
|
}
|