From 64af6b3676dc6dfbd0b13b71466da089d8412ba7 Mon Sep 17 00:00:00 2001 From: Muhammad Amin Ghorbani Date: Mon, 12 Oct 2026 14:36:15 +0330 Subject: [PATCH] feat(support): add support channels UI with 5 languages, country dial codes, and whatsapp testing --- .env.example | 2 +- src/components/layout/ServerStatusDialog.tsx | 2 +- src/config/navigation.ts | 2 +- src/features/services/ServicesPage.tsx | 227 +----- .../components/SupportServiceView.tsx | 747 ++++++++++++++++++ src/icons.tsx | 2 + vite.config.ts | 2 +- 7 files changed, 764 insertions(+), 220 deletions(-) create mode 100644 src/features/services/components/SupportServiceView.tsx diff --git a/.env.example b/.env.example index 582e7ec..6d3b134 100644 --- a/.env.example +++ b/.env.example @@ -3,4 +3,4 @@ VITE_API_BASE_URL=/api # مقصد پراکسی توسعه Vite برای هدایت درخواست‌های /api به سرور بک‌اند -VITE_API_TARGET=http://localhost:8000 +VITE_API_TARGET=http://localhost:8001 diff --git a/src/components/layout/ServerStatusDialog.tsx b/src/components/layout/ServerStatusDialog.tsx index 070b74c..711ae56 100644 --- a/src/components/layout/ServerStatusDialog.tsx +++ b/src/components/layout/ServerStatusDialog.tsx @@ -127,7 +127,7 @@ export default function ServerStatusDialog({ open, onOpenChange }: ServerStatusD
{t('serverStatus.targetServer')}: - http://127.0.0.1:8000 + http://127.0.0.1:8001
{t('serverStatus.apiPrefix')}: diff --git a/src/config/navigation.ts b/src/config/navigation.ts index 042840c..e8490a7 100644 --- a/src/config/navigation.ts +++ b/src/config/navigation.ts @@ -128,7 +128,7 @@ export function getNavigationConfig(t: (key: string) => string): NavGroupConfig[ ], }, { title: t('navigation.costEstimate'), titleKey: 'navigation.costEstimate', to: '/services/cost-estimate', icon: 'calculator' }, - { title: t('navigation.support'), titleKey: 'navigation.support', isExternalSupport: true, icon: 'headset' }, + { title: t('navigation.support'), titleKey: 'navigation.support', to: '/services/consultant', icon: 'headset' }, { title: t('navigation.faq'), titleKey: 'navigation.faq', to: '/services/faq', icon: 'helpCircle' }, { title: t('navigation.settings'), titleKey: 'navigation.settings', to: '/settings', icon: 'settings' }, ], diff --git a/src/features/services/ServicesPage.tsx b/src/features/services/ServicesPage.tsx index be03ac8..6bee8ca 100644 --- a/src/features/services/ServicesPage.tsx +++ b/src/features/services/ServicesPage.tsx @@ -14,6 +14,7 @@ import { CategoriesServiceView } from './components/CategoriesServiceView' import { CitiesServiceView } from './components/CitiesServiceView' import { FaqServiceView } from './components/FaqServiceView' import { StoreServiceView } from './components/StoreServiceView' +import { SupportServiceView } from './components/SupportServiceView' // تعریف ساختار داده‌های سرویس‌ها export interface ServiceMeta { @@ -166,10 +167,10 @@ export const SERVICES_LIST: ServiceMeta[] = [ color: 'text-primary', bgColor: 'bg-primary/10', borderColor: 'border-primary/20', - description: 'شبکه کارشناسان تخصصی پزشکی سفر، احکام، امور حقوقی، گمشدگان و پشتیبانی 24 ساعته زائرین', + description: 'مدیریت کانال‌های تماس، راهنمای پشتیبانی زائرین، پاسخگویی ۵ زبانه و کنترل وضعیت فعال/غیرفعال‌سازی در اپلیکیشن', endpoint: '/consultants/', - statsLabel: 'کارشناس فعال', - defaultCount: 12, + statsLabel: 'کانال فعال', + defaultCount: 5, badge: 'پشتیبانی زنده', }, ] @@ -182,6 +183,11 @@ export default function ServicesPage() { return } + // اگر مسیر /services/support فراخوانی شد، به صفحه پشتیبانی هدایت شود + if (serviceKey === 'support') { + return + } + const currentService = useMemo(() => { if (!serviceKey) return null const base = SERVICES_LIST.find((s) => s.key === serviceKey) @@ -284,7 +290,7 @@ function ServiceDetailRenderer({ case 'trip-etiquette': return case 'consultant': - return + return case 'faq': return case 'store': @@ -629,219 +635,8 @@ function TripEtiquetteServiceView({ service }: { service: ServiceMeta }) { } // ───────────────────────────────────────────────────────────────────────────── -// ۵. مشاوره و پشتیبانی (Consultant / Support) +// ۵. مشاوره و پشتیبانی (Support) در components/SupportServiceView پیاده‌سازی شده است // ───────────────────────────────────────────────────────────────────────────── -function ConsultantServiceView({ service }: { service: ServiceMeta }) { - const [searchTerm, setSearchTerm] = useState('') - const [specialtyFilter, setSpecialtyFilter] = useState('ALL') - - const consultants = [ - { id: 1, name: 'دکتر علیرضا رضایی', specialty: 'پزشکی سفر، گرمازدگی و مراقبت‌های بهداشتی', category: 'medical', status: 'آنلاین و آماده پاسخ', isOnline: true, activeChats: 4, satisfaction: '99%', experience: '8 سال' }, - { id: 2, name: 'حجت‌الاسلام سید هادی موسوی', specialty: 'احکام و مناسک حج، عمره و عتبات عالیات', category: 'rituals', status: 'آنلاین و آماده پاسخ', isOnline: true, activeChats: 6, satisfaction: '98%', experience: '12 سال' }, - { id: 3, name: 'مهندس مهدی کمالی', specialty: 'لجستیک، حمل‌ونقل بین‌شهری، گذرنامه و امور گمشدگان', category: 'logistics', status: 'مشغول در تماس', isOnline: false, activeChats: 2, satisfaction: '97%', experience: '5 سال' }, - { id: 4, name: 'سرکار خانم فاطمه سعیدی', specialty: 'مشاوره خانواده، بانوان، اقامت و کودکان در سفر', category: 'family', status: 'آنلاین و آماده پاسخ', isOnline: true, activeChats: 3, satisfaction: '100%', experience: '6 سال' }, - ] - - const filteredConsultants = useMemo(() => { - return consultants.filter((c) => { - if (specialtyFilter !== 'ALL' && c.category !== specialtyFilter) return false - if (searchTerm.trim()) { - const q = searchTerm.toLowerCase().trim() - const matchName = c.name.toLowerCase().includes(q) - const matchSpec = c.specialty.toLowerCase().includes(q) - if (!matchName && !matchSpec) return false - } - return true - }) - }, [consultants, specialtyFilter, searchTerm]) - - return ( -
- {/* ۱. کارت‌های شاخص عملکرد مرکز پشتیبانی و مشاوره */} -
-
-
- مشاوران شیفت جاری - - - آنلاین - -
-
- {toFa(8)} نفر -
-
- از 12 کارشناس پشتیبانی 24 ساعته -
-
- -
-
- مشاوره‌های امروز - - موفق - -
-
- {toFa(64)} -
-
- پاسخگویی به مسائل بهداشتی و شرعی -
-
- -
-
- میانگین زمان انتظار - -
-
- 45 ثانیه -
-
- استاندارد عالی (زیر 1 دقیقه) -
-
- -
-
- رضایت‌مندی زائرین - -
-
- 98.7% -
-
- بر اساس 1,420 بازخورد ثبت‌شده -
-
-
- - {/* ۲. فیلترها و نوار جستجوی کارشناسان */} -
-
- - - - -
- -
- - setSearchTerm(e.target.value)} - placeholder="جستجوی نام یا حوزه تخصص..." - className="h-9 ps-9 text-xs bg-surface-base rounded-xl border-border-soft" - /> - {searchTerm && ( - - )} -
-
- - {/* ۳. کارت‌های کارشناسان با استایل مدرن */} -
-
-
-

کادر کارشناسان پشتیبانی و مشاوره عقیله

- - {toFa(filteredConsultants.length)} کارشناس - -
- پشتیبانی زنده صوتی و متنی زائرین -
- -
- {filteredConsultants.map((c) => ( -
-
-
-
- -
- - {c.status} - -
- -
-

{c.name}

-

{c.specialty}

-
-
- -
-
- گفتگوهای فعال: - {toFa(c.activeChats)} جلسه -
-
- رضایت مخاطبان: - {c.satisfaction} -
-
- سابقه تخصصی: - {c.experience} -
-
-
- ))} -
-
-
- ) -} // ───────────────────────────────────────────────────────────────────────────── // ۶. فروشگاه معصومه (Massoumah Store) در components/StoreServiceView پیاده‌سازی شده است diff --git a/src/features/services/components/SupportServiceView.tsx b/src/features/services/components/SupportServiceView.tsx new file mode 100644 index 0000000..29a8371 --- /dev/null +++ b/src/features/services/components/SupportServiceView.tsx @@ -0,0 +1,747 @@ +import React, { useState, useMemo } from 'react' +import { Ic, type IconName } from '@/icons' +import { Card, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { Switch } from '@/components/ui/switch' +import { toFa, cn } from '@/lib/utils' +import { toast } from '@/stores/toast' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog' +import { + ServiceTranslation, + type ServiceLanguageCode, + SERVICE_LANGUAGES, +} from '@/components/shared/ServiceTranslation' + +export interface SupportChannel { + id: string + icon: string + phoneNumber: string + whatsappNumber: string + order: number + isActive: boolean + titles: Record + descriptions: Record +} + +const INITIAL_SUPPORT_CHANNELS: SupportChannel[] = [ + { + id: 'ch-1', + icon: 'ticket', + phoneNumber: '+442071234577', + whatsappNumber: '+442071234577', + order: 1, + isActive: true, + titles: { + ar: 'الحجز والتذاكر', + fa: 'رزرو و خرید بلیت', + en: 'Booking & Reservation', + ru: 'Бронирование и билеты', + id: 'Pemesanan & Reservasi', + }, + descriptions: { + ar: 'حجز تذاكر الطيران، القطارات، تأكيد المواعيد وإلغاء أو تعديل الحجوزات', + fa: 'صدور و پیگیری بلیت پرواز، قطار، استرداد و تغییر زمان حرکت', + en: 'Flight & train ticket reservations, confirmation and scheduling support', + ru: 'Бронирование авиа и ж/д билетов, подтверждение и изменение рейсов', + id: 'Pemesanan tiket pesawat & kereta, konfirmasi dan perubahan jadwal', + }, + }, + { + id: 'ch-2', + icon: 'calendar', + phoneNumber: '+442071234547', + whatsappNumber: '+442071234547', + order: 2, + isActive: true, + titles: { + ar: 'الرحلة وجدول السفر', + fa: 'برنامه و هماهنگی سفر', + en: 'Trip & Itinerary', + ru: 'Поездка и маршруت', + id: 'Jadwal Perjalanan', + }, + descriptions: { + ar: 'تنظيم مواعيد الرحلات، برامج الزيارات الميدانية ومرافقة القوافل', + fa: 'هماهنگی جدول زمانی کاروان‌ها، گشت‌های زیارتی و هماهنگی تورلیدر', + en: 'Travel schedules, group coordination, ziyarah guides and itineraries', + ru: 'Расписание поездок, координация групп и экскурсионных программ', + id: 'Jadwal perjalanan, koordinasi rombongan dan panduan ziarah', + }, + }, + { + id: 'ch-3', + icon: 'building', + phoneNumber: '+442071234560', + whatsappNumber: '+442071234560', + order: 3, + isActive: false, + titles: { + ar: 'الإقامة والمواصلات', + fa: 'اقامت و ترانسفر', + en: 'Accommodation & Transportation', + ru: 'Проживание и транспорт', + id: 'Akomodasi & Transportasi', + }, + descriptions: { + ar: 'حجز الفنادق القريبة من الحرمين، الشقق المفروشة والتنقل بين المدن', + fa: 'هتل‌های نزدیک به حرم، ترانسفر فرودگاهی و تاکسی‌های بین‌شهری', + en: 'Hotel reservations near holy shrines, VIP transit and airport transfers', + ru: 'Отели рядом со святынями, трансфер из аэропорта и между городами', + id: 'Reservasi hotel dekat makam suci, transportasi bandara dan antar kota', + }, + }, + { + id: 'ch-4', + icon: 'plane', + phoneNumber: '+442071234590', + whatsappNumber: '+442071234590', + order: 4, + isActive: true, + titles: { + ar: 'التأشيرة وجواز السفر', + fa: 'ویزا و گذرنامه', + en: 'Visa and Passport', + ru: 'Виза и загранпаспорт', + id: 'Visa dan Paspor', + }, + descriptions: { + ar: 'إجراءات استخراج وتجديد التأشيرات السياحية والدينية والاستشارات القنصلية', + fa: 'پیگیری روادید الکترونیک، تمدید اقامت، امور کنسولی و گذرنامه‌های مفقودی', + en: 'Visa processing, stay extensions, embassy affairs and lost passport aid', + ru: 'Оформление электронных виз, продление пребывания и консульская помощь', + id: 'Pengurusan visa ziarah, perpanjangan izin tinggal dan bantuan paspor', + }, + }, + { + id: 'ch-5', + icon: 'shrine', + phoneNumber: '+442071234555', + whatsappNumber: '+442071234555', + order: 5, + isActive: true, + titles: { + ar: 'المسائل والأحكام الشرعية', + fa: 'احکام و مسائل شرعی', + en: 'Religious Issues', + ru: 'Религиозные вопросы', + id: 'Masalah Keagamaan', + }, + descriptions: { + ar: 'الاستفتاءات الشرعية، أحكام صلاة وقصر المسافر، ومناسك الحج والعمرة والزيارة', + fa: 'پاسخ به سوالات شرعی، احکام نماز و روزه مسافر، مناسک و آداب زیارت', + en: 'Religious decrees, prayer travel rulings, and pilgrim etiquette and rituals', + ru: 'Шариатские ответы, правила молитвы в пути и обряды паломничества', + id: 'Konsultasi hukum syariah, tata cara shalat musafir dan manasik ziarah', + }, + }, +] + +const AVAILABLE_ICONS: { id: string; label: string; icon: IconName }[] = [ + { id: 'ticket', label: 'بلیت', icon: 'ticket' }, + { id: 'calendar', label: 'تقویم', icon: 'calendar' }, + { id: 'building', label: 'هتل', icon: 'building' }, + { id: 'plane', label: 'ویزا', icon: 'plane' }, + { id: 'shrine', label: 'احکام', icon: 'shrine' }, + { id: 'headset', label: 'پشتیبانی', icon: 'headset' }, + { id: 'phone', label: 'تماس', icon: 'phone' }, +] + +export const COUNTRY_DIAL_CODES = [ + { code: 'GB', name: 'انگلستان', flag: '🇬🇧', dialCode: '+44' }, + { code: 'IQ', name: 'عراق', flag: '🇮🇶', dialCode: '+964' }, + { code: 'IR', name: 'ایران', flag: '🇮🇷', dialCode: '+98' }, + { code: 'SA', name: 'عربستان', flag: '🇸🇦', dialCode: '+966' }, + { code: 'AE', name: 'امارات', flag: '🇦🇪', dialCode: '+971' }, + { code: 'KW', name: 'کویت', flag: '🇰🇼', dialCode: '+965' }, + { code: 'BH', name: 'بحرین', flag: '🇧🇭', dialCode: '+973' }, + { code: 'OM', name: 'عمان', flag: '🇴🇲', dialCode: '+968' }, + { code: 'QA', name: 'قطر', flag: '🇶🇦', dialCode: '+974' }, + { code: 'TR', name: 'ترکیه', flag: '🇹🇷', dialCode: '+90' }, + { code: 'LB', name: 'لبنان', flag: '🇱🇧', dialCode: '+961' }, + { code: 'SY', name: 'سوریه', flag: '🇸🇾', dialCode: '+963' }, + { code: 'RU', name: 'روسیه', flag: '🇷🇺', dialCode: '+7' }, + { code: 'ID', name: 'اندونزی', flag: '🇮🇩', dialCode: '+62' }, + { code: 'PK', name: 'پاکستان', flag: '🇵🇰', dialCode: '+92' }, + { code: 'IN', name: 'هند', flag: '🇮🇳', dialCode: '+91' }, + { code: 'AF', name: 'افغانستان', flag: '🇦🇫', dialCode: '+93' }, + { code: 'EG', name: 'مصر', flag: '🇪🇬', dialCode: '+20' }, + { code: 'DE', name: 'آلمان', flag: '🇩🇪', dialCode: '+49' }, + { code: 'FR', name: 'فرانسه', flag: '🇫🇷', dialCode: '+33' }, + { code: 'US', name: 'آمریکا / کانادا', flag: '🇺🇸', dialCode: '+1' }, +] + +function splitPhoneNumber(fullPhone: string): { dialCode: string; body: string } { + const clean = (fullPhone || '').trim().replace(/^00/, '+') + if (!clean) return { dialCode: '+44', body: '' } + const sorted = [...COUNTRY_DIAL_CODES].sort((a, b) => b.dialCode.length - a.dialCode.length) + for (const c of sorted) { + if (clean.startsWith(c.dialCode)) { + return { dialCode: c.dialCode, body: clean.slice(c.dialCode.length).replace(/[^\d]/g, '') } + } + } + return { dialCode: '+44', body: clean.replace(/[^\d]/g, '') } +} + +export function SupportServiceView() { + const [channels, setChannels] = useState(() => { + try { + const saved = localStorage.getItem('aqila_support_channels') + if (saved) return JSON.parse(saved) + } catch { + // fallback + } + return INITIAL_SUPPORT_CHANNELS + }) + + const persistChannels = (updated: SupportChannel[]) => { + setChannels(updated) + try { + localStorage.setItem('aqila_support_channels', JSON.stringify(updated)) + } catch { + // ignore + } + } + + // زبان پیش‌نمایش کارت‌ها (پیش‌فرض عربی مطابق اپلیکیشن) + const [previewLang, setPreviewLang] = useState('ar') + + // فیلترها + const [statusFilter, setStatusFilter] = useState<'ALL' | 'ACTIVE' | 'INACTIVE'>('ALL') + const [searchQuery, setSearchQuery] = useState('') + + // دیالوگ افزودن / ویرایش + const [isDialogOpen, setIsDialogOpen] = useState(false) + const [editingChannel, setEditingChannel] = useState(null) + const [dialogLangTab, setDialogLangTab] = useState('ar') + + const [formTitles, setFormTitles] = useState>({ + ar: '', fa: '', en: '', ru: '', id: '', + }) + const [formDescriptions, setFormDescriptions] = useState>({ + ar: '', fa: '', en: '', ru: '', id: '', + }) + const [formCountryDialCode, setFormCountryDialCode] = useState('+44') + const [formPhoneBody, setFormPhoneBody] = useState('') + const [formIcon, setFormIcon] = useState('headset') + const [formOrder, setFormOrder] = useState(1) + const [formIsActive, setFormIsActive] = useState(true) + + const handleOpenCreate = () => { + setEditingChannel(null) + setDialogLangTab('ar') + setFormTitles({ ar: '', fa: '', en: '', ru: '', id: '' }) + setFormDescriptions({ ar: '', fa: '', en: '', ru: '', id: '' }) + setFormCountryDialCode('+44') + setFormPhoneBody('') + setFormIcon('headset') + setFormOrder(channels.length + 1) + setFormIsActive(true) + setIsDialogOpen(true) + } + + const handleOpenEdit = (channel: SupportChannel) => { + setEditingChannel(channel) + setDialogLangTab('ar') + setFormTitles({ ...channel.titles }) + setFormDescriptions({ ...channel.descriptions }) + const parsed = splitPhoneNumber(channel.phoneNumber) + setFormCountryDialCode(parsed.dialCode) + setFormPhoneBody(parsed.body) + setFormIcon(channel.icon) + setFormOrder(channel.order) + setFormIsActive(channel.isActive) + setIsDialogOpen(true) + } + + const handleTestWhatsAppInModal = () => { + const cleanBody = formPhoneBody.replace(/\D/g, '') + if (!cleanBody) { + toast.error('لطفاً شماره تماس را وارد نمایید.') + return + } + const full = `${formCountryDialCode.replace('+', '')}${cleanBody}` + window.open(`https://wa.me/${full}`, '_blank') + toast.success(`چت واتس‌اپ برای شماره ${formCountryDialCode} ${cleanBody} باز شد.`) + } + + const handleSaveChannel = (e: React.FormEvent) => { + e.preventDefault() + if (!formTitles.ar.trim() && !formTitles.fa.trim() && !formTitles.en.trim()) { + toast.error('لطفاً عنوان کانال را وارد کنید.') + return + } + const cleanBody = formPhoneBody.replace(/\D/g, '') + if (!cleanBody) { + toast.error('لطفاً شماره تماس را وارد کنید.') + return + } + const fullPhone = `${formCountryDialCode}${cleanBody}` + + if (editingChannel) { + const updated = channels.map((c) => + c.id === editingChannel.id + ? { + ...c, + icon: formIcon, + phoneNumber: fullPhone, + whatsappNumber: fullPhone, + order: formOrder, + isActive: formIsActive, + titles: { ...formTitles }, + descriptions: { ...formDescriptions }, + } + : c + ) + persistChannels(updated) + toast.success('کانال پشتیبانی به‌روزرسانی شد.') + } else { + const newChannel: SupportChannel = { + id: `ch-${Date.now()}`, + icon: formIcon, + phoneNumber: fullPhone, + whatsappNumber: fullPhone, + order: formOrder, + isActive: formIsActive, + titles: { ...formTitles }, + descriptions: { ...formDescriptions }, + } + persistChannels([...channels, newChannel]) + toast.success('کانال پشتیبانی با موفقیت اضافه شد.') + } + setIsDialogOpen(false) + } + + const handleToggleActive = (id: string, e?: React.MouseEvent) => { + e?.stopPropagation() + const updated = channels.map((c) => { + if (c.id === id) { + const nextState = !c.isActive + toast.success(nextState ? 'کانال فعال شد.' : 'کانال غیرفعال شد.') + return { ...c, isActive: nextState } + } + return c + }) + persistChannels(updated) + } + + const handleDeleteChannel = (channel: SupportChannel) => { + if (confirm(`آیا از حذف این کانال اطمینان دارید؟`)) { + const updated = channels.filter((c) => c.id !== channel.id) + persistChannels(updated) + toast.success('کانال پشتیبانی حذف شد.') + } + } + + const handleTestWhatsApp = (phone: string) => { + const cleanNumber = phone.replace(/[^\d+]/g, '') + const url = `https://wa.me/${cleanNumber.replace('+', '')}` + window.open(url, '_blank') + } + + const filteredChannels = useMemo(() => { + return channels + .filter((c) => { + if (statusFilter === 'ACTIVE' && !c.isActive) return false + if (statusFilter === 'INACTIVE' && c.isActive) return false + + if (searchQuery.trim()) { + const q = searchQuery.toLowerCase().trim() + const matchPhone = c.phoneNumber.includes(q) + const matchTitle = Object.values(c.titles).some((t) => t.toLowerCase().includes(q)) + if (!matchPhone && !matchTitle) return false + } + return true + }) + .sort((a, b) => a.order - b.order) + }, [channels, statusFilter, searchQuery]) + + const activeCount = channels.filter((c) => c.isActive).length + const inactiveCount = channels.length - activeCount + + return ( +
+ {/* نوار کنترل مینیمال و یکپارچه */} +
+ {/* فیلتر وضعیت و پیش‌نمایش زبان */} +
+ {/* تب‌های وضعیت */} +
+ + + +
+ +
+ + {/* انتخابگر زبان پیش‌نمایش ۵ زبانه */} +
+ {SERVICE_LANGUAGES.map((l) => ( + + ))} +
+
+ + {/* جستجو و دکمه افزودن */} +
+
+ + setSearchQuery(e.target.value)} + placeholder="جستجو..." + className="h-8 ps-8 text-xs bg-surface-base rounded-xl border-border-soft" + /> +
+ + +
+
+ + {/* کارت‌های ساده، خلوت و مدرن کانال‌ها */} +
+ {filteredChannels.map((channel) => { + const title = channel.titles[previewLang] || channel.titles.ar || channel.titles.fa + const desc = channel.descriptions[previewLang] || channel.descriptions.ar + + return ( + + + {/* ردیف اول: آیکون، عنوان و سوئیچ وضعیت */} +
+
+
+ +
+
+

+ {title} +

+
+ + #{toFa(channel.order)} + + + {channel.isActive ? 'فعال' : 'غیرفعال'} + +
+
+
+ + handleToggleActive(channel.id)} + className="data-[state=checked]:bg-emerald-500" + /> +
+ + {/* توضیحات مختصر در صورت وجود */} + {desc && ( +

+ {desc} +

+ )} + + {/* ردیف شماره تماس و اکشن‌ها */} +
+ + +
+ + +
+
+
+
+ ) + })} +
+ + {/* دیالوگ تمیز افزودن و ویرایش */} + + + + + {editingChannel ? 'ویرایش کانال پشتیبانی' : 'افزودن کانال پشتیبانی'} + + + +
+ {/* تب‌های ۵ زبان با کامپوننت مشترک ServiceTranslation */} + setDialogLangTab(lang)} + completedLangs={Object.fromEntries( + Object.entries(formTitles).map(([k, v]) => [k, Boolean(v?.trim())]) + )} + primaryLang="ar" + title="زبان محتوا" + variant="grid" + /> + + {/* عنوان به زبان فعال */} + {(() => { + const currentMeta = SERVICE_LANGUAGES.find((l) => l.code === dialogLangTab) || SERVICE_LANGUAGES[0] + return ( +
+
+ + + setFormTitles({ ...formTitles, [dialogLangTab]: e.target.value }) + } + dir={currentMeta.dir} + placeholder={`عنوان به ${currentMeta.name}...`} + className="h-9 text-xs bg-surface-base rounded-xl border-border-soft" + /> +
+ +
+ + + setFormDescriptions({ ...formDescriptions, [dialogLangTab]: e.target.value }) + } + dir={currentMeta.dir} + placeholder="توضیح کوتاه اختیاری..." + className="h-9 text-xs bg-surface-base rounded-xl border-border-soft" + /> +
+
+ ) + })()} + + {/* شماره تماس همراه با انتخابگر کشور و پرچم + دکمه تست مستقیم واتس‌اپ */} +
+
+
+ + + {/* دکمه اختصاصی باز کردن و تست مستقیم واتس‌اپ */} + +
+ + {/* کامپوننت ترکیبی انتخاب کشور با پرچم + اینپوت شماره */} +
+ {/* سلکتور پیش‌شماره کشور همراه با پرچم */} +
+ + +
+ + {/* فیلد وارد کردن شماره تماس */} + setFormPhoneBody(e.target.value)} + dir="ltr" + placeholder="2071234577" + className="h-9 border-0 bg-transparent text-xs font-mono focus-visible:ring-0 shadow-none px-3" + required + /> +
+ + {/* پیش‌نمایش زنده شماره */} +
+ شماره نهایی: + + {formCountryDialCode} {formPhoneBody || '---'} + +
+
+ + {/* ترتیب نمایش */} +
+ + setFormOrder(parseInt(e.target.value) || 1)} + className="h-9 text-xs bg-surface-base rounded-xl border-border-soft font-mono" + /> +
+
+ + {/* آیکون و وضعیت فعال */} +
+ +
+ {AVAILABLE_ICONS.map((item) => ( + + ))} +
+
+ +
+ + وضعیت فعال در اپلیکیشن + + +
+ + + + + + +
+
+
+ ) +} +export default SupportServiceView diff --git a/src/icons.tsx b/src/icons.tsx index 20c4ec9..7c96e93 100644 --- a/src/icons.tsx +++ b/src/icons.tsx @@ -113,6 +113,8 @@ const PATHS: Record = { train: '', bus: '', building: '', + phone: '', + whatsapp: '', } export type IconName = keyof typeof PATHS diff --git a/vite.config.ts b/vite.config.ts index bdde8fc..09fe0bb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -111,7 +111,7 @@ function addDataLocatorPlugin(): Plugin { export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), '') /* مقصد بک‌اند برای پراکسیِ dev — همهٔ '/api/*' بدون CORS به این‌جا می‌رود. */ - const apiTarget = env.VITE_API_TARGET ?? 'http://localhost:8000' + const apiTarget = env.VITE_API_TARGET ?? 'http://localhost:8001' return { plugins: [