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
-
244src/components/layout/AppSidebar.tsx
-
67src/components/layout/LanguageSwitcher.tsx
-
60src/components/layout/Topbar.tsx
-
124src/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
-
47src/features/global-search/GlobalSearchDialog.tsx
-
402src/features/services/ServicesPage.tsx
-
4src/features/services/components/CategoriesServiceView.tsx
-
4src/features/services/components/CreateLocationDialog.tsx
-
8src/features/services/components/DuasServiceView.tsx
-
1368src/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> |
||||
|
) |
||||
|
} |
||||
1368
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