Browse Source
feat(support): add support channels UI with 5 languages, country dial codes, and whatsapp testing
master
feat(support): add support channels UI with 5 languages, country dial codes, and whatsapp testing
master
7 changed files with 764 additions and 220 deletions
-
2.env.example
-
2src/components/layout/ServerStatusDialog.tsx
-
2src/config/navigation.ts
-
227src/features/services/ServicesPage.tsx
-
747src/features/services/components/SupportServiceView.tsx
-
2src/icons.tsx
-
2vite.config.ts
@ -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<ServiceLanguageCode, string> |
|||
descriptions: Record<ServiceLanguageCode, string> |
|||
} |
|||
|
|||
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<SupportChannel[]>(() => { |
|||
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<ServiceLanguageCode>('ar') |
|||
|
|||
// فیلترها
|
|||
const [statusFilter, setStatusFilter] = useState<'ALL' | 'ACTIVE' | 'INACTIVE'>('ALL') |
|||
const [searchQuery, setSearchQuery] = useState('') |
|||
|
|||
// دیالوگ افزودن / ویرایش
|
|||
const [isDialogOpen, setIsDialogOpen] = useState(false) |
|||
const [editingChannel, setEditingChannel] = useState<SupportChannel | null>(null) |
|||
const [dialogLangTab, setDialogLangTab] = useState<ServiceLanguageCode>('ar') |
|||
|
|||
const [formTitles, setFormTitles] = useState<Record<ServiceLanguageCode, string>>({ |
|||
ar: '', fa: '', en: '', ru: '', id: '', |
|||
}) |
|||
const [formDescriptions, setFormDescriptions] = useState<Record<ServiceLanguageCode, string>>({ |
|||
ar: '', fa: '', en: '', ru: '', id: '', |
|||
}) |
|||
const [formCountryDialCode, setFormCountryDialCode] = useState('+44') |
|||
const [formPhoneBody, setFormPhoneBody] = useState('') |
|||
const [formIcon, setFormIcon] = useState('headset') |
|||
const [formOrder, setFormOrder] = useState<number>(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 ( |
|||
<div className="space-y-4"> |
|||
{/* نوار کنترل مینیمال و یکپارچه */} |
|||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 rounded-2xl border border-border-soft bg-card/40 p-3 backdrop-blur-xs"> |
|||
{/* فیلتر وضعیت و پیشنمایش زبان */} |
|||
<div className="flex items-center gap-2 flex-wrap"> |
|||
{/* تبهای وضعیت */} |
|||
<div className="flex items-center rounded-xl border border-border-soft bg-surface-base p-0.5"> |
|||
<button |
|||
onClick={() => setStatusFilter('ALL')} |
|||
className={cn( |
|||
'px-2.5 py-1 rounded-lg text-xs font-bold transition', |
|||
statusFilter === 'ALL' |
|||
? 'bg-card text-foreground shadow-xs' |
|||
: 'text-grey-3 hover:text-foreground' |
|||
)} |
|||
> |
|||
همه ({toFa(channels.length)}) |
|||
</button> |
|||
<button |
|||
onClick={() => setStatusFilter('ACTIVE')} |
|||
className={cn( |
|||
'px-2.5 py-1 rounded-lg text-xs font-bold transition flex items-center gap-1', |
|||
statusFilter === 'ACTIVE' |
|||
? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400' |
|||
: 'text-grey-3 hover:text-foreground' |
|||
)} |
|||
> |
|||
<span className="size-1.5 rounded-full bg-emerald-500" /> |
|||
<span>فعال ({toFa(activeCount)})</span> |
|||
</button> |
|||
<button |
|||
onClick={() => setStatusFilter('INACTIVE')} |
|||
className={cn( |
|||
'px-2.5 py-1 rounded-lg text-xs font-bold transition flex items-center gap-1', |
|||
statusFilter === 'INACTIVE' |
|||
? 'bg-zinc-500/15 text-zinc-400' |
|||
: 'text-grey-3 hover:text-foreground' |
|||
)} |
|||
> |
|||
<span>غیرفعال ({toFa(inactiveCount)})</span> |
|||
</button> |
|||
</div> |
|||
|
|||
<div className="h-5 w-px bg-border-soft hidden sm:block" /> |
|||
|
|||
{/* انتخابگر زبان پیشنمایش ۵ زبانه */} |
|||
<div className="flex items-center gap-1"> |
|||
{SERVICE_LANGUAGES.map((l) => ( |
|||
<button |
|||
key={l.code} |
|||
onClick={() => setPreviewLang(l.code)} |
|||
title={l.name} |
|||
className={cn( |
|||
'flex items-center gap-1 px-2 py-1 rounded-lg text-[11px] font-bold transition', |
|||
previewLang === l.code |
|||
? 'bg-primary text-primary-foreground shadow-xs' |
|||
: 'text-grey-3 hover:text-foreground hover:bg-surface-base' |
|||
)} |
|||
> |
|||
<span>{l.flag}</span> |
|||
<span>{l.badge}</span> |
|||
</button> |
|||
))} |
|||
</div> |
|||
</div> |
|||
|
|||
{/* جستجو و دکمه افزودن */} |
|||
<div className="flex items-center gap-2"> |
|||
<div className="relative min-w-[180px] flex-1 sm:flex-none"> |
|||
<Ic name="search" className="absolute start-2.5 top-1/2 -translate-y-1/2 size-3.5 text-grey-3" /> |
|||
<Input |
|||
value={searchQuery} |
|||
onChange={(e) => setSearchQuery(e.target.value)} |
|||
placeholder="جستجو..." |
|||
className="h-8 ps-8 text-xs bg-surface-base rounded-xl border-border-soft" |
|||
/> |
|||
</div> |
|||
|
|||
<Button |
|||
onClick={handleOpenCreate} |
|||
size="sm" |
|||
className="rounded-xl text-xs gap-1.5 h-8 font-bold bg-primary text-primary-foreground hover:bg-primary/90 shrink-0" |
|||
> |
|||
<Ic name="plus" className="size-3.5" /> |
|||
<span>افزودن</span> |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* کارتهای ساده، خلوت و مدرن کانالها */} |
|||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> |
|||
{filteredChannels.map((channel) => { |
|||
const title = channel.titles[previewLang] || channel.titles.ar || channel.titles.fa |
|||
const desc = channel.descriptions[previewLang] || channel.descriptions.ar |
|||
|
|||
return ( |
|||
<Card |
|||
key={channel.id} |
|||
className={cn( |
|||
'rounded-2xl border transition p-4 flex flex-col justify-between shadow-xs', |
|||
channel.isActive |
|||
? 'border-border-soft bg-card/40 hover:border-primary/40' |
|||
: 'border-dashed border-border-soft/60 bg-card/15 opacity-65' |
|||
)} |
|||
> |
|||
<CardContent className="p-0 space-y-3"> |
|||
{/* ردیف اول: آیکون، عنوان و سوئیچ وضعیت */} |
|||
<div className="flex items-start justify-between gap-2.5"> |
|||
<div className="flex items-center gap-3"> |
|||
<div className="size-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center shrink-0"> |
|||
<Ic name={channel.icon as IconName || 'headset'} className="size-5" /> |
|||
</div> |
|||
<div> |
|||
<h4 className="text-sm font-black text-foreground line-clamp-1"> |
|||
{title} |
|||
</h4> |
|||
<div className="flex items-center gap-2 mt-0.5"> |
|||
<span className="text-[10px] text-grey-3 font-mono"> |
|||
#{toFa(channel.order)} |
|||
</span> |
|||
<span |
|||
className={cn( |
|||
'text-[10px] font-bold', |
|||
channel.isActive ? 'text-emerald-500' : 'text-zinc-500' |
|||
)} |
|||
> |
|||
{channel.isActive ? 'فعال' : 'غیرفعال'} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<Switch |
|||
checked={channel.isActive} |
|||
onCheckedChange={() => handleToggleActive(channel.id)} |
|||
className="data-[state=checked]:bg-emerald-500" |
|||
/> |
|||
</div> |
|||
|
|||
{/* توضیحات مختصر در صورت وجود */} |
|||
{desc && ( |
|||
<p className="text-xs text-grey-3 line-clamp-1 leading-relaxed"> |
|||
{desc} |
|||
</p> |
|||
)} |
|||
|
|||
{/* ردیف شماره تماس و اکشنها */} |
|||
<div className="flex items-center justify-between pt-2.5 border-t border-border-soft text-xs"> |
|||
<button |
|||
onClick={() => handleTestWhatsApp(channel.whatsappNumber || channel.phoneNumber)} |
|||
className="flex items-center gap-1.5 font-mono dir-ltr text-grey-3 hover:text-emerald-500 transition text-[11px]" |
|||
title="ارسال پیام واتساپ" |
|||
> |
|||
<Ic name="whatsapp" className="size-3.5 text-emerald-500 shrink-0" /> |
|||
<span>{channel.phoneNumber}</span> |
|||
</button> |
|||
|
|||
<div className="flex items-center gap-1"> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => handleOpenEdit(channel)} |
|||
className="size-7 p-0 rounded-lg text-primary hover:bg-primary/10" |
|||
title="ویرایش" |
|||
> |
|||
<Ic name="edit" className="size-3.5" /> |
|||
</Button> |
|||
<Button |
|||
variant="ghost" |
|||
size="sm" |
|||
onClick={() => handleDeleteChannel(channel)} |
|||
className="size-7 p-0 rounded-lg text-rose-500 hover:bg-rose-500/10" |
|||
title="حذف" |
|||
> |
|||
<Ic name="trash" className="size-3.5" /> |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
</CardContent> |
|||
</Card> |
|||
) |
|||
})} |
|||
</div> |
|||
|
|||
{/* دیالوگ تمیز افزودن و ویرایش */} |
|||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> |
|||
<DialogContent className="max-w-lg max-h-[85vh] overflow-y-auto rounded-3xl p-5 bg-card border-border-soft"> |
|||
<DialogHeader className="text-right pb-1"> |
|||
<DialogTitle className="text-sm font-black text-foreground"> |
|||
{editingChannel ? 'ویرایش کانال پشتیبانی' : 'افزودن کانال پشتیبانی'} |
|||
</DialogTitle> |
|||
</DialogHeader> |
|||
|
|||
<form onSubmit={handleSaveChannel} className="space-y-4 pt-2"> |
|||
{/* تبهای ۵ زبان با کامپوننت مشترک ServiceTranslation */} |
|||
<ServiceTranslation |
|||
activeLang={dialogLangTab} |
|||
onLangChange={(lang) => 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 ( |
|||
<div className="space-y-2.5"> |
|||
<div className="space-y-1"> |
|||
<label className="text-xs font-bold text-foreground block"> |
|||
عنوان کانال ({currentMeta.name}) |
|||
</label> |
|||
<Input |
|||
value={formTitles[dialogLangTab] || ''} |
|||
onChange={(e) => |
|||
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" |
|||
/> |
|||
</div> |
|||
|
|||
<div className="space-y-1"> |
|||
<label className="text-xs font-medium text-grey-3 block"> |
|||
توضیح کوتاه ({currentMeta.name}) |
|||
</label> |
|||
<Input |
|||
value={formDescriptions[dialogLangTab] || ''} |
|||
onChange={(e) => |
|||
setFormDescriptions({ ...formDescriptions, [dialogLangTab]: e.target.value }) |
|||
} |
|||
dir={currentMeta.dir} |
|||
placeholder="توضیح کوتاه اختیاری..." |
|||
className="h-9 text-xs bg-surface-base rounded-xl border-border-soft" |
|||
/> |
|||
</div> |
|||
</div> |
|||
) |
|||
})()} |
|||
|
|||
{/* شماره تماس همراه با انتخابگر کشور و پرچم + دکمه تست مستقیم واتساپ */} |
|||
<div className="space-y-3"> |
|||
<div className="space-y-1.5"> |
|||
<div className="flex items-center justify-between"> |
|||
<label className="text-xs font-bold text-foreground flex items-center gap-1"> |
|||
<span>شماره تماس و واتساپ</span> |
|||
<span className="text-rose-500">*</span> |
|||
</label> |
|||
|
|||
{/* دکمه اختصاصی باز کردن و تست مستقیم واتساپ */} |
|||
<button |
|||
type="button" |
|||
onClick={handleTestWhatsAppInModal} |
|||
className="h-7 px-2.5 rounded-lg text-[11px] font-bold bg-emerald-600 hover:bg-emerald-500 text-white flex items-center gap-1.5 transition active:scale-95 shadow-xs" |
|||
title="باز کردن مستقیم چت در واتساپ" |
|||
> |
|||
<Ic name="whatsapp" className="size-3.5 text-white" /> |
|||
<span>تست مستقیم واتساپ</span> |
|||
</button> |
|||
</div> |
|||
|
|||
{/* کامپوننت ترکیبی انتخاب کشور با پرچم + اینپوت شماره */} |
|||
<div className="flex items-center rounded-xl border border-border-soft bg-surface-base overflow-hidden focus-within:border-primary transition"> |
|||
{/* سلکتور پیششماره کشور همراه با پرچم */} |
|||
<div className="relative shrink-0 border-l border-border-soft bg-card/60"> |
|||
<select |
|||
value={formCountryDialCode} |
|||
onChange={(e) => setFormCountryDialCode(e.target.value)} |
|||
className="h-9 bg-transparent px-2.5 text-xs font-mono font-bold text-foreground border-none outline-none cursor-pointer appearance-none pr-7 pl-2" |
|||
dir="ltr" |
|||
> |
|||
{COUNTRY_DIAL_CODES.map((c) => ( |
|||
<option key={c.code} value={c.dialCode} className="bg-card text-foreground"> |
|||
{c.flag} {c.dialCode} ({c.name}) |
|||
</option> |
|||
))} |
|||
</select> |
|||
<Ic name="chevDown" className="size-3 text-grey-3 absolute end-2 top-1/2 -translate-y-1/2 pointer-events-none" /> |
|||
</div> |
|||
|
|||
{/* فیلد وارد کردن شماره تماس */} |
|||
<Input |
|||
value={formPhoneBody} |
|||
onChange={(e) => 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 |
|||
/> |
|||
</div> |
|||
|
|||
{/* پیشنمایش زنده شماره */} |
|||
<div className="flex items-center justify-between text-[11px] text-grey-3 px-1"> |
|||
<span>شماره نهایی:</span> |
|||
<span className="font-mono font-bold text-emerald-500 dir-ltr"> |
|||
{formCountryDialCode} {formPhoneBody || '---'} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* ترتیب نمایش */} |
|||
<div className="space-y-1"> |
|||
<label className="text-xs font-bold text-foreground block"> |
|||
ترتیب نمایش در اپلیکیشن |
|||
</label> |
|||
<Input |
|||
type="number" |
|||
min="1" |
|||
value={formOrder} |
|||
onChange={(e) => setFormOrder(parseInt(e.target.value) || 1)} |
|||
className="h-9 text-xs bg-surface-base rounded-xl border-border-soft font-mono" |
|||
/> |
|||
</div> |
|||
</div> |
|||
|
|||
{/* آیکون و وضعیت فعال */} |
|||
<div className="space-y-2"> |
|||
<label className="text-xs font-bold text-foreground block"> |
|||
انتخاب آیکون |
|||
</label> |
|||
<div className="grid grid-cols-7 gap-1.5"> |
|||
{AVAILABLE_ICONS.map((item) => ( |
|||
<button |
|||
key={item.id} |
|||
type="button" |
|||
onClick={() => setFormIcon(item.id)} |
|||
className={cn( |
|||
'p-2 rounded-xl border transition flex items-center justify-center', |
|||
formIcon === item.id |
|||
? 'border-primary bg-primary/10 text-primary' |
|||
: 'border-border-soft bg-surface-base text-grey-3 hover:text-foreground' |
|||
)} |
|||
title={item.label} |
|||
> |
|||
<Ic name={item.icon} className="size-4" /> |
|||
</button> |
|||
))} |
|||
</div> |
|||
</div> |
|||
|
|||
<div className="rounded-xl border border-border-soft bg-surface-base p-2.5 flex items-center justify-between"> |
|||
<span className="text-xs font-bold text-foreground"> |
|||
وضعیت فعال در اپلیکیشن |
|||
</span> |
|||
<Switch |
|||
checked={formIsActive} |
|||
onCheckedChange={setFormIsActive} |
|||
className="data-[state=checked]:bg-emerald-500" |
|||
/> |
|||
</div> |
|||
|
|||
<DialogFooter className="flex items-center justify-between gap-2 pt-2 border-t border-border-soft"> |
|||
<Button |
|||
type="button" |
|||
variant="outline" |
|||
onClick={() => setIsDialogOpen(false)} |
|||
className="rounded-xl text-xs h-8" |
|||
> |
|||
انصراف |
|||
</Button> |
|||
<Button |
|||
type="submit" |
|||
className="rounded-xl text-xs h-8 bg-primary text-primary-foreground hover:bg-primary/90 font-bold" |
|||
> |
|||
ذخیره |
|||
</Button> |
|||
</DialogFooter> |
|||
</form> |
|||
</DialogContent> |
|||
</Dialog> |
|||
</div> |
|||
) |
|||
} |
|||
export default SupportServiceView |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue