import React, { useState, useEffect, useRef } from 'react' import { useParams, useNavigate, Link } from 'react-router-dom' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { Avatar } from '@/components/ui/avatar' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Ic } from '@/icons' import { ShimmerBox, ShimmerLine } from '@/components/shared/shimmer_frontend' import { toast } from '@/stores/toast' import { useTranslation } from '@/i18n' import { useBillingBadgeStore } from '@/stores/billing-badge' import { formatDateByLanguage, formatDateTimeByLanguage, formatDualDate } from '@/lib/date-utils' import { cn, resolveMediaUrl, getMediaFallbackUrl, toFa } from '@/lib/utils' import type { FactorItem, FactorStatus } from './types' import { fetchFactorDetail, reviewFactor, cancelFactor, uploadFactorTicket, deleteFactorTicket, clearFactorReceipt, } from './services/billing-api' import { BillingStatusBadge } from './components/BillingStatusBadge' import { BillingServiceBadge } from './components/BillingServiceBadge' import { ReceiptLightboxDialog } from './components/ReceiptLightboxDialog' 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-500' }, { value: 'cancelled', label: 'کنسل‌شده', dotClass: 'bg-red-500' }, ] export default function BillingDetailPage() { const { id } = useParams<{ id: string }>() const navigate = useNavigate() const { t, language } = useTranslation() const [factor, setFactor] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [selectedStatus, setSelectedStatus] = useState('awaiting_payment') const [rejectReason, setRejectReason] = useState('') const [hasChanges, setHasChanges] = useState(false) const [isSaving, setIsSaving] = useState(false) const [isUploadingTicket, setIsUploadingTicket] = useState(false) const [isDeletingTicket, setIsDeletingTicket] = useState(false) const [isDraggingTicket, setIsDraggingTicket] = useState(false) const ticketFileInputRef = useRef(null) const [receiptLightboxOpen, setReceiptLightboxOpen] = useState(false) const [isClearingReceipt, setIsClearingReceipt] = useState(false) const [receiptError, setReceiptError] = useState(false) const [copiedCard, setCopiedCard] = useState(false) const [copiedFactorNumber, setCopiedFactorNumber] = useState(false) const loadFactor = async (targetId: string | number) => { setIsLoading(true) setError(null) try { const data = await fetchFactorDetail(targetId) setFactor(data) setSelectedStatus(data.status) setRejectReason(data.rejected_description || '') setHasChanges(false) setReceiptError(false) } catch (err: any) { console.error('Failed to load factor details:', err) setError(err?.message || 'صورت‌حساب مالی مورد نظر یافت نشد یا دسترسی به آن امکان‌پذیر نیست.') setFactor(null) } finally { setIsLoading(false) } } useEffect(() => { if (id) { loadFactor(id) } }, [id]) if (isLoading) { return (
) } if (error || !factor) { return (

خطا در بارگذاری صورت‌حساب مالی

{error}

) } const detail = factor.detail_service const user = factor.user_info const isTourOrCustomTrip = factor.service === 'tour' || factor.service === 'custom_trip' const isApproved = factor.status === 'approved' const isApprovedSelectedWithoutTicket = isTourOrCustomTrip && selectedStatus === 'approved' && !factor.ticket_file const handleCopyCard = (cardNumber: string) => { navigator.clipboard.writeText(cardNumber) setCopiedCard(true) setTimeout(() => setCopiedCard(false), 2000) } const handleCopyFactorNumber = (num: string | number) => { navigator.clipboard.writeText(String(num)) setCopiedFactorNumber(true) setTimeout(() => setCopiedFactorNumber(false), 2000) } const handleSaveStatus = async () => { if (!factor) return if (selectedStatus === 'rejected' && !rejectReason.trim()) { toast.error('وارد کردن دلیل رد صورت‌حساب الزامی است.', 'دلیل رد الزامی است') return } if (isApprovedSelectedWithoutTicket) { toast.error( t('billing.ticketRequiredForApprovedDesc'), t('billing.ticketRequiredForApprovedTitle') ) return } setIsSaving(true) try { const payload = { status: selectedStatus, rejected_description: selectedStatus === 'rejected' ? rejectReason.trim() : undefined, } const updated = await reviewFactor(factor.id, payload) setFactor(updated) setSelectedStatus(updated.status) setRejectReason(updated.rejected_description || '') setHasChanges(false) useBillingBadgeStore.setState({ lastFetched: null }) useBillingBadgeStore.getState().fetchCounts() const statusLabel = STATUS_OPTIONS.find((s) => s.value === selectedStatus)?.label || selectedStatus toast.success( `وضعیت صورت‌حساب #${factor.factor_number || factor.id} با موفقیت به «${statusLabel}» تغییر یافت.`, 'بروزرسانی وضعیت' ) if (selectedStatus === 'approved') { navigate('/billing') } } catch (err: any) { toast.error(err?.message || 'خطا در ذخیره‌سازی وضعیت صورت‌حساب.', 'خطا در عملیات') } finally { setIsSaving(false) } } const handleCancelFactor = async () => { if (!factor) return if (!window.confirm(t('billing.cancelInvoiceConfirm'))) return setIsSaving(true) try { const updated = await cancelFactor(factor.id) setFactor(updated) setSelectedStatus(updated.status) setHasChanges(false) useBillingBadgeStore.setState({ lastFetched: null }) useBillingBadgeStore.getState().fetchCounts() toast.success(t('billing.invoiceCancelledSuccess'), 'کنسل کردن فاکتور') navigate('/billing') } catch (err: any) { toast.error(err?.message || 'خطا در کنسل کردن فاکتور.', 'خطا در لغو') } finally { setIsSaving(false) } } const handleUploadTicketFile = async (file: File) => { if (!factor) return setIsUploadingTicket(true) try { const updated = await uploadFactorTicket(factor.id, file) setFactor(updated) if (updated.status !== 'approved') { setSelectedStatus('approved') setHasChanges(true) } else { setSelectedStatus(updated.status) setHasChanges(false) } useBillingBadgeStore.setState({ lastFetched: null }) useBillingBadgeStore.getState().fetchCounts() toast.success( updated.status !== 'approved' ? t('billing.ticketUploadSuccessPendingSave') : t('billing.ticketUploadSuccess') ) } catch (err: any) { toast.error(err?.message || 'خطا در بارگذاری فایل بلیت.') } finally { setIsUploadingTicket(false) if (ticketFileInputRef.current) ticketFileInputRef.current.value = '' } } const handleDeleteTicket = async () => { if (!factor) return if (factor.status === 'approved') { toast.warning( t('billing.cannotDeleteTicketWhenApproved'), t('billing.ticketRequiredTitle') ) return } if (!window.confirm(t('billing.deleteTicketConfirm'))) return setIsDeletingTicket(true) try { const updated = await deleteFactorTicket(factor.id) setFactor(updated) if (selectedStatus === 'approved') { setSelectedStatus(updated.status) setHasChanges(false) } toast.success(t('billing.ticketDeleteSuccess')) } catch (err: any) { toast.error(err?.message || 'خطا در حذف بلیت.') } finally { setIsDeletingTicket(false) } } const handleClearReceipt = async () => { if (!factor) return if (!window.confirm(t('billing.confirmClearReceipt'))) return setIsClearingReceipt(true) try { await clearFactorReceipt(factor.id) setFactor((prev) => (prev ? { ...prev, transaction_receipt: null } : null)) setReceiptError(false) toast.success(t('billing.receiptClearedSuccess')) } catch (err: any) { toast.error(err?.message || 'خطا در حذف تصویر رسید.') } finally { setIsClearingReceipt(false) } } return (
{/* Top Header & Breadcrumbs */}
صورت‌حساب‌های مالی / #{toFa(factor.factor_number || factor.id)}

صورت‌حساب مالی #{toFa(factor.factor_number || factor.id)}

{/* Back button */} {/* Quick link to reference order */} {factor.service === 'custom_trip' && (factor.object_id || factor.order_id) && ( )}
{/* Expiration date banner if exists */} {factor.expiration_date && (
مهلت پرداخت فاکتور (تاریخ انقضای اعتبار): {formatDateByLanguage(factor.expiration_date, language as any)}
)} {/* 2-Column Responsive Layout */}
{/* Main Column (8 cols): Service Details, Travel Ticket, Bank Receipt */}
{/* Polymorphic Service Details Card */} ریز اقلام و مشخصات سفارش ({factor.service?.toUpperCase() || ''}) {formatDateTimeByLanguage(factor.created_at, language as any)} {/* 1. Custom Trip Service */} {factor.service === 'custom_trip' && (
{detail?.origin_city && (
مبدأ حرکت زائر: {detail.origin_city}
)} {detail?.legs && Array.isArray(detail.legs) && detail.legs.length > 0 && (
مسیرها و هتل‌های تأییدشده در فاکتور: {toFa(detail.legs.length)} ایستگاه سفر
{detail.legs.map((leg: any, idx: number) => { const isReturnLeg = Boolean(leg.is_return) const stay = detail.stays?.find((s: any) => s.city === leg.to_city) return (
{toFa(idx + 1)} {leg.from_city} ← {leg.to_city} {isReturnLeg ? ( (بازگشت به شهر مبدأ — بدون هتل) ) : ( stay?.nights && ( ({toFa(stay.nights)} شب اقامت) ) )}
✈️ {leg.transport_method_display || (leg.transport_method === 1 ? 'هوایی' : leg.transport_method === 2 ? 'ریلی' : 'زمینی')} {leg.departure_date && ( 📅 {formatDualDate(leg.departure_date, (language as any) || 'fa', { withTime: true, time: leg.departure_time })} )} {!isReturnLeg && stay?.hotel_name && ( 🏨 {stay.hotel_name} )}
) })}
)} {detail?.admin_notes && (
توضیحات تأیید نهایی و صدور فاکتور:

{detail.admin_notes}

)} {detail?.detail && (
درخواست اولیه زائر:

{typeof detail.detail === 'string' ? detail.detail : JSON.stringify(detail.detail)}

)}
)} {/* 2. Tour Service */} {factor.service === 'tour' && (
عنوان تور انتخابی: {detail?.tour || '—'}
{detail?.passenger_data && (

اسامی و رده سنی مسافران:

{(detail.passenger_data.adults?.count ?? 0) > 0 && (
بزرگسال ({toFa(detail.passenger_data.adults?.count)} نفر): {Array.isArray(detail.passenger_data.adults?.names) ? detail.passenger_data.adults?.names.join('، ') : String(detail.passenger_data.adults?.names || '')}
)} {(detail.passenger_data.children?.count ?? 0) > 0 && (
کودک ({toFa(detail.passenger_data.children?.count)} نفر): {Array.isArray(detail.passenger_data.children?.names) ? detail.passenger_data.children?.names.join('، ') : String(detail.passenger_data.children?.names || '')}
)} {(detail.passenger_data.infants?.count ?? 0) > 0 && (
نوزاد ({toFa(detail.passenger_data.infants?.count)} نفر): {Array.isArray(detail.passenger_data.infants?.names) ? detail.passenger_data.infants?.names.join('، ') : String(detail.passenger_data.infants?.names || '')}
)}
)}
)} {/* 3. Simcard Service */} {factor.service === 'simcard' && (
اپراتور و بسته: {detail?.title || 'سیم‌کارت بین‌المللی'}
تعداد: {detail?.count || 1}
)} {/* 4. Exchange (Tasrif) Service */} {factor.service === 'tasrif' && (
نوع تبدیل ارز: {detail?.exchange_type || 'صرافی و تبدیل ارز'}
)} {/* 5. Shop Service */} {factor.service === 'shop' && (
محصول فروشگاه: {detail?.product_title || 'خرید محصول'}
)}
{/* Travel Ticket Management Card (Tour & Custom Trip) */} {isTourOrCustomTrip && (
{t('billing.ticketCardTitle')}
{factor.ticket_file ? ( {t('billing.ticketIssuedBadge')} ) : ( {t('billing.ticketPendingBadge')} )}
{ const file = e.target.files?.[0] if (file) handleUploadTicketFile(file) }} /> {factor.ticket_file ? (

{t('billing.ticketIssuedTitle')}

{factor.ticket_file.split('/').pop() || factor.ticket_file}

) : (
{ e.preventDefault() setIsDraggingTicket(true) }} onDragLeave={() => setIsDraggingTicket(false)} onDrop={(e) => { e.preventDefault() setIsDraggingTicket(false) const file = e.dataTransfer.files?.[0] if (file) handleUploadTicketFile(file) }} className={`flex flex-col items-center justify-center p-6 rounded-xl border border-dashed transition-all duration-200 text-center space-y-3 ${ isDraggingTicket ? 'border-primary bg-primary/10 ring-2 ring-primary/20' : 'border-border/80 bg-muted/20 hover:border-primary/50 hover:bg-muted/30' }`} >

{t('billing.ticketUploadPrompt')}

{t('billing.ticketDragDropPrompt')}

{t('billing.ticketSupportedFormats')}

{!isApproved && (
{t('billing.ticketAutoApproveNotice')}
)}
)}
)} {/* Bank Transaction Receipt Card */} فیش واریزی و رسید پرداخت بانکی {factor.transaction_receipt ? (
setReceiptLightboxOpen(true)} className="relative cursor-pointer group overflow-hidden rounded-xl border border-border/60 size-28 sm:size-32 shrink-0 bg-muted/40 shadow-xs flex items-center justify-center hover:border-primary/50 transition-colors" title="مشاهده در اندازه بزرگ" > {`فیش { const el = e.currentTarget if (!el.dataset.fallbackApplied && factor.transaction_receipt) { el.dataset.fallbackApplied = 'true' const fallback = getMediaFallbackUrl(factor.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' }} />
عدم لود تصویر
{receiptError ? ( <>

فایل تصویر رسید در سرور یافت نشد یا آسیب دیده است

آدرس فیش در پایگاه داده ثبت شده، اما فایل فیزیکی در سرور در دسترس نیست. جهت جلوگیری از سردرگمی می‌توانید فیش نامعتبر را پاکسازی کنید.

) : ( <>

فیش بانکی توسط مشتری در اپلیکیشن آپلود شده است.

جهت بررسی صحت اطلاعات و انطباق با واریز حساب، می‌توانید تصویر فیش را در سایز کامل باز کرده و جزئیات آن را بررسی نمایید.

)}
) : (

عدم بارگذاری فیش واریزی

هنوز تصویر فیش واریزی توسط مشتری در سامانه بارگذاری نشده است (صورت‌حساب در انتظار پرداخت می‌باشد).

)}
{/* Sidebar Column (4 cols): User Dossier, Financial Pricing, Status Management */}
{/* Customer Dossier Card */} مشخصات خریدار / مسافر
{user?.fullname ? user.fullname.slice(0, 1) : }

{user?.fullname || 'نام ثبت نشده'}

{user?.email || '—'}

شماره تماس: {user?.phone_number || '—'}
شناسه کاربری: {user?.id ?? '—'}
{user?.id && ( )}
{/* Financial Dossier Card */} مشخصات مالی و پرداخت
مبلغ کل فاکتور: ${Number(factor.amount).toLocaleString()}
{factor.card_number && (
شماره کارت بانکی مقصد: {factor.card_number}
)}
{factor.factor_number && (
{t('billing.factorNumber')}:
#{factor.factor_number}
)}
{t('billing.id')}: #{factor.id}
شناسه سفارش مرجع: {factor.order_id || factor.object_id}
{factor.description && (
توضیحات: {factor.description}
)}
{/* Status & Review Controller Card */} مدیریت وضعیت فاکتور
{/* Warning if approved without ticket */} {isApprovedSelectedWithoutTicket && (

{t('billing.ticketRequiredForApprovedDesc')}

)} {/* Rejection reason textarea if rejected */} {selectedStatus === 'rejected' && (
دلیل رد صورت‌حساب (الزامی): نمایش به زائر