From 18461f6bb1c8c65e5d31b0a5ce1a3fe6e5146c52 Mon Sep 17 00:00:00 2001 From: Muhammad Amin Ghorbani Date: Sat, 17 Oct 2026 19:26:09 +0330 Subject: [PATCH] feat: add social media settings tab and related API services --- .../CustomTripDestinationDetailPage.tsx | 7 +- .../components/CustomTripDestinationsTab.tsx | 27 +- .../CustomTripOrderReviewDialog.tsx | 65 + .../components/CustomTripOrdersTab.tsx | 14 + .../custom-trip/services/custom-trip-api.ts | 2777 +++-------------- src/features/custom-trip/types.ts | 11 + src/features/settings/SettingsPage.tsx | 32 +- .../components/SocialMediaSettingsTab.tsx | 714 +++++ .../settings/services/settings-api.ts | 25 + src/features/settings/types.ts | 34 + src/icons.tsx | 7 + src/locales/ar.json | 35 +- src/locales/en.json | 35 +- src/locales/fa.json | 35 +- src/services/http.ts | 9 +- 15 files changed, 1382 insertions(+), 2445 deletions(-) create mode 100644 src/features/settings/components/SocialMediaSettingsTab.tsx diff --git a/src/features/custom-trip/CustomTripDestinationDetailPage.tsx b/src/features/custom-trip/CustomTripDestinationDetailPage.tsx index 0d98dcb..8d0596e 100644 --- a/src/features/custom-trip/CustomTripDestinationDetailPage.tsx +++ b/src/features/custom-trip/CustomTripDestinationDetailPage.tsx @@ -390,7 +390,12 @@ export default function CustomTripDestinationDetailPage() { e.preventDefault() if (!destination || isSavingDestination) return - const primaryName = names.fa?.trim() || names.ar?.trim() || names.en?.trim() || destName.trim() + const primaryName = + names[activeLangTab]?.trim() || + names.fa?.trim() || + names.ar?.trim() || + names.en?.trim() || + destName.trim() if (!primaryName) return setIsSavingDestination(true) diff --git a/src/features/custom-trip/components/CustomTripDestinationsTab.tsx b/src/features/custom-trip/components/CustomTripDestinationsTab.tsx index 96a078d..0e05b29 100644 --- a/src/features/custom-trip/components/CustomTripDestinationsTab.tsx +++ b/src/features/custom-trip/components/CustomTripDestinationsTab.tsx @@ -277,28 +277,17 @@ export function CustomTripDestinationsTab({ const [selectedCountry, setSelectedCountry] = useState('all') const getTransportCount = (item: CustomTripDestinationItem) => { + if (item.transportsCount !== undefined) return item.transportsCount if (relations && relations.length > 0) { const dName = (item.name || '').trim() - const count = relations.filter( + return relations.filter( (r) => r.toCity.trim() === dName || dName.includes(r.toCity.trim()) || r.toCity.trim().includes(dName) ).length - if (count > 0) return count - } - if (item.transportsCount !== undefined) return item.transportsCount - const defaults: Record = { - karbala: 7, - najaf: 5, - kadhimiya: 4, - samarra: 3, - mashhad: 6, - qom: 4, - medina: 4, - mecca: 5, } - return defaults[String(item.id)] || 4 + return 0 } // Modals state @@ -444,9 +433,9 @@ export function CustomTripDestinationsTab({ if (e) e.preventDefault() const primaryName = - names.ar.trim() || - names.fa.trim() || names[activeLangTab].trim() || + names.fa.trim() || + names.ar.trim() || names.en.trim() || names.ru.trim() || names.id.trim() @@ -479,7 +468,7 @@ export function CustomTripDestinationsTab({ if (editingItem && onUpdateDestination) { const updated: CustomTripDestinationItem = { ...editingItem, - name: names.fa.trim() || names.ar.trim() || primaryName, + name: primaryName, nameEn: names.en.trim() || undefined, country: formCountry.trim(), flag: formFlag, @@ -491,8 +480,8 @@ export function CustomTripDestinationsTab({ onUpdateDestination(updated) } else { const newDest: CustomTripDestinationItem = { - id: `dest_${Date.now()}`, - name: names.fa.trim() || names.ar.trim() || primaryName, + id: selectedCityPresetId && selectedCityPresetId !== 'custom' ? selectedCityPresetId : `dest_${Date.now()}`, + name: primaryName, nameEn: names.en.trim() || undefined, country: formCountry.trim() || 'عراق', flag: formFlag, diff --git a/src/features/custom-trip/components/CustomTripOrderReviewDialog.tsx b/src/features/custom-trip/components/CustomTripOrderReviewDialog.tsx index 2f0a0d0..94af876 100644 --- a/src/features/custom-trip/components/CustomTripOrderReviewDialog.tsx +++ b/src/features/custom-trip/components/CustomTripOrderReviewDialog.tsx @@ -161,6 +161,70 @@ export function CustomTripOrderReviewDialog({ + {/* Passengers & Passport Images */} + {order.passengers && order.passengers.length > 0 && ( +
+

+ {t('customTrip.passengersList')}: +

+
+ {order.passengers.map((pax, idx) => ( +
+
+ + {toFa(idx + 1)} + +
+
+ {`${pax.firstName} ${pax.lastName}`.trim() || '—'} + + {pax.passengerType === 'INFANT' + ? t('customTrip.infants') + : pax.passengerType === 'CHILD' + ? t('customTrip.children') + : t('customTrip.adults')} + +
+
+ {pax.passportNumber && ( + + {t('customTrip.passportNumber')}:{' '} + {pax.passportNumber} + + )} + {pax.phoneNumber && ( + {pax.phoneNumber} + )} +
+
+
+ +
+ {pax.passportImage ? ( + + + {t('customTrip.viewPassportImage')} + + ) : ( + + {t('customTrip.noPassportImage')} + + )} +
+
+ ))} +
+
+ )} + {/* User note */} {order.detail && (
@@ -187,6 +251,7 @@ export function CustomTripOrderReviewDialog({ +
diff --git a/src/features/custom-trip/components/CustomTripOrdersTab.tsx b/src/features/custom-trip/components/CustomTripOrdersTab.tsx index 2800637..480228e 100644 --- a/src/features/custom-trip/components/CustomTripOrdersTab.tsx +++ b/src/features/custom-trip/components/CustomTripOrdersTab.tsx @@ -54,6 +54,20 @@ export function CustomTripOrdersTab({ orders, onOrderUpdated }: CustomTripOrders {t('customTrip.statusRejected')} ) + case 'EXPIRED': + return ( + + + {t('customTrip.statusExpired')} + + ) + case 'CANCELLED': + return ( + + + {t('customTrip.statusCancelled')} + + ) default: return ( diff --git a/src/features/custom-trip/services/custom-trip-api.ts b/src/features/custom-trip/services/custom-trip-api.ts index c15530d..b10cb3a 100644 --- a/src/features/custom-trip/services/custom-trip-api.ts +++ b/src/features/custom-trip/services/custom-trip-api.ts @@ -9,2198 +9,96 @@ import type { CustomTripOrderStatus, } from '../types' -// Mock fallback orders -const MOCK_ORDERS: CustomTripOrderItem[] = [ - { - id: 1042, - user: { - id: 201, - username: 'mohammed_ali', - fullName: 'محمد علی الحسینی', - phoneNumber: '+968 9123 4567', - email: 'm.ali@example.com', - }, - originCity: 'مسقط', - destinations: ['مشهد', 'کربلا'], - legs: [ - { - fromCity: 'مسقط', - toCity: 'مشهد', - transportMethod: 'هوایی (پرواز مستقیم سلام‌ایر)', - hotelName: 'هتل مجلل درویشی', - stayNights: 4, - mealPlan: 'فول‌برد (صبحانه، ناهار، شام)', - }, - { - fromCity: 'مشهد', - toCity: 'کربلا', - transportMethod: 'هوایی (مشهد - نجف چارتر)', - hotelName: 'هتل رویال کربلا', - stayNights: 5, - mealPlan: 'فول‌برد (صبحانه، ناهار، شام)', - }, - ], - adultsCount: 3, - childrenCount: 1, - infantsCount: 0, - priceEstimate: 1450, - status: 'PENDING', - created: '2026-10-04T12:30:00Z', - detail: 'درخواست سفر زیارتی برای ۳ بزرگسال و ۱ کودک، نیازمند ترانسفر فرودگاهی و خدمات ویلچر در حرم مطهر.', - adminNotes: 'در حال استعلام نرخ هتل بارون برای ایام عید.', - }, - { - id: 1038, - user: { - id: 204, - username: 'fatemeh_zahra', - fullName: 'فاطمه الزهراء', - phoneNumber: '+965 9988 7766', - email: 'fatemeh@example.com', - }, - originCity: 'کویت', - destinations: ['نجف', 'کربلا', 'سامرا'], - legs: [ - { - fromCity: 'کویت', - toCity: 'نجف', - transportMethod: 'هوایی (هواپیمایی الجزیره)', - hotelName: 'هتل قصر الدر نجف', - stayNights: 3, - mealPlan: 'صبحانه', - }, - { - fromCity: 'نجف', - toCity: 'کربلا', - transportMethod: 'زمینی (ون VIP اختصاصی)', - hotelName: 'هتل ریحانة کربلا', - stayNights: 4, - mealPlan: 'فول‌برد', - }, - { - fromCity: 'کربلا', - toCity: 'سامرا', - transportMethod: 'زمینی (خودرو اختصاصی)', - hotelName: 'اقامت نیم‌روزه سامرا', - stayNights: 1, - mealPlan: 'ناهار', - }, - ], - adultsCount: 2, - childrenCount: 0, - infantsCount: 0, - priceEstimate: 980, - status: 'APPROVED', - created: '2026-10-03T18:15:00Z', - detail: 'سفر دونفره اختصاصی برای سالگرد ازدواج. هتل‌های نزدیک به حرم اولویت دارند.', - adminNotes: 'تأیید شد و پیش‌فاکتور برای زائر ارسال شد.', - }, - { - id: 1031, - user: { - id: 215, - username: 'sadeq_bahraini', - fullName: 'صادق البحرانی', - phoneNumber: '+973 3344 5566', - email: 'sadeq@example.com', - }, - originCity: 'منامه', - destinations: ['مشهد'], - legs: [ - { - fromCity: 'منامه', - toCity: 'مشهد', - transportMethod: 'هوایی (گلف ایر / فلای دبی ترانزیت)', - hotelName: 'هتل مجلل درویشی مشهد', - stayNights: 6, - mealPlan: 'فول‌برد', - }, - ], - adultsCount: 2, - childrenCount: 2, - infantsCount: 1, - priceEstimate: 620, - status: 'AWAITING_PAYMENT', - created: '2026-10-02T09:40:00Z', - detail: 'سفر خانوادگی به مشهد مقدس همراه با دو فرزند خردسال و یک نوزاد.', - adminNotes: 'فاکتور صادر شده و منتظر پرداخت زائر است.', - }, -] - -// Mock Origins organized by core countries -const MOCK_ORIGINS: CustomTripOriginItem[] = [ - // ۱. عربستان سعودی (Saudi Arabia) - { - id: 'riyadh', - name: 'الرياض', - nameEn: 'Riyadh', - country: 'عربستان سعودی', - countryCode: 'SA', - flag: '🇸🇦', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'الرياض' }, - fa: { name: 'ریاض' }, - en: { name: 'Riyadh' }, - ru: { name: 'Эр-Рияд' }, - id: { name: 'Riyadh' }, - }, - }, - { - id: 'qatif', - name: 'القطيف', - nameEn: 'Qatif', - country: 'عربستان سعودی', - countryCode: 'SA', - flag: '🇸🇦', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'القطيف' }, - fa: { name: 'قطیف' }, - en: { name: 'Qatif' }, - ru: { name: 'Катиф' }, - id: { name: 'Qatif' }, - }, - }, - { - id: 'dammam', - name: 'الدمام', - nameEn: 'Dammam', - country: 'عربستان سعودی', - countryCode: 'SA', - flag: '🇸🇦', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'الدمام' }, - fa: { name: 'دمام' }, - en: { name: 'Dammam' }, - ru: { name: 'Даммам' }, - id: { name: 'Dammam' }, - }, - }, - { - id: 'al_ahsa', - name: 'الأحساء', - nameEn: 'Al-Ahsa', - country: 'عربستان سعودی', - countryCode: 'SA', - flag: '🇸🇦', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'الأحساء' }, - fa: { name: 'احساء' }, - en: { name: 'Al-Ahsa' }, - ru: { name: 'Эль-Ахса' }, - id: { name: 'Al-Ahsa' }, - }, - }, - { - id: 'jeddah', - name: 'جدة', - nameEn: 'Jeddah', - country: 'عربستان سعودی', - countryCode: 'SA', - flag: '🇸🇦', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'جدة' }, - fa: { name: 'جده' }, - en: { name: 'Jeddah' }, - ru: { name: 'Джидда' }, - id: { name: 'Jeddah' }, - }, - }, - { - id: 'medina', - name: 'المدينة المنورة', - nameEn: 'Medina', - country: 'عربستان سعودی', - countryCode: 'SA', - flag: '🇸🇦', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'المدينة المنورة' }, - fa: { name: 'مدینه منوره' }, - en: { name: 'Medina' }, - ru: { name: 'Медина' }, - id: { name: 'Madinah' }, - }, - }, - { - id: 'mecca', - name: 'مكة المكرمة', - nameEn: 'Mecca', - country: 'عربستان سعودی', - countryCode: 'SA', - flag: '🇸🇦', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'مكة المكرمة' }, - fa: { name: 'مکه مکرمه' }, - en: { name: 'Mecca' }, - ru: { name: 'Мекка' }, - id: { name: 'Makkah' }, - }, - }, - - // ۲. امارات متحده عربی (United Arab Emirates) - { - id: 'dubai', - name: 'دبي', - nameEn: 'Dubai', - country: 'امارات متحده عربی', - countryCode: 'AE', - flag: '🇦🇪', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'دبي' }, - fa: { name: 'دبی' }, - en: { name: 'Dubai' }, - ru: { name: 'Дубай' }, - id: { name: 'Dubai' }, - }, - }, - { - id: 'abu_dhabi', - name: 'أبوظبي', - nameEn: 'Abu Dhabi', - country: 'امارات متحده عربی', - countryCode: 'AE', - flag: '🇦🇪', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'أبوظبي' }, - fa: { name: 'ابوظبی' }, - en: { name: 'Abu Dhabi' }, - ru: { name: 'Абу-Даби' }, - id: { name: 'Abu Dhabi' }, - }, - }, - { - id: 'sharjah', - name: 'الشارقة', - nameEn: 'Sharjah', - country: 'امارات متحده عربی', - countryCode: 'AE', - flag: '🇦🇪', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'الشارقة' }, - fa: { name: 'شارجه' }, - en: { name: 'Sharjah' }, - ru: { name: 'Шарджа' }, - id: { name: 'Sharjah' }, - }, - }, - - // ۳. قطر (Qatar) - { - id: 'doha', - name: 'الدوحة', - nameEn: 'Doha', - country: 'قطر', - countryCode: 'QA', - flag: '🇶🇦', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'الدوحة' }, - fa: { name: 'دوحه' }, - en: { name: 'Doha' }, - ru: { name: 'Доха' }, - id: { name: 'Doha' }, - }, - }, - { - id: 'al_rayyan', - name: 'الريان', - nameEn: 'Al Rayyan', - country: 'قطر', - countryCode: 'QA', - flag: '🇶🇦', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'الريان' }, - fa: { name: 'الریان' }, - en: { name: 'Al Rayyan' }, - ru: { name: 'Эр-Райян' }, - id: { name: 'Al Rayyan' }, - }, - }, - - // ۴. کویت (Kuwait) - { - id: 'kuwait_city', - name: 'مدينة الكويت', - nameEn: 'Kuwait City', - country: 'کویت', - countryCode: 'KW', - flag: '🇰🇼', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'مدينة الكويت' }, - fa: { name: 'شهر کویت' }, - en: { name: 'Kuwait City' }, - ru: { name: 'Эль-Кувейт' }, - id: { name: 'Kota Kuwait' }, - }, - }, - { - id: 'al_ahmadi', - name: 'الأحمدي', - nameEn: 'Al Ahmadi', - country: 'کویت', - countryCode: 'KW', - flag: '🇰🇼', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'الأحمدي' }, - fa: { name: 'الاحمدی' }, - en: { name: 'Al Ahmadi' }, - ru: { name: 'Аль-Ахмади' }, - id: { name: 'Al Ahmadi' }, - }, - }, - { - id: 'hawally', - name: 'حولي', - nameEn: 'Hawally', - country: 'کویت', - countryCode: 'KW', - flag: '🇰🇼', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'حولي' }, - fa: { name: 'حولی' }, - en: { name: 'Hawally' }, - ru: { name: 'Хавалли' }, - id: { name: 'Hawally' }, - }, - }, - - // ۵. بحرین (Bahrain) - { - id: 'manama', - name: 'المنامة', - nameEn: 'Manama', - country: 'بحرین', - countryCode: 'BH', - flag: '🇧🇭', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'المنامة' }, - fa: { name: 'منامه' }, - en: { name: 'Manama' }, - ru: { name: 'Манама' }, - id: { name: 'Manama' }, - }, - }, - { - id: 'muharraq', - name: 'المحرق', - nameEn: 'Muharraq', - country: 'بحرین', - countryCode: 'BH', - flag: '🇧🇭', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'المحرق' }, - fa: { name: 'المحرق' }, - en: { name: 'Muharraq' }, - ru: { name: 'Мухаррак' }, - id: { name: 'Muharraq' }, - }, - }, - { - id: 'sitra', - name: 'سترة', - nameEn: 'Sitra', - country: 'بحرین', - countryCode: 'BH', - flag: '🇧🇭', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'سترة' }, - fa: { name: 'ستره' }, - en: { name: 'Sitra' }, - ru: { name: 'Ситра' }, - id: { name: 'Sitra' }, - }, - }, - { - id: 'riffa', - name: 'الرفاع', - nameEn: 'Riffa', - country: 'بحرین', - countryCode: 'BH', - flag: '🇧🇭', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'الرفاع' }, - fa: { name: 'الرفاع' }, - en: { name: 'Riffa' }, - ru: { name: 'Эр-Рифа' }, - id: { name: 'Riffa' }, - }, - }, - - // ۶. عُمان (Oman) - { - id: 'muscat', - name: 'مسقط', - nameEn: 'Muscat', - country: 'عُمان', - countryCode: 'OM', - flag: '🇴🇲', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'مسقط' }, - fa: { name: 'مسقط' }, - en: { name: 'Muscat' }, - ru: { name: 'Маскат' }, - id: { name: 'Muskat' }, - }, - }, - { - id: 'salalah', - name: 'صلالة', - nameEn: 'Salalah', - country: 'عُمان', - countryCode: 'OM', - flag: '🇴🇲', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'صلالة' }, - fa: { name: 'صلاله' }, - en: { name: 'Salalah' }, - ru: { name: 'Салала' }, - id: { name: 'Salalah' }, - }, - }, - { - id: 'sohar', - name: 'صحار', - nameEn: 'Sohar', - country: 'عُمان', - countryCode: 'OM', - flag: '🇴🇲', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'صحار' }, - fa: { name: 'صحار' }, - en: { name: 'Sohar' }, - ru: { name: 'Сохар' }, - id: { name: 'Sohar' }, - }, - }, - { - id: 'nizwa', - name: 'نزوى', - nameEn: 'Nizwa', - country: 'عُمان', - countryCode: 'OM', - flag: '🇴🇲', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'نزوى' }, - fa: { name: 'نزوی' }, - en: { name: 'Nizwa' }, - ru: { name: 'Низва' }, - id: { name: 'Nizwa' }, - }, - }, - - // ۷. عراق (Iraq) - { - id: 'baghdad', - name: 'بغداد', - nameEn: 'Baghdad', - country: 'عراق', - countryCode: 'IQ', - flag: '🇮🇶', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'بغداد' }, - fa: { name: 'بغداد' }, - en: { name: 'Baghdad' }, - ru: { name: 'Багдад' }, - id: { name: 'Bagdad' }, - }, - }, - { - id: 'basra', - name: 'البصرة', - nameEn: 'Basra', - country: 'عراق', - countryCode: 'IQ', - flag: '🇮🇶', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'البصرة' }, - fa: { name: 'بصره' }, - en: { name: 'Basra' }, - ru: { name: 'Басра' }, - id: { name: 'Basra' }, - }, - }, - { - id: 'erbil', - name: 'أربيل', - nameEn: 'Erbil', - country: 'عراق', - countryCode: 'IQ', - flag: '🇮🇶', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'أربيل' }, - fa: { name: 'اربیل' }, - en: { name: 'Erbil' }, - ru: { name: 'Эрбиль' }, - id: { name: 'Erbil' }, - }, - }, - { - id: 'najaf', - name: 'النجف الأشرف', - nameEn: 'Najaf', - country: 'عراق', - countryCode: 'IQ', - flag: '🇮🇶', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'النجف الأشرف' }, - fa: { name: 'نجف اشرف' }, - en: { name: 'Najaf' }, - ru: { name: 'Эн-Наджаф' }, - id: { name: 'Najaf' }, - }, - }, - { - id: 'karbala', - name: 'كربلاء المقدسة', - nameEn: 'Karbala', - country: 'عراق', - countryCode: 'IQ', - flag: '🇮🇶', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'كربلاء المقدسة' }, - fa: { name: 'کربلای معلی' }, - en: { name: 'Karbala' }, - ru: { name: 'Кербела' }, - id: { name: 'Karbala' }, - }, - }, - - // ۸. لبنان (Lebanon) - { - id: 'beirut', - name: 'بيروت', - nameEn: 'Beirut', - country: 'لبنان', - countryCode: 'LB', - flag: '🇱🇧', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'بيروت' }, - fa: { name: 'بیروت' }, - en: { name: 'Beirut' }, - ru: { name: 'Бейрут' }, - id: { name: 'Beirut' }, - }, - }, - { - id: 'sidon', - name: 'صيدا', - nameEn: 'Sidon', - country: 'لبنان', - countryCode: 'LB', - flag: '🇱🇧', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'صيدا' }, - fa: { name: 'صیدا' }, - en: { name: 'Sidon' }, - ru: { name: 'Сайда' }, - id: { name: 'Sidon' }, - }, - }, - - // ۹. سوریه (Syria) - { - id: 'damascus', - name: 'دمشق', - nameEn: 'Damascus', - country: 'سوریه', - countryCode: 'SY', - flag: '🇸🇾', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'دمشق' }, - fa: { name: 'دمشق' }, - en: { name: 'Damascus' }, - ru: { name: 'Дамаск' }, - id: { name: 'Damaskus' }, - }, - }, - - // ۱۰. اردن (Jordan) - { - id: 'amman', - name: 'عمّان', - nameEn: 'Amman', - country: 'اردن', - countryCode: 'JO', - flag: '🇯🇴', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'عمّان' }, - fa: { name: 'امان' }, - en: { name: 'Amman' }, - ru: { name: 'Амман' }, - id: { name: 'Amman' }, - }, - }, - - // ۱۱. مصر (Egypt) - { - id: 'cairo', - name: 'القاهرة', - nameEn: 'Cairo', - country: 'مصر', - countryCode: 'EG', - flag: '🇪🇬', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'القاهرة' }, - fa: { name: 'قاهره' }, - en: { name: 'Cairo' }, - ru: { name: 'Каир' }, - id: { name: 'Kairo' }, - }, - }, - { - id: 'alexandria', - name: 'الإسكندرية', - nameEn: 'Alexandria', - country: 'مصر', - countryCode: 'EG', - flag: '🇪🇬', - isInternational: true, - isActive: true, - translations: { - ar: { name: 'الإسكندرية' }, - fa: { name: 'اسکندریه' }, - en: { name: 'Alexandria' }, - ru: { name: 'Александрия' }, - id: { name: 'Aleksandria' }, - }, - }, - - // ۱۲. ایران (Iran) - { - id: 'tehran', - name: 'تهران', - nameEn: 'Tehran', - country: 'ایران', - countryCode: 'IR', - flag: '🇮🇷', - isInternational: false, - isActive: true, - translations: { - ar: { name: 'طهران' }, - fa: { name: 'تهران' }, - en: { name: 'Tehran' }, - ru: { name: 'Тегеран' }, - id: { name: 'Teheran' }, - }, - }, - { - id: 'mashhad', - name: 'مشهد', - nameEn: 'Mashhad', - country: 'ایران', - countryCode: 'IR', - flag: '🇮🇷', - isInternational: false, - isActive: true, - translations: { - ar: { name: 'مشهد' }, - fa: { name: 'مشهد' }, - en: { name: 'Mashhad' }, - ru: { name: 'Мешхед' }, - id: { name: 'Masyhad' }, - }, - }, - { - id: 'qom', - name: 'قم', - nameEn: 'Qom', - country: 'ایران', - countryCode: 'IR', - flag: '🇮🇷', - isInternational: false, - isActive: true, - translations: { - ar: { name: 'قم' }, - fa: { name: 'قم' }, - en: { name: 'Qom' }, - ru: { name: 'Кум' }, - id: { name: 'Qom' }, - }, - }, - { - id: 'isfahan', - name: 'اصفهان', - nameEn: 'Isfahan', - country: 'ایران', - countryCode: 'IR', - flag: '🇮🇷', - isInternational: false, - isActive: true, - translations: { - ar: { name: 'أصفهان' }, - fa: { name: 'اصفهان' }, - en: { name: 'Isfahan' }, - ru: { name: 'Исфахан' }, - id: { name: 'Isfahan' }, - }, - }, - { - id: 'shiraz', - name: 'شیراز', - nameEn: 'Shiraz', - country: 'ایران', - countryCode: 'IR', - flag: '🇮🇷', - isInternational: false, - isActive: true, - translations: { - ar: { name: 'شيراز' }, - fa: { name: 'شیراز' }, - en: { name: 'Shiraz' }, - ru: { name: 'Шираз' }, - id: { name: 'Syiraz' }, - }, - }, - { - id: 'ahvaz', - name: 'اهواز', - nameEn: 'Ahvaz', - country: 'ایران', - countryCode: 'IR', - flag: '🇮🇷', - isInternational: false, - isActive: true, - translations: { - ar: { name: 'الأهواز' }, - fa: { name: 'اهواز' }, - en: { name: 'Ahvaz' }, - ru: { name: 'Ахваз' }, - id: { name: 'Ahvaz' }, - }, - }, -] - -// Mock Destinations -const MOCK_DESTINATIONS: CustomTripDestinationItem[] = [ - // ۱. عراق (Iraq) - { - id: 'karbala', - name: 'کربلای معلی', - nameEn: 'Karbala', - country: 'عراق', - flag: '🇮🇶', - description: 'حرم مطهر امام حسین (ع)، حرم حضرت ابوالفضل العباس (ع)، خیمه‌گاه و تل زینبیه', - holyPlaces: ['حرم سیدالشهدا (ع)', 'حرم حضرت عباس (ع)', 'مقام حضرت علی‌اکبر (ع)', 'تل زینبیه', 'خیمه‌گاه حسینی'], - hotelsCount: 11, - transportsCount: 12, - isActive: true, - }, - { - id: 'najaf', - name: 'نجف اشرف', - nameEn: 'Najaf', - country: 'عراق', - flag: '🇮🇶', - description: 'حرم مطهر امیرالمؤمنین امام علی (ع)، مسجد کوفه، مسجد سهله و وادی‌السلام', - holyPlaces: ['حرم امیرالمؤمنین (ع)', 'مسجد کوفه', 'مسجد سهله', 'قبرستان وادی‌السلام'], - hotelsCount: 6, - transportsCount: 18, - isActive: true, - }, - { - id: 'kadhimiya', - name: 'کاظمین مقدسه', - nameEn: 'Kadhimiya', - country: 'عراق', - flag: '🇮🇶', - description: 'حرم مطهر امامین جوادین (ع)، مرقد شریف شیخ مفید و خواجه نصیر طوسی', - holyPlaces: ['حرم امامین جوادین (ع)', 'مرقد شیخ مفید', 'مسجد صفوی'], - hotelsCount: 3, - transportsCount: 3, - isActive: true, - }, - { - id: 'samarra', - name: 'سامرا', - nameEn: 'Samarra', - country: 'عراق', - flag: '🇮🇶', - description: 'حرم مطهر امامین عسکریین (ع)، سرداب مقدس غیبت امام زمان (عج)', - holyPlaces: ['حرم امامین عسکریین (ع)', 'سرداب غیبت', 'مزار نرجس خاتون'], - hotelsCount: 1, - transportsCount: 3, - isActive: true, - }, - - // ۲. ایران (Iran) - { - id: 'mashhad', - name: 'مشهد مقدس', - nameEn: 'Mashhad', - country: 'ایران', - flag: '🇮🇷', - description: 'بارگاه ملکوتی حضرت علی بن موسی الرضا (ع)، مسجد گوهرشاد و آرامگاه خواجه ربیع', - holyPlaces: ['حرم مطهر امام رضا (ع)', 'مسجد گوهرشاد', 'خواجه ربیع'], - hotelsCount: 11, - transportsCount: 9, - isActive: true, - }, - { - id: 'qom', - name: 'قم مقدسه', - nameEn: 'Qom', - country: 'ایران', - flag: '🇮🇷', - description: 'حرم مطهر حضرت فاطمه معصومه (س)، مسجد مقدس جمکران و کوه خضر نبی', - holyPlaces: ['حرم حضرت فاطمه معصومه (س)', 'مسجد مقدس جمکران', 'کوه خضر نبی'], - hotelsCount: 6, - transportsCount: 6, - isActive: true, - }, - { - id: 'tehran', - name: 'تهران', - nameEn: 'Tehran', - country: 'ایران', - flag: '🇮🇷', - description: 'پایتخت ایران، دسترسی به مراکز زیارتی امامزاده صالح (ع) و شاه عبدالعظیم حسنی (ع)', - holyPlaces: ['امامزاده صالح (ع)', 'حرم حضرت عبدالعظیم حسنی (ع)'], - hotelsCount: 3, - transportsCount: 14, - isActive: true, - }, - - // ۳. عربستان سعودی (Saudi Arabia) - { - id: 'medina', - name: 'مدینه منوره', - nameEn: 'Medina', - country: 'عربستان سعودی', - flag: '🇸🇦', - description: 'مسجد النبی (ص)، مرقد مطهر رسول اکرم (ص)، قبرستان بقیع، مسجد قبا و مسجد قبلتین', - holyPlaces: ['مسجد النبی (ص)', 'قبرستان بقیع', 'مسجد قبا', 'شهدای احد'], - hotelsCount: 6, - transportsCount: 3, - isActive: true, - }, - { - id: 'mecca', - name: 'مکه مکرمه', - nameEn: 'Mecca', - country: 'عربستان سعودی', - flag: '🇸🇦', - description: 'مسجد الحرام، کعبه مشرفه، غار حرا، کوه نور و صحرای عرفات و منا', - holyPlaces: ['مسجد الحرام', 'کعبه مشرفه', 'غار حرا', 'منا و عرفات'], - hotelsCount: 9, - transportsCount: 2, - isActive: true, - }, - - // ۴. سوریه، لبنان و مصر - { - id: 'damascus', - name: 'دمشق', - nameEn: 'Damascus', - country: 'سوریه', - flag: '🇸🇾', - description: 'حرم مطهر حضرت زینب کبری (س) و حرم حضرت رقیه (س)', - holyPlaces: ['حرم حضرت زینب (س)', 'حرم حضرت رقیه (س)', 'مسجد اموی'], - hotelsCount: 0, - transportsCount: 5, - isActive: true, - }, - { - id: 'beirut', - name: 'بیروت', - nameEn: 'Beirut', - country: 'لبنان', - flag: '🇱🇧', - description: 'مراکز زیارتی و مزار شهدای مقاومت، روضة الشهیدین و مقام حضرت خضر (ع)', - holyPlaces: ['روضة الشهیدین', 'مقام خضر نبی (ع)'], - hotelsCount: 0, - transportsCount: 5, - isActive: true, - }, - { - id: 'cairo', - name: 'قاهره', - nameEn: 'Cairo', - country: 'مصر', - flag: '🇪🇬', - description: 'مقام رأس‌الحسین (ع)، بارگاه حضرت زینب (س) و مسجد السیدة نفیسة (س)', - holyPlaces: ['مسجد رأس‌الحسین (ع)', 'مرقد سیده زینب (س)', 'مرقد سیده نفیسه (س)'], - hotelsCount: 0, - transportsCount: 3, - isActive: true, - }, -] - -// Mock Relations: From Origins to Destinations with Airplane, Train, Road -const MOCK_RELATIONS: CustomTripTransportRelationItem[] = [ - // --- حمل‌ونقل ایران (هوایی، زمینی، ریلی) --- - { - id: 'rel_muscat_mashhad', - fromCity: 'مسقط', - fromCountry: 'عُمان', - toCity: 'مشهد مقدس', - toCountry: 'ایران', - hasAirplane: true, - hasTrain: false, - hasRoad: false, - airplaneDetails: 'مسقط – مشهد و بالعکس (پرواز مستقیم سلام‌ایر / تابان)', - isActive: true, - }, - { - id: 'rel_mashhad_tehran', - fromCity: 'مشهد مقدس', - fromCountry: 'ایران', - toCity: 'تهران', - toCountry: 'ایران', - hasAirplane: true, - hasTrain: true, - hasRoad: true, - airplaneDetails: 'مشهد – تهران و بالعکس (پروازهای روزانه کلیه خطوط هوایی)', - trainDetails: 'مشهد – تهران و بالعکس (قطارهای ۵ ستاره فدک، زندگی و نورالرضا)', - roadDetails: 'اتوبوس‌های VIP ۲۵ نفره تخت‌شو', - isActive: true, - }, - { - id: 'rel_tehran_muscat', - fromCity: 'تهران', - fromCountry: 'ایران', - toCity: 'مسقط', - toCountry: 'عُمان', - hasAirplane: true, - hasTrain: false, - hasRoad: false, - airplaneDetails: 'تهران – مسقط و بالعکس (پروازهای مستقیم هفتگی)', - isActive: true, - }, - { - id: 'rel_domestic_iran_air', - fromCity: 'تهران', - fromCountry: 'ایران', - toCity: 'مشهد مقدس', - toCountry: 'ایران', - hasAirplane: true, - hasTrain: true, - hasRoad: true, - airplaneDetails: 'مسیرهای هوایی بین شهرهای ایران (تهران، مشهد، اصفهان، شیراز، اهواز)', - trainDetails: 'مسیرهای ریلی بین شهرهای ایران', - roadDetails: 'جابه‌جایی بین شهرهای ایران با اتوبوس و ون‌های VIP', - isActive: true, - }, - { - id: 'rel_tehran_qom', - fromCity: 'تهران', - fromCountry: 'ایران', - toCity: 'قم مقدسه', - toCountry: 'ایران', - hasAirplane: false, - hasTrain: true, - hasRoad: true, - trainDetails: 'قطار سریع‌السیر پردیس تهران – قم و بالعکس', - roadDetails: 'تهران – قم و بالعکس (اتوبوس، ون و تاکسی‌های فرودگاهی)', - isActive: true, - }, - { - id: 'rel_tehran_shomal', - fromCity: 'تهران', - fromCountry: 'ایران', - toCity: 'شمال', - toCountry: 'ایران', - hasAirplane: true, - hasTrain: false, - hasRoad: true, - roadDetails: 'تهران – شمال و بالعکس (ترانسفر توریستی جاده چالوس و هراز)', - isActive: true, - }, - { - id: 'rel_qom_mashhad', - fromCity: 'قم مقدسه', - fromCountry: 'ایران', - toCity: 'مشهد مقدس', - toCountry: 'ایران', - hasAirplane: false, - hasTrain: true, - hasRoad: true, - trainDetails: 'مشهد – قم و بالعکس (قطارهای لوکس ۴ و ۵ تخته مستقیم)', - roadDetails: 'قم – مشهد و بالعکس (اتوبوس‌های VIP مستقیم)', - isActive: true, - }, - { - id: 'rel_mashhad_shomal', - fromCity: 'مشهد مقدس', - fromCountry: 'ایران', - toCity: 'شمال', - toCountry: 'ایران', - hasAirplane: true, - hasTrain: true, - hasRoad: true, - trainDetails: 'مشهد – شمال و بالعکس (قطار گرگان/ساری)', - roadDetails: 'مشهد – شمال و بالعکس (ترانسفر جاده‌ای)', - isActive: true, - }, - { - id: 'rel_tehran_turkey', - fromCity: 'تهران', - fromCountry: 'ایران', - toCity: 'استانبول', - toCountry: 'ترکیه', - hasAirplane: true, - hasTrain: true, - hasRoad: true, - airplaneDetails: 'ایران – ترکیه و بالعکس (پروازهای مستقیم)', - trainDetails: 'تهران – ترکیه (قطار بین‌المللی مسافری و ترانزیت)', - roadDetails: 'ایران – ترکیه و بالعکس (اتوبوس‌های بین‌المللی مرز بازرگان)', - isActive: true, - }, - { - id: 'rel_iran_iraq', - fromCity: 'تهران', - fromCountry: 'ایران', - toCity: 'کربلای معلی', - toCountry: 'عراق', - hasAirplane: true, - hasTrain: false, - hasRoad: true, - airplaneDetails: 'پروازهای مستقیم تهران/مشهد به فرودگاه بین‌المللی نجف و بغداد', - roadDetails: 'ایران – عراق و بالعکس (اتوبوس و ون‌های VIP از مرز مهران، چذابه و خسروی)', - isActive: true, - }, - - // --- حمل‌ونقل عراق (هوایی و زمینی) --- - { - id: 'rel_muscat_baghdad', - fromCity: 'مسقط', - fromCountry: 'عُمان', - toCity: 'بغداد', - toCountry: 'عراق', - hasAirplane: true, - hasTrain: false, - hasRoad: false, - airplaneDetails: 'مسقط – بغداد و بالعکس (پرواز مستقیم)', - isActive: true, - }, - { - id: 'rel_muscat_najaf', - fromCity: 'مسقط', - fromCountry: 'عُمان', - toCity: 'نجف اشرف', - toCountry: 'عراق', - hasAirplane: true, - hasTrain: false, - hasRoad: false, - airplaneDetails: 'مسقط – نجف و بالعکس (پرواز مستقیم زیارتی)', - isActive: true, - }, - { - id: 'rel_muscat_basra', - fromCity: 'مسقط', - fromCountry: 'عُمان', - toCity: 'بصره', - toCountry: 'عراق', - hasAirplane: true, - hasTrain: false, - hasRoad: false, - airplaneDetails: 'مسقط – بصره و بالعکس (پرواز مستقیم تجاری و زیارتی)', - isActive: true, - }, - { - id: 'rel_kuwait_iraq', - fromCity: 'کویت', - fromCountry: 'کویت', - toCity: 'نجف اشرف', - toCountry: 'عراق', - hasAirplane: true, - hasTrain: false, - hasRoad: true, - airplaneDetails: 'کویت – نجف پرواز مستقیم الجزیره', - roadDetails: 'کویت – عراق و بالعکس (ترانسفر زمینی گذرگاه مرزی صفوان / بصره)', - isActive: true, - }, - { - id: 'rel_bahrain_iraq', - fromCity: 'منامه', - fromCountry: 'بحرین', - toCity: 'کربلای معلی', - toCountry: 'عراق', - hasAirplane: true, - hasTrain: false, - hasRoad: true, - airplaneDetails: 'بحرین – نجف پرواز مستقیم گلف‌ایر', - roadDetails: 'بحرین – عراق و بالعکس (ترانسفر زمینی کاروانی)', - isActive: true, - }, - { - id: 'rel_oman_iraq_road', - fromCity: 'مسقط', - fromCountry: 'عُمان', - toCity: 'کربلای معلی', - toCountry: 'عراق', - hasAirplane: true, - hasTrain: false, - hasRoad: true, - roadDetails: 'عمان – عراق و بالعکس (خطوط کاروانی زمینی)', - isActive: true, - }, - { - id: 'rel_jordan_iraq', - fromCity: 'امان', - fromCountry: 'اردن', - toCity: 'بغداد', - toCountry: 'عراق', - hasAirplane: true, - hasTrain: false, - hasRoad: true, - roadDetails: 'اردن – عراق و بالعکس (گذرگاه مرزی طریبیل)', - isActive: true, - }, - { - id: 'rel_iraq_intercity', - fromCity: 'نجف اشرف', - fromCountry: 'عراق', - toCity: 'کربلای معلی', - toCountry: 'عراق', - hasAirplane: false, - hasTrain: false, - hasRoad: true, - roadDetails: 'جابه‌جایی بین شهرهای داخل عراق (نجف – کربلا – کاظمین – سامرا با خودروهای VIP و ون‌های اختصاصی)', - isActive: true, - }, - - // --- حمل‌ونقل سوریه (هوایی و زمینی) --- - { - id: 'rel_muscat_damascus', - fromCity: 'مسقط', - fromCountry: 'عُمان', - toCity: 'دمشق', - toCountry: 'سوریه', - hasAirplane: true, - hasTrain: false, - hasRoad: false, - airplaneDetails: 'مسقط – دمشق و بالعکس (پرواز مستقیم / کانکشن شام وینگ)', - isActive: true, - }, - { - id: 'rel_iraq_damascus', - fromCity: 'بغداد', - fromCountry: 'عراق', - toCity: 'دمشق', - toCountry: 'سوریه', - hasAirplane: true, - hasTrain: false, - hasRoad: false, - airplaneDetails: 'عراق – دمشق و بالعکس (پروازهای منظم بغداد/نجف به دمشق)', - isActive: true, - }, - { - id: 'rel_lebanon_syria', - fromCity: 'بیروت', - fromCountry: 'لبنان', - toCity: 'دمشق', - toCountry: 'سوریه', - hasAirplane: false, - hasTrain: false, - hasRoad: true, - roadDetails: 'لبنان – سوریه (مرز المصنع / جدیده یابوس)', - isActive: true, - }, - { - id: 'rel_jordan_syria', - fromCity: 'امان', - fromCountry: 'اردن', - toCity: 'دمشق', - toCountry: 'سوریه', - hasAirplane: false, - hasTrain: false, - hasRoad: true, - roadDetails: 'اردن – سوریه (گذرگاه مرزی جابر / نصیب)', - isActive: true, - }, - { - id: 'rel_syria_intercity', - fromCity: 'دمشق', - fromCountry: 'سوریه', - toCity: 'حلب', - toCountry: 'سوریه', - hasAirplane: false, - hasTrain: false, - hasRoad: true, - roadDetails: 'جابه‌جایی بین شهرهای داخل سوریه', - isActive: true, - }, - - // --- حمل‌ونقل لبنان (هوایی و زمینی) --- - { - id: 'rel_muscat_beirut', - fromCity: 'مسقط', - fromCountry: 'عُمان', - toCity: 'بیروت', - toCountry: 'لبنان', - hasAirplane: true, - hasTrain: false, - hasRoad: false, - airplaneDetails: 'مسقط – بیروت و بالعکس (پرواز هوایی فرودگاه رفیق حریری)', - isActive: true, - }, - { - id: 'rel_iraq_beirut', - fromCity: 'نجف اشرف', - fromCountry: 'عراق', - toCity: 'بیروت', - toCountry: 'لبنان', - hasAirplane: true, - hasTrain: false, - hasRoad: false, - airplaneDetails: 'عراق – بیروت و بالعکس (پروازهای مستقیم نجف/بغداد به بیروت)', - isActive: true, - }, - { - id: 'rel_syria_lebanon', - fromCity: 'دمشق', - fromCountry: 'سوریه', - toCity: 'بیروت', - toCountry: 'لبنان', - hasAirplane: false, - hasTrain: false, - hasRoad: true, - roadDetails: 'سوریه – لبنان (ترانسفر جاده‌ای دمشق به بیروت)', - isActive: true, - }, - { - id: 'rel_lebanon_intercity', - fromCity: 'بیروت', - fromCountry: 'لبنان', - toCity: 'صیدا', - toCountry: 'لبنان', - hasAirplane: false, - hasTrain: false, - hasRoad: true, - roadDetails: 'جابه‌جایی بین شهرهای لبنان (بیروت، صیدا، صور، بعلبک)', - isActive: true, - }, - - // --- حمل‌ونقل مصر (هوایی) --- - { - id: 'rel_muscat_cairo', - fromCity: 'مسقط', - fromCountry: 'عُمان', - toCity: 'قاهره', - toCountry: 'مصر', - hasAirplane: true, - hasTrain: false, - hasRoad: false, - airplaneDetails: 'مسقط – مصر و بالعکس (پرواز مستقیم فرودگاه بین‌المللی قاهره)', - isActive: true, - }, - { - id: 'rel_iraq_cairo', - fromCity: 'بغداد', - fromCountry: 'عراق', - toCity: 'قاهره', - toCountry: 'مصر', - hasAirplane: true, - hasTrain: false, - hasRoad: false, - airplaneDetails: 'عراق – مصر و بالعکس (پروازهای بغداد و اربیل به قاهره)', - isActive: true, - }, - - // --- حمل‌ونقل حرمین شریفین (سعودی) --- - { - id: 'rel_makkah_medina_train', - fromCity: 'مدینه منوره', - fromCountry: 'عربستان سعودی', - toCity: 'مکه مکرمه', - toCountry: 'عربستان سعودی', - hasAirplane: false, - hasTrain: true, - hasRoad: true, - trainDetails: 'قطار سریع‌السیر الحرمین (Haramain Express) بین مکه و مدینه (۲ ساعت)', - roadDetails: 'اتوبوس‌های توریستی SAPTCO VIP اختصاصی', - isActive: true, - }, -] - - -// Mock Legacy Transports -const MOCK_TRANSPORTS: CustomTripTransportItem[] = [ - { id: 1, fromCity: 'مشهد', toCity: 'کربلا', method: 1, methodName: 'هوایی (پرواز چارتر)', priceAdult: 220, priceChild: 180, priceInfant: 40, isActive: true }, - { id: 2, fromCity: 'تهران', toCity: 'مشهد', method: 1, methodName: 'هوایی (ایران‌ایر / ماهان)', priceAdult: 65, priceChild: 50, priceInfant: 15, isActive: true }, - { id: 3, fromCity: 'تهران', toCity: 'مشهد', method: 2, methodName: 'قطار ۵ ستاره فدک', priceAdult: 35, priceChild: 25, priceInfant: 5, isActive: true }, - { id: 4, fromCity: 'مسقط', toCity: 'مشهد', method: 1, methodName: 'هوایی بین‌المللی (سلام ایر)', priceAdult: 280, priceChild: 230, priceInfant: 50, isActive: true }, - { id: 5, fromCity: 'قم', toCity: 'کربلا', method: 3, methodName: 'ترانسفر زمینی اتوبوس VIP اختصاصی', priceAdult: 45, priceChild: 35, priceInfant: 0, isActive: true }, -] - -// Mock Hotels strictly matching user specifications (56 hotels) -const MOCK_HOTELS: CustomTripHotelItem[] = [ - // --- مشهد مقدس (۱۱ هتل) --- - { - id: 'hotel_mashhad_qasr_al_diyafa_quds', - name: 'هتل قصر الضیافه (القدس)', - cityName: 'مشهد مقدس', - stars: 5, - score: 4.9, - distanceToShrine: 'واقع در صحن هدایت حرم مطهر امام رضا (ع)', - distanceToShrineKm: 0.05, - amenities: ['اینترنت رایگان وای‌فای', 'رستوران مجلل بوفه باز', 'نزدیک‌ترین هتل به حرم', 'لاندری و خدمات رفاهی'], - description: 'تنها هتل واقع در صحن آستان قدس رضوی با دسترسی مستقیم و بی‌نظیر به بارگاه منور امام رضا (ع).', - pricePerDayAdult: 110, - pricePerDayChild: 55, - isActive: true, - }, - { - id: 'hotel_mashhad_qasr_al_diyafa_noor', - name: 'هتل قصر الضیافه (النور)', - cityName: 'مشهد مقدس', - stars: 5, - score: 4.9, - distanceToShrine: 'ضلع شمالی حرم مطهر رضوی', - distanceToShrineKm: 0.1, - amenities: ['وای‌فای پرسرعت', 'رستوران بین‌المللی', 'چشم‌انداز گنبد طلایی', 'صندوق امانات'], - description: 'هتل مجلل و ۵ ستاره با استانداردهای برتر هتلداری در مجاورت حریم قدس رضوی.', - pricePerDayAdult: 105, - pricePerDayChild: 50, - isActive: true, - }, - { - id: 'hotel_mashhad_atrak', - name: 'هتل اترک', - cityName: 'مشهد مقدس', - stars: 4, - score: 4.6, - distanceToShrine: 'فلکه آب (میدان بیت‌المقدس) - ۳۰۰ متر تا باب‌الرضا (ع)', - distanceToShrineKm: 0.3, - amenities: ['رستوران و کافی‌شاپ بام', 'اینترنت رایگان', 'سرویس ایرانی و فرنگی', 'مجموعه ورزشی'], - description: 'یکی از اصیل‌ترین هتل‌های مجاور حرم مطهر با دید فوق‌العاده به گنبد و بارگاه.', - pricePerDayAdult: 75, - pricePerDayChild: 35, - isActive: true, - }, - { - id: 'hotel_mashhad_ziyafat_al_zahra', - name: 'هتل ضیافت الزهرا', - cityName: 'مشهد مقدس', - stars: 5, - score: 4.8, - distanceToShrine: 'خیابان شیرازی، شارستان رضوی', - distanceToShrineKm: 0.4, - amenities: ['رستوران صفوی', 'مجموعه آبی و اسپا', 'اینترنت پرسرعت', 'اتاق‌های VIP'], - description: 'هتل لوکس ۵ ستاره با طراحی اصیل ایرانی-اسلامی و دسترسی آسان به صحن انقلاب.', - pricePerDayAdult: 95, - pricePerDayChild: 45, - isActive: true, - }, - { - id: 'hotel_mashhad_ziyafat_al_reza', - name: 'هتل ضیافت الرضا', - cityName: 'مشهد مقدس', - stars: 5, - score: 4.8, - distanceToShrine: 'خیابان طبرسی، نزدیک به حرم مطهر', - distanceToShrineKm: 0.4, - amenities: ['بوفه باز مجلل', 'اینترنت وای‌فای', 'ترانسفر فرودگاهی', 'خدمات ویژه زائرین'], - description: 'هتل نوساز و ۵ ستاره با بهترین امکانات اقامتی برای خانواده‌های محترم زائر.', - pricePerDayAdult: 95, - pricePerDayChild: 45, - isActive: true, - }, - { - id: 'hotel_mashhad_boshra', - name: 'هتل بشری', - cityName: 'مشهد مقدس', - stars: 4, - score: 4.6, - distanceToShrine: 'سرشور، روبروی باب الجواد (ع) - ۱۰۰ متر', - distanceToShrineKm: 0.1, - amenities: ['رستوران ایرانی رضوی', 'اینترنت رایگان', 'آسانسور سریع', 'خدمات لاندری'], - description: 'هتل آپارتمان ۴ ستاره تاپ با فاصله بسیار اندک تا ورودی باب الجواد (ع).', - pricePerDayAdult: 70, - pricePerDayChild: 35, - isActive: true, - }, - { - id: 'hotel_mashhad_darvishi', - name: 'هتل مجلل درویشی', - cityName: 'مشهد مقدس', - stars: 5, - score: 4.9, - distanceToShrine: 'خیابان امام رضا (ع) - ترانسفر رفت و برگشت منظم', - distanceToShrineKm: 1.4, - amenities: ['مجموعه آبی مجلل', 'رستوران آتریوم و برکه', 'خدمات CIP فرودگاهی', 'اینترنت پرسرعت'], - description: 'تنها هتل آتریوم شرق کشور با معماری باشکوه مرتفع و بالاترین سطح پذیرایی.', - pricePerDayAdult: 120, - pricePerDayChild: 60, - isActive: true, - }, - { - id: 'hotel_mashhad_almas_2', - name: 'هتل الماس ۲', - cityName: 'مشهد مقدس', - stars: 5, - score: 4.8, - distanceToShrine: 'خیابان امام رضا (ع) - نبش امام رضا ۲۰', - distanceToShrineKm: 1.2, - amenities: ['مجموعه آبی و استخر', 'رستوران گردان ۳۶۰ درجه', 'کافی‌شاپ ۲۴ ساعته', 'ترانسفر حرم'], - description: 'هتل ۵ ستاره با معماری تمدن‌های ملل مختلف و کیفیت پذیرایی ممتاز بین‌المللی.', - pricePerDayAdult: 100, - pricePerDayChild: 50, - isActive: true, - }, - { - id: 'hotel_mashhad_negin_pasargad', - name: 'هتل نگین پاسارگاد', - cityName: 'مشهد مقدس', - stars: 4, - score: 4.5, - distanceToShrine: 'خیابان شیرازی ۴ - دسترسی آسان به صحن غدیر', - distanceToShrineKm: 0.5, - amenities: ['مجموعه سونا و استخر', 'رستوران بوفه', 'اینترنت رایگان', 'صندوق امانات'], - description: 'هتل ۴ ستاره مجهز و تمیز با محیطی آرام در مجاورت حرم مطهر.', - pricePerDayAdult: 65, - pricePerDayChild: 30, - isActive: true, - }, - { - id: 'hotel_mashhad_jahan', - name: 'هتل جهان', - cityName: 'مشهد مقدس', - stars: 5, - score: 4.7, - distanceToShrine: 'خیابان شیرازی، نبش شیرازی ۲ - چسبیده به حرم', - distanceToShrineKm: 0.2, - amenities: ['رستوران‌های متعدد', 'کافی‌شاپ روباز', 'سالن ورزشی', 'ترانسفر فرودگاهی'], - description: 'هتل ۵ ستاره بین‌المللی در فاصله بسیار کوتاه از ورودی‌های اصلی حرم مطهر.', - pricePerDayAdult: 90, - pricePerDayChild: 45, - isActive: true, - }, - { - id: 'hotel_mashhad_sepehr_arad', - name: 'هتل سپهر آراد', - cityName: 'مشهد مقدس', - stars: 4, - score: 4.4, - distanceToShrine: 'خیابان خسروی نو - نزدیک به باب الجواد', - distanceToShrineKm: 0.6, - amenities: ['رستوران سنتی و ایرانی', 'اینترنت در لابی و اتاق', 'خدمات نظافت روزانه'], - description: 'هتل ۴ ستاره نوساز با دسترسی آسان به بازار رضا و صحن جامع رضوی.', - pricePerDayAdult: 60, - pricePerDayChild: 30, - isActive: true, - }, - - // --- قم مقدسه (۶ هتل) --- - { - id: 'hotel_qom_fatima', - name: 'هتل فاطیما قم', - cityName: 'قم مقدسه', - stars: 3, - score: 4.3, - distanceToShrine: 'خیابان ارم - ۳۰۰ متر تا حرم مطهر حضرت معصومه (س)', - distanceToShrineKm: 0.3, - amenities: ['رستوران ایرانی', 'اینترنت وای‌فای', 'آسانسور', 'سرویس بهداشتی ایرانی و فرنگی'], - description: 'هتل ۳ ستاره شیک با دسترسی سریع پیاده به حرم مطهر حضرت فاطمه معصومه (س).', - pricePerDayAdult: 45, - pricePerDayChild: 25, - isActive: true, - }, - { - id: 'hotel_qom_khorshid', - name: 'هتل خورشید قم', - cityName: 'قم مقدسه', - stars: 4, - score: 4.7, - distanceToShrine: 'میدان آستانه - روبروی صحن جوادالائمه (ع)', - distanceToShrineKm: 0.1, - amenities: ['رستوران با چشم‌انداز گنبد مطهر', 'پارکینگ اختصاصی', 'اینترنت پرسرعت', 'کافی‌شاپ'], - description: 'نزدیک‌ترین هتل ۴ ستاره به بارگاه ملکوتی حضرت معصومه (س) با ویوی مستقیم گنبد.', - pricePerDayAdult: 65, - pricePerDayChild: 35, - isActive: true, - }, - { - id: 'hotel_qom_al_nabi', - name: 'هتل النبی', - cityName: 'قم مقدسه', - stars: 4, - score: 4.6, - distanceToShrine: 'بلوار بهار - ۲۵۰ متر تا حرم مطهر', - distanceToShrineKm: 0.25, - amenities: ['بوفه ایرانی و عربی', 'اینترنت رایگان', 'صندوق امانات', 'خدمات لاندری'], - description: 'هتل ۴ ستاره محبوب زائرین کشورهای حاشیه خلیج فارس با اتاق‌های بزرگ و دلباز.', - pricePerDayAdult: 60, - pricePerDayChild: 30, - isActive: true, - }, - { - id: 'hotel_qom_mahtab', - name: 'هتل مهتاب', - cityName: 'قم مقدسه', - stars: 2, - score: 4.0, - distanceToShrine: 'کیلومتر ۲ اتوبان قم - تهران', - distanceToShrineKm: 4.0, - amenities: ['رستوران سنتی و فضای سبز', 'پارکینگ وسیع', 'سوپرمارکت', 'کافی‌شاپ'], - description: 'مجتمع اقامتی اقتصادی ۲ ستاره با محیطی وسیع و امکانات مناسب استراحت بین راهی و زیارتی.', - pricePerDayAdult: 35, - pricePerDayChild: 20, - isActive: true, - }, - { - id: 'hotel_qom_sadeghieh', - name: 'هتل صادقیه', - cityName: 'قم مقدسه', - stars: 3, - score: 4.4, - distanceToShrine: 'خیابان بلوار بهار - نزدیک به صحن حضرت معصومه (س)', - distanceToShrineKm: 0.35, - amenities: ['رستوران ایرانی', 'اینترنت رایگان', 'سیستم تهویه مطبوع', 'آسانسور'], - description: 'هتل ۳ ستاره با سابقه و تمیز در موقعیت مکانی بسیار مناسب برای پیاده‌روی زائرین.', - pricePerDayAdult: 50, - pricePerDayChild: 25, - isActive: true, - }, - { - id: 'hotel_qom_karimeh', - name: 'هتل کریمه قم', - cityName: 'قم مقدسه', - stars: 4, - score: 4.6, - distanceToShrine: 'میدان مطهری - ابتدای پل آهنچی (۱۰۰ متر تا حرم)', - distanceToShrineKm: 0.1, - amenities: ['رستوران سنتی و مدرن', 'وای‌فای رایگان', 'پارکینگ اختصاصی', 'خدمات تاکسی‌سرویس'], - description: 'هتل ۴ ستاره لوکس متعلق به هتل‌های پارسیان با فاصله چندقدمی از حرم مطهر.', - pricePerDayAdult: 65, - pricePerDayChild: 35, - isActive: true, - }, - - // --- تهران (۳ هتل) --- - { - id: 'hotel_tehran_espinas_boulevard', - name: 'هتل اسپیناس بلوار', - cityName: 'تهران', - stars: 5, - score: 4.8, - distanceToShrine: 'بلوار کشاورز، مرکز تهران', - distanceToShrineKm: 10.0, - amenities: ['رستوران بین‌المللی بلوار', 'مجموعه ورزشی و اسپا', 'اینترنت فیبر نوری', 'کافی‌شاپ آتریوم'], - description: 'هتل مجلل ۵ ستاره بین‌المللی در قلب پایتخت با امکانات تشریفاتی کامل.', - pricePerDayAdult: 125, - pricePerDayChild: 60, - isActive: true, - }, - { - id: 'hotel_tehran_espinas_palace', - name: 'هتل اسپیناس پالاس', - cityName: 'تهران', - stars: 5, - score: 4.9, - distanceToShrine: 'سعادت‌آباد، میدان بهرود', - distanceToShrineKm: 15.0, - amenities: ['چندین رستوران ملل و سنتی', 'سالن همایش‌های بین‌المللی', 'هلی‌پد', 'مجموعه آبی و ماساژ لوکس'], - description: 'کاخ باشکوه ۵ ستاره بر فراز تهران با چشم‌انداز ۳۶۰ درجه و خدمات فوق‌العاده VIP.', - pricePerDayAdult: 150, - pricePerDayChild: 75, - isActive: true, - }, - { - id: 'hotel_tehran_parsian_azadi', - name: 'هتل پارسیان آزادی', - cityName: 'تهران', - stars: 5, - score: 4.7, - distanceToShrine: 'تقاطع بزرگراه چمران و یادگار امام', - distanceToShrineKm: 12.0, - amenities: ['رستوران پانوراما ۲۶', 'استخر و سالن بدنسازی', 'خدمات CIP فرودگاهی', 'اینترنت پرسرعت'], - description: 'از اصیل‌ترین هتل‌های ۵ ستاره تهران با چشم‌انداز خیره‌کننده رشته کوه البرز.', - pricePerDayAdult: 110, - pricePerDayChild: 55, - isActive: true, - }, - - // --- کربلای معلی (۱۱ هتل) --- - { - id: 'hotel_karbala_naeim_al_wareth', - name: 'هتل نعیم الوارث', - cityName: 'کربلای معلی', - stars: 4, - score: 4.6, - distanceToShrine: 'خیابان میثم تمار - نزدیک به حرم مطهر حضرت عباس (ع)', - distanceToShrineKm: 0.35, - amenities: ['اینترنت وای‌فای', 'رستوران ایرانی و عراقی', 'ترانسفر فرودگاهی', 'خدمات ویلچر'], - description: 'هتل باکیفیت و نزدیک به حرم متعلق به عتبه مقدسه حسینی با استانداردهای بالای نظافت.', - pricePerDayAdult: 80, - pricePerDayChild: 40, - isActive: true, - }, - { - id: 'hotel_karbala_al_tayf', - name: 'هتل الطیف', - cityName: 'کربلای معلی', - stars: 4, - score: 4.5, - distanceToShrine: 'باب طویریج - ۵۰۰ متر تا حرم امام حسین (ع)', - distanceToShrineKm: 0.5, - amenities: ['وای‌فای رایگان', 'بوفه باز', 'آسانسورهای مدرن', 'لاندری'], - description: 'اقامتگاه ۴ ستاره تمیز و مناسب کاروان‌ها و خانواده‌های زائر.', - pricePerDayAdult: 75, - pricePerDayChild: 35, - isActive: true, - }, - { - id: 'hotel_karbala_vatin', - name: 'هتل وتین کربلا', - cityName: 'کربلای معلی', - stars: 4, - score: 4.6, - distanceToShrine: 'شارع الشهداء - ۴۰۰ متر تا صحن عقیله بنی‌هاشم', - distanceToShrineKm: 0.4, - amenities: ['آشپز ایرانی', 'اینترنت پرسرعت', 'صندوق امانات', 'اتاق‌های خانوادگی'], - description: 'هتل نوساز با دسترسی آسان به تل زینبیه و صحن مطهر حسینی.', - pricePerDayAdult: 75, - pricePerDayChild: 35, - isActive: true, - }, - { - id: 'hotel_karbala_qasr_karbala', - name: 'هتل قصر کربلا', - cityName: 'کربلای معلی', - stars: 3, - score: 4.3, - distanceToShrine: 'خیابان قبله الحسین (ع) - دسترسی پیاده آسان', - distanceToShrineKm: 0.6, - amenities: ['رستوران', 'اینترنت در لابی', 'سرویس ایرانی و فرنگی'], - description: 'هتل ۳ ستاره اقتصادی و تمیز با دسترسی مستقیم به باب القبله.', - pricePerDayAdult: 55, - pricePerDayChild: 25, - isActive: true, - }, - { - id: 'hotel_karbala_al_wareth', - name: 'هتل الوارث', - cityName: 'کربلای معلی', - stars: 4, - score: 4.6, - distanceToShrine: 'خیابان قزوینیه - نزدیک به بین الحرمین', - distanceToShrineKm: 0.3, - amenities: ['وای‌فای پرسرعت', 'صبحانه و شام بوفه', 'خدمات نظافت روزانه'], - description: 'هتل معروف و معتبر در مجاورت حریم شریف با محیطی معنوی و آرام.', - pricePerDayAdult: 80, - pricePerDayChild: 40, - isActive: true, - }, - { - id: 'hotel_karbala_ataa_al_wareth', - name: 'هتل عطاء الوارث', - cityName: 'کربلای معلی', - stars: 4, - score: 4.6, - distanceToShrine: 'شارع میثم تمار - ۴۰۰ متر تا حرم مطهر', - distanceToShrineKm: 0.4, - amenities: ['رستوران مجلل', 'اینترنت رایگان', 'خدمات لاندری', 'پزشک مقیم'], - description: 'هتل نوساز عتبه حسینیه با تجهیزات مدرن هتلداری و خدمات کامل پذیرایی.', - pricePerDayAdult: 85, - pricePerDayChild: 40, - isActive: true, - }, - { - id: 'hotel_karbala_ard_al_yass', - name: 'هتل أرض الیاس', - cityName: 'کربلای معلی', - stars: 4, - score: 4.5, - distanceToShrine: 'شارع السدره - ۳۵۰ متر تا حرم امام حسین (ع)', - distanceToShrineKm: 0.35, - amenities: ['اینترنت در تمام اتاق‌ها', 'رستوران ایرانی', 'آسانسور سریع'], - description: 'موقعیت مکانی عالی برای زائرینی که مایل به پیاده‌روی سریع به حرم مطهر هستند.', - pricePerDayAdult: 70, - pricePerDayChild: 35, - isActive: true, - }, - { - id: 'hotel_karbala_royal_karbala', - name: 'هتل رویال کربلا', - cityName: 'کربلای معلی', - stars: 5, - score: 4.8, - distanceToShrine: 'خیابان باب بغداد - نزدیک به حرم حضرت عباس (ع)', - distanceToShrineKm: 0.3, - amenities: ['رستوران بوفه باز بین‌المللی', 'سوئیت‌های لوکس VIP', 'کافی‌شاپ ۲۴ ساعته', 'اینترنت فیبر نوری'], - description: 'هتل ۵ ستاره شیک و مجلل با اتاق‌های بزرگ و دکوراسیون عالی اسلامی.', - pricePerDayAdult: 110, - pricePerDayChild: 55, - isActive: true, - }, - { - id: 'hotel_karbala_almas_karbala', - name: 'هتل الماس کربلا', - cityName: 'کربلای معلی', - stars: 4, - score: 4.5, - distanceToShrine: 'شارع جمهوری - ۵۰۰ متر تا حرم مطهر', - distanceToShrineKm: 0.5, - amenities: ['وای‌فای رایگان', 'غذای ایرانی باکیفیت', 'لاندری'], - description: 'هتل ۴ ستاره دلنشین با سابقه درخشان در جلب رضایت کاروان‌های زیارتی.', - pricePerDayAdult: 75, - pricePerDayChild: 35, - isActive: true, - }, - { - id: 'hotel_karbala_hayat_3', - name: 'هتل حیات ۳', - cityName: 'کربلای معلی', - stars: 3, - score: 4.2, - distanceToShrine: 'شارع میثم تمار - ۶۰۰ متر تا حرم', - distanceToShrineKm: 0.6, - amenities: ['اینترنت در لابی', 'رستوران سنتی', 'سرویس ایرانی'], - description: 'هتل اقتصادی ۳ ستاره با خدمات مناسب برای زائرین گرامی.', - pricePerDayAdult: 50, - pricePerDayChild: 25, - isActive: true, - }, - { - id: 'hotel_karbala_tayba_karbala', - name: 'هتل طیبه کربلا', - cityName: 'کربلای معلی', - stars: 4, - score: 4.5, - distanceToShrine: 'شارع الاسکان - ۴۵۰ متر تا حرم مطهر حسینی', - distanceToShrineKm: 0.45, - amenities: ['صبحانه بوفه', 'اینترنت رایگان', 'آسانسورهای سریع', 'صندوق امانات'], - description: 'هتل ۴ ستاره مجهز و تمیز با محیطی آرام برای استراحت بعد از زیارت.', - pricePerDayAdult: 70, - pricePerDayChild: 35, - isActive: true, - }, - - // --- نجف اشرف (۶ هتل) --- - { - id: 'hotel_najaf_najaf_do', - name: 'هتل نجف دو', - cityName: 'نجف اشرف', - stars: 5, - score: 4.8, - distanceToShrine: 'شارع الرسول - ۲۰۰ متر تا صحن حضرت فاطمه زهرا (س)', - distanceToShrineKm: 0.2, - amenities: ['اینترنت رایگان', 'رستوران ایرانی و عربی', 'خدمات تشریفات', 'آسانسور سریع'], - description: 'هتل ۵ ستاره با امکانات مدرن و موقعیت استثنایی نزدیک به بارگاه علوی.', - pricePerDayAdult: 90, - pricePerDayChild: 45, - isActive: true, - }, - { - id: 'hotel_najaf_jar_al_amir', - name: 'هتل جار الامیر', - cityName: 'نجف اشرف', - stars: 5, - score: 4.8, - distanceToShrine: 'شارع زین‌العابدین (ع) - ۱۵۰ متر تا حرم مطهر', - distanceToShrineKm: 0.15, - amenities: ['چشم‌انداز گنبد علوی', 'بوفه باز مجلل', 'وای‌فای پرسرعت', 'صندوق امانات'], - description: 'در جوار امیرالمؤمنین (ع)، اقامتی خاطره‌انگیز و ۵ ستاره با دید مستقیم به حرم.', - pricePerDayAdult: 95, - pricePerDayChild: 45, - isActive: true, - }, - { - id: 'hotel_najaf_qasr_al_dur', - name: 'هتل قصر الدر', - cityName: 'نجف اشرف', - stars: 5, - score: 4.9, - distanceToShrine: 'روبروی باب السلام حرم امیرالمؤمنین (ع) - ۱۰۰ متر', - distanceToShrineKm: 0.1, - amenities: ['وای‌فای رایگان', 'رستوران بوفه', 'لابی باشکوه', 'سرویس چای ۲۴ ساعته'], - description: 'مجلل‌ترین هتل ۵ ستاره شهر نجف با موقعیت فوق‌العاده برای پیاده‌روی سالمندان.', - pricePerDayAdult: 105, - pricePerDayChild: 50, - isActive: true, - }, - { - id: 'hotel_najaf_naba_al_kowsar', - name: 'هتل نبع الکوثر', - cityName: 'نجف اشرف', - stars: 5, - score: 4.8, - distanceToShrine: 'شارع الصادق (ع) - ۲۵۰ متر تا حرم علوی', - distanceToShrineKm: 0.25, - amenities: ['رستوران بین‌المللی', 'اینترنت پرسرعت', 'خدمات لاندری', 'اتاق‌های VIP'], - description: 'هتل نوساز ۵ ستاره با استانداردهای عالی پذیرایی و نزدیکی به بازار بزرگ نجف.', - pricePerDayAdult: 90, - pricePerDayChild: 45, - isActive: true, - }, - { - id: 'hotel_najaf_nasim_al_najaf', - name: 'هتل نسیم النجف', - cityName: 'نجف اشرف', - stars: 5, - score: 4.7, - distanceToShrine: 'شارع مدینه - ۳۰۰ متر تا حرم مطهر', - distanceToShrineKm: 0.3, - amenities: ['اینترنت در تمام اتاق‌ها', 'آشپز ایرانی', 'آسانسورهای مدرن'], - description: 'هتل ۵ ستاره با فضای آرام، غذاهای متنوع و خدمات درجه یک به زائرین.', - pricePerDayAdult: 85, - pricePerDayChild: 40, - isActive: true, - }, - { - id: 'hotel_najaf_zamzam', - name: 'هتل زمزم', - cityName: 'نجف اشرف', - stars: 5, - score: 4.7, - distanceToShrine: 'شارع بنات الحسن - ۲۵۰ متر تا صحن مطهر', - distanceToShrineKm: 0.25, - amenities: ['بوفه باز ایرانی', 'اینترنت وای‌فای', 'ترانسفر فرودگاهی', 'صندوق امانات'], - description: 'هتل ۵ ستاره آرام و دلپذیر با سرویس‌دهی ممتاز در چند قدمی حرم امام علی (ع).', - pricePerDayAdult: 85, - pricePerDayChild: 40, - isActive: true, - }, - - // --- کاظمین مقدسه (۳ هتل) --- - { - id: 'hotel_kadhimiya_burj_al_jawad', - name: 'هتل برج الجواد', - cityName: 'کاظمین مقدسه', - stars: 4, - score: 4.6, - distanceToShrine: 'شارع باب المراد - ۱۵۰ متر تا حرم امامین جوادین (ع)', - distanceToShrineKm: 0.15, - amenities: ['اینترنت پرسرعت', 'رستوران ایرانی و عراقی', 'آسانسور سریع', 'خدمات لاندری'], - description: 'هتل ۴ ستاره تاپ در فاصله بسیار نزدیک به آستان مقدس امامین جوادین (ع).', - pricePerDayAdult: 75, - pricePerDayChild: 35, - isActive: true, - }, - { - id: 'hotel_kadhimiya_al_aqila', - name: 'هتل العقیله', - cityName: 'کاظمین مقدسه', - stars: 4, - score: 4.6, - distanceToShrine: 'شارع صاحب الزمان (عج) - ۲۰۰ متر تا حرم مطهر', - distanceToShrineKm: 0.2, - amenities: ['وای‌فای رایگان', 'بوفه باز', 'سیستم تهویه مطبوع', 'صندوق امانات'], - description: 'هتل نوساز با نام مبارک عقیله، محیطی پاکیزه و دسترسی پیاده آسان به حرم.', - pricePerDayAdult: 75, - pricePerDayChild: 35, - isActive: true, - }, - { - id: 'hotel_kadhimiya_al_khafaji', - name: 'هتل الخفاجی', - cityName: 'کاظمین مقدسه', - stars: 4, - score: 4.5, - distanceToShrine: 'شارع الشریف الرضی - ۲۵۰ متر تا حرم مطهر', - distanceToShrineKm: 0.25, - amenities: ['رستوران', 'اینترنت رایگان', 'خدمات نظافت روزانه', 'سرویس ایرانی و فرنگی'], - description: 'هتل ۴ ستاره معروف و محبوب برای زائرین با پذیرایی عالی.', - pricePerDayAdult: 70, - pricePerDayChild: 35, - isActive: true, - }, - - // --- سامرا (۱ هتل) --- - { - id: 'hotel_samarra_ayatollah_boroujerdi', - name: 'هتل و حسینیه آیت‌الله بروجردی', - cityName: 'سامرا', - stars: 4, - score: 4.6, - distanceToShrine: 'مجاورت مستقیم حرم مطهر امامین عسکریین (ع) و سرداب مقدس', - distanceToShrineKm: 0.2, - amenities: ['تدابیر امنیتی کامل', 'رستوران ایرانی', 'نمازخانه وسیع', 'خدمات پزشکی مقیم'], - description: 'مجهزترین و مطمئن‌ترین مجتمع اقامتی و حسینیه در مجاورت آستان مقدس عسکریین (ع).', - pricePerDayAdult: 65, - pricePerDayChild: 30, - isActive: true, - }, - - // --- مدینه منوره (۶ هتل) --- - { - id: 'hotel_medina_pullman_zamzam', - name: 'هتل پولمن زمزم مدینه', - cityName: 'مدینه منوره', - stars: 5, - score: 4.9, - distanceToShrine: '۱۵۰ متر تا باب السلام مسجد النبی (ص)', - distanceToShrineKm: 0.15, - amenities: ['وای‌فای پرسرعت', 'رستوران‌های متعدد بین‌المللی', 'خدمات VIP', 'پارکینگ اختصاصی'], - description: 'هتل بین‌المللی ۵ ستاره زنجیره‌ای پولمن با استانداردهای لوکس در مجاورت حرم نبوی.', - pricePerDayAdult: 160, - pricePerDayChild: 80, - isActive: true, - }, - { - id: 'hotel_medina_shaza_al_madina', - name: 'هتل شذا المدینه', - cityName: 'مدینه منوره', - stars: 5, - score: 4.9, - distanceToShrine: 'درب شماره ۱۲ حرم مطهر نبوی - ۱۰۰ متر', - distanceToShrineKm: 0.1, - amenities: ['طراحی مجلل شرقی', 'رستوران مضیف', 'سالن لانژ اختصاصی', 'اینترنت نامحدود'], - description: 'یکی از زیباترین هتل‌های ۵ ستاره مدینه با معماری اصیل اسلامی و پذیرایی شاهانه.', - pricePerDayAdult: 170, - pricePerDayChild: 85, - isActive: true, - }, - { - id: 'hotel_medina_ankira', - name: 'هتل انکیرا', - cityName: 'مدینه منوره', - stars: 5, - score: 4.8, - distanceToShrine: 'محوطه شمالی مسجد النبی (ص) - ۲۰۰ متر', - distanceToShrineKm: 0.2, - amenities: ['بوفه باز شرقی و غربی', 'اینترنت رایگان', 'آسانسورهای سریع', 'لاندری'], - description: 'هتل ۵ ستاره با امکانات رفاهی درجه یک و دسترسی آسان به روضه شریفه.', - pricePerDayAdult: 140, - pricePerDayChild: 70, - isActive: true, - }, - { - id: 'hotel_medina_crowne_plaza', - name: 'هتل کراون پلازا مدینه', - cityName: 'مدینه منوره', - stars: 5, - score: 4.8, - distanceToShrine: 'منطقه مرکزی جنوبی - روبروی روضه منوره', - distanceToShrineKm: 0.2, - amenities: ['رستوران الروضه', 'اینترنت فیبر نوری', 'خدمات تشریفات فرودگاهی', 'صندوق دیجیتال'], - description: 'هتل مجلل زنجیره‌ای کراون پلازا با اتاق‌های مشرف به گنبد خضراء و مسجد النبی.', - pricePerDayAdult: 150, - pricePerDayChild: 75, - isActive: true, - }, - { - id: 'hotel_medina_maden_tayba', - name: 'هتل مادن طیبه', - cityName: 'مدینه منوره', - stars: 4, - score: 4.6, - distanceToShrine: 'شارع السلام - ۲۵۰ متر تا مسجد النبی', - distanceToShrineKm: 0.25, - amenities: ['رستوران ایرانی و عربی', 'اینترنت پرسرعت', 'سیستم تهویه هوشمند'], - description: 'هتل ۴ ستاره تاپ با اتاق‌های تمیز و خدمات شبانه‌روزی برای زائرین روضه نبوی.', - pricePerDayAdult: 110, - pricePerDayChild: 55, - isActive: true, - }, - { - id: 'hotel_medina_hilton_medina', - name: 'هتل هیلتون مدینه', - cityName: 'مدینه منوره', - stars: 5, - score: 4.9, - distanceToShrine: 'چسبیده به صحن مسجد النبی (ص) - چند قدم تا ورودی اصلی', - distanceToShrineKm: 0.05, - amenities: ['رستوران مدینه و کافه کنکور', 'ترانسفر VIP', 'دید کامل به مسجد النبی', 'خدمات مستخدم شخصی'], - description: 'برترین و شناخته‌شده‌ترین هتل ۵ ستاره مدینه منوره چسبیده به محوطه شریف نبوی.', - pricePerDayAdult: 180, - pricePerDayChild: 90, - isActive: true, - }, +// Clear any legacy localStorage keys so panel strictly uses PostgreSQL database +if (typeof window !== 'undefined' && window.localStorage) { + const legacyKeys = [ + 'aqila_custom_destinations_v5', + 'aqila_custom_hotels_v5', + 'aqila_custom_relations_v5', + 'aqila_custom_origins_v4', + 'aqila_custom_destinations_v4', + 'aqila_custom_hotels_v4', + 'aqila_custom_relations_v4', + ] + for (const key of legacyKeys) { + try { + window.localStorage.removeItem(key) + } catch {} + } +} - // --- مکه مکرمه (۹ هتل) --- - { - id: 'hotel_makkah_swissotel_al_maqam', - name: 'هتل سوئیس‌المقام مکه (برج‌ها)', - cityName: 'مکه مکرمه', - stars: 5, - score: 4.9, - distanceToShrine: 'مجموعه ابراج البیت - روبروی کعبه مشرفه', - distanceToShrineKm: 0.05, - amenities: ['دید مستقیم به کعبه', 'رستوران البیت بوفه', 'دسترسی اختصاصی به حرم', 'اینترنت پرسرعت'], - description: 'واقع در مجموعه برجهای ساعت ابراج البیت با آسانسورهای اختصاصی به داخل صحن مسجد الحرام.', - pricePerDayAdult: 220, - pricePerDayChild: 110, - isActive: true, - }, - { - id: 'hotel_makkah_swissotel_makkah', - name: 'هتل سوئیس‌اوتل مکه (برج‌ها)', - cityName: 'مکه مکرمه', - stars: 5, - score: 4.9, - distanceToShrine: 'مجموعه ابراج البیت - ورودی مستقیم از خیابان اجیاد', - distanceToShrineKm: 0.05, - amenities: ['رستوران الرواد', 'وای‌فای نامحدود', 'نمازخانه با صوت مستقیم حرم', 'خدمات کانسیرج'], - description: 'هتل لوکس ۵ ستاره در ابراج البیت با پذیرایی بین‌المللی و چشم‌انداز کعبه معظمه.', - pricePerDayAdult: 210, - pricePerDayChild: 105, - isActive: true, - }, - { - id: 'hotel_makkah_movenpick_hajar_tower', - name: 'هتل موونپیک هاجر تاور مکه (برج‌ها)', - cityName: 'مکه مکرمه', - stars: 4, - score: 4.7, - distanceToShrine: 'برج هاجر - مجموعه ابراج البیت', - distanceToShrineKm: 0.05, - amenities: ['رستوران النسیم بوفه', 'اینترنت رایگان', 'مرکز خرید ابراج البیت', 'دسترسی سریع به حرم'], - description: 'هتل ۴ ستاره مجلل در برج هاجر با خدمات کامل پذیرایی و دسترسی فوق‌العاده به مسجد الحرام.', - pricePerDayAdult: 160, - pricePerDayChild: 80, - isActive: true, - }, - { - id: 'hotel_makkah_hyatt_regency', - name: 'هتل حیات ریجنسی مکه', - cityName: 'مکه مکرمه', - stars: 5, - score: 4.9, - distanceToShrine: 'جبل عمر - یک دقیقه پیاده‌روی تا مسجد الحرام', - distanceToShrineKm: 0.1, - amenities: ['رستوران الواحه', 'سالن بدنسازی مجهز', 'اینترنت فیبر نوری', 'اتاق‌های رو به حرم'], - description: 'هتل ۵ ستاره مجلل در پروژه جبل عمر با طراحی مدرن و دسترسی سریع به صحن مسجد الحرام.', - pricePerDayAdult: 200, - pricePerDayChild: 100, - isActive: true, - }, - { - id: 'hotel_makkah_conrad_jabal_omar', - name: 'هتل کنراد جبل عمر مکه', - cityName: 'مکه مکرمه', - stars: 5, - score: 4.9, - distanceToShrine: 'پروژه جبل عمر - ۲۰۰ متر تا درب ملک فهد', - distanceToShrineKm: 0.15, - amenities: ['رستوران المعراج و الفرسان', 'خدمات VIP اختصاصی', 'اینترنت پرسرعت', 'دید مستقیم کعبه'], - description: 'تجربه بی‌نظیر اقامت فوق‌لوکس هتل‌های کنراد با خدمات هیلتون در جوار بیت الله الحرام.', - pricePerDayAdult: 230, - pricePerDayChild: 115, - isActive: true, - }, - { - id: 'hotel_makkah_hilton_suites', - name: 'هتل هیلتون سوئیتس مکه', - cityName: 'مکه مکرمه', - stars: 5, - score: 4.9, - distanceToShrine: 'جبل عمر - خیابان ابراهیم الخلیل', - distanceToShrineKm: 0.15, - amenities: ['سوئیت‌های خانوادگی رو به کعبه', 'چندین رستوران بین‌المللی', 'آسانسورهای سریع', 'اینترنت'], - description: 'سوئیت‌های خانوادگی لوکس با تمام امکانات رفاهی و ویوی فوق‌العاده به حریم کعبه.', - pricePerDayAdult: 220, - pricePerDayChild: 110, - isActive: true, - }, - { - id: 'hotel_makkah_hilton_convention', - name: 'هتل هیلتون کانونشن مکه', - cityName: 'مکه مکرمه', - stars: 5, - score: 4.8, - distanceToShrine: 'جبل عمر - ۳۰۰ متر تا صحن مسجد الحرام', - distanceToShrineKm: 0.3, - amenities: ['سالن همایش‌های بین‌المللی', 'رستوران‌های متعدد', 'کافی‌شاپ ۲۴ ساعته', 'ترانسفر اختصاصی'], - description: 'هتل ۵ ستاره بزرگ و باشکوه با معماری چشم‌نواز و خدمات عالی هتلداری.', - pricePerDayAdult: 190, - pricePerDayChild: 95, - isActive: true, - }, - { - id: 'hotel_makkah_address_jabal_omar', - name: 'هتل آدرس جبل عمر', - cityName: 'مکه مکرمه', - stars: 5, - score: 4.9, - distanceToShrine: 'بلندترین برج پروژه جبل عمر - متصل به پل هوایی حرم', - distanceToShrineKm: 0.1, - amenities: ['مرتفع‌ترین نمازخانه معلق جهان رو به کعبه', 'اسپا و مرکز سلامتی', 'رستوران‌های لوکس ملل', 'خدمات VIP باتلر'], - description: 'هتل ۵ ستاره مجلل آدرس (Address Hotels) با بلندترین نمازخانه معلق مشرف به کعبه مشرفه.', - pricePerDayAdult: 240, - pricePerDayChild: 120, - isActive: true, - }, - { - id: 'hotel_makkah_holiday_inn', - name: 'هتل هالیدی این مکه', - cityName: 'مکه مکرمه', - stars: 5, - score: 4.8, - distanceToShrine: 'منطقه العزیزیه مکه - ترانسفر شبانه‌روزی رایگان هر ۵ دقیقه', - distanceToShrineKm: 3.5, - amenities: ['ترانسفر رایگان شبانه‌روزی به حرم', 'مجموعه استخر و اسپا', 'رستوران‌های بوفه باز', 'اینترنت پرسرعت'], - description: 'هتل ۵ ستاره مدرن و بسیار وسیع در العزیزیه با شاتل اتوبوسی سریع و رایگان به مسجد الحرام.', - pricePerDayAdult: 150, - pricePerDayChild: 75, - isActive: true, - }, -] +function cleanLegacyDetail(rawDetail: any): string { + if (!rawDetail || typeof rawDetail !== 'string') return '' + const s = rawDetail.trim() + if (s.startsWith('{') && (s.includes('beginTrip') || s.includes('number_passenger'))) { + return '' + } + return s +} +function mapBackendOrderToPanel(raw: any): CustomTripOrderItem { + const stays = Array.isArray(raw.stays) ? raw.stays : [] + const rawLegs = Array.isArray(raw.legs) ? raw.legs : [] + const passengers = Array.isArray(raw.passengers) ? raw.passengers : [] + + const legs = rawLegs.map((leg: any, idx: number) => { + const matchingStay = stays.find((s: any) => s.city === leg.to_city) || stays[idx] + return { + fromCity: leg.from_city || '', + toCity: leg.to_city || '', + transportMethod: leg.transport_method_display || leg.transport_name_snapshot || leg.transport_method || '', + hotelName: matchingStay?.hotel_name_snapshot || '', + stayNights: matchingStay?.nights || 0, + mealPlan: matchingStay?.meal_plan || '', + } + }) + + const adultsCount = passengers.filter((p: any) => p.passenger_type === 'ADULT').length || raw.passenger_count || 1 + const childrenCount = passengers.filter((p: any) => p.passenger_type === 'CHILD').length || 0 + const infantsCount = passengers.filter((p: any) => p.passenger_type === 'INFANT').length || 0 + + const mappedPassengers = passengers.map((p: any) => ({ + firstName: p.first_name || p.firstName || '', + lastName: p.last_name || p.lastName || '', + passportNumber: p.passport_number || p.passportNumber || '', + phoneNumber: p.phone_number || p.phoneNumber || '', + birthDate: p.birth_date || p.birthDate || '', + passengerType: (p.passenger_type || p.passengerType || 'ADULT') as 'ADULT' | 'CHILD' | 'INFANT', + passportImage: p.passport_image || p.passportImage || null, + })) + + return { + id: Number(raw.id), + user: { + id: raw.user?.id || raw.id, + username: raw.customer_name || raw.user?.username || `user_${raw.id}`, + fullName: raw.customer_name || raw.user?.fullName || raw.user?.fullname || 'زائر گرامی', + phoneNumber: raw.contact_phone || raw.whatsapp_number || raw.user?.phoneNumber || '', + email: raw.user?.email || '', + }, + originCity: raw.origin_city || raw.originCity || '', + destinations: Array.isArray(raw.destinations) ? raw.destinations : [], + legs, + passengers: mappedPassengers, + adultsCount, + childrenCount, + infantsCount, + priceEstimate: Number(raw.final_price ?? raw.price_estimate ?? raw.priceEstimate ?? 0), + status: (raw.status || 'PENDING') as CustomTripOrderStatus, + created: raw.created || new Date().toISOString(), + detail: cleanLegacyDetail(raw.detail), + adminNotes: raw.admin_notes || raw.adminNotes || '', + } +} export async function fetchCustomTripOrders(): Promise { try { - const res = await apiFetch('custom/orders/?view=admin') - if (Array.isArray(res)) return res - if (res?.results && Array.isArray(res.results)) return res.results - return MOCK_ORDERS - } catch { - return MOCK_ORDERS + const res = await apiFetch('trip/custom/v2/admin/orders/?page_size=100') + const list = Array.isArray(res) ? res : Array.isArray(res?.results) ? res.results : [] + return list.map(mapBackendOrderToPanel) + } catch (err) { + console.warn('[CustomTrip API] Could not fetch admin orders from v2 endpoint:', err) + return [] } } @@ -2212,110 +110,113 @@ export async function updateCustomTripOrder( adminNotes?: string } ): Promise { - try { - return await apiFetch(`custom/orders/${orderId}/`, { - method: 'PATCH', - body: JSON.stringify(data), + if (data.status === 'AWAITING_PAYMENT' && data.priceEstimate !== undefined && data.priceEstimate > 0) { + const res = await apiFetch(`trip/custom/v2/admin/orders/${orderId}/quote/`, { + method: 'POST', + body: { + final_price: data.priceEstimate, + admin_notes: data.adminNotes || '', + }, }) - } catch { - const found = MOCK_ORDERS.find((o) => o.id === orderId) - if (found) { - if (data.status) found.status = data.status - if (data.priceEstimate !== undefined) found.priceEstimate = data.priceEstimate - if (data.adminNotes !== undefined) found.adminNotes = data.adminNotes - return { ...found } - } - throw new Error('سفارش یافت نشد.') + return mapBackendOrderToPanel(res) } -} - -const STORAGE_KEYS = { - DESTINATIONS: 'aqila_custom_destinations_v5', - HOTELS: 'aqila_custom_hotels_v5', - RELATIONS: 'aqila_custom_relations_v5', - ORIGINS: 'aqila_custom_origins_v4', -} - -function getStored(key: string, fallback: T): T { - try { - if (typeof window !== 'undefined' && window.localStorage) { - const raw = window.localStorage.getItem(key) - if (raw) return JSON.parse(raw) - } - } catch {} - return fallback -} - -function setStored(key: string, value: T): void { - try { - if (typeof window !== 'undefined' && window.localStorage) { - window.localStorage.setItem(key, JSON.stringify(value)) - } - } catch {} + if (data.status === 'REJECTED') { + const noteText = data.adminNotes || 'رد شده توسط مدیریت' + const res = await apiFetch(`trip/custom/v2/admin/orders/${orderId}/reject/`, { + method: 'POST', + body: { + note: noteText, + reason: noteText, + }, + }) + return mapBackendOrderToPanel(res) + } + const res = await apiFetch(`trip/custom/v2/admin/orders/${orderId}/`, { + method: 'PATCH', + body: { + status: data.status, + final_price: data.priceEstimate, + admin_notes: data.adminNotes, + }, + }) + return mapBackendOrderToPanel(res) } export async function fetchCustomTripOrigins(): Promise { - const stored = getStored(STORAGE_KEYS.ORIGINS, null) - if (!stored || !Array.isArray(stored) || stored.length < 25) { - setStored(STORAGE_KEYS.ORIGINS, MOCK_ORIGINS) - return MOCK_ORIGINS - } - return stored + const remote = await apiFetch('trip/custom/v2/catalog/cities/') + if (!Array.isArray(remote)) return [] + return remote.map((c: any) => ({ + id: c.id ?? c.slug, + name: c.name || c.nameEn || '', + nameEn: c.nameEn || c.name || '', + country: c.country || '', + countryCode: c.countryCode || c.country_code || '', + flag: c.flag || '📍', + isInternational: Boolean(c.isInternational), + isActive: c.isActive !== undefined ? Boolean(c.isActive) : true, + translations: c.translations, + })) } export async function saveCustomTripOrigin(item: CustomTripOriginItem): Promise { - const list = await fetchCustomTripOrigins() - const exists = list.some((o) => o.id === item.id) - const updated = exists ? list.map((o) => (o.id === item.id ? item : o)) : [item, ...list] - setStored(STORAGE_KEYS.ORIGINS, updated) - return item + const payload = { + name: item.nameEn || item.name, + country: item.country, + countryCode: item.countryCode, + has_airport: true, + has_road_access: true, + } + const isNumericId = typeof item.id === 'number' || /^\d+$/.test(String(item.id)) + const res = isNumericId + ? await apiFetch(`trip/custom/v2/catalog/cities/${item.id}/`, { + method: 'PUT', + body: JSON.stringify(payload), + }).catch(() => + apiFetch('trip/custom/v2/catalog/cities/', { + method: 'POST', + body: JSON.stringify(payload), + }) + ) + : await apiFetch('trip/custom/v2/catalog/cities/', { + method: 'POST', + body: JSON.stringify(payload), + }) + + return { + id: res.id ?? item.id, + name: res.name || item.name, + nameEn: res.nameEn || item.nameEn, + country: res.country || item.country, + countryCode: res.countryCode || item.countryCode, + flag: res.flag || item.flag || '📍', + isInternational: res.isInternational !== undefined ? Boolean(res.isInternational) : item.isInternational, + isActive: res.isActive !== undefined ? Boolean(res.isActive) : item.isActive, + translations: res.translations || item.translations, + } } export async function deleteCustomTripOrigin(id: string | number): Promise { - const list = await fetchCustomTripOrigins() - setStored(STORAGE_KEYS.ORIGINS, list.filter((o) => String(o.id) !== String(id))) + await apiFetch(`trip/custom/v2/catalog/cities/${id}/`, { + method: 'DELETE', + }) } export async function fetchCustomTripDestinations(): Promise { - try { - const remote = await apiFetch('trip/custom/v2/catalog/destinations/?include_inactive=true') - if (remote && Array.isArray(remote) && remote.length > 0) { - // If local storage has user-modified values (such as added 1s or custom test names), sync them - const stored = getStored(STORAGE_KEYS.DESTINATIONS, null) - if (stored && Array.isArray(stored)) { - for (const localItem of stored) { - const match = remote.find((r) => String(r.id) === String(localItem.id) || r.name === localItem.name) - if (match && localItem.name && localItem.name !== match.name) { - match.name = localItem.name - apiFetch(`trip/custom/v2/catalog/destinations/${match.id}/`, { - method: 'PATCH', - body: JSON.stringify({ name: localItem.name }), - }).catch(() => {}) - } - } - } - setStored(STORAGE_KEYS.DESTINATIONS, remote) - return remote - } - } catch (err) { - console.warn('[CustomTrip API] Could not fetch destinations from backend, falling back to cache:', err) - } - - const stored = getStored(STORAGE_KEYS.DESTINATIONS, null) - if ( - !stored || - !Array.isArray(stored) || - stored.length < 8 || - stored.some((d) => d.id === 'samarra' && d.name.includes('کاظمین')) || - !stored.some((d) => d.id === 'kadhimiya') - ) { - setStored(STORAGE_KEYS.DESTINATIONS, MOCK_DESTINATIONS) - return MOCK_DESTINATIONS - } - return stored + const remote = await apiFetch( + 'trip/custom/v2/catalog/destinations/?include_inactive=true' + ) + return Array.isArray(remote) ? remote : [] } -export async function fetchCustomTripDestinationById(id: string | number): Promise { +export async function fetchCustomTripDestinationById( + id: string | number +): Promise { + try { + const detail = await apiFetch( + `trip/custom/v2/catalog/destinations/${id}/` + ) + if (detail && detail.id) return detail + } catch {} const list = await fetchCustomTripDestinations() const found = list.find( (d) => @@ -2326,169 +227,227 @@ export async function fetchCustomTripDestinationById(id: string | number): Promi return found || null } -export async function saveCustomTripDestination(item: CustomTripDestinationItem): Promise { - let savedItem = item - try { - const payload = { - id: String(item.id), - city_key: String(item.id), - name: item.name, - nameEn: item.nameEn, - name_en: item.nameEn, - country: item.country, - flag: item.flag, - description: item.description, - holyPlaces: item.holyPlaces, - holy_places: item.holyPlaces, - hotelsCount: item.hotelsCount, - hotels_count: item.hotelsCount, - transportsCount: item.transportsCount, - transports_count: item.transportsCount, - isActive: item.isActive, - is_active: item.isActive, - translations: item.translations, - } - const res = await apiFetch(`trip/custom/v2/catalog/destinations/${item.id}/`, { +export async function saveCustomTripDestination( + item: CustomTripDestinationItem +): Promise { + const payload = { + id: String(item.id), + city_key: String(item.id), + name: item.name, + nameEn: item.nameEn, + name_en: item.nameEn, + country: item.country, + flag: item.flag, + description: item.description, + holyPlaces: item.holyPlaces, + holy_places: item.holyPlaces, + hotelsCount: item.hotelsCount, + hotels_count: item.hotelsCount, + transportsCount: item.transportsCount, + transports_count: item.transportsCount, + isActive: item.isActive, + is_active: item.isActive, + translations: item.translations, + } + const res = await apiFetch( + `trip/custom/v2/catalog/destinations/${item.id}/`, + { method: 'PUT', body: JSON.stringify(payload), - }).catch(async () => { - return await apiFetch('trip/custom/v2/catalog/destinations/', { - method: 'POST', - body: JSON.stringify(payload), - }) - }) - if (res && res.id) { - savedItem = { ...item, ...res } } - } catch (err) { - console.warn('[CustomTrip API] Could not save destination to backend, cached locally:', err) - } - - const list = getStored(STORAGE_KEYS.DESTINATIONS, MOCK_DESTINATIONS) - const exists = list.some((d) => String(d.id) === String(savedItem.id)) - const updated = exists ? list.map((d) => (String(d.id) === String(savedItem.id) ? savedItem : d)) : [savedItem, ...list] - setStored(STORAGE_KEYS.DESTINATIONS, updated) - return savedItem + ).catch(async () => { + return await apiFetch('trip/custom/v2/catalog/destinations/', { + method: 'POST', + body: JSON.stringify(payload), + }) + }) + return { ...item, ...res } } export async function deleteCustomTripDestination(id: string | number): Promise { - try { - await apiFetch(`trip/custom/v2/catalog/destinations/${id}/`, { - method: 'DELETE', - }) - } catch (err) { - console.warn('[CustomTrip API] Could not delete destination on backend:', err) - } - const list = getStored(STORAGE_KEYS.DESTINATIONS, MOCK_DESTINATIONS) - setStored(STORAGE_KEYS.DESTINATIONS, list.filter((d) => String(d.id) !== String(id))) + await apiFetch(`trip/custom/v2/catalog/destinations/${id}/`, { + method: 'DELETE', + }) +} + +export async function fetchCustomTripTransports(): Promise { + const remote = await apiFetch('trip/custom/v2/catalog/transports/') + if (!Array.isArray(remote)) return [] + return remote.map((t: any) => ({ + id: Number(t.id), + fromCity: t.from_city_display || t.from_city_name || '', + toCity: t.to_city_display || t.to_city_name || '', + method: (Number(t.method) || 1) as 1 | 2 | 3, + methodName: t.method_name || (t.method === 1 ? 'پرواز (هوایی)' : t.method === 2 ? 'قطار (ریلی)' : 'زمینی (اتوبوس / ون VIP)'), + priceAdult: Number(t.price) || 0, + priceChild: Number(t.price_child) || 0, + priceInfant: Number(t.price_infant) || 0, + isActive: true, + })) } export async function fetchCustomTripTransportRelations(): Promise { - const stored = getStored(STORAGE_KEYS.RELATIONS, null) - if (!stored || !Array.isArray(stored) || stored.length < 15) { - setStored(STORAGE_KEYS.RELATIONS, MOCK_RELATIONS) - return MOCK_RELATIONS + const remote = await apiFetch('trip/custom/v2/catalog/transports/') + if (!Array.isArray(remote)) return [] + + const grouped = new Map() + for (const t of remote) { + const fromCity = t.from_city_display || t.from_city_name || '' + const toCity = t.to_city_display || t.to_city_name || '' + const key = `${t.from_city_id}_${t.to_city_id}` + const existing = grouped.get(key) + const method = Number(t.method) + + if (!existing) { + grouped.set(key, { + id: `rel_${t.from_city_id}_${t.to_city_id}`, + fromCity, + fromCountry: t.from_country || '', + toCity, + toCountry: t.to_country || '', + hasAirplane: method === 1, + hasTrain: method === 2, + hasRoad: method === 3, + airplaneDetails: method === 1 ? `نرخ پایه بزرگسال: $${Number(t.price || 0)}` : undefined, + trainDetails: method === 2 ? `نرخ پایه بزرگسال: $${Number(t.price || 0)}` : undefined, + roadDetails: method === 3 ? `نرخ پایه بزرگسال: $${Number(t.price || 0)}` : undefined, + isActive: true, + }) + } else { + if (method === 1) { + existing.hasAirplane = true + existing.airplaneDetails = `نرخ پایه بزرگسال: $${Number(t.price || 0)}` + } else if (method === 2) { + existing.hasTrain = true + existing.trainDetails = `نرخ پایه بزرگسال: $${Number(t.price || 0)}` + } else if (method === 3) { + existing.hasRoad = true + existing.roadDetails = `نرخ پایه بزرگسال: $${Number(t.price || 0)}` + } + } } - return stored + + return Array.from(grouped.values()) } export async function saveCustomTripTransportRelation( item: CustomTripTransportRelationItem ): Promise { - const list = await fetchCustomTripTransportRelations() - const exists = list.some((r) => r.id === item.id) - const updated = exists ? list.map((r) => (r.id === item.id ? item : r)) : [item, ...list] - setStored(STORAGE_KEYS.RELATIONS, updated) + const methods: number[] = [] + if (item.hasAirplane) methods.push(1) + if (item.hasTrain) methods.push(2) + if (item.hasRoad) methods.push(3) + if (methods.length === 0) methods.push(1) + + for (const method of methods) { + await apiFetch('trip/custom/v2/catalog/transports/', { + method: 'POST', + body: JSON.stringify({ + fromCity: item.fromCity, + fromCountry: item.fromCountry, + toCity: item.toCity, + toCountry: item.toCountry, + method, + }), + }) + } return item } export async function deleteCustomTripTransportRelation(id: string | number): Promise { - const list = await fetchCustomTripTransportRelations() - setStored(STORAGE_KEYS.RELATIONS, list.filter((r) => r.id !== id)) -} - -export async function fetchCustomTripTransports(): Promise { - return MOCK_TRANSPORTS -} - -export async function fetchCustomTripHotels(): Promise { - try { - const remote = await apiFetch('trip/custom/v2/catalog/hotels/') - if (remote && Array.isArray(remote) && remote.length > 0) { - const mapped: CustomTripHotelItem[] = remote.map((h: any) => { - const found = MOCK_HOTELS.find((m) => m.name === h.name || (h.name && m.name.includes(h.name))) - return { - id: h.id, - name: h.name, - cityName: found?.cityName || h.city_name || h.cityName || 'مشهد مقدس', - stars: h.stars || found?.stars || 4, - score: h.score || found?.score || 4.8, - distanceToShrine: found?.distanceToShrine || (h.distance_to_city_km ? `${h.distance_to_city_km} کیلومتر تا حرم` : 'نزدیک به حرم مطهر'), - distanceToShrineKm: h.distance_to_city_km || found?.distanceToShrineKm || 0.3, - amenities: found?.amenities || ['اینترنت رایگان', 'رستوران ایرانی و بوفه', 'ترانسفر حرم'], - description: found?.description || 'هتل درجه یک و مجهز جهت اقامت زائرین گرامی.', - pricePerDayAdult: Number(h.price_per_day) || found?.pricePerDayAdult || 80, - pricePerDayChild: Number(h.price_per_day_child) || found?.pricePerDayChild || 40, - image: '', - isActive: true, - translations: found?.translations, + const strId = String(id) + if (strId.startsWith('rel_')) { + const parts = strId.split('_') + if (parts.length >= 3) { + const fromId = Number(parts[1]) + const toId = Number(parts[2]) + const all = await apiFetch('trip/custom/v2/catalog/transports/') + if (Array.isArray(all)) { + const matches = all.filter( + (t) => Number(t.from_city_id) === fromId && Number(t.to_city_id) === toId + ) + for (const m of matches) { + await apiFetch(`trip/custom/v2/catalog/transports/${m.id}/`, { + method: 'DELETE', + }) } - }) - setStored(STORAGE_KEYS.HOTELS, mapped) - return mapped + return + } } - } catch (err) { - console.warn('[CustomTrip API] Could not fetch hotels from backend, using strictly seeded mock:', err) } + await apiFetch(`trip/custom/v2/catalog/transports/${id}/`, { + method: 'DELETE', + }) +} - const stored = getStored(STORAGE_KEYS.HOTELS, null) - if (!stored || !Array.isArray(stored) || stored.length !== MOCK_HOTELS.length) { - setStored(STORAGE_KEYS.HOTELS, MOCK_HOTELS) - return MOCK_HOTELS - } - return stored +export async function fetchCustomTripHotels(): Promise { + const remote = await apiFetch('trip/custom/v2/catalog/hotels/') + if (!Array.isArray(remote)) return [] + return remote.map((h: any) => ({ + id: h.id, + name: h.name || '', + cityName: h.cityName || h.city_name || '', + stars: Number(h.stars) || 4, + score: Number(h.score) || 4.8, + distanceToShrine: + h.distanceToShrine || + (h.distance_to_city_km ? `${h.distance_to_city_km} کیلومتر تا حرم مطهر` : 'نزدیک به حرم مطهر'), + distanceToShrineKm: Number(h.distanceToShrineKm ?? h.distance_to_city_km ?? 0.3), + amenities: Array.isArray(h.amenities) + ? h.amenities + : ['اینترنت رایگان', 'رستوران و بوفه', 'نزدیک به حرم مطهر'], + description: h.description || 'هتل درجه یک و مجهز جهت اقامت زائرین گرامی.', + guideNote: h.guideNote || undefined, + pricePerDayAdult: Number(h.pricePerDayAdult ?? h.price_per_day ?? 80), + pricePerDayChild: Number(h.pricePerDayChild ?? h.price_per_day_child ?? 40), + image: '', + isActive: h.isActive !== undefined ? Boolean(h.isActive) : true, + translations: h.translations, + })) } export async function saveCustomTripHotel(item: CustomTripHotelItem): Promise { - const list = await fetchCustomTripHotels() - const exists = list.some((h) => h.id === item.id) - const updated = exists ? list.map((h) => (h.id === item.id ? item : h)) : [item, ...list] - setStored(STORAGE_KEYS.HOTELS, updated) - - // Also sync hotelsCount in destination - const dests = await fetchCustomTripDestinations() - const matchingDest = dests.find( - (d) => d.name === item.cityName || item.cityName.includes(d.name) || d.name.includes(item.cityName) - ) - if (matchingDest) { - const destHotels = updated.filter( - (h) => h.cityName === matchingDest.name || matchingDest.name.includes(h.cityName) || h.cityName.includes(matchingDest.name) - ) - matchingDest.hotelsCount = destHotels.length - await saveCustomTripDestination(matchingDest) + const payload = { + name: item.name, + cityName: item.cityName, + stars: item.stars, + score: item.score, + distanceToShrine: item.distanceToShrine, + distanceToShrineKm: item.distanceToShrineKm ?? 0.3, + amenities: item.amenities, + description: item.description, + guideNote: item.guideNote, + pricePerDayAdult: item.pricePerDayAdult, + pricePerDayChild: item.pricePerDayChild, + isActive: item.isActive, + translations: item.translations, } + const isNumericId = typeof item.id === 'number' || /^\d+$/.test(String(item.id)) + const res = isNumericId + ? await apiFetch(`trip/custom/v2/catalog/hotels/${item.id}/`, { + method: 'PUT', + body: JSON.stringify(payload), + }).catch(() => + apiFetch('trip/custom/v2/catalog/hotels/', { + method: 'POST', + body: JSON.stringify(payload), + }) + ) + : await apiFetch('trip/custom/v2/catalog/hotels/', { + method: 'POST', + body: JSON.stringify(payload), + }) - return item + return { + ...item, + id: res.id ?? item.id, + name: res.name || item.name, + cityName: res.cityName || item.cityName, + } } export async function deleteCustomTripHotel(id: string | number): Promise { - const list = await fetchCustomTripHotels() - const target = list.find((h) => h.id === id) - const remaining = list.filter((h) => h.id !== id) - setStored(STORAGE_KEYS.HOTELS, remaining) - - if (target) { - const dests = await fetchCustomTripDestinations() - const matchingDest = dests.find( - (d) => d.name === target.cityName || target.cityName.includes(d.name) || d.name.includes(target.cityName) - ) - if (matchingDest) { - const destHotels = remaining.filter( - (h) => h.cityName === matchingDest.name || matchingDest.name.includes(h.cityName) || h.cityName.includes(matchingDest.name) - ) - matchingDest.hotelsCount = destHotels.length - await saveCustomTripDestination(matchingDest) - } - } + await apiFetch(`trip/custom/v2/catalog/hotels/${id}/`, { + method: 'DELETE', + }) } - diff --git a/src/features/custom-trip/types.ts b/src/features/custom-trip/types.ts index 45553ed..208a506 100644 --- a/src/features/custom-trip/types.ts +++ b/src/features/custom-trip/types.ts @@ -23,12 +23,23 @@ export interface CustomTripLeg { mealPlan?: string } +export interface CustomTripPassenger { + firstName: string + lastName: string + passportNumber?: string + phoneNumber?: string + birthDate?: string + passengerType: 'ADULT' | 'CHILD' | 'INFANT' + passportImage?: string | null +} + export interface CustomTripOrderItem { id: number user: CustomTripUser originCity: string destinations: string[] legs: CustomTripLeg[] + passengers?: CustomTripPassenger[] adultsCount: number childrenCount: number infantsCount: number diff --git a/src/features/settings/SettingsPage.tsx b/src/features/settings/SettingsPage.tsx index b4318ce..86af638 100644 --- a/src/features/settings/SettingsPage.tsx +++ b/src/features/settings/SettingsPage.tsx @@ -5,6 +5,7 @@ import { Badge } from '@/components/ui/badge' import { Ic, IconName } from '@/icons' import { AdminManagementTab } from './components/AdminManagementTab' import { NotificationsTab } from './components/NotificationsTab' +import { SocialMediaSettingsTab } from './components/SocialMediaSettingsTab' interface SettingsTabConfig { id: string @@ -15,6 +16,7 @@ interface SettingsTabConfig { const SETTINGS_TABS: SettingsTabConfig[] = [ { id: 'admins', labelKey: 'settings.tabAdmins', icon: 'users', isCore: true }, + { id: 'social', labelKey: 'settings.tabSocial', icon: 'share' }, { id: 'notifications', labelKey: 'settings.tabNotifications', icon: 'bell' }, ] @@ -24,24 +26,24 @@ export default function SettingsPage() { return (
- {/* ۱. سربرگ اصلی صفحه تنظیمات (مطابق الگوی طراحی جامع عقیله) */} -
-
+ {/* ۱. سربرگ اصلی صفحه تنظیمات (مطابق الگوی طراحی جامع عقیله - کالیبره دارک) */} +
+
-
+
-

+

{t('settings.pageTitle')}

{t('settings.badgeRoleControl')}
-

+

{t('settings.pageSubtitle')}

@@ -56,10 +58,10 @@ export default function SettingsPage() { key={tab.id} type="button" onClick={() => setActiveTab(tab.id)} - className={`flex items-center gap-2 px-3.5 py-2 rounded-xl text-xs font-bold transition-all whitespace-nowrap border shrink-0 ${ + className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs font-bold transition-all whitespace-nowrap border shrink-0 ${ isActive - ? 'bg-primary text-white border-primary shadow-xs' - : 'bg-card/60 text-grey-2 border-border-soft hover:bg-card hover:text-foreground' + ? 'bg-gradient-to-r from-[#d09460] to-[#b37745] text-white border-primary shadow-md shadow-[#d09460]/20' + : 'bg-[#1e2129] text-slate-300 border-white/[0.08] hover:bg-[#252833] hover:text-white' }`} > @@ -69,7 +71,7 @@ export default function SettingsPage() { className={`text-[10px] px-1.5 py-0.2 rounded-md font-bold ${ isActive ? 'bg-white/20 text-white' - : 'bg-primary/15 text-primary' + : 'bg-primary/20 text-primary' }`} > {t('billing.coreTourBadge')} @@ -81,7 +83,13 @@ export default function SettingsPage() {
{/* ۳. محتوای تب فعال */} - {activeTab === 'admins' ? : } + {activeTab === 'admins' ? ( + + ) : activeTab === 'social' ? ( + + ) : ( + + )}
) } diff --git a/src/features/settings/components/SocialMediaSettingsTab.tsx b/src/features/settings/components/SocialMediaSettingsTab.tsx new file mode 100644 index 0000000..472e8b8 --- /dev/null +++ b/src/features/settings/components/SocialMediaSettingsTab.tsx @@ -0,0 +1,714 @@ +import * as React from 'react' +import { useTranslation } from '@/i18n' +import { Card, CardContent } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Switch } from '@/components/ui/switch' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog' +import { Ic, type IconName } from '@/icons' +import { toast } from '@/stores/toast' +import { + fetchSocialMediaSettings, + saveSocialMediaSettings, +} from '../services/settings-api' +import type { + SocialMediaItem, + SocialMediaContactUs, +} from '../types' + +interface PlatformMeta { + key: string + name: string + nameEn: string + icon: IconName + brandColor: string + defaultUrl: string + placeholder: string +} + +const PRESET_PLATFORMS: Record = { + instagram: { + key: 'instagram', + name: 'اینستاگرام', + nameEn: 'Instagram', + icon: 'instagram', + brandColor: '#E1306C', + defaultUrl: 'https://instagram.com/', + placeholder: 'https://instagram.com/aqila_group', + }, + telegram: { + key: 'telegram', + name: 'تلگرام', + nameEn: 'Telegram', + icon: 'telegram', + brandColor: '#229ED9', + defaultUrl: 'https://t.me/', + placeholder: 'https://t.me/aqila_official', + }, + whatsapp: { + key: 'whatsapp', + name: 'واتساپ', + nameEn: 'WhatsApp', + icon: 'whatsapp', + brandColor: '#25D366', + defaultUrl: 'https://wa.me/', + placeholder: 'https://wa.me/989123456789', + }, + youtube: { + key: 'youtube', + name: 'یوتیوب', + nameEn: 'YouTube', + icon: 'youtube', + brandColor: '#FF0000', + defaultUrl: 'https://youtube.com/@', + placeholder: 'https://youtube.com/@aqila_tours', + }, + x: { + key: 'x', + name: 'ایکس / توییتر', + nameEn: 'X (Twitter)', + icon: 'twitter', + brandColor: '#E2E8F0', + defaultUrl: 'https://x.com/', + placeholder: 'https://x.com/aqila', + }, + linkedin: { + key: 'linkedin', + name: 'لینکدین', + nameEn: 'LinkedIn', + icon: 'linkedin', + brandColor: '#0A66C2', + defaultUrl: 'https://linkedin.com/company/', + placeholder: 'https://linkedin.com/company/aqila', + }, + facebook: { + key: 'facebook', + name: 'فیس‌بوک', + nameEn: 'Facebook', + icon: 'facebook', + brandColor: '#1877F2', + defaultUrl: 'https://facebook.com/', + placeholder: 'https://facebook.com/aqila', + }, + tiktok: { + key: 'tiktok', + name: 'تیک‌تاک', + nameEn: 'TikTok', + icon: 'tiktok', + brandColor: '#F43F5E', + defaultUrl: 'https://tiktok.com/@', + placeholder: 'https://tiktok.com/@aqila', + }, + aparat: { + key: 'aparat', + name: 'آپارات', + nameEn: 'Aparat', + icon: 'video', + brandColor: '#EA1D5D', + defaultUrl: 'https://aparat.com/', + placeholder: 'https://aparat.com/aqila', + }, + bale: { + key: 'bale', + name: 'پیام‌رسان بله', + nameEn: 'Bale', + icon: 'message', + brandColor: '#10B981', + defaultUrl: 'https://ble.ir/', + placeholder: 'https://ble.ir/aqila_group', + }, + eitaa: { + key: 'eitaa', + name: 'پیام‌رسان ایتا', + nameEn: 'Eitaa', + icon: 'message', + brandColor: '#F97316', + defaultUrl: 'https://eitaa.com/', + placeholder: 'https://eitaa.com/aqila_official', + }, + threads: { + key: 'threads', + name: 'تردز', + nameEn: 'Threads', + icon: 'share2', + brandColor: '#F1F5F9', + defaultUrl: 'https://threads.net/@', + placeholder: 'https://threads.net/@aqila', + }, + website: { + key: 'website', + name: 'وب‌سایت رسمی', + nameEn: 'Official Website', + icon: 'globe', + brandColor: '#38BDF8', + defaultUrl: 'https://', + placeholder: 'https://aqila.ir', + }, +} + +export function SocialMediaSettingsTab() { + const { t } = useTranslation() + + const [loading, setLoading] = React.useState(true) + const [saving, setSaving] = React.useState(false) + const [items, setItems] = React.useState([]) + const [contactus, setContactus] = React.useState({ + phone_number: '', + whatsapp_number: '', + address: '', + }) + + // Modal State for adding new platform + const [addModalOpen, setAddModalOpen] = React.useState(false) + const [selectedPlatformKey, setSelectedPlatformKey] = React.useState('telegram') + const [newChannelName, setNewChannelName] = React.useState('') + const [newChannelLink, setNewChannelLink] = React.useState('') + + const loadData = React.useCallback(async () => { + setLoading(true) + try { + const res = await fetchSocialMediaSettings() + setItems(res.items || []) + setContactus(res.contactus || {}) + } catch (err: any) { + console.error('Error fetching social media settings:', err) + toast.error(err?.message || t('settings.socialFetchError')) + } finally { + setLoading(false) + } + }, [t]) + + React.useEffect(() => { + loadData() + }, [loadData]) + + const handleOpenAddModal = (platformKey?: string) => { + const pKey = platformKey || 'telegram' + const preset = PRESET_PLATFORMS[pKey] || PRESET_PLATFORMS.telegram + setSelectedPlatformKey(pKey) + setNewChannelName(preset.name) + setNewChannelLink(preset.defaultUrl) + setAddModalOpen(true) + } + + const handleSelectPreset = (key: string) => { + const preset = PRESET_PLATFORMS[key] + setSelectedPlatformKey(key) + if (preset) { + setNewChannelName(preset.name) + setNewChannelLink(preset.defaultUrl) + } + } + + const handleAddChannel = () => { + if (!newChannelLink.trim()) { + toast.error('لطفاً آدرس پیوند (URL) را وارد نمایید.') + return + } + + const newItem: SocialMediaItem = { + id: `social-${selectedPlatformKey}-${Date.now()}`, + platform: selectedPlatformKey, + name: newChannelName.trim() || selectedPlatformKey, + link: newChannelLink.trim(), + is_active: true, + } + + setItems((prev) => [...prev, newItem]) + setAddModalOpen(false) + toast.success(t('settings.channelAdded')) + } + + const handleItemChange = (index: number, field: keyof SocialMediaItem, value: any) => { + setItems((prev) => { + const next = [...prev] + next[index] = { ...next[index], [field]: value } + return next + }) + } + + const handleDeleteItem = (index: number) => { + setItems((prev) => prev.filter((_, idx) => idx !== index)) + } + + const handleCopyLink = (link: string) => { + if (!link) return + navigator.clipboard.writeText(link) + toast.success(t('settings.linkCopied')) + } + + const handleSaveToDatabase = async () => { + setSaving(true) + try { + const res = await saveSocialMediaSettings({ + items, + contactus, + }) + if (res.items) { + setItems(res.items) + } + if (res.contactus) { + setContactus(res.contactus) + } + toast.success(res.message || t('settings.socialSaveSuccess')) + } catch (err: any) { + console.error('Error saving social media settings:', err) + toast.error(err?.message || t('settings.socialSaveError')) + } finally { + setSaving(false) + } + } + + const activeCount = items.filter((i) => i.is_active).length + + return ( +
+ {/* ۱. کارت سربرگ و راهنمای تنظیمات شبکه‌های اجتماعی (کالیبره حالت دارک) */} + + +
+
+
+ +
+
+
+

+ {t('settings.socialTabTitle')} +

+ + {activeCount} {t('settings.activeStatus')} + + + PostgreSQL Live Sync + +
+

+ {t('settings.socialTabSubtitle')} +

+
+
+ +
+ + + +
+
+ + {/* نوار انتخاب سریع پلتفرم‌های مطرح (کالیبره و منسجم در تم تیره) */} +
+
+ + پلتفرم‌های پیشنهادی جهت افزودن فوری: +
+
+ {Object.values(PRESET_PLATFORMS).map((preset) => { + const isAlreadyAdded = items.some((it) => it.platform.toLowerCase() === preset.key.toLowerCase()) + return ( + + ) + })} +
+
+
+
+ + {/* ۲. فهرست شبکه‌های اجتماعی ثبت‌شده */} + {loading ? ( + +
+ +

در حال دریافت شبکه‌های اجتماعی از دیتابیس...

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

+ {t('settings.noSocialChannels')} +

+

+ {t('settings.noSocialChannelsDesc')} +

+
+ +
+
+ ) : ( +
+ {items.map((item, index) => { + const preset = PRESET_PLATFORMS[item.platform.toLowerCase()] || { + key: item.platform, + name: item.name || item.platform, + nameEn: item.platform, + icon: 'link' as IconName, + brandColor: '#38BDF8', + defaultUrl: '', + placeholder: 'https://...', + } + + return ( + + + {/* ردیف بالا: آیکون برند، عنوان پلتفرم، بج وضعیت، سوییچ و دکمه حذف */} +
+
+
+ +
+
+
+ + {preset.name} + + + {preset.nameEn || item.platform} + +
+
+ {item.is_active ? ( + + + {t('settings.activeStatus')} + + ) : ( + + {t('settings.inactiveStatus')} + + )} +
+
+
+ +
+ handleItemChange(index, 'is_active', checked)} + /> + +
+
+ + {/* فیلدهای ورودی عنوان نمایشی و آدرس اینترنتی (با اصلاح کامل مشکل همپوشانی) */} +
+
+ + handleItemChange(index, 'name', e.target.value)} + placeholder="مثال: کانال رسمی اطلاع‌رسانی عقیله" + className="w-full h-9.5 rounded-xl bg-[#14161c] border border-white/[0.1] px-3.5 text-xs font-medium text-slate-100 placeholder:text-slate-500 outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary/40" + /> +
+ +
+ + {/* کانتینر LTR اختصاصی جهت جلوگیری از هرگونه تداخل آیکون‌ها با متن */} +
+ handleItemChange(index, 'link', e.target.value)} + placeholder={preset.placeholder} + className="w-full h-9.5 rounded-xl bg-[#14161c] border border-white/[0.1] pl-3.5 pr-20 text-xs font-mono text-slate-100 placeholder:text-slate-500 outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary/40 text-left" + /> + {/* دکمه‌های عملیات ثابت در سمت راست فیلد LTR */} +
+ + {item.link ? ( + + + + ) : null} +
+
+
+
+
+
+ ) + })} +
+ )} + + {/* ۳. کارت اطلاعات تماس تکمیلی (Contact Us - کالیبره در تم تیره) */} + + +
+
+ +
+
+

+ {t('settings.contactInfoCardTitle')} +

+

+ {t('settings.contactInfoCardDesc')} +

+
+
+ +
+ {/* شماره‌های پشتیبانی */} +
+ +
+ setContactus((prev) => ({ ...prev, phone_number: e.target.value }))} + placeholder="+982188888888 | +9647700000000" + className="w-full h-10 rounded-xl bg-[#14161c] border border-white/[0.1] px-3.5 text-xs font-mono text-slate-100 placeholder:text-slate-500 outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary/40 text-left" + /> +
+
+ + {/* شماره اختصاصی واتساپ */} +
+ +
+ setContactus((prev) => ({ ...prev, whatsapp_number: e.target.value }))} + placeholder="+989120000000" + className="w-full h-10 rounded-xl bg-[#14161c] border border-white/[0.1] px-3.5 text-xs font-mono text-slate-100 placeholder:text-slate-500 outline-none transition-colors focus:border-primary focus:ring-1 focus:ring-primary/40 text-left" + /> +
+
+ + {/* آدرس دفتر مرکزی */} +
+ +