diff --git a/src/app/providers.tsx b/src/app/providers.tsx index b9fc1c3..62b6fc8 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -5,14 +5,16 @@ import { DirectionProvider } from '@radix-ui/react-direction' import { queryClient } from '@/lib/query-client' import { TooltipProvider } from '@/components/ui/tooltip' +import { ToastContainer } from '@/components/ui/toast-container' -/* پروایدرهای سراسری: React Query + RTL + Tooltips + Router */ +/* پروایدرهای سراسری: React Query + RTL + Tooltips + Router + Toasts */ export default function Providers({ children }: { children: React.ReactNode }) { return ( {children} + diff --git a/src/components/layout/AppSidebar.tsx b/src/components/layout/AppSidebar.tsx index b39f0dc..a8f5547 100644 --- a/src/components/layout/AppSidebar.tsx +++ b/src/components/layout/AppSidebar.tsx @@ -41,7 +41,7 @@ function BillingSidebarBadges() { {/* ۱. دایره نارنجی: در انتظار بررسی ادمین (فیش پرداخت توسط کاربر آپلود شده) */} {pendingReviewCount > 0 && ( {pendingReviewCount} diff --git a/src/components/layout/ServerHealthTrigger.tsx b/src/components/layout/ServerHealthTrigger.tsx index 619d073..f0585dc 100644 --- a/src/components/layout/ServerHealthTrigger.tsx +++ b/src/components/layout/ServerHealthTrigger.tsx @@ -29,19 +29,13 @@ export default function ServerHealthTrigger() { } className="relative flex size-9 items-center justify-center rounded-xl border border-border-soft bg-card/60 transition-all hover:bg-card hover:border-primary/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring shadow-sm cursor-pointer" > - - {health.status === 'connected' && ( - - )} - {health.status === 'error' && ( - - )} + diff --git a/src/components/layout/ServerStatusDialog.tsx b/src/components/layout/ServerStatusDialog.tsx index c03a59a..38acbc8 100644 --- a/src/components/layout/ServerStatusDialog.tsx +++ b/src/components/layout/ServerStatusDialog.tsx @@ -60,11 +60,12 @@ export default function ServerStatusDialog({ open, onOpenChange }: ServerStatusD variant="outline" size="sm" onClick={handleRefresh} - disabled={isRefreshing} + isLoading={isRefreshing} + loadingText="در حال تست..." className="h-8 gap-1.5 text-xs" > - - {isRefreshing ? 'در حال تست...' : 'بررسی مجدد'} + + بررسی مجدد @@ -79,7 +80,7 @@ export default function ServerStatusDialog({ open, onOpenChange }: ServerStatusD health.status === 'connected' ? 'bg-emerald-500 shadow-[0_0_8px_#10b981]' : health.status === 'checking' - ? 'bg-amber-400 animate-pulse' + ? 'bg-amber-400' : 'bg-rose-500 shadow-[0_0_8px_#f43f5e]' }`} /> diff --git a/src/components/shared/AppImage.tsx b/src/components/shared/AppImage.tsx index 1681458..1f94ffe 100644 --- a/src/components/shared/AppImage.tsx +++ b/src/components/shared/AppImage.tsx @@ -119,7 +119,7 @@ export function AppImage({ return ( {showSkeleton && ( - + )} {showFallback && fallback} diff --git a/src/components/shared/DeleteConfirmDialog.tsx b/src/components/shared/DeleteConfirmDialog.tsx index 9e3791a..6fd6c56 100644 --- a/src/components/shared/DeleteConfirmDialog.tsx +++ b/src/components/shared/DeleteConfirmDialog.tsx @@ -45,12 +45,13 @@ export default function DeleteConfirmDialog({ + ) } export { Button, buttonVariants } export type { ButtonProps } + diff --git a/src/components/ui/multi-select.tsx b/src/components/ui/multi-select.tsx index e5ac1b8..543c7f4 100644 --- a/src/components/ui/multi-select.tsx +++ b/src/components/ui/multi-select.tsx @@ -7,6 +7,8 @@ import { PopoverContent, PopoverTrigger, } from '@/components/ui/popover' +import { ShimmerLine } from '@/components/shared/shimmer_frontend' +import { ButtonSpinner } from '@/components/ui/button' type MultiSelectProps = { label?: string @@ -147,7 +149,11 @@ export default function MultiSelect({ )}
{infinite.isLoading ? ( -

در حال بارگذاری…

+
+ + + +
) : filtered.length === 0 ? (

نتیجه‌ای یافت نشد

) : ( @@ -178,7 +184,7 @@ export default function MultiSelect({ )} {infinite.isFetchingNextPage && (
- + بارگذاری موارد بیشتر…
)} diff --git a/src/components/ui/shimmer.tsx b/src/components/ui/shimmer.tsx index c27cdba..5bbfcbb 100644 --- a/src/components/ui/shimmer.tsx +++ b/src/components/ui/shimmer.tsx @@ -1,2 +1,3 @@ -export * from '@/components/shared/Shimmer' -export { default } from '@/components/shared/Shimmer' +export * from '@/components/shared/shimmer_frontend' +export { default } from '@/components/shared/shimmer_frontend' + diff --git a/src/components/ui/single-select.tsx b/src/components/ui/single-select.tsx index cb7336f..33e6423 100644 --- a/src/components/ui/single-select.tsx +++ b/src/components/ui/single-select.tsx @@ -3,6 +3,8 @@ import { useInfiniteQuery } from '@tanstack/react-query' import { Ic } from '@/icons' import { cn } from '@/lib/utils' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { ShimmerLine } from '@/components/shared/shimmer_frontend' +import { ButtonSpinner } from '@/components/ui/button' type SingleSelectProps = { label?: string @@ -117,7 +119,11 @@ export default function SingleSelect({
{infinite.isLoading ? ( -

در حال بارگذاری…

+
+ + + +
) : renderedOptions.length === 0 ? (

نتیجه‌ای یافت نشد

) : ( @@ -143,7 +149,7 @@ export default function SingleSelect({ )} {infinite.isFetchingNextPage && (
- + بارگذاری موارد بیشتر…
)} diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx index 1ef9215..676167d 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/components/ui/skeleton.tsx @@ -1,13 +1,15 @@ +import * as React from 'react' import { cn } from '@/lib/utils' -function Skeleton({ className, ...props }) { +function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { return (
) } export { Skeleton } + diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx index 27d51d0..7e81096 100644 --- a/src/components/ui/switch.tsx +++ b/src/components/ui/switch.tsx @@ -2,18 +2,35 @@ import * as React from 'react' import * as SwitchPrimitive from '@radix-ui/react-switch' import { cn } from '@/lib/utils' -function Switch({ className, ...props }: React.ComponentProps) { +export interface SwitchProps extends React.ComponentPropsWithoutRef { + isLoading?: boolean +} + +const Switch = React.forwardRef< + React.ElementRef, + SwitchProps +>(({ className, isLoading, disabled, ...props }, ref) => { return ( - + + {isLoading ? ( + + ) : null} + ) -} +}) +Switch.displayName = SwitchPrimitive.Root.displayName export { Switch } + diff --git a/src/components/ui/toast-container.tsx b/src/components/ui/toast-container.tsx new file mode 100644 index 0000000..dc2aa9d --- /dev/null +++ b/src/components/ui/toast-container.tsx @@ -0,0 +1,95 @@ +import React from 'react' +import { useToastStore, type ToastItem } from '@/stores/toast' +import { Ic, type IconName } from '@/icons' +import { cn } from '@/lib/utils' + +export function ToastContainer() { + const { toasts, removeToast } = useToastStore() + + if (toasts.length === 0) return null + + return ( +
+ {toasts.map((toast) => ( + removeToast(toast.id)} /> + ))} +
+ ) +} + +function ToastCard({ toast, onClose }: { toast: ToastItem; onClose: () => void }) { + const configs: Record< + ToastItem['type'], + { + containerClass: string + icon: IconName + iconClass: string + borderClass: string + } + > = { + success: { + containerClass: + 'bg-card/95 text-foreground dark:bg-card/95 border-emerald-500/30 shadow-lg shadow-emerald-500/5', + icon: 'checkCircle', + iconClass: 'text-emerald-500 dark:text-emerald-400', + borderClass: 'border-l-4 border-l-emerald-500', + }, + error: { + containerClass: + 'bg-card/95 text-foreground dark:bg-card/95 border-rose-500/30 shadow-lg shadow-rose-500/5', + icon: 'xCircle', + iconClass: 'text-rose-500 dark:text-rose-400', + borderClass: 'border-l-4 border-l-rose-500', + }, + warning: { + containerClass: + 'bg-card/95 text-foreground dark:bg-card/95 border-amber-500/30 shadow-lg shadow-amber-500/5', + icon: 'alert', + iconClass: 'text-amber-500 dark:text-amber-400', + borderClass: 'border-l-4 border-l-amber-500', + }, + info: { + containerClass: + 'bg-card/95 text-foreground dark:bg-card/95 border-primary/30 shadow-lg shadow-primary/5', + icon: 'helpCircle', + iconClass: 'text-primary dark:text-primary', + borderClass: 'border-l-4 border-l-primary', + }, + } + + const current = configs[toast.type] || configs.info + + return ( +
+
+ +
+ +
+ {toast.title && ( +
{toast.title}
+ )} +

{toast.message}

+
+ + +
+ ) +} diff --git a/src/config/navigation.ts b/src/config/navigation.ts index 923f01e..0135b3d 100644 --- a/src/config/navigation.ts +++ b/src/config/navigation.ts @@ -78,25 +78,8 @@ export function getNavigationConfig(t: (key: string) => string): NavGroupConfig[ { title: t('navigation.shrines'), titleKey: 'navigation.shrines', to: '/services/shrines', icon: 'shrine' }, { title: t('navigation.store'), titleKey: 'navigation.store', to: '/services/store', icon: 'store' }, { title: t('navigation.costEstimate'), titleKey: 'navigation.costEstimate', to: '/services/cost-estimate', icon: 'calculator' }, - { - title: t('navigation.support'), - titleKey: 'navigation.support', - icon: 'headset', - children: [ - { - title: t('navigation.supporters'), - titleKey: 'navigation.supporters', - to: '/services/consultant', - icon: 'headset', - }, - { - title: t('navigation.faq'), - titleKey: 'navigation.faq', - to: '/services/faq', - icon: 'helpCircle', - }, - ], - }, + { title: t('navigation.support'), titleKey: 'navigation.support', to: '/services/consultant', icon: 'headset' }, + { title: t('navigation.faq'), titleKey: 'navigation.faq', to: '/services/faq', icon: 'helpCircle' }, { title: t('navigation.settings'), titleKey: 'navigation.settings', to: '/settings', icon: 'settings' }, ], }, diff --git a/src/features/auth/LoginPage.tsx b/src/features/auth/LoginPage.tsx index ffb50a4..60d98e1 100644 --- a/src/features/auth/LoginPage.tsx +++ b/src/features/auth/LoginPage.tsx @@ -139,10 +139,12 @@ export default function LoginPage() { diff --git a/src/features/billing/BillingPage.tsx b/src/features/billing/BillingPage.tsx index 02171a2..f8565f1 100644 --- a/src/features/billing/BillingPage.tsx +++ b/src/features/billing/BillingPage.tsx @@ -15,7 +15,7 @@ import { TableRow, } from '@/components/ui/table' import { Ic, IconName } from '@/icons' -import { Shimmer } from '@/components/shared/Shimmer' +import { Shimmer, ShimmerBox, ShimmerCircle, ShimmerLine } from '@/components/shared/shimmer_frontend' import { fetchFactors, reviewFactor, clearFactorReceipt } from './services/billing-api' import type { FactorItem, FactorStatus, FactorService, BillingStats } from './types' import { BillingStatusBadge } from './components/BillingStatusBadge' @@ -26,6 +26,8 @@ import { ReceiptLightboxDialog } from './components/ReceiptLightboxDialog' import { useTranslation } from '@/i18n' import { useBillingBadgeStore } from '@/stores/billing-badge' import { formatDateByLanguage } from '@/lib/date-utils' +import { resolveMediaUrl, getMediaFallbackUrl } from '@/lib/utils' +import { toast } from '@/stores/toast' const PAGE_SIZE = 20 @@ -81,7 +83,7 @@ export default function BillingPage() { const params = new URLSearchParams() if (service && service !== 'all') params.set('service', service) if (status && status !== 'all') params.set('status', status) - if (search) params.set('search', search) + if (search.trim()) params.set('search', search.trim()) if (page > 1) params.set('page', page.toString()) setSearchParams(params, { replace: true }) }, @@ -95,10 +97,10 @@ export default function BillingPage() { const urlSearch = searchParams.get('search') || '' const urlPage = parseInt(searchParams.get('page') || '1', 10) || 1 - setSelectedService(urlService) - setSelectedStatus(urlStatus) - setSearchQuery(urlSearch) - setCurrentPage(urlPage) + setSelectedService((prev) => (prev !== urlService ? urlService : prev)) + setSelectedStatus((prev) => (prev !== urlStatus ? urlStatus : prev)) + setSearchQuery((prev) => (prev !== urlSearch ? urlSearch : prev)) + setCurrentPage((prev) => (prev !== urlPage ? urlPage : prev)) }, [searchParams]) // Load factors from API @@ -110,18 +112,18 @@ export default function BillingPage() { const res = await fetchFactors({ status: selectedStatus !== 'all' ? selectedStatus : undefined, service: selectedService !== 'all' ? selectedService : undefined, - search: searchQuery || undefined, + search: searchQuery.trim() || undefined, limit: PAGE_SIZE, offset, }) setFactors(res.results || []) setTotalCount(res.count || 0) } catch (err: any) { - setError(err?.message || t('common.error')) + setError(err?.message || 'خطا در بارگذاری فهرست صورت‌حساب‌ها') } finally { setIsLoading(false) } - }, [selectedStatus, selectedService, searchQuery, currentPage, t]) + }, [selectedStatus, selectedService, searchQuery, currentPage]) useEffect(() => { loadFactors() @@ -252,9 +254,10 @@ export default function BillingPage() { if (selectedFactorForLightbox?.id === factorId) { setSelectedFactorForLightbox((prev) => (prev ? { ...prev, transaction_receipt: null } : null)) } + toast.success(t('billing.receiptClearedSuccess')) showToastMessage(t('billing.receiptClearedSuccess')) } catch (err: any) { - alert(`Error: ${err?.message || 'Unexpected issue'}`) + toast.error(err?.message || 'خطا در حذف فیش') } finally { setUpdatingId(null) } @@ -300,7 +303,7 @@ export default function BillingPage() { className="border-primary/30 bg-primary/10 text-primary font-bold font-mono text-xs min-w-[65px] justify-center" > {isLoading ? ( - + ) : ( `${totalCount} ${t('billing.invoices')}` )} @@ -366,7 +369,7 @@ export default function BillingPage() {

{t('billing.totalInvoices')}

{isLoading ? ( -
+ ) : (

{stats.total}

)} @@ -385,7 +388,7 @@ export default function BillingPage() { {t('billing.pendingReview')}

{isLoading ? ( -
+ ) : (

{stats.pendingReview} @@ -393,7 +396,7 @@ export default function BillingPage() { )}

- +
@@ -406,7 +409,7 @@ export default function BillingPage() { {t('billing.awaitingPayment')}

{isLoading ? ( -
+ ) : (

{stats.awaitingPayment} @@ -427,7 +430,7 @@ export default function BillingPage() { {t('billing.approvedPayments')}

{isLoading ? ( -
+ ) : (

{stats.approved} @@ -448,7 +451,7 @@ export default function BillingPage() { {t('billing.rejectedOrExpired')}

{isLoading ? ( -
+ ) : (

{stats.rejectedOrExpired} @@ -602,50 +605,49 @@ export default function BillingPage() { {/* شناسه */} -

+ {/* خریدار */}
-
+
-
-
+ +
{/* سرویس */} -
+ {/* مبلغ */} -
+ {/* وضعیت */} -
+ {/* تصویر فیش */} -
+ {/* تاریخ */} -
+ {/* عملیات */} -
-
-
+
+
@@ -671,11 +673,7 @@ export default function BillingPage() { {/* مشخصات مشتری و خریدار */}
- +

{user?.fullname || '—'} @@ -708,14 +706,31 @@ export default function BillingPage() { {hasReceipt ? ( ) : ( {t('billing.noReceipt')} @@ -732,52 +747,24 @@ export default function BillingPage() { {/* تاریخ صدور */} - + {formatDateByLanguage(factor.created_at, language as any)} {/* دکمه‌های عملیات */} -

+
{/* دکمه مشاهده جزئیات کامل خریدار و پرداخت */} - - {/* تایید سریع فیش */} - {factor.status !== 'approved' && ( - - )} - - {/* رد سریع فیش */} - {factor.status !== 'rejected' && ( - - )}
@@ -836,28 +823,23 @@ export default function BillingPage() { factor={selectedFactorForDetail} open={Boolean(selectedFactorForDetail)} onClose={() => setSelectedFactorForDetail(null)} - onApprove={handleApprove} - onOpenRejectDialog={(f) => { + onFactorUpdated={(updated) => { + setFactors((prev) => prev.map((f) => (f.id === updated.id ? updated : f))) setSelectedFactorForDetail(null) - setSelectedFactorForReject(f) + loadFactors() + useBillingBadgeStore.getState().fetchCounts() }} onOpenReceiptLightbox={(f) => setSelectedFactorForLightbox(f)} onClearReceipt={handleClearReceipt} isUpdating={updatingId === selectedFactorForDetail?.id} /> - {/* ۲. مودال لایت‌باکس پیش‌نمایش بزرگ فیش واریزی */} + {/* ۲. مودال لایت‌باکس پیش‌نمایش بزرگ فیش واریزی (صرفاً بزرگ‌نمایی و دکمه بستن) */} setSelectedFactorForLightbox(null)} - onApprove={handleApprove} - onReject={(factorId) => { - setSelectedFactorForLightbox(null) - const f = factors.find((item) => item.id === factorId) || null - setSelectedFactorForReject(f) - }} - isUpdating={updatingId === selectedFactorForLightbox?.id} + onClearReceipt={handleClearReceipt} /> {/* ۳. مودال دریافت دلیل رد فاکتور */} diff --git a/src/features/billing/components/BillingDetailModal.tsx b/src/features/billing/components/BillingDetailModal.tsx index 91fa46a..774b2bf 100644 --- a/src/features/billing/components/BillingDetailModal.tsx +++ b/src/features/billing/components/BillingDetailModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect, useRef } from 'react' import { Dialog, DialogContent, @@ -10,39 +10,124 @@ import { import { Button } from '@/components/ui/button' import { Avatar } from '@/components/ui/avatar' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Textarea } from '@/components/ui/textarea' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' import { Ic } from '@/icons' -import type { FactorItem } from '../types' -import { BillingStatusBadge } from './BillingStatusBadge' +import type { FactorItem, FactorStatus } from '../types' import { BillingServiceBadge } from './BillingServiceBadge' +import { fetchFactorDetail, reviewFactor } from '../services/billing-api' +import { ShimmerLine, ShimmerBox } from '@/components/shared/shimmer_frontend' +import { toast } from '@/stores/toast' import { useTranslation } from '@/i18n' import { formatDateByLanguage, formatDateTimeByLanguage } from '@/lib/date-utils' +import { cn, resolveMediaUrl, getMediaFallbackUrl } from '@/lib/utils' interface Props { factor: FactorItem | null open: boolean onClose: () => void - onApprove: (factorId: number) => Promise - onOpenRejectDialog: (factor: FactorItem) => void + onFactorUpdated?: (updatedFactor: FactorItem) => void + onApprove?: (factorId: number) => Promise + onOpenRejectDialog?: (factor: FactorItem) => void onOpenReceiptLightbox: (factor: FactorItem) => void onClearReceipt?: (factorId: number) => Promise isUpdating?: boolean } +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-400', + }, +] + export function BillingDetailModal({ factor, open, onClose, - onApprove, - onOpenRejectDialog, + onFactorUpdated, onOpenReceiptLightbox, onClearReceipt, isUpdating, }: Props) { const { t, language } = useTranslation() const [copiedCard, setCopiedCard] = useState(false) + const [currentFactor, setCurrentFactor] = useState(factor) + const [selectedStatus, setSelectedStatus] = useState(factor?.status || 'awaiting_payment') + const [rejectReason, setRejectReason] = useState(factor?.rejected_description || '') + const [isLoadingDetail, setIsLoadingDetail] = useState(false) + const [isSaving, setIsSaving] = useState(false) + const [hasChanges, setHasChanges] = useState(false) + const [receiptError, setReceiptError] = useState(false) + const [isClearingReceipt, setIsClearingReceipt] = useState(false) + const lastLoadedFactorIdRef = useRef(null) + + useEffect(() => { + if (!open || !factor) { + lastLoadedFactorIdRef.current = null + return + } + + // مقداردهی اولیه وضعیت و فیلدها منحصراً در زمان باز شدن مودال یا تغییر شناسه صورت‌حساب + if (lastLoadedFactorIdRef.current !== factor.id) { + lastLoadedFactorIdRef.current = factor.id + setCurrentFactor(factor) + setSelectedStatus(factor.status) + setRejectReason(factor.rejected_description || '') + setHasChanges(false) + setReceiptError(false) - if (!factor) return null + if (!factor.detail_service) { + setIsLoadingDetail(true) + fetchFactorDetail(factor.id) + .then((res) => { + // بروزرسانی جزئیات بدون بازنویسی وضعیت تغییریافته توسط کاربر (جلوگیری قطعی از برگشت به حالت رد شده) + setCurrentFactor((prev) => (prev ? { ...prev, ...res } : res)) + }) + .catch((err) => { + console.error('Failed to load factor details:', err) + }) + .finally(() => { + setIsLoadingDetail(false) + }) + } + } + }, [factor?.id, open]) + + const activeFactor = currentFactor || factor + if (!factor || !activeFactor) return null const handleCopyCard = (cardNumber: string) => { navigator.clipboard.writeText(cardNumber) @@ -50,65 +135,215 @@ export function BillingDetailModal({ setTimeout(() => setCopiedCard(false), 2000) } - const detail = factor.detail_service - const user = factor.user_info + const handleSaveStatus = async () => { + if (!activeFactor) return + if (selectedStatus === 'rejected' && !rejectReason.trim()) { + toast.error('وارد کردن دلیل رد صورت‌حساب الزامی است.', 'دلیل رد الزامی است') + return + } + setIsSaving(true) + try { + const payload = { + status: selectedStatus, + rejected_description: selectedStatus === 'rejected' ? rejectReason.trim() : undefined, + } + const updated = await reviewFactor(activeFactor.id, payload) + setCurrentFactor(updated) + setSelectedStatus(updated.status) + setRejectReason(updated.rejected_description || '') + setHasChanges(false) + if (onFactorUpdated) { + onFactorUpdated(updated) + } + const statusLabel = + STATUS_OPTIONS.find((s) => s.value === selectedStatus)?.label || selectedStatus + toast.success( + `وضعیت صورت‌حساب #${activeFactor.id} با موفقیت به «${statusLabel}» تغییر یافت.`, + 'بروزرسانی وضعیت' + ) + onClose() + } catch (err: any) { + toast.error(err?.message || 'خطا در ذخیره‌سازی وضعیت صورت‌حساب.', 'خطا در عملیات') + } finally { + setIsSaving(false) + } + } + + const detail = activeFactor.detail_service + const user = activeFactor.user_info return ( !val && onClose()}> - - {/* هدر مودال */} - + + {/* هدر مودال ثابت با پس‌زمینه سالید بدون تداخل اسکرول */} +
-
+
- صورت‌حساب شماره #{factor.id} + صورت‌حساب شماره #{activeFactor.id} - ثبت شده در: {formatDateTimeByLanguage(factor.created_at, language as any)} + ثبت شده در: {formatDateTimeByLanguage(activeFactor.created_at, language as any)}
-
- - +
+ + + {/* دراپ‌داون تعاملی انتخاب وضعیت صورت‌حساب و دکمه ذخیره سریع در هدر */} +
+
+ +
+ + {/* دکمه ذخیره وضعیت سریع در کنار منو برای جلوگیری از سردرگمی کاربر */} + {hasChanges && ( + + )} +
-
- {/* کارت وضعیت انقضا یا هشدار رد */} - {factor.status === 'rejected' && factor.rejected_description && ( -
- -
-

دلیل رد صورت‌حساب:

-

{factor.rejected_description}

+ {/* بدنه محتوا با اسکرول داخلی مجزا */} +
+ {/* کادر ویرایش/مشاهده دلیل رد صورت‌حساب در صورت انتخاب وضعیت رد شده */} + {selectedStatus === 'rejected' && ( +
+
+
+
+ +
+

+ دلیل رد صورت‌حساب: + + (الزامی) + +

+
+ نمایش برای مسافر در اپلیکیشن
+