Browse Source

feat: integrate dual calendar standard and UniversalDatePicker across tour and user views

master
parent
commit
d49ac20fa7
  1. 7
      .agents/AGENTS.md
  2. 16
      .agents/rules/dual_calendar_standard.md
  3. 3
      .agents/rules/translation_synchronization.md
  4. 7
      AGENTS.md
  5. 6
      src/components/shared/TourFeatureIconPicker.tsx
  6. 4
      src/components/shared/UniversalDatePicker.tsx
  7. 8
      src/features/custom-trip/CustomTripOrderDetailPage.tsx
  8. 8
      src/features/custom-trip/components/CustomTripOrderReviewDialog.tsx
  9. 4
      src/features/dashboard/DashboardHome.tsx
  10. 2
      src/features/dashboard/services/dashboard-api.ts
  11. 5
      src/features/services/ServicesPage.tsx
  12. 5
      src/features/services/components/BlogsServiceView.tsx
  13. 6
      src/features/services/components/SupportServiceView.tsx
  14. 13
      src/features/settings/components/AdminUserTable.tsx
  15. 18
      src/features/tours/components/CreateTourDialog.tsx
  16. 31
      src/features/tours/components/TourDetailDialog.tsx
  17. 79
      src/features/tours/components/TourDetailSheet.tsx
  18. 1
      src/features/tours/services/tour-api.ts
  19. 36
      src/features/tours/types.ts
  20. 17
      src/features/users/components/UserFactorsTab.tsx
  21. 20
      src/features/users/components/UserProfileTab.tsx
  22. 5
      src/features/users/components/UserToursTab.tsx
  23. 108
      src/lib/date-utils.ts
  24. 14
      src/lib/jalali.ts
  25. 8
      src/locales/ar.json
  26. 4
      src/locales/en.json
  27. 6
      src/locales/fa.json

7
.agents/AGENTS.md

@ -44,6 +44,13 @@
- **ممنوعیت مطلق متن هاردکد:** تمام متون جدول‌ها، ستون‌ها، فیلدها، دکمه‌ها، نشان‌ها، پیام‌های مودال و نوتیفیکیشن‌ها باید منحصراً از طریق `useTranslation` فراخوانی شوند.
- **همگام‌سازی همزمان در هر ۳ زبان:** هر کلید جدید یا اصلاح‌شده در یکی از این ۳ زبان باید الزاماً و همزمان در دو زبان دیگر (`fa.json`, `ar.json`, `en.json`) با ترجمه حرفه‌ای و بومی اعمال شود تا تست اعتبارسنجی `npm run check:i18n` و هوک پیش از کامیت (`pre-commit`) کاملاً پاس شود و هیچ کلید ناقص، خالی یا نامتقارنی باقی نماند.
- **تقارن کامل تغییرات کلمات (Word Modification Parity):** هر زمان کلمه‌ای در یک متن تغییر کند یا کلماتی حذف شوند، باید همان تغییر مفهومی بلافاصله در هر ۳ فایل اعمال گردد (مطابق قانون اختصاصی `.agents/rules/translation_synchronization.md`).
- **استاندارد سراسری نمایش و انتخاب دوگانه تاریخ میلادی و قمری با نام ماه (Mandatory Dual Gregorian + Hijri Date Standard):**
- در تمامی بخش‌ها، جداول، مودال‌ها، کارت‌ها و فرم‌های پنل مدیریت (`aqila_panel`)، کلیه تاریخ‌ها باید **به‌صورت همزمان میلادی و هجری قمری (ام‌القری) همراه با ذکر نام ماه** (مشابه بخش سفر انتخابی / Custom Trip) نمایش داده شوند:
- در زبان فارسی (`fa`): `نوامبر 2، 2026 (جمادی‌الاول 22، 1448)`
- در زبان عربی (`ar`): `نوفمبر 2، 2026 (جمادى الأولى 22، 1448 هـ)`
- در زبان انگلیسی (`en`): `November 2, 2026 (Jumada al-Awwal 22, 1448 AH)`
- برای نمایش هرگونه تاریخ یا زمان در پنل، استفاده از `toLocaleDateString` خام یا نمایش تک‌تقویمی (صرفاً شمسی یا صرفاً عددی `YYYY-MM-DD`) ممنوع است و باید منحصراً از توابع استاندارد `formatDualDate`، `formatDateByLanguage`، `formatDateRangeByLanguage` و `formatDateTimeByLanguage` در `src/lib/date-utils.ts` استفاده شود.
- برای کلیه فیلدهای انتخاب تاریخ در فرم‌ها و دیالوگ‌ها، استفاده از `<input type="date">` بومی مرورگر ممنوع بوده و باید منحصراً از کامپوننت `<UniversalDatePicker>` با `allowedModes={['islamic', 'gregorian']}` استفاده گردد.
---

16
.agents/rules/dual_calendar_standard.md

@ -0,0 +1,16 @@
# استاندارد سراسری نمایش و انتخاب دوگانه تاریخ میلادی و قمری در پنل مدیریت (Dual Gregorian + Hijri Date Standard)
در پنل مدیریت عقیله (`aqila_panel`)، کلیه تاریخ‌ها در تمام صفحات، جداول، مودال‌ها، کارت‌ها و انتخابگرهای تاریخ باید بدون استثنا به صورت همزمان **میلادی + هجری قمری (ام‌القری) با ذکر صریح نام ماه** (مطابق الگوی بخش کاستوم تریپ) نمایش داده شوند:
1. **فرمت استاندارد نمایش در هر سه زبان پنل:**
- **فارسی (`fa`):** `نوامبر 2، 2026 (جمادی‌الاول 22، 1448)`
- **عربی (`ar`):** `نوفمبر 2، 2026 (جمادى الأولى 22، 1448 هـ)`
- **انگلیسی (`en`):** `November 2, 2026 (Jumada al-Awwal 22, 1448 AH)`
2. **توابع مرجع الزامی (`src/lib/date-utils.ts`):**
- هرگز از `new Date().toLocaleDateString(...)`، `Intl.DateTimeFormat('fa-IR')` یا رشته‌های خام `YYYY-MM-DD` برای رندر تاریخ در UI استفاده نشود.
- همواره از `formatDualDate`، `formatDateByLanguage`، `formatDateRangeByLanguage` و `formatDateTimeByLanguage` استفاده گردد.
3. **انتخابگر تاریخ استاندارد (`UniversalDatePicker`):**
- استفاده از `<input type="date">` بومی مرورگر در فرم‌ها و مودال‌ها ممنوع است.
- برای تمام ورودی‌های تاریخ از `<UniversalDatePicker allowedModes={['islamic', 'gregorian']} defaultMode="islamic" />` استفاده شود.

3
.agents/rules/translation_synchronization.md

@ -17,3 +17,6 @@ Whenever adding, updating, or modifying any text, table header, button label, mo
3. **Zero Hardcoded Text in UI Components:**
- No hardcoded strings in React components, Ant Design or Tailwind elements, table columns, action buttons, or notification toasts. All copy must be fetched via `useTranslation`.
- Ensure RTL/LTR alignment and smart bidirectional input handling via `rtlUtils.ts`.
4. **Mandatory Multi-Language Check on Any Change & Pre-Existing Audit:**
- Whenever any component, page, or static data is modified, its multi-language compliance must be verified. If the text, key, or component already existed beforehand, its existing translations across all 3 panel languages (`fa`, `ar`, `en`) must also be inspected and fixed if incomplete, hardcoded, or out-of-sync.

7
AGENTS.md

@ -44,6 +44,13 @@
- **ممنوعیت مطلق متن هاردکد:** تمام متون جدول‌ها، ستون‌ها، فیلدها، دکمه‌ها، نشان‌ها، پیام‌های مودال و نوتیفیکیشن‌ها باید منحصراً از طریق `useTranslation` فراخوانی شوند.
- **همگام‌سازی همزمان در هر ۳ زبان:** هر کلید جدید یا اصلاح‌شده در یکی از این ۳ زبان باید الزاماً و همزمان در دو زبان دیگر (`fa.json`, `ar.json`, `en.json`) با ترجمه حرفه‌ای و بومی اعمال شود تا تست اعتبارسنجی `npm run check:i18n` و هوک پیش از کامیت (`pre-commit`) کاملاً پاس شود و هیچ کلید ناقص، خالی یا نامتقارنی باقی نماند.
- **تقارن کامل تغییرات کلمات (Word Modification Parity):** هر زمان کلمه‌ای در یک متن تغییر کند یا کلماتی حذف شوند، باید همان تغییر مفهومی بلافاصله در هر ۳ فایل اعمال گردد (مطابق قانون اختصاصی `.agents/rules/translation_synchronization.md`).
- **استاندارد سراسری نمایش و انتخاب دوگانه تاریخ میلادی و قمری با نام ماه (Mandatory Dual Gregorian + Hijri Date Standard):**
- در تمامی بخش‌ها، جداول، مودال‌ها، کارت‌ها و فرم‌های پنل مدیریت (`aqila_panel`)، کلیه تاریخ‌ها باید **به‌صورت همزمان میلادی و هجری قمری (ام‌القری) همراه با ذکر نام ماه** (مشابه بخش سفر انتخابی / Custom Trip) نمایش داده شوند:
- در زبان فارسی (`fa`): `نوامبر 2، 2026 (جمادی‌الاول 22، 1448)`
- در زبان عربی (`ar`): `نوفمبر 2، 2026 (جمادى الأولى 22، 1448 هـ)`
- در زبان انگلیسی (`en`): `November 2, 2026 (Jumada al-Awwal 22, 1448 AH)`
- برای نمایش هرگونه تاریخ یا زمان در پنل، استفاده از `toLocaleDateString` خام یا نمایش تک‌تقویمی (صرفاً شمسی یا صرفاً عددی `YYYY-MM-DD`) ممنوع است و باید منحصراً از توابع استاندارد `formatDualDate`، `formatDateByLanguage`، `formatDateRangeByLanguage` و `formatDateTimeByLanguage` در `src/lib/date-utils.ts` استفاده شود.
- برای کلیه فیلدهای انتخاب تاریخ در فرم‌ها و دیالوگ‌ها، استفاده از `<input type="date">` بومی مرورگر ممنوع بوده و باید منحصراً از کامپوننت `<UniversalDatePicker>` با `allowedModes={['islamic', 'gregorian']}` استفاده گردد.
---

6
src/components/shared/TourFeatureIconPicker.tsx

@ -55,10 +55,10 @@ export const TOUR_FEATURE_ICONS: FeatureIconDef[] = [
{ name: 'holyTour', labelFa: 'زیارت دوره جامع', category: 'ziyarat', keywords: ['زیارت دوره', 'اماکن مقدسه', 'قبور شهدا'] },
// ۵. کادر، راهنما و لیدر
{ name: 'tourGuide', labelFa: 'مدیر کاروان و تورلیدر', category: 'staff', keywords: ['لیدر', 'راهنما', 'مدیر', 'guide'] },
{ name: 'scholar', labelFa: 'روحانی کاروان', category: 'staff', keywords: ['روحانی', 'شیخ', 'احکام', 'عالم'] },
{ name: 'tourGuide', labelFa: 'مدیر تور و تورلیدر', category: 'staff', keywords: ['لیدر', 'راهنما', 'مدیر', 'guide'] },
{ name: 'scholar', labelFa: 'روحانی تور', category: 'staff', keywords: ['روحانی', 'شیخ', 'احکام', 'عالم'] },
{ name: 'translator', labelFa: 'مترجم بومی مسلط', category: 'staff', keywords: ['مترجم', 'عربی', 'انگلیسی', 'زبان'] },
{ name: 'doctor', labelFa: 'پزشک همراه کاروان', category: 'staff', keywords: ['پزشک', 'دکتر', 'درمان', 'کادر پزشکی'] },
{ name: 'doctor', labelFa: 'پزشک همراه تور', category: 'staff', keywords: ['پزشک', 'دکتر', 'درمان', 'کادر پزشکی'] },
{ name: 'medical', labelFa: 'خدمات امدادی و دارویی', category: 'staff', keywords: ['دارو', 'پزشکی', 'اورژانس'] },
{ name: 'camera', labelFa: 'عکاس و مستندساز', category: 'staff', keywords: ['عکاس', 'فیلمبردار', 'مستند'] },
{ name: 'support24', labelFa: 'پشتیبانی ۲۴ ساعته', category: 'staff', keywords: ['پشتیبانی', 'شبانه روزی', '۲۴ ساعته'] },

4
src/components/shared/UniversalDatePicker.tsx

@ -227,7 +227,7 @@ export function UniversalDatePicker({
{label} {required && <span className="text-rose-500">*</span>}
</span>
<span className="text-[10px] text-primary font-normal">
{language === 'fa' ? 'نمایش دوگانه میلادی و قمری' : 'میلادی و هجري'}
{language === 'fa' ? 'نمایش دوگانه میلادی و قمری' : language === 'ar' ? 'ميلادي وهجري' : 'Dual Gregorian & Hijri'}
</span>
</label>
)}
@ -262,7 +262,7 @@ export function UniversalDatePicker({
type="button"
onClick={handleClear}
className="size-5 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted flex items-center justify-center transition"
title={language === 'fa' ? 'پاک کردن' : 'Clear'}
title={language === 'fa' ? 'پاک کردن' : language === 'ar' ? 'مسح' : 'Clear'}
>
<Ic name="x" className="size-3" />
</button>

8
src/features/custom-trip/CustomTripOrderDetailPage.tsx

@ -1574,15 +1574,15 @@ export default function CustomTripOrderDetailPage() {
<div className="flex items-center gap-2.5 text-xs font-mono select-none flex-wrap">
<div className="flex items-center gap-1.5 bg-background px-3 py-1.5 rounded-lg border border-border/70 shadow-2xs">
<span className="text-[11px] text-muted-foreground font-sans font-medium">{t('customTrip.checkInLabel') || 'ورود'}:</span>
<span className="font-bold text-foreground text-xs dir-ltr">
{leg.checkIn ? leg.checkIn.slice(0, 10) : '—'}
<span className="font-bold text-foreground text-xs" style={{ unicodeBidi: 'plaintext' }}>
{leg.checkIn ? formatDualDate(leg.checkIn, language) : '—'}
</span>
</div>
<span className="text-primary font-bold shrink-0">←</span>
<div className="flex items-center gap-1.5 bg-background px-3 py-1.5 rounded-lg border border-border/70 shadow-2xs">
<span className="text-[11px] text-muted-foreground font-sans font-medium">{t('customTrip.checkOutLabel') || 'خروج'}:</span>
<span className="font-bold text-primary text-xs dir-ltr">
{leg.checkOut ? leg.checkOut.slice(0, 10) : '—'}
<span className="font-bold text-primary text-xs" style={{ unicodeBidi: 'plaintext' }}>
{leg.checkOut ? formatDualDate(leg.checkOut, language) : '—'}
</span>
</div>
</div>

8
src/features/custom-trip/components/CustomTripOrderReviewDialog.tsx

@ -1448,15 +1448,15 @@ export function CustomTripOrderReviewDialog({
<div className="flex items-center gap-2.5 text-xs font-mono select-none flex-wrap">
<div className="flex items-center gap-1.5 bg-background px-3 py-1.5 rounded-lg border border-border/70 shadow-2xs">
<span className="text-[11px] text-muted-foreground font-sans font-medium">{t('customTrip.checkInLabel') || 'ورود'}:</span>
<span className="font-bold text-foreground text-xs dir-ltr">
{leg.checkIn ? leg.checkIn.slice(0, 10) : '—'}
<span className="font-bold text-foreground text-xs" style={{ unicodeBidi: 'plaintext' }}>
{leg.checkIn ? formatDualDate(leg.checkIn, language) : '—'}
</span>
</div>
<span className="text-primary font-bold shrink-0">←</span>
<div className="flex items-center gap-1.5 bg-background px-3 py-1.5 rounded-lg border border-border/70 shadow-2xs">
<span className="text-[11px] text-muted-foreground font-sans font-medium">{t('customTrip.checkOutLabel') || 'خروج'}:</span>
<span className="font-bold text-primary text-xs dir-ltr">
{leg.checkOut ? leg.checkOut.slice(0, 10) : '—'}
<span className="font-bold text-primary text-xs" style={{ unicodeBidi: 'plaintext' }}>
{leg.checkOut ? formatDualDate(leg.checkOut, language) : '—'}
</span>
</div>
</div>

4
src/features/dashboard/DashboardHome.tsx

@ -241,7 +241,7 @@ export default function DashboardHome() {
{toFa(d.customTrips.totalCount)} سفر اختصاصی
</span>
<span className="text-primary font-bold bg-primary/10 px-2 py-0.5 rounded-lg">
{toFa(d.tours.totalCount)} کاروان تور
{toFa(d.tours.totalCount)} تور
</span>
</div>
<div className="absolute bottom-0 inset-x-0 h-1 bg-gradient-to-r from-primary/40 via-primary to-primary/40 opacity-0 group-hover:opacity-100 transition-opacity" />
@ -408,7 +408,7 @@ export default function DashboardHome() {
{t('dashboard.toursSectionTitle')} و {t('dashboard.customTripSectionTitle')}
</h2>
<p className="text-xs text-muted-foreground mt-0.5">
پایش وضعیت کاروان‌های گروهی و درخواست‌های سفر اختصاصی زائران
پایش وضعیت تورهای گروهی و درخواست‌های سفر اختصاصی زائران
</p>
</div>
</div>

2
src/features/dashboard/services/dashboard-api.ts

@ -189,7 +189,7 @@ export async function fetchLiveDashboardMetrics(range: DashboardTimeRange = 'all
service: 'tour',
detail_service: null,
transaction_receipt: null,
description: 'رزرو کاروان عتبات عالیات',
description: 'رزرو تور عتبات عالیات',
amount: '980.00',
card_number: '5892-****-****-8819',
object_id: 3,

5
src/features/services/ServicesPage.tsx

@ -7,6 +7,8 @@ import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Shimmer } from '@/components/shared/shimmer_frontend'
import { toFa } from '@/lib/utils'
import { formatDualDate } from '@/lib/date-utils'
import { useTranslation } from '@/i18n'
import { SimcardServiceView } from './components/SimcardServiceView'
import { ZayerGuideServiceView } from './components/ZayerGuideServiceView'
import { DuasServiceView } from './components/DuasServiceView'
@ -347,6 +349,7 @@ function ServiceDetailRenderer({
// ۳. صرافی آنلاین و تشریفات (Online Exchange)
// ─────────────────────────────────────────────────────────────────────────────
function ExchangeServiceView({ service }: { service: ServiceMeta }) {
const { language } = useTranslation()
const [searchTerm, setSearchTerm] = useState('')
const [statusFilter, setStatusFilter] = useState<'ALL' | 'CONFIRMED' | 'DONE' | 'PENDING'>('ALL')
@ -584,7 +587,7 @@ function ExchangeServiceView({ service }: { service: ServiceMeta }) {
<span>{ord.deliverType}</span>
</div>
</td>
<td className="p-3.5 text-grey-3 font-mono text-[11px]">{ord.date}</td>
<td className="p-3.5 text-grey-3 font-mono text-[11px]">{formatDualDate(ord.date, language as any)}</td>
<td className="p-3.5">
<Badge
className={`text-[10px] rounded-full border ${

5
src/features/services/components/BlogsServiceView.tsx

@ -18,6 +18,8 @@ import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import { toast } from '@/stores/toast'
import { toFa } from '@/lib/utils'
import { formatDualDate } from '@/lib/date-utils'
import { useTranslation } from '@/i18n'
import {
fetchAdminBlogs,
saveAdminArticle,
@ -50,6 +52,7 @@ interface FormTranslationState {
}
export function BlogsServiceView() {
const { language } = useTranslation()
const qc = useQueryClient()
const [selectedFilterLang, setSelectedFilterLang] = useState<string>('ALL')
const [statusFilter, setStatusFilter] = useState<string>('ALL')
@ -547,7 +550,7 @@ export function BlogsServiceView() {
{/* تاریخ ثبت */}
<td className="py-3 px-4 text-center font-mono text-[11px] text-grey-3">
{article.created_at ? new Date(article.created_at).toLocaleDateString('fa-IR') : '-'}
{article.created_at ? formatDualDate(article.created_at, language as any) : '—'}
</td>
{/* عملیات */}

6
src/features/services/components/SupportServiceView.tsx

@ -84,11 +84,11 @@ const INITIAL_SUPPORT_CHANNELS: SupportChannel[] = [
id: 'Jadwal Perjalanan',
},
descriptions: {
ar: 'تنظيم مواعيد الرحلات، برامج الزيارات الميدانية ومرافقة القوافل',
fa: 'هماهنگی جدول زمانی کاروان‌ها، گشت‌های زیارتی و هماهنگی تورلیدر',
ar: 'تنظيم مواعيد الرحلات، برامج الزيارات الميدانية ومرافقة المجموعات',
fa: 'هماهنگی جدول زمانی تورها، گشت‌های زیارتی و هماهنگی تورلیدر',
en: 'Travel schedules, group coordination, ziyarah guides and itineraries',
ru: 'Расписание поездок, координация групп и экскурсионных программ',
id: 'Jadwal perjalanan, koordinasi rombongan dan panduan ziarah',
id: 'Jadwal perjalanan, koordinasi tur dan panduan ziarah',
},
},
{

13
src/features/settings/components/AdminUserTable.tsx

@ -1,6 +1,7 @@
import * as React from 'react'
import { Link } from 'react-router-dom'
import { useTranslation } from '@/i18n'
import { formatDateTimeByLanguage } from '@/lib/date-utils'
import { Button } from '@/components/ui/button'
import { Ic } from '@/icons'
import { ShimmerBox } from '@/components/ui/shimmer'
@ -21,7 +22,7 @@ export function AdminUserTable({
onSetPassword,
onAdminUpdated,
}: AdminUserTableProps) {
const { t } = useTranslation()
const { t, language } = useTranslation()
const [togglingId, setTogglingId] = React.useState<number | null>(null)
const [copiedId, setCopiedId] = React.useState<number | null>(null)
const [revealedIds, setRevealedIds] = React.useState<Record<number, boolean>>({})
@ -61,15 +62,7 @@ export function AdminUserTable({
const formatDate = (isoString?: string | null) => {
if (!isoString) return '—'
try {
const date = new Date(isoString)
return new Intl.DateTimeFormat('fa-IR', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(date)
} catch {
return isoString
}
return formatDateTimeByLanguage(isoString, language as any)
}
return (

18
src/features/tours/components/CreateTourDialog.tsx

@ -17,6 +17,7 @@ import {
type ServiceLanguageCode,
} from '@/components/shared/ServiceTranslation'
import { TourFeatureIconPicker } from '@/components/shared/TourFeatureIconPicker'
import { UniversalDatePicker } from '@/components/shared/UniversalDatePicker'
import type { TourTranslationData } from '../types'
interface CreateTourDialogProps {
@ -692,11 +693,11 @@ export function CreateTourDialog({
<label className="block text-xs font-bold text-foreground mb-1.5">
تاریخ شروع تور <span className="text-rose-500">*</span>:
</label>
<Input
type="date"
<UniversalDatePicker
value={startedAt}
onChange={(e) => setStartedAt(e.target.value)}
className="bg-surface-2 border-border/50 text-sm font-mono"
onChange={setStartedAt}
allowedModes={['islamic', 'gregorian']}
defaultMode="islamic"
required
/>
</div>
@ -705,11 +706,12 @@ export function CreateTourDialog({
<label className="block text-xs font-bold text-foreground mb-1.5">
تاریخ پایان تور <span className="text-rose-500">*</span>:
</label>
<Input
type="date"
<UniversalDatePicker
value={endedAt}
onChange={(e) => setEndedAt(e.target.value)}
className="bg-surface-2 border-border/50 text-sm font-mono"
onChange={setEndedAt}
minDate={startedAt || undefined}
allowedModes={['islamic', 'gregorian']}
defaultMode="islamic"
required
/>
</div>

31
src/features/tours/components/TourDetailDialog.tsx

@ -51,6 +51,9 @@ import type {
import { TourStatusBadge } from './TourStatusBadge'
import DeleteConfirmDialog from '@/components/shared/DeleteConfirmDialog'
import { TourFeatureIconPicker } from '@/components/shared/TourFeatureIconPicker'
import { UniversalDatePicker } from '@/components/shared/UniversalDatePicker'
import { formatDualDate } from '@/lib/date-utils'
import { useTranslation } from '@/i18n'
interface TourDetailDialogProps {
tourId: number | null
@ -142,6 +145,7 @@ export function TourDetailDialog({
onClose,
onUpdated,
}: TourDetailDialogProps) {
const { language } = useTranslation()
const [activeTab, setActiveTab] = useState<'overview' | 'itinerary' | 'features' | 'passengers' | 'comments'>('overview')
const [selectedLang, setSelectedLang] = useState<LangCode>('ar')
@ -1314,26 +1318,29 @@ export function TourDetailDialog({
<span>مشخصات عمومی و زمان‌بندی تور</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3.5">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3.5">
<div className="space-y-1">
<label className="text-xs font-bold text-grey-2">تاریخ شروع:</label>
<Input
type="date"
<UniversalDatePicker
value={coreFields.started_at}
disabled={!isEditMode || isSaving}
onChange={(e) => setCoreFields((prev) => ({ ...prev, started_at: e.target.value }))}
className="bg-card text-xs font-mono rounded-xl border-border-soft disabled:opacity-85 disabled:cursor-default"
onChange={(val) => setCoreFields((prev) => ({ ...prev, started_at: val }))}
allowedModes={['islamic', 'gregorian']}
defaultMode="islamic"
size="sm"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-grey-2">تاریخ پایان:</label>
<Input
type="date"
<UniversalDatePicker
value={coreFields.ended_at}
minDate={coreFields.started_at || undefined}
disabled={!isEditMode || isSaving}
onChange={(e) => setCoreFields((prev) => ({ ...prev, ended_at: e.target.value }))}
className="bg-card text-xs font-mono rounded-xl border-border-soft disabled:opacity-85 disabled:cursor-default"
onChange={(val) => setCoreFields((prev) => ({ ...prev, ended_at: val }))}
allowedModes={['islamic', 'gregorian']}
defaultMode="islamic"
size="sm"
/>
</div>
@ -1788,7 +1795,7 @@ export function TourDetailDialog({
<span className="text-xs font-bold text-grey-3">
مرحله #{itId} &bull;{' '}
{(item as any).started_at
? new Date((item as any).started_at).toLocaleDateString('fa-IR')
? formatDualDate((item as any).started_at, language as any)
: `روز ${idx + 1}`}
</span>
</div>
@ -2137,9 +2144,9 @@ export function TourDetailDialog({
className="rounded-xl border border-border-soft bg-surface-base p-3 space-y-1 text-xs"
>
<div className="font-bold text-foreground">{p.fullname}</div>
<div className="flex justify-between text-grey-3 font-mono text-[11px]" dir="ltr">
<div className="flex flex-wrap justify-between gap-2 text-grey-3 font-mono text-[11px]" dir="ltr">
<span>Passport: {p.passport_number || '-'}</span>
<span>DOB: {p.birthdate || '-'}</span>
<span>DOB: {p.birthdate ? formatDualDate(p.birthdate, language as any) : '-'}</span>
</div>
{p.phone_number && (
<div className="flex justify-between items-center text-grey-3 font-mono text-[11px] pt-1 border-t border-border-soft/60" dir="ltr">

79
src/features/tours/components/TourDetailSheet.tsx

@ -401,9 +401,9 @@ export function TourDetailSheet({ tourId, isOpen, onClose }: TourDetailSheetProp
<span>گذرنامه:</span>
<span className="font-mono font-medium text-foreground">{p.passport_number}</span>
</div>
<div className="text-[11px] text-grey-3 flex justify-between">
<div className="text-[11px] text-grey-3 flex justify-between gap-2">
<span>تولد:</span>
<span className="font-mono">{p.birthdate}</span>
<span className="font-mono">{p.birthdate ? formatDateByLanguage(p.birthdate, language as any) : '—'}</span>
</div>
<div className="text-[11px] text-grey-3 flex justify-between">
<span>واتساپ:</span>
@ -466,6 +466,81 @@ export function TourDetailSheet({ tourId, isOpen, onClose }: TourDetailSheetProp
<p className="text-xs text-grey-3">توصیه خاصی ثبت نشده است.</p>
)}
</div>
{/* هتل‌ها و اقامتگاه‌ها */}
{tour.tour_meta?.accommodations && tour.tour_meta.accommodations.length > 0 && (
<div className="space-y-3">
<h4 className="text-xs font-extrabold text-foreground">اقامتگاه‌ها و هتل‌های پکیج</h4>
<div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2">
{tour.tour_meta.accommodations.map((acc, aIdx) => (
<div key={aIdx} className="rounded-xl border border-border-soft bg-surface-base p-3 space-y-1">
<div className="flex items-center justify-between">
<span className="text-xs font-bold text-foreground">{acc.hotel_name}</span>
<span className="text-[11px] text-amber-500 font-bold">{'★'.repeat(acc.stars || 5)}</span>
</div>
<div className="text-[11px] text-grey-3 flex items-center justify-between">
<span>شهر: {acc.city}</span>
{acc.nights ? <span>{acc.nights} شب اقامت</span> : null}
</div>
<p className="text-[10px] text-primary">{acc.distance_to_shrine}</p>
</div>
))}
</div>
</div>
)}
{/* حمل‌ونقل */}
{tour.tour_meta?.transport_info && (
<div className="space-y-3">
<h4 className="text-xs font-extrabold text-foreground">اطلاعات حمل‌ونقل و پرواز</h4>
<div className="rounded-xl border border-border-soft bg-surface-base p-3 space-y-2 text-xs">
<div className="flex justify-between">
<span className="text-grey-3">وسیله نقلیه:</span>
<span className="font-bold text-foreground">{tour.tour_meta.transport_info.vehicle_type}</span>
</div>
<div className="flex justify-between">
<span className="text-grey-3">مسیر و ناوگان:</span>
<span className="text-foreground">{tour.tour_meta.transport_info.carrier_name}</span>
</div>
<div className="flex justify-between">
<span className="text-grey-3">بار مجاز:</span>
<span className="text-foreground">{tour.tour_meta.transport_info.baggage_allowance}</span>
</div>
</div>
</div>
)}
{/* خدمات شامل و غیرشامل */}
{(tour.tour_meta?.included_services?.length || tour.tour_meta?.excluded_services?.length) && (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{tour.tour_meta?.included_services && tour.tour_meta.included_services.length > 0 && (
<div className="space-y-2">
<h4 className="text-xs font-bold text-emerald-600 dark:text-emerald-400">خدمات شامل پکیج</h4>
<div className="space-y-1">
{tour.tour_meta.included_services.map((inc, iIdx) => (
<div key={iIdx} className="flex items-start gap-1.5 text-[11px] text-foreground">
<Ic name="check" className="size-3.5 text-emerald-500 shrink-0 mt-0.5" />
<span>{inc}</span>
</div>
))}
</div>
</div>
)}
{tour.tour_meta?.excluded_services && tour.tour_meta.excluded_services.length > 0 && (
<div className="space-y-2">
<h4 className="text-xs font-bold text-rose-500">هزینه‌های غیرشامل</h4>
<div className="space-y-1">
{tour.tour_meta.excluded_services.map((exc, eIdx) => (
<div key={eIdx} className="flex items-start gap-1.5 text-[11px] text-grey-2">
<Ic name="close" className="size-3.5 text-rose-500 shrink-0 mt-0.5" />
<span>{exc}</span>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
)}

1
src/features/tours/services/tour-api.ts

@ -211,6 +211,7 @@ export interface CreateTourPayload {
trip_status?: string
tour_feature?: { title: string; icon?: string }[]
travel_tips?: Record<string, string>
tour_meta?: import('../types').TourMeta
visibility?: boolean
translations?: Record<string, import('../types').TourTranslationData>
image_id?: number

36
src/features/tours/types.ts

@ -54,6 +54,38 @@ export interface TravelTipItem {
description: string
}
export interface AccommodationItem {
city: string
hotel_name: string
stars: number
distance_to_shrine: string
nights?: number
}
export interface TransportInfo {
vehicle_type?: string
carrier_name?: string
departure_city?: string
arrival_city?: string
departure_time?: string
baggage_allowance?: string
}
export interface CancellationRule {
period: string
penalty_percent: number
description: string
}
export interface TourMeta {
accommodations?: AccommodationItem[]
transport_info?: TransportInfo
included_services?: string[]
excluded_services?: string[]
cancellation_policy?: CancellationRule[]
required_documents?: string[]
}
export interface TourDetailItem {
id: number
title: string
@ -66,6 +98,7 @@ export interface TourDetailItem {
number_sold: number
tour_features: TourFeatureItem[]
travel_tips: TravelTipItem[]
tour_meta?: TourMeta
status: TourSaleStatus
price: string
price_child: string
@ -75,6 +108,7 @@ export interface TourDetailItem {
images: TourImageItem[]
trip_status: TourTripStatus
destination_country?: string | null
origin_country?: string | null
is_access: boolean
visibility?: boolean
}
@ -162,6 +196,7 @@ export interface TourTranslationData {
description?: string
tour_feature?: { title: string; icon?: string }[]
travel_tips?: Record<string, string>
tour_meta?: TourMeta
}
export interface TourTranslationsResponse {
@ -171,6 +206,7 @@ export interface TourTranslationsResponse {
description: string
tour_feature: { title: string; icon?: string }[]
travel_tips: Record<string, string>
tour_meta?: TourMeta
}
translations: Record<string, TourTranslationData>
}

17
src/features/users/components/UserFactorsTab.tsx

@ -1,5 +1,6 @@
import * as React from 'react'
import { useTranslation } from '@/i18n'
import { formatDateTimeByLanguage } from '@/lib/date-utils'
import type { UserFactorItem } from '../types'
import { fetchUserFactors } from '../services/users-api'
import { Ic } from '@/icons'
@ -11,7 +12,7 @@ interface UserFactorsTabProps {
type ServiceFilter = 'all' | 'simcard' | 'tasrif' | 'shop' | 'tour' | 'custom_trip'
export function UserFactorsTab({ userId }: UserFactorsTabProps) {
const { t } = useTranslation()
const { t, language } = useTranslation()
const [service, setService] = React.useState<ServiceFilter>('all')
const [loading, setLoading] = React.useState(true)
const [factors, setFactors] = React.useState<UserFactorItem[]>([])
@ -35,18 +36,8 @@ export function UserFactorsTab({ userId }: UserFactorsTabProps) {
}, [loadFactors])
const formatDate = (isoString: string) => {
try {
const d = new Date(isoString)
return d.toLocaleDateString('fa-IR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
} catch {
return isoString?.slice(0, 10) || '—'
}
if (!isoString) return '—'
return formatDateTimeByLanguage(isoString, language as any)
}
const getServiceLabel = (srv: string) => {

20
src/features/users/components/UserProfileTab.tsx

@ -1,5 +1,6 @@
import * as React from 'react'
import { useTranslation } from '@/i18n'
import { formatDateTimeByLanguage, formatDualDate } from '@/lib/date-utils'
import type { UserDetail } from '../types'
import { Ic } from '@/icons'
@ -8,22 +9,11 @@ interface UserProfileTabProps {
}
export function UserProfileTab({ user }: UserProfileTabProps) {
const { t } = useTranslation()
const { t, language } = useTranslation()
const formatDate = (isoString: string | null) => {
if (!isoString) return t('users.noData')
try {
const d = new Date(isoString)
return d.toLocaleDateString('fa-IR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
} catch {
return isoString
}
return formatDateTimeByLanguage(isoString, language as any)
}
return (
@ -197,7 +187,9 @@ export function UserProfileTab({ user }: UserProfileTabProps) {
<td className="py-3 px-3 font-mono text-muted-foreground">{idx + 1}</td>
<td className="py-3 px-3 font-semibold text-foreground">{p.fullname || t('users.noData')}</td>
<td className="py-3 px-3 font-mono text-foreground">{p.passport_number || '—'}</td>
<td className="py-3 px-3 font-mono text-muted-foreground">{p.birthdate || '—'}</td>
<td className="py-3 px-3 font-mono text-muted-foreground">
{p.birthdate ? formatDualDate(p.birthdate, language as any) : '—'}
</td>
<td className="py-3 px-3 font-mono dir-ltr text-end">{p.phone_number || '—'}</td>
<td className="py-3 px-3">
<span className="inline-flex px-2 py-0.5 rounded-full text-xs font-medium bg-muted text-foreground">

5
src/features/users/components/UserToursTab.tsx

@ -1,5 +1,6 @@
import * as React from 'react'
import { useTranslation } from '@/i18n'
import { formatDualDate } from '@/lib/date-utils'
import type { UserTourItem } from '../types'
import { fetchUserTours } from '../services/users-api'
import { Ic } from '@/icons'
@ -11,7 +12,7 @@ interface UserToursTabProps {
type TimelineFilter = 'all' | 'active' | 'upcoming' | 'past'
export function UserToursTab({ userId }: UserToursTabProps) {
const { t } = useTranslation()
const { t, language } = useTranslation()
const [timeline, setTimeline] = React.useState<TimelineFilter>('all')
const [loading, setLoading] = React.useState(true)
const [tours, setTours] = React.useState<UserTourItem[]>([])
@ -35,7 +36,7 @@ export function UserToursTab({ userId }: UserToursTabProps) {
const formatDate = (isoDate: string | null | undefined) => {
if (!isoDate) return '—'
return isoDate
return formatDualDate(isoDate, language as any)
}
const getOrderStatusBadge = (status: string) => {

108
src/lib/date-utils.ts

@ -4,10 +4,12 @@ import { toFa } from './utils'
/* ──────────────────────────────────────────────────────────────────────────
* سیستم جامع تبدیل و نمایش تاریخ‌های چندزبانه در پنل مدیریت عقیله
*
* منطق تبدیل تقویم‌ها:
* ۱. زبان فارسی ('fa') → هجری شمسی (جلالی / Solar Hijri)
* ۲. زبان عربی ('ar') → هجری قمری (تقویم اسلامی أم القری / Islamic Hijri)
* ۳. انگلیسی ('en') و سایر → میلادی (گریگوری / Gregorian)
* استاندارد سراسری نمایش تاریخ در پنل (Dual Gregorian + Hijri):
* در تمامی بخش‌های پنل و در هر سه زبان (فارسی، عربی، انگلیسی)، کلیه تاریخ‌ها به صورت همزمان
* میلادی + هجری قمری (ام‌القری) همراه با ذکر نام ماه نمایش داده می‌شوند:
* - فارسی ('fa'): نوامبر 2، 2026 (جمادی‌الاول 22، 1448)
* - عربی ('ar'): نوفمبر 2، 2026 (جمادى الأولى 22، 1448 هـ)
* - انگلیسی ('en'): November 2, 2026 (Jumada al-Awwal 22, 1448 AH)
*
* داده‌های پایه بک‌اند و مقادیر فرم‌ها همواره در استاندارد ISO YYYY-MM-DD ذخیره می‌شوند.
* ────────────────────────────────────────────────────────────────────────── */
@ -19,6 +21,7 @@ export interface FormatDateOptions {
withTime?: boolean
withSeconds?: boolean
showEra?: boolean
compact?: boolean
}
/**
@ -35,10 +38,15 @@ export function parseSafeDate(input: string | number | Date | null | undefined):
const matchOnlyDate = str.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})$/)
if (matchOnlyDate) {
const y = parseInt(matchOnlyDate[1], 10)
const m = parseInt(matchOnlyDate[2], 10) - 1
const m = parseInt(matchOnlyDate[2], 10)
const d = parseInt(matchOnlyDate[3], 10)
// اگر تاریخ ورودی به صورت سال شمسی (مثلاً ۱۳۰۰ تا ۱۴۱۵) ثبت شده باشد، به میلادی تبدیل شود
if (y >= 1300 && y <= 1415) {
const g = jalaliToGregorian(y, m, d)
return new Date(g.year, g.month - 1, g.day, 12, 0, 0)
}
// استفاده از ساعت ۱۲ ظهر برای جلوگیری از تغییر روز در شیفت‌های تابستانی/زمستانی
return new Date(y, m, d, 12, 0, 0)
return new Date(y, m - 1, d, 12, 0, 0)
}
const parsed = new Date(str)
@ -46,69 +54,20 @@ export function parseSafeDate(input: string | number | Date | null | undefined):
}
/**
* قالب‌بندی یک تاریخ مشخص بر اساس زبان انتخابی (یا پیش‌فرض سامانه)
* قالب‌بندی یک تاریخ مشخص به صورت دوگانه میلادی و هجری قمری با ذکر نام ماه بر اساس زبان انتخابی
*/
export function formatDateByLanguage(
dateInput: string | number | Date | null | undefined,
lang: SupportedLanguage = 'fa',
options: FormatDateOptions = {}
): string {
const d = parseSafeDate(dateInput)
if (!d) return '—'
const style = options.style || 'medium'
try {
if (lang === 'fa') {
if (style === 'numeric') {
return new Intl.DateTimeFormat('fa-IR-u-ca-persian-nu-latn', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(d)
}
return new Intl.DateTimeFormat('fa-IR-u-ca-persian-nu-latn', {
year: 'numeric',
month: style === 'short' ? 'short' : 'long',
day: 'numeric',
}).format(d)
}
if (lang === 'ar') {
if (style === 'numeric') {
return new Intl.DateTimeFormat('ar-SA-u-ca-islamic-umalqura-nu-latn', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(d)
}
return new Intl.DateTimeFormat('ar-SA-u-ca-islamic-umalqura-nu-latn', {
year: 'numeric',
month: style === 'short' ? 'short' : 'long',
day: 'numeric',
}).format(d)
}
// انگلیسی و سایر زبان‌ها (میلادی)
if (style === 'numeric') {
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: style === 'short' ? 'short' : 'long',
day: 'numeric',
}).format(d)
} catch {
return d.toISOString().split('T')[0]
}
return formatDualDate(dateInput, lang, {
compact: options.compact,
})
}
/**
* قالب‌بندی بازه زمانی تاریخ (شروع تا پایان) بدون باگ جهت متن (BiDi)
* قالب‌بندی بازه زمانی تاریخ (شروع تا پایان) به صورت دوگانه میلادی و قمری بدون باگ جهت متن (BiDi)
*/
export function formatDateRangeByLanguage(
startDateInput: string | number | Date | null | undefined,
@ -120,11 +79,11 @@ export function formatDateRangeByLanguage(
const e = parseSafeDate(endDateInput)
if (!s && !e) return '—'
if (s && !e) return formatDateByLanguage(s, lang, options)
if (!s && e) return formatDateByLanguage(e, lang, options)
if (s && !e) return formatDualDate(s, lang, { compact: options.compact })
if (!s && e) return formatDualDate(e, lang, { compact: options.compact })
const startFormatted = formatDateByLanguage(s, lang, options)
const endFormatted = formatDateByLanguage(e, lang, options)
const startFormatted = formatDualDate(s, lang, { compact: options.compact })
const endFormatted = formatDualDate(e, lang, { compact: options.compact })
if (lang === 'fa') {
return `${startFormatted} تا ${endFormatted}`
@ -136,7 +95,7 @@ export function formatDateRangeByLanguage(
}
/**
* قالب‌بندی تاریخ و ساعت به صورت همزمان
* قالب‌بندی تاریخ (دوگانه میلادی و قمری با نام ماه) و ساعت به صورت همزمان
*/
export function formatDateTimeByLanguage(
dateInput: string | number | Date | null | undefined,
@ -146,19 +105,22 @@ export function formatDateTimeByLanguage(
const d = parseSafeDate(dateInput)
if (!d) return '—'
const dateStr = formatDateByLanguage(d, lang, { ...options, style: options.style || 'numeric' })
const isDateOnlyString =
typeof dateInput === 'string' && /^\d{4}[-/]\d{1,2}[-/]\d{1,2}$/.test(dateInput.trim())
if (isDateOnlyString && !options.withTime) {
return formatDualDate(d, lang, { compact: options.compact })
}
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const timeStr = `${hours}:${minutes}`
if (lang === 'fa') {
return `${dateStr}، ${toFa(timeStr)}`
}
if (lang === 'ar') {
return `${dateStr}، ${timeStr}`
}
return `${dateStr} ${timeStr}`
return formatDualDate(d, lang, {
withTime: true,
time: timeStr,
compact: options.compact,
})
}
/* ──────────────────────────────────────────────────────────────────────────

14
src/lib/jalali.ts

@ -3,23 +3,27 @@
* این ماژول جهت حفظ سازگاری کدهای قبلی حفظ شده و از موتور اصلی date-utils ارث‌بری می‌کند.
* ────────────────────────────────────────────────────────────────────────── */
import { formatDateByLanguage, formatDateTimeByLanguage } from './date-utils'
import { useLanguageStore } from '@/i18n'
import { formatDateByLanguage, formatDateTimeByLanguage, formatDualDate } from './date-utils'
export {
formatDateByLanguage,
formatDateRangeByLanguage,
formatDateTimeByLanguage,
formatDualDate,
useFormattedDate,
useFormattedDateRange,
useFormattedDateTime,
} from './date-utils'
/** تاریخ ISO → «۱۴۰۳/۰۱/۲۵» (جلالی در فارسی، هجری در عربی، میلادی در انگلیسی). */
/** نمایش تاریخ به صورت دوگانه میلادی و هجری قمری با ذکر نام ماه متناسب با زبان فعال پنل */
export function toJalali(iso: string | null | undefined): string {
return formatDateByLanguage(iso, 'fa', { style: 'numeric' })
const currentLang = useLanguageStore.getState().language
return formatDateByLanguage(iso, currentLang)
}
/** تاریخ ISO → «۱۴۰۳/۰۱/۲۵، ۱۴:۳۰» (جلالی + ساعت). */
/** نمایش تاریخ (دوگانه میلادی و هجری قمری با ذکر نام ماه) + ساعت متناسب با زبان فعال پنل */
export function toJalaliDateTime(iso: string | null | undefined): string {
return formatDateTimeByLanguage(iso, 'fa')
const currentLang = useLanguageStore.getState().language
return formatDateTimeByLanguage(iso, currentLang)
}

8
src/locales/ar.json

@ -98,12 +98,12 @@
"rejectedInvoices": "ملغاة أو منتهية",
"collectionRate": "نسبة التحصيل",
"viewAllBilling": "عرض كل الفواتير",
"toursSectionTitle": "رحلات القوافل الجماعية",
"toursSectionDesc": "حالة الطاقة الاستيعابية والقوافل النشطة والمتاحة للحجز",
"toursSectionTitle": "الرحلات الجماعية",
"toursSectionDesc": "حالة الطاقة الاستيعابية والرحلات النشطة والمتاحة للحجز",
"activeTours": "رحلة نشطة ومتاحة",
"soldOutTours": "مكتملة العدد أو متوقفة",
"travelingTours": "قوافل في الطريق حالياً",
"occupancyRate": "نسبة إشغال القوافل",
"travelingTours": "رحلات في الطريق حالياً",
"occupancyRate": "نسبة إشغال الرحلات",
"viewAllTours": "إدارة الرحلات",
"customTripSectionTitle": "الرحلات الخاصة (تصميم الزائر)",
"customTripSectionDesc": "طلبات تخطيط الرحلات المخصصة ومراجعة الفنادق ووسائل النقل",

4
src/locales/en.json

@ -99,11 +99,11 @@
"collectionRate": "Collection Rate",
"viewAllBilling": "View All Invoices",
"toursSectionTitle": "Group Pilgrimage Tours",
"toursSectionDesc": "Capacity, active caravans and departing packages",
"toursSectionDesc": "Capacity, active tours and departing packages",
"activeTours": "Active Available Tours",
"soldOutTours": "Sold Out / Closed",
"travelingTours": "Currently Traveling",
"occupancyRate": "Caravan Occupancy Rate",
"occupancyRate": "Tour Occupancy Rate",
"viewAllTours": "Manage Tours",
"customTripSectionTitle": "Custom Trips (Pilgrim Design)",
"customTripSectionDesc": "Custom itinerary requests, hotel & transport confirmations",

6
src/locales/fa.json

@ -99,11 +99,11 @@
"collectionRate": "نرخ وصول مطالبات",
"viewAllBilling": "مشاهده همه صورت‌حساب‌ها",
"toursSectionTitle": "تورهای گروهی زیارتی",
"toursSectionDesc": "وضعیت ظرفیت، کاروان‌های فعال و در حال پذیرش زائر",
"toursSectionDesc": "وضعیت ظرفیت، تورهای فعال و در حال پذیرش زائر",
"activeTours": "تور فعال و در حال پذیرش",
"soldOutTours": "تکمیل‌ظرفیت یا متوقف",
"travelingTours": "کاروان در حال سفر",
"occupancyRate": "نرخ تکمیل ظرفیت کاروان‌ها",
"travelingTours": "تور در حال سفر",
"occupancyRate": "نرخ تکمیل ظرفیت تورها",
"viewAllTours": "مدیریت تورها",
"customTripSectionTitle": "سفرهای اختصاصی (طراحی زائر)",
"customTripSectionDesc": "درخواست‌های برنامه‌ریزی سفر اختصاصی، بررسی هتل و ترنسپورت",

Loading…
Cancel
Save