,
+ 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 ? (