Browse Source
feat: add core panel UI features including billing, tours, services, layout components, and i18n support
master
feat: add core panel UI features including billing, tours, services, layout components, and i18n support
master
27 changed files with 2629 additions and 1396 deletions
-
548guide/aqila_panel_ui_design_system_standard.md
-
6src/app/router.tsx
-
242src/components/layout/AppSidebar.tsx
-
67src/components/layout/LanguageSwitcher.tsx
-
50src/components/layout/Topbar.tsx
-
94src/config/navigation.ts
-
428src/features/billing/BillingPage.tsx
-
5src/features/billing/components/BillingDetailModal.tsx
-
21src/features/billing/components/BillingServiceBadge.tsx
-
14src/features/billing/components/BillingStatusBadge.tsx
-
28src/features/billing/components/RejectFactorDialog.tsx
-
25src/features/global-search/GlobalSearchDialog.tsx
-
398src/features/services/ServicesPage.tsx
-
4src/features/services/components/CategoriesServiceView.tsx
-
4src/features/services/components/CreateLocationDialog.tsx
-
8src/features/services/components/DuasServiceView.tsx
-
1080src/features/services/components/FaqServiceView.tsx
-
2src/features/services/components/ShrinesServiceView.tsx
-
26src/features/tours/OngoingToursPage.tsx
-
34src/features/tours/ToursPage.tsx
-
141src/i18n/index.ts
-
4src/index.css
-
133src/locales/ar.json
-
133src/locales/en.json
-
133src/locales/fa.json
-
2src/main.tsx
-
39src/stores/billing-badge.ts
@ -0,0 +1,67 @@ |
|||
import React from 'react' |
|||
import { |
|||
DropdownMenu, |
|||
DropdownMenuTrigger, |
|||
DropdownMenuContent, |
|||
DropdownMenuItem, |
|||
DropdownMenuLabel, |
|||
DropdownMenuSeparator, |
|||
} from '@/components/ui/dropdown-menu' |
|||
import { Button } from '@/components/ui/button' |
|||
import { Ic } from '@/icons' |
|||
import { useTranslation, SupportedLanguage } from '@/i18n' |
|||
|
|||
export default function LanguageSwitcher() { |
|||
const { language, setLanguage, languages, t, currentLanguageInfo } = useTranslation() |
|||
|
|||
return ( |
|||
<DropdownMenu> |
|||
<DropdownMenuTrigger asChild> |
|||
<Button |
|||
variant="outline" |
|||
size="sm" |
|||
className="h-9 px-2.5 sm:px-3 rounded-xl border-border/70 bg-card/40 hover:bg-card text-xs font-semibold gap-2 shadow-xs transition-all" |
|||
title={t('topbar.language')} |
|||
> |
|||
<span className="text-sm leading-none">{currentLanguageInfo.flag}</span> |
|||
<span className="hidden sm:inline-block text-[12px] text-foreground"> |
|||
{currentLanguageInfo.nativeName} |
|||
</span> |
|||
<Ic name="chevDown" className="size-3 text-muted-foreground transition-transform opacity-70" /> |
|||
</Button> |
|||
</DropdownMenuTrigger> |
|||
|
|||
<DropdownMenuContent align="end" className="w-44 rounded-2xl p-1.5 shadow-xl border-border/70"> |
|||
<DropdownMenuLabel className="text-[11px] font-bold text-muted-foreground px-2 py-1"> |
|||
{t('topbar.selectLanguage')} |
|||
</DropdownMenuLabel> |
|||
<DropdownMenuSeparator /> |
|||
|
|||
{languages.map((item) => { |
|||
const isSelected = item.code === language |
|||
return ( |
|||
<DropdownMenuItem |
|||
key={item.code} |
|||
onSelect={() => setLanguage(item.code as SupportedLanguage)} |
|||
onClick={() => setLanguage(item.code as SupportedLanguage)} |
|||
className={`flex items-center justify-between px-2.5 py-2 rounded-xl text-xs font-medium cursor-pointer transition-colors ${ |
|||
isSelected |
|||
? 'bg-primary/15 text-primary font-bold shadow-2xs' |
|||
: 'text-foreground hover:bg-muted/50' |
|||
}`}
|
|||
> |
|||
<div className="flex items-center gap-2"> |
|||
<span className="text-base leading-none">{item.flag}</span> |
|||
<span>{item.nativeName}</span> |
|||
<span className="text-[10px] text-muted-foreground uppercase font-mono"> |
|||
({item.code}) |
|||
</span> |
|||
</div> |
|||
{isSelected && <Ic name="check" className="size-3.5 text-primary" />} |
|||
</DropdownMenuItem> |
|||
) |
|||
})} |
|||
</DropdownMenuContent> |
|||
</DropdownMenu> |
|||
) |
|||
} |
|||
1080
src/features/services/components/FaqServiceView.tsx
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,141 @@ |
|||
import { create } from 'zustand' |
|||
import { persist } from 'zustand/middleware' |
|||
import fa from '@/locales/fa.json' |
|||
import ar from '@/locales/ar.json' |
|||
import en from '@/locales/en.json' |
|||
|
|||
export type SupportedLanguage = 'fa' | 'ar' | 'en' |
|||
export type Direction = 'rtl' | 'ltr' |
|||
|
|||
export interface LanguageOption { |
|||
code: SupportedLanguage |
|||
label: string |
|||
nativeName: string |
|||
dir: Direction |
|||
flag: string |
|||
} |
|||
|
|||
export const SUPPORTED_LANGUAGES: LanguageOption[] = [ |
|||
{ code: 'fa', label: 'فارسی (FA)', nativeName: 'فارسی', dir: 'rtl', flag: '🇮🇷' }, |
|||
{ code: 'ar', label: 'العربية (AR)', nativeName: 'العربية', dir: 'rtl', flag: '🇸🇦' }, |
|||
{ code: 'en', label: 'English (EN)', nativeName: 'English', dir: 'ltr', flag: '🇬🇧' }, |
|||
] |
|||
|
|||
const dictionaries: Record<SupportedLanguage, Record<string, any>> = { |
|||
fa, |
|||
ar, |
|||
en, |
|||
} |
|||
|
|||
/** |
|||
* دریافت مقدار ترجمه بر اساس کلید نقطهای (dot-notation) |
|||
* پشتیبانی از fallback به فارسی و پارامترهای درونمتنی {key} |
|||
*/ |
|||
export function translate( |
|||
lang: SupportedLanguage, |
|||
key: string, |
|||
params?: Record<string, string | number> |
|||
): string { |
|||
const getNestedValue = (obj: any, path: string): string | undefined => { |
|||
const keys = path.split('.') |
|||
let current = obj |
|||
for (const k of keys) { |
|||
if (current && typeof current === 'object' && k in current) { |
|||
current = current[k] |
|||
} else { |
|||
return undefined |
|||
} |
|||
} |
|||
return typeof current === 'string' ? current : undefined |
|||
} |
|||
|
|||
// تلاش برای خواندن از زبان فعلی
|
|||
let text = getNestedValue(dictionaries[lang], key) |
|||
|
|||
// در صورت نبودن، تلاش برای خواندن از زبان فارسی (Fallback)
|
|||
if (!text && lang !== 'fa') { |
|||
text = getNestedValue(dictionaries.fa, key) |
|||
} |
|||
|
|||
// اگر کلاً پیدا نشد، خود کلید بازگردانده شود
|
|||
if (!text) { |
|||
const lastPart = key.split('.').pop() || key |
|||
return lastPart |
|||
} |
|||
|
|||
// جایگذاری پارامترها مانند {current} یا {total}
|
|||
if (params) { |
|||
Object.entries(params).forEach(([paramKey, paramVal]) => { |
|||
text = text!.replace(new RegExp(`\\{${paramKey}\\}`, 'g'), String(paramVal)) |
|||
}) |
|||
} |
|||
|
|||
return text |
|||
} |
|||
|
|||
export function applyLanguageToDOM(lang: SupportedLanguage) { |
|||
if (typeof document !== 'undefined') { |
|||
const isLtr = lang === 'en' |
|||
document.documentElement.lang = lang |
|||
document.documentElement.dir = isLtr ? 'ltr' : 'rtl' |
|||
if (isLtr) { |
|||
document.documentElement.classList.add('lang-en') |
|||
document.documentElement.classList.remove('lang-rtl') |
|||
} else { |
|||
document.documentElement.classList.add('lang-rtl') |
|||
document.documentElement.classList.remove('lang-en') |
|||
} |
|||
} |
|||
} |
|||
|
|||
interface LanguageState { |
|||
language: SupportedLanguage |
|||
setLanguage: (lang: SupportedLanguage) => void |
|||
} |
|||
|
|||
export const useLanguageStore = create<LanguageState>()( |
|||
persist( |
|||
(set) => ({ |
|||
language: 'fa', |
|||
setLanguage: (lang: SupportedLanguage) => { |
|||
set({ language: lang }) |
|||
applyLanguageToDOM(lang) |
|||
}, |
|||
}), |
|||
{ |
|||
name: 'aqila_panel_language', |
|||
onRehydrateStorage: () => (state) => { |
|||
const lang = state?.language || 'fa' |
|||
applyLanguageToDOM(lang) |
|||
}, |
|||
} |
|||
) |
|||
) |
|||
|
|||
/** |
|||
* هوک سراسری ترجمه جهت استفاده آسان در تمامی کامپوننتهای پنل |
|||
*/ |
|||
export function useTranslation() { |
|||
const language = useLanguageStore((s) => s.language) |
|||
const setLanguage = useLanguageStore((s) => s.setLanguage) |
|||
|
|||
const dir: Direction = language === 'en' ? 'ltr' : 'rtl' |
|||
const isRTL = dir === 'rtl' |
|||
|
|||
const t = (key: string, params?: Record<string, string | number>): string => { |
|||
return translate(language, key, params) |
|||
} |
|||
|
|||
const currentLanguageInfo = |
|||
SUPPORTED_LANGUAGES.find((l) => l.code === language) || SUPPORTED_LANGUAGES[0] |
|||
|
|||
return { |
|||
t, |
|||
language, |
|||
setLanguage, |
|||
dir, |
|||
isRTL, |
|||
languages: SUPPORTED_LANGUAGES, |
|||
currentLanguageInfo, |
|||
} |
|||
} |
|||
@ -0,0 +1,133 @@ |
|||
{ |
|||
"navigation": { |
|||
"brand": "لوحة إدارة عقيلة", |
|||
"brandShort": "عقيلة", |
|||
"dashboard": "لوحة القيادة", |
|||
"dashboardTitle": "لوحة إدارة النظام", |
|||
"tours": "الجولات", |
|||
"allTours": "جميع الجولات", |
|||
"ongoingTours": "الجولات الجارية", |
|||
"tourOrders": "الطلبات والحجوزات", |
|||
"services": "مركز الخدمات (الكل)", |
|||
"simcard": "شرائح الاتصال (Simcard)", |
|||
"zayerGuide": "دليل الزائر (Zayer Guide)", |
|||
"exchange": "الصرافة الإلكترونية (Exchange)", |
|||
"duas": "الأدعية والزيارات (Duas)", |
|||
"duasList": "الأدعية والزيارات", |
|||
"categories": "التصنيفات", |
|||
"shrines": "المزارات والعتبات (Shrines)", |
|||
"store": "متجر معصومة (Store)", |
|||
"costEstimate": "تقدير التكاليف (Cost Estimate)", |
|||
"support": "الدعم والاستشارات (Support)", |
|||
"supporters": "فريق الدعم (Supporters)", |
|||
"faq": "الأسئلة الشائعة (FAQ)", |
|||
"faqCategories": "التصنيفات الموضوعية", |
|||
"faqQuestions": "الأسئلة والأجوبة", |
|||
"consultant": "الدعم والاستشارات (Support)", |
|||
"billing": "الفواتير والمالية", |
|||
"settings": "الإعدادات", |
|||
"logout": "تسجيل الخروج", |
|||
"soon": "قريباً", |
|||
"adminRole": "المدير العام للنظام" |
|||
}, |
|||
"topbar": { |
|||
"serverOnline": "الخادم متصل", |
|||
"serverOffline": "انقطع الاتصال بالخادم", |
|||
"theme": "المظهر", |
|||
"language": "تغيير اللغة", |
|||
"selectLanguage": "اختر لغة النظام", |
|||
"search": "بحث..." |
|||
}, |
|||
"common": { |
|||
"save": "حفظ التغييرات", |
|||
"cancel": "إلغاء", |
|||
"confirm": "تأكيد", |
|||
"delete": "حذف", |
|||
"edit": "تعديل", |
|||
"close": "إغلاق", |
|||
"back": "رجوع", |
|||
"refresh": "تحديث", |
|||
"loading": "جاري التحميل...", |
|||
"status": "الحالة", |
|||
"actions": "الإجراءات", |
|||
"all": "الكل", |
|||
"active": "نشط", |
|||
"inactive": "غير نشط", |
|||
"noData": "لا توجد بيانات للعرض.", |
|||
"pageOf": "صفحة {current} من {total}", |
|||
"success": "تمت العملية بنجاح.", |
|||
"error": "حدث خطأ في الاتصال بالخادم.", |
|||
"retry": "إعادة المحاولة", |
|||
"previous": "السابق", |
|||
"next": "التالي", |
|||
"showing": "عرض", |
|||
"to": "إلى", |
|||
"of": "من" |
|||
}, |
|||
"billing": { |
|||
"title": "إدارة الفواتير والمدفوعات", |
|||
"description": "مراجعة إيصالات التحويل البنكي، واعتماد أو رفض المدفوعات ومتابعة تفاصيل فواتير خدمات عقيلة", |
|||
"totalInvoices": "إجمالي الفواتير", |
|||
"pendingReview": "بانتظار مراجعة الإيصال", |
|||
"awaitingPayment": "بانتظار دفع العميل", |
|||
"approvedPayments": "المدفوعات المعتمدة", |
|||
"rejectedOrExpired": "مرفوض / منتهي", |
|||
"searchPlaceholder": "البحث في الفواتير (اسم المستخدم، البريد، رقم البطاقة، رقم الفاتورة)...", |
|||
"statusFilter": "حالة الفاتورة", |
|||
"serviceFilter": "نوع الخدمة", |
|||
"clearFilters": "مسح الفلاتر", |
|||
"invoiceList": "قائمة الفواتير", |
|||
"customer": "العميل", |
|||
"amount": "المبلغ", |
|||
"service": "الخدمة", |
|||
"date": "تاريخ الإصدار", |
|||
"receipt": "إيصال الدفع", |
|||
"hasReceipt": "عرض الإيصال", |
|||
"noReceipt": "لا يوجد", |
|||
"approve": "اعتماد الدفع", |
|||
"reject": "رفض الفاتورة", |
|||
"rejectTitle": "رفض الفاتورة", |
|||
"rejectReason": "سبب رفض الفاتورة / الإيصال البنكي:", |
|||
"rejectReasonPlaceholder": "مثال: الإيصال غير واضح، المبلغ غير مطابق، رقم المعاملة غير صحيح...", |
|||
"rejectNotice": "سيتم عرض هذا التوضيح في تطبيق المسافر ضمن تفاصيل الفاتورة.", |
|||
"submitReject": "تسجيل ورفض الفاتورة", |
|||
"submitting": "جاري التسجيل...", |
|||
"statusPending": "قيد مراجعة الإيصال", |
|||
"statusAwaiting": "بانتظار الدفع", |
|||
"statusApproved": "تم الدفع والاعتماد", |
|||
"statusRejected": "مرفوض", |
|||
"statusExpired": "منتهي الصلاحية", |
|||
"invoice": "فاتورة", |
|||
"invoices": "فواتير", |
|||
"id": "المعرف", |
|||
"viewDetails": "التفاصيل", |
|||
"viewReceipt": "عرض إيصال الدفع", |
|||
"noReceiptUploaded": "لم يتم رفع إيصال", |
|||
"serviceTour": "جولات الزيارة", |
|||
"serviceSimcard": "شرائح الاتصال", |
|||
"serviceShop": "متجر الهدايا", |
|||
"serviceExchange": "الصرافة (تصريف)", |
|||
"serviceCustomTrip": "رحلة خاصة", |
|||
"allStatuses": "جميع الحالات", |
|||
"allServices": "جميع الخدمات", |
|||
"noFactorsFound": "لم يتم العثور على فواتير.", |
|||
"noFactorsFilter": "لم يتم العثور على نتائج مطابقة للفلاتر الحالية. يمكنك مسح الفلاتر.", |
|||
"noFactorsEmpty": "لم تصدر أي فواتير في النظام بعد.", |
|||
"confirmClearReceipt": "هل أنت متأكد من حذف صورة الإيصال المرفوعة لهذا الفاتورة؟", |
|||
"receiptClearedSuccess": "تم مسح إيصال الدفع بنجاح.", |
|||
"tabAll": "جميع المعاملات", |
|||
"tabTour": "طلبات وحجوزات الجولات", |
|||
"tabSimcard": "شرائح الاتصال", |
|||
"tabExchange": "الصرافة والعملات", |
|||
"tabShop": "متجر الهدايا", |
|||
"tabCustomTrip": "الرحلات الخاصة", |
|||
"coreTourBadge": "المحور الأساسي", |
|||
"liveStatus": "الحالة اللحظية", |
|||
"quickFilter": "فلترة الحالة" |
|||
}, |
|||
"languages": { |
|||
"fa": "فارسی", |
|||
"ar": "العربية", |
|||
"en": "English" |
|||
} |
|||
} |
|||
@ -0,0 +1,133 @@ |
|||
{ |
|||
"navigation": { |
|||
"brand": "Aqila Admin Panel", |
|||
"brandShort": "Aqila", |
|||
"dashboard": "Dashboard", |
|||
"dashboardTitle": "Admin Dashboard", |
|||
"tours": "Tours", |
|||
"allTours": "All Tours", |
|||
"ongoingTours": "Ongoing Tours", |
|||
"tourOrders": "Orders & Bookings", |
|||
"services": "Services Center", |
|||
"simcard": "SIM Card", |
|||
"zayerGuide": "Zayer Guide", |
|||
"exchange": "Online Exchange", |
|||
"duas": "Duas & Ziyarat", |
|||
"duasList": "Duas & Ziyarat", |
|||
"categories": "Categories", |
|||
"shrines": "Holy Shrines", |
|||
"store": "Massoumah Store", |
|||
"costEstimate": "Cost Estimate", |
|||
"support": "Support Center", |
|||
"supporters": "Support Staff", |
|||
"faq": "FAQ", |
|||
"faqCategories": "Subject Categories", |
|||
"faqQuestions": "Questions & Answers", |
|||
"consultant": "Support", |
|||
"billing": "Billing & Finance", |
|||
"settings": "Settings", |
|||
"logout": "Logout", |
|||
"soon": "Soon", |
|||
"adminRole": "System Administrator" |
|||
}, |
|||
"topbar": { |
|||
"serverOnline": "Server is Online", |
|||
"serverOffline": "Server Disconnected", |
|||
"theme": "Theme", |
|||
"language": "Language", |
|||
"selectLanguage": "Select System Language", |
|||
"search": "Search..." |
|||
}, |
|||
"common": { |
|||
"save": "Save Changes", |
|||
"cancel": "Cancel", |
|||
"confirm": "Confirm", |
|||
"delete": "Delete", |
|||
"edit": "Edit", |
|||
"close": "Close", |
|||
"back": "Back", |
|||
"refresh": "Refresh", |
|||
"loading": "Loading...", |
|||
"status": "Status", |
|||
"actions": "Actions", |
|||
"all": "All", |
|||
"active": "Active", |
|||
"inactive": "Inactive", |
|||
"noData": "No records found.", |
|||
"pageOf": "Page {current} of {total}", |
|||
"success": "Operation completed successfully.", |
|||
"error": "Server connection error.", |
|||
"retry": "Retry", |
|||
"previous": "Previous", |
|||
"next": "Next", |
|||
"showing": "Showing", |
|||
"to": "to", |
|||
"of": "of" |
|||
}, |
|||
"billing": { |
|||
"title": "Invoices & Billing Management", |
|||
"description": "Review bank receipts, approve or reject payments, and track invoices across Aqila services", |
|||
"totalInvoices": "Total Invoices", |
|||
"pendingReview": "Pending Receipt Review", |
|||
"awaitingPayment": "Awaiting Customer Payment", |
|||
"approvedPayments": "Approved Payments", |
|||
"rejectedOrExpired": "Rejected / Expired", |
|||
"searchPlaceholder": "Search invoices (Customer name, email, card number, invoice ID)...", |
|||
"statusFilter": "Invoice Status", |
|||
"serviceFilter": "Service Type", |
|||
"clearFilters": "Clear Filters", |
|||
"invoiceList": "Invoice List", |
|||
"customer": "Customer", |
|||
"amount": "Amount", |
|||
"service": "Service", |
|||
"date": "Issue Date", |
|||
"receipt": "Payment Receipt", |
|||
"hasReceipt": "View Receipt", |
|||
"noReceipt": "None", |
|||
"approve": "Approve Payment", |
|||
"reject": "Reject Invoice", |
|||
"rejectTitle": "Reject Invoice", |
|||
"rejectReason": "Rejection Reason / Bank Receipt:", |
|||
"rejectReasonPlaceholder": "e.g., Unreadable receipt, amount mismatch, invalid transaction reference...", |
|||
"rejectNotice": "This explanation will be displayed to the traveler in their invoice details.", |
|||
"submitReject": "Submit & Reject Invoice", |
|||
"submitting": "Submitting...", |
|||
"statusPending": "Pending Receipt Review", |
|||
"statusAwaiting": "Awaiting Payment", |
|||
"statusApproved": "Payment Approved", |
|||
"statusRejected": "Rejected", |
|||
"statusExpired": "Expired", |
|||
"invoice": "Invoice", |
|||
"invoices": "Invoices", |
|||
"id": "ID", |
|||
"viewDetails": "Details", |
|||
"viewReceipt": "View Receipt", |
|||
"noReceiptUploaded": "No receipt uploaded", |
|||
"serviceTour": "Pilgrimage Tours", |
|||
"serviceSimcard": "SIM Card", |
|||
"serviceShop": "Massoumah Store", |
|||
"serviceExchange": "Exchange (Tasreef)", |
|||
"serviceCustomTrip": "Custom Trip", |
|||
"allStatuses": "All Statuses", |
|||
"allServices": "All Services", |
|||
"noFactorsFound": "No invoices found.", |
|||
"noFactorsFilter": "No records match current filters. You can clear filters.", |
|||
"noFactorsEmpty": "No invoices have been issued in the system yet.", |
|||
"confirmClearReceipt": "Are you sure you want to remove the uploaded receipt image?", |
|||
"receiptClearedSuccess": "Payment receipt cleared successfully.", |
|||
"tabAll": "All Transactions", |
|||
"tabTour": "Tour Orders & Bookings", |
|||
"tabSimcard": "SIM Cards", |
|||
"tabExchange": "Currency & Exchange", |
|||
"tabShop": "Souvenir Store", |
|||
"tabCustomTrip": "Custom Trips", |
|||
"coreTourBadge": "Primary Core", |
|||
"liveStatus": "Live Status", |
|||
"quickFilter": "Status Filter" |
|||
}, |
|||
"languages": { |
|||
"fa": "فارسی", |
|||
"ar": "العربية", |
|||
"en": "English" |
|||
} |
|||
} |
|||
@ -0,0 +1,133 @@ |
|||
{ |
|||
"navigation": { |
|||
"brand": "پنل مدیریت عقیله", |
|||
"brandShort": "عقیله", |
|||
"dashboard": "داشبورد", |
|||
"dashboardTitle": "داشبورد مدیریت", |
|||
"tours": "تورها", |
|||
"allTours": "فهرست همه تورها", |
|||
"ongoingTours": "تورهای در حال برگزاری", |
|||
"tourOrders": "سفارشات و رزروها", |
|||
"services": "مرکز خدمات (همه)", |
|||
"simcard": "سیمکارت (Simcard)", |
|||
"zayerGuide": "راهنمای زائر (Zayer Guide)", |
|||
"exchange": "صرافی آنلاین (Exchange)", |
|||
"duas": "ادعیه و زیارات (Duas)", |
|||
"duasList": "ادعیه و زیارات", |
|||
"categories": "دستهبندیها", |
|||
"shrines": "اماکن زیارتی (Shrines)", |
|||
"store": "فروشگاه معصومه (Store)", |
|||
"costEstimate": "تخمین هزینه (Cost Estimate)", |
|||
"support": "پشتیبانی (Support)", |
|||
"supporters": "پشتیبانها (Supporters)", |
|||
"faq": "سوالات متداول (FAQ)", |
|||
"faqCategories": "دستهبندیهای موضوعی", |
|||
"faqQuestions": "پرسش و پاسخها", |
|||
"consultant": "پشتیبانی (Support)", |
|||
"billing": "صورتحسابها و مالی", |
|||
"settings": "تنظیمات", |
|||
"logout": "خروج از سیستم", |
|||
"soon": "بهزودی", |
|||
"adminRole": "مدیر کل سیستم" |
|||
}, |
|||
"topbar": { |
|||
"serverOnline": "سرور متصل است", |
|||
"serverOffline": "قطع ارتباط با سرور", |
|||
"theme": "تغییر تم", |
|||
"language": "تغییر زبان", |
|||
"selectLanguage": "انتخاب زبان سامانه", |
|||
"search": "جستجو..." |
|||
}, |
|||
"common": { |
|||
"save": "ذخیره تغییرات", |
|||
"cancel": "انصراف", |
|||
"confirm": "تایید", |
|||
"delete": "حذف", |
|||
"edit": "ویرایش", |
|||
"close": "بستن", |
|||
"back": "بازگشت", |
|||
"refresh": "بهروزرسانی", |
|||
"loading": "در حال بارگذاری...", |
|||
"status": "وضعیت", |
|||
"actions": "عملیات", |
|||
"all": "همه", |
|||
"active": "فعال", |
|||
"inactive": "غیرفعال", |
|||
"noData": "موردی برای نمایش یافت نشد.", |
|||
"pageOf": "صفحه {current} از {total}", |
|||
"success": "عملیات با موفقیت انجام شد.", |
|||
"error": "خطا در برقراری ارتباط با سرور.", |
|||
"retry": "تلاش مجدد", |
|||
"previous": "قبلی", |
|||
"next": "بعدی", |
|||
"showing": "نمایش", |
|||
"to": "تا", |
|||
"of": "از" |
|||
}, |
|||
"billing": { |
|||
"title": "مدیریت صورتحسابها و فاکتورها", |
|||
"description": "بررسی فیشهای بانکی، تایید یا رد پرداختها و پیگیری ریز فاکتورهای ۵ سرویس سامانه عقیله", |
|||
"totalInvoices": "کل صورتحسابها", |
|||
"pendingReview": "در انتظار بررسی فیش", |
|||
"awaitingPayment": "در انتظار پرداخت مشتری", |
|||
"approvedPayments": "پرداختهای تایید شده", |
|||
"rejectedOrExpired": "رد شده / منقضی", |
|||
"searchPlaceholder": "جستجو در فاکتورها (نام کاربر، ایمیل، شماره کارت، شماره فاکتور)...", |
|||
"statusFilter": "وضعیت فاکتور", |
|||
"serviceFilter": "نوع سرویس", |
|||
"clearFilters": "پاکسازی فیلترها", |
|||
"invoiceList": "فهرست فاکتورها", |
|||
"customer": "مشتری", |
|||
"amount": "مبلغ", |
|||
"service": "سرویس", |
|||
"date": "تاریخ صدور", |
|||
"receipt": "فیش پرداختی", |
|||
"hasReceipt": "مشاهده فیش", |
|||
"noReceipt": "ندارد", |
|||
"approve": "تایید پرداخت", |
|||
"reject": "رد صورتحساب", |
|||
"rejectTitle": "رد صورتحساب", |
|||
"rejectReason": "دلیل رد صورتحساب / فیش پرداختی:", |
|||
"rejectReasonPlaceholder": "مثال: فیش ناخوانا است، مبلغ واریزی مغایرت دارد، شماره رهگیری نامعتبر است...", |
|||
"rejectNotice": "این توضیحات در اپلیکیشن مسافر / کلاینت در بخش جزئیات صورتحساب نمایش داده میشود.", |
|||
"submitReject": "ثبت و رد فاکتور", |
|||
"submitting": "در حال ثبت...", |
|||
"statusPending": "در انتظار بررسی فیش", |
|||
"statusAwaiting": "در انتظار پرداخت", |
|||
"statusApproved": "پرداخت تایید شده", |
|||
"statusRejected": "رد شده", |
|||
"statusExpired": "منقضی شده", |
|||
"invoice": "فاکتور", |
|||
"invoices": "فاکتور", |
|||
"id": "شناسه", |
|||
"viewDetails": "جزئیات", |
|||
"viewReceipt": "مشاهده فیش واریزی", |
|||
"noReceiptUploaded": "فیش واریزی بارگذاری نشده", |
|||
"serviceTour": "تورهای زیارتی", |
|||
"serviceSimcard": "سیمکارت", |
|||
"serviceShop": "فروشگاه سوغات", |
|||
"serviceExchange": "صرافی (تسریف)", |
|||
"serviceCustomTrip": "سفر اختصاصی", |
|||
"allStatuses": "همه وضعیتها", |
|||
"allServices": "همه سرویسها", |
|||
"noFactorsFound": "صورتحسابی یافت نشد.", |
|||
"noFactorsFilter": "با فیلترهای جستجوی فعلی موردی پیدا نشد. میتوانید فیلترها را پاکسازی نمایید.", |
|||
"noFactorsEmpty": "هنوز هیچ صورتحسابی در سیستم صادر نشده است.", |
|||
"confirmClearReceipt": "آیا از حذف تصویر فیش بارگذاریشده برای این صورتحساب اطمینان دارید؟", |
|||
"receiptClearedSuccess": "فیش واریزی با موفقیت پاک شد.", |
|||
"tabAll": "همه تراکنشها", |
|||
"tabTour": "سفارشات و رزروهای تور", |
|||
"tabSimcard": "خرید سیمکارت", |
|||
"tabExchange": "صرافی و ارز", |
|||
"tabShop": "فروشگاه سوغات", |
|||
"tabCustomTrip": "سفر اختصاصی", |
|||
"coreTourBadge": "محور اصلی", |
|||
"liveStatus": "وضعیت لحظهای", |
|||
"quickFilter": "فیلتر وضعیت" |
|||
}, |
|||
"languages": { |
|||
"fa": "فارسی", |
|||
"ar": "العربية", |
|||
"en": "English" |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
import { create } from 'zustand' |
|||
import { fetchFactors } from '@/features/billing/services/billing-api' |
|||
|
|||
interface BillingBadgeState { |
|||
pendingReviewCount: number |
|||
awaitingPaymentCount: number |
|||
isLoading: boolean |
|||
lastFetched: number | null |
|||
fetchCounts: () => Promise<void> |
|||
setCounts: (pending: number, awaiting: number) => void |
|||
} |
|||
|
|||
export const useBillingBadgeStore = create<BillingBadgeState>((set) => ({ |
|||
pendingReviewCount: 0, |
|||
awaitingPaymentCount: 0, |
|||
isLoading: false, |
|||
lastFetched: null, |
|||
fetchCounts: async () => { |
|||
try { |
|||
set({ isLoading: true }) |
|||
// دریافت تعداد فاکتورهای در انتظار بررسی فیش (Pending Review)
|
|||
const pendingRes = await fetchFactors({ status: 'pending', limit: 1 }) |
|||
// دریافت تعداد فاکتورهای در انتظار پرداخت کاربر (Awaiting Payment)
|
|||
const awaitingRes = await fetchFactors({ status: 'awaiting_payment', limit: 1 }) |
|||
|
|||
set({ |
|||
pendingReviewCount: pendingRes?.count || 0, |
|||
awaitingPaymentCount: awaitingRes?.count || 0, |
|||
isLoading: false, |
|||
lastFetched: Date.now(), |
|||
}) |
|||
} catch { |
|||
set({ isLoading: false }) |
|||
} |
|||
}, |
|||
setCounts: (pending: number, awaiting: number) => { |
|||
set({ pendingReviewCount: pending, awaitingPaymentCount: awaiting }) |
|||
}, |
|||
})) |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue