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.
 
 
 
 
 

482 lines
23 KiB

import React, { useState } 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 { Ic } from '@/icons'
import type { FactorItem } from '../types'
import { BillingStatusBadge } from './BillingStatusBadge'
import { BillingServiceBadge } from './BillingServiceBadge'
interface Props {
factor: FactorItem | null
open: boolean
onClose: () => void
onApprove: (factorId: number) => Promise<void>
onOpenRejectDialog: (factor: FactorItem) => void
onOpenReceiptLightbox: (factor: FactorItem) => void
onClearReceipt?: (factorId: number) => Promise<void>
isUpdating?: boolean
}
export function BillingDetailModal({
factor,
open,
onClose,
onApprove,
onOpenRejectDialog,
onOpenReceiptLightbox,
onClearReceipt,
isUpdating,
}: Props) {
const [copiedCard, setCopiedCard] = useState(false)
if (!factor) return null
const handleCopyCard = (cardNumber: string) => {
navigator.clipboard.writeText(cardNumber)
setCopiedCard(true)
setTimeout(() => setCopiedCard(false), 2000)
}
const detail = factor.detail_service
const user = factor.user_info
return (
<Dialog open={open} onOpenChange={(val) => !val && onClose()}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto border border-border/60 bg-background/95 backdrop-blur-md p-0">
{/* هدر مودال */}
<DialogHeader className="p-5 border-b border-border/40 sticky top-0 bg-background/95 backdrop-blur-sm 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">
<Ic name="receipt" className="size-5" />
</div>
<div>
<DialogTitle className="text-lg font-bold flex items-center gap-2">
<span>صورتحساب شماره #{factor.id}</span>
</DialogTitle>
<DialogDescription className="text-xs text-muted-foreground mt-0.5">
ثبت شده در: {new Date(factor.created_at).toLocaleString('fa-IR')}
</DialogDescription>
</div>
</div>
<div className="flex items-center gap-2">
<BillingServiceBadge service={factor.service} />
<BillingStatusBadge status={factor.status} />
</div>
</div>
</DialogHeader>
<div className="p-5 space-y-5">
{/* کارت وضعیت انقضا یا هشدار رد */}
{factor.status === 'rejected' && factor.rejected_description && (
<div className="bg-rose-500/10 border border-rose-500/20 rounded-xl p-4 flex items-start gap-3 text-rose-700 dark:text-rose-300">
<Ic name="xCircle" className="size-5 shrink-0 mt-0.5 text-rose-500" />
<div>
<h4 className="font-bold text-xs mb-1">دلیل رد صورتحساب:</h4>
<p className="text-xs leading-relaxed">{factor.rejected_description}</p>
</div>
</div>
)}
{factor.status === 'expired' && (
<div className="bg-slate-500/10 border border-slate-500/20 rounded-xl p-4 flex items-start gap-3 text-slate-700 dark:text-slate-300">
<Ic name="clock" className="size-5 shrink-0 mt-0.5 text-slate-500" />
<div>
<h4 className="font-bold text-xs mb-1">دلیل انقضا:</h4>
<p className="text-xs leading-relaxed">{factor.rejected_description || 'انقضای مهلت پرداخت (سپری شدن ۷۲ ساعت)'}</p>
</div>
</div>
)}
{factor.expiration_date && (
<div className="bg-muted/40 border border-border/40 rounded-xl p-3 flex items-center justify-between text-xs">
<span className="text-muted-foreground flex items-center gap-1.5">
<Ic name="clock" className="size-4 text-amber-500" />
<span>مهلت پرداخت (تاریخ انقضا):</span>
</span>
<span className="font-semibold">{factor.expiration_date}</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-muted-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 border border-border/40 text-xs font-bold"
src={user?.avatar || undefined}
letter={user?.fullname ? user.fullname.slice(0, 2) : 'ک'}
/>
<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">{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-muted-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">
{Number(factor.amount).toLocaleString()} $
</span>
</div>
{factor.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">
{factor.card_number}
</span>
</div>
<Button
variant="ghost"
size="icon"
className="size-7 text-muted-foreground hover:text-foreground"
onClick={() => handleCopyCard(factor.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">
{factor.order_id || factor.object_id}
</span>
</div>
{factor.description && (
<div className="flex justify-between gap-2">
<span className="text-muted-foreground shrink-0">توضیحات:</span>
<span className="text-foreground text-end truncate">{factor.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>ریز اقلام و مشخصات سرویس ({factor.service.toUpperCase()})</span>
</CardTitle>
</CardHeader>
<CardContent className="p-4 text-xs space-y-4">
{/* ۱. تور */}
{factor.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/30 p-2.5 rounded-lg border border-border/30">
<span className="font-bold text-purple-600 dark:text-purple-400">
بزرگسال ({detail.passenger_data.adults?.count} نفر):
</span>
<span className="ms-2 text-foreground">
{detail.passenger_data.adults?.names.join('، ')}
</span>
</div>
)}
{/* کودکان */}
{(detail.passenger_data.children?.count ?? 0) > 0 && (
<div className="bg-muted/30 p-2.5 rounded-lg border border-border/30">
<span className="font-bold text-blue-600 dark:text-blue-400">
کودک ({detail.passenger_data.children?.count} نفر):
</span>
<span className="ms-2 text-foreground">
{detail.passenger_data.children?.names.join('، ')}
</span>
</div>
)}
{/* نوزادان */}
{(detail.passenger_data.infants?.count ?? 0) > 0 && (
<div className="bg-muted/30 p-2.5 rounded-lg border border-border/30">
<span className="font-bold text-pink-600 dark:text-pink-400">
نوزاد ({detail.passenger_data.infants?.count} نفر):
</span>
<span className="ms-2 text-foreground">
{detail.passenger_data.infants?.names.join('، ')}
</span>
</div>
)}
</div>
) : (
<p className="text-muted-foreground">اطلاعات مسافران ثبت نشده است.</p>
)}
</div>
)}
{/* ۲. سیم‌کارت */}
{factor.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>
)}
{/* ۳. فروشگاه */}
{factor.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>
)}
{/* ۴. صرافی / تسریف */}
{factor.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-t 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) */}
{factor.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 flex flex-row items-center justify-between">
<CardTitle className="text-xs font-bold flex items-center gap-2">
<Ic name="image" className="size-4 text-primary" />
<span>فیش واریزی و رسید بانکی</span>
</CardTitle>
{factor.transaction_receipt && onClearReceipt && (
<Button
variant="ghost"
size="sm"
className="h-7 text-xs text-muted-foreground hover:text-destructive"
onClick={() => onClearReceipt(factor.id)}
disabled={isUpdating}
>
<Ic name="trash" className="size-3.5 me-1" />
<span>حذف فیش</span>
</Button>
)}
</CardHeader>
<CardContent className="p-4">
{factor.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(factor)}
className="relative cursor-pointer group overflow-hidden rounded-lg border border-border/40 size-32 shrink-0 bg-black/5"
>
<img
src={factor.transaction_receipt}
alt="فیش پرداختی"
className="size-full object-cover group-hover:scale-105 transition-transform"
/>
<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">
<p className="font-bold text-foreground">فیش بانکی توسط مشتری آپلود شده است.</p>
<p className="text-muted-foreground text-[11px]">
جهت بررسی صحت اطلاعات و انطباق با واریز حساب، میتوانید تصویر فیش را در سایز
کامل باز کرده و سپس وضعیت صورتحساب را تایید یا رد نمایید.
</p>
<Button
variant="outline"
size="sm"
className="h-8 gap-1 text-xs"
onClick={() => onOpenReceiptLightbox(factor)}
>
<Ic name="eye" className="size-3.5" />
<span>مشاهده در اندازه بزرگ</span>
</Button>
</div>
</div>
) : (
<div className="flex items-center gap-3 p-4 bg-amber-500/10 rounded-xl border border-amber-500/20 text-amber-700 dark:text-amber-300">
<Ic name="alert" className="size-5 text-amber-500 shrink-0" />
<p className="text-xs">
هنوز تصویر فیش واریزی توسط مشتری در سامانه بارگذاری نشده است.
</p>
</div>
)}
</CardContent>
</Card>
</div>
{/* فوتر مودال و دکمه‌های عملیاتی ادمین */}
<DialogFooter className="p-4 border-t border-border/40 bg-muted/20 sticky bottom-0 z-10 flex items-center justify-between sm:justify-between w-full">
<Button variant="outline" size="sm" onClick={onClose}>
بستن
</Button>
<div className="flex items-center gap-2">
{factor.status !== 'approved' && (
<Button
variant="default"
size="sm"
className="bg-emerald-600 hover:bg-emerald-700 text-white gap-1.5"
disabled={isUpdating}
onClick={() => onApprove(factor.id)}
>
<Ic name="checkCircle" className="size-4" />
<span>تایید پرداخت و فاکتور</span>
</Button>
)}
{factor.status !== 'rejected' && (
<Button
variant="destructive"
size="sm"
className="gap-1.5"
disabled={isUpdating}
onClick={() => onOpenRejectDialog(factor)}
>
<Ic name="xCircle" className="size-4" />
<span>رد صورتحساب</span>
</Button>
)}
</div>
</DialogFooter>
</DialogContent>
</Dialog>
)
}