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