From ad4b770334230f0ffbd6da8860070c8ab6b67b5a Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Sun, 27 Sep 2026 17:04:19 +0330 Subject: [PATCH] feat: add services feature with localized resources, navigation, and management views --- src/components/ui/dialog.tsx | 23 +- src/config/navigation.ts | 7 +- src/features/services/ServicesPage.tsx | 34 +- .../components/CategoriesServiceView.tsx | 939 ++++---- .../services/components/CitiesServiceView.tsx | 401 ++++ .../services/components/DuasServiceView.tsx | 1913 ++++++++++------- .../components/ShrinesServiceView.tsx | 24 +- .../services/services/trip-etiquette-api.ts | 126 +- src/features/services/types/trip-etiquette.ts | 58 +- src/icons.tsx | 2 + src/locales/ar.json | 2 +- src/locales/en.json | 2 +- src/locales/fa.json | 2 +- 13 files changed, 2339 insertions(+), 1194 deletions(-) create mode 100644 src/features/services/components/CitiesServiceView.tsx diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 503e225..b20df6c 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -1,6 +1,7 @@ import * as React from 'react' import * as DialogPrimitive from '@radix-ui/react-dialog' import { cn } from '@/lib/utils' +import { Ic } from '@/icons' const Dialog = DialogPrimitive.Root const DialogTrigger = DialogPrimitive.Trigger @@ -16,19 +17,24 @@ function DialogOverlay({ className, ...props }: React.ComponentProps { + showCloseButton?: boolean +} + function DialogContent({ className, children, + showCloseButton = true, onPointerDownOutside, onInteractOutside, ...props -}: React.ComponentProps) { +}: DialogContentProps) { return ( { @@ -43,6 +49,15 @@ function DialogContent({ {...props} > {children} + {showCloseButton && ( + + + بستن + + )} ) @@ -54,7 +69,7 @@ const DialogDescription = DialogPrimitive.Description function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
) @@ -63,7 +78,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) { function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) { return (
) diff --git a/src/config/navigation.ts b/src/config/navigation.ts index d7c4e7f..758a35f 100644 --- a/src/config/navigation.ts +++ b/src/config/navigation.ts @@ -73,9 +73,14 @@ export function getNavigationConfig(t: (key: string) => string): NavGroupConfig[ to: '/services/categories', icon: 'folder', }, + { + title: t('navigation.cities'), + titleKey: 'navigation.cities', + to: '/services/cities', + icon: 'map', + }, ], }, - { title: t('navigation.shrines'), titleKey: 'navigation.shrines', to: '/services/shrines', icon: 'shrine' }, { title: t('navigation.store'), titleKey: 'navigation.store', diff --git a/src/features/services/ServicesPage.tsx b/src/features/services/ServicesPage.tsx index 0dc4c74..c290269 100644 --- a/src/features/services/ServicesPage.tsx +++ b/src/features/services/ServicesPage.tsx @@ -10,7 +10,7 @@ import { toFa } from '@/lib/utils' import { ZayerGuideServiceView } from './components/ZayerGuideServiceView' import { DuasServiceView } from './components/DuasServiceView' import { CategoriesServiceView } from './components/CategoriesServiceView' -import { ShrinesServiceView } from './components/ShrinesServiceView' +import { CitiesServiceView } from './components/CitiesServiceView' import { FaqServiceView } from './components/FaqServiceView' import { StoreServiceView } from './components/StoreServiceView' @@ -73,6 +73,20 @@ export const SERVICES_LIST: ServiceMeta[] = [ defaultCount: 19, badge: 'مالی و ارزی', }, + { + key: 'cities', + title: 'شهرهای زیارتی', + titleEn: 'Holy Cities', + icon: 'map', + color: 'text-primary', + bgColor: 'bg-primary/10', + borderColor: 'border-primary/20', + description: 'فهرست استاتیک و رسمی شهرهای زیارتی اپلیکیشن عقیله به همراه شناسه‌های ثابت دیتابیس و دسته‌بندی‌های اعمال', + endpoint: '/trip-etiquette/cities/', + statsLabel: 'شهر زیارتی مرجع', + defaultCount: 10, + badge: 'استاتیک و مرجع', + }, { key: 'duas', title: 'ادعیه و زیارات', @@ -101,20 +115,6 @@ export const SERVICES_LIST: ServiceMeta[] = [ defaultCount: 6, badge: 'شهرها و پوشه‌ها', }, - { - key: 'shrines', - title: 'اماکن زیارتی', - titleEn: 'Holy Shrines', - icon: 'shrine', - color: 'text-primary', - bgColor: 'bg-primary/10', - borderColor: 'border-primary/20', - description: 'بانک داینامیک حرم‌ها و اماکن متبرکه، ثبت کشور و شهر، مختصات جغرافیایی و پیوند ادعیه', - endpoint: '/trip-etiquette/shrines/', - statsLabel: 'مکان زیارتی ثبت‌شده', - defaultCount: 3, - badge: 'اماکن و حرم‌ها', - }, { key: 'store', title: 'فروشگاه معصومه', @@ -260,12 +260,14 @@ function ServiceDetailRenderer({ return case 'exchange': return + case 'cities': + return case 'duas': return case 'categories': return case 'shrines': - return + return case 'trip-etiquette': return case 'consultant': diff --git a/src/features/services/components/CategoriesServiceView.tsx b/src/features/services/components/CategoriesServiceView.tsx index 78f1c55..d6b90cf 100644 --- a/src/features/services/components/CategoriesServiceView.tsx +++ b/src/features/services/components/CategoriesServiceView.tsx @@ -4,8 +4,8 @@ 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 { Shimmer, ShimmerBox } from '@/components/shared/shimmer_frontend' -import { toFa } from '@/lib/utils' +import { ShimmerBox, ShimmerLine } from '@/components/shared/shimmer_frontend' +import { toFa, cn } from '@/lib/utils' import { Dialog, DialogContent, @@ -21,20 +21,58 @@ import { deleteCategory, fetchDuas, } from '../services/trip-etiquette-api' -import { fetchCityGuideCountries } from '../services/zayer-guide-api' -import type { CityGuideCountry } from '../types/zayer-guide' +export const PILGRIMAGE_CITIES = [ + { id: 5, name: 'Karbala', nameFa: 'کربلای معلی', flag: '🇮🇶' }, + { id: 6, name: 'Najaf', nameFa: 'نجف اشرف', flag: '🇮🇶' }, + { id: 7, name: 'Koufa', nameFa: 'کوفه', flag: '🇮🇶' }, + { id: 8, name: 'Kazemein', nameFa: 'کاظمین', flag: '🇮🇶' }, + { id: 9, name: 'Samara', nameFa: 'سامرا', flag: '🇮🇶' }, + { id: 16, name: 'Mashhad', nameFa: 'مشهد مقدس', flag: '🇮🇷' }, + { id: 17, name: 'Qom', nameFa: 'قم مقدسه', flag: '🇮🇷' }, + { id: 11, name: 'Madineh', nameFa: 'مدینه منوره', flag: '🇸🇦' }, + { id: 10, name: 'Make', nameFa: 'مکه مکرمه', flag: '🇸🇦' }, +] + +export function getCityDisplay(id?: number | null, rawName?: string): { nameFa: string; flag: string } { + const found = PILGRIMAGE_CITIES.find((c) => c.id === id) + if (found) return { nameFa: found.nameFa, flag: found.flag } + if (rawName) { + const byName = PILGRIMAGE_CITIES.find( + (c) => c.name.toLowerCase() === rawName.toLowerCase() || c.nameFa.includes(rawName) + ) + if (byName) return { nameFa: byName.nameFa, flag: byName.flag } + } + return { nameFa: rawName || 'نامشخص', flag: '📍' } +} + +export function getCategoryCities(cat: CategoryItem): Array<{ id: number; name: string; nameFa: string; flag: string }> { + const list: Array<{ id: number; name: string; nameFa: string; flag: string }> = [] + if (cat.cities && cat.cities.length > 0) { + cat.cities.forEach((c) => { + const info = getCityDisplay(c.id, c.name) + if (!list.some((item) => item.id === c.id)) { + list.push({ id: c.id, name: c.name, nameFa: info.nameFa, flag: info.flag }) + } + }) + } else if (cat.city_info) { + const info = getCityDisplay(cat.city_info.id, cat.city_info.name) + list.push({ id: cat.city_info.id, name: cat.city_info.name, nameFa: info.nameFa, flag: info.flag }) + } else if (cat.city) { + const info = getCityDisplay(cat.city) + list.push({ id: cat.city, name: info.nameFa, nameFa: info.nameFa, flag: info.flag }) + } + return list +} export function CategoriesServiceView() { const [categories, setCategories] = useState([]) const [allDuas, setAllDuas] = useState([]) - const [countries, setCountries] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isRefreshing, setIsRefreshing] = useState(false) const [errorMessage, setErrorMessage] = useState(null) - // فیلترها و مرتب‌سازی + // فیلترها و مرتب‌سازی بر مبنای شهر const [searchTerm, setSearchTerm] = useState('') - const [selectedCountryCode, setSelectedCountryCode] = useState('ALL') const [selectedCityId, setSelectedCityId] = useState('ALL') const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid') @@ -43,12 +81,13 @@ export function CategoriesServiceView() { const [editingCategory, setEditingCategory] = useState(null) const [isSaving, setIsSaving] = useState(false) - // فرم ایجاد/ویرایش - const [formNameFa, setFormNameFa] = useState('') + // فرم ایجاد/ویرایش (۵ زبانه + انتخاب چندگانه شهرها) const [formNameAr, setFormNameAr] = useState('') + const [formNameFa, setFormNameFa] = useState('') const [formNameEn, setFormNameEn] = useState('') - const [formCountryCode, setFormCountryCode] = useState('') - const [formCityId, setFormCityId] = useState('') + const [formNameRu, setFormNameRu] = useState('') + const [formNameId, setFormNameId] = useState('') + const [formSelectedCityIds, setFormSelectedCityIds] = useState([]) const [formSelectedDuaIds, setFormSelectedDuaIds] = useState([]) const [duaFilterSearch, setDuaFilterSearch] = useState('') @@ -64,14 +103,12 @@ export function CategoriesServiceView() { setErrorMessage(null) try { - const [categoriesRes, duasRes, countriesRes] = await Promise.all([ + const [categoriesRes, duasRes] = await Promise.all([ fetchCategories(), fetchDuas(), - fetchCityGuideCountries(), ]) setCategories(categoriesRes) setAllDuas(duasRes) - setCountries(countriesRes) } catch (err: any) { setErrorMessage(err?.message || 'خطا در برقراری ارتباط با وب‌سرویس دسته‌بندی‌های ادعیه.') } finally { @@ -84,62 +121,39 @@ export function CategoriesServiceView() { loadData() }, []) - // لیست کلیه شهرهای در دسترس از کشورها - const allCities = useMemo(() => { - const list: { id: number; name: string; countryCode: string; countryName: string }[] = [] - countries.forEach((country) => { - if (country.city) { - country.city.forEach((c) => { - list.push({ - id: c.id, - name: c.name, - countryCode: country.code, - countryName: country.name, - }) - }) - } - }) - return list - }, [countries]) - - // شهرهای قابل انتخاب بر اساس کشور انتخابی در فیلتر - const filterAvailableCities = useMemo(() => { - if (selectedCountryCode === 'ALL') return allCities - return allCities.filter((c) => c.countryCode.toLowerCase() === selectedCountryCode.toLowerCase()) - }, [allCities, selectedCountryCode]) - - // شهرهای قابل انتخاب در فرم ایجاد/ویرایش - const formAvailableCities = useMemo(() => { - if (!formCountryCode) return allCities - return allCities.filter((c) => c.countryCode.toLowerCase() === formCountryCode.toLowerCase()) - }, [allCities, formCountryCode]) - // فیلتر کردن لیست دسته‌بندی‌ها const filteredCategories = useMemo(() => { return categories.filter((c) => { - // فیلتر کشور - if (selectedCountryCode !== 'ALL') { - const countryCode = c.country_info?.code || '' - if (countryCode.toLowerCase() !== selectedCountryCode.toLowerCase()) return false - } - - // فیلتر شهر + // فیلتر شهر (مبنای شهر، بدون نیاز به فیلتر کشور) if (selectedCityId !== 'ALL') { - if (String(c.city) !== String(selectedCityId)) return false + const catCities = getCategoryCities(c) + if (selectedCityId === 'GENERAL') { + if (catCities.length > 0) return false + } else { + const match = catCities.some((ct) => String(ct.id) === String(selectedCityId)) + if (!match && String(c.city) !== String(selectedCityId)) return false + } } - // فیلتر جستجو در عنوان + // فیلتر جستجو در عنوان یا شهرها if (searchTerm.trim()) { const term = searchTerm.toLowerCase().trim() - const nameFa = (c.name?.fa || c.name_display || '').toLowerCase() const nameAr = (c.name?.ar || '').toLowerCase() + const nameFa = (c.name?.fa || c.name_display || '').toLowerCase() const nameEn = (c.name?.en || '').toLowerCase() - const cityName = (c.city_info?.name || '').toLowerCase() + const nameRu = (c.name?.ru || '').toLowerCase() + const nameId = (c.name?.id || '').toLowerCase() + const catCities = getCategoryCities(c) + const matchCity = catCities.some( + (ct) => ct.name.toLowerCase().includes(term) || ct.nameFa.toLowerCase().includes(term) + ) if ( - !nameFa.includes(term) && !nameAr.includes(term) && + !nameFa.includes(term) && !nameEn.includes(term) && - !cityName.includes(term) + !nameRu.includes(term) && + !nameId.includes(term) && + !matchCity ) { return false } @@ -147,12 +161,16 @@ export function CategoriesServiceView() { return true }) - }, [categories, selectedCountryCode, selectedCityId, searchTerm]) + }, [categories, selectedCityId, searchTerm]) // آمار کلی const totalCategoriesCount = categories.length const coveredCitiesCount = useMemo(() => { - const cityIds = new Set(categories.map((c) => c.city).filter(Boolean)) + const cityIds = new Set() + categories.forEach((c) => { + const list = getCategoryCities(c) + list.forEach((item) => cityIds.add(item.id)) + }) return cityIds.size }, [categories]) @@ -163,11 +181,12 @@ export function CategoriesServiceView() { // باز کردن فرم ایجاد دسته‌بندی جدید const handleOpenCreateDialog = () => { setEditingCategory(null) - setFormNameFa('') setFormNameAr('') + setFormNameFa('') setFormNameEn('') - setFormCountryCode(selectedCountryCode !== 'ALL' ? selectedCountryCode : '') - setFormCityId(selectedCityId !== 'ALL' ? Number(selectedCityId) : '') + setFormNameRu('') + setFormNameId('') + setFormSelectedCityIds(selectedCityId !== 'ALL' && selectedCityId !== 'GENERAL' ? [Number(selectedCityId)] : []) setFormSelectedDuaIds([]) setDuaFilterSearch('') setIsDialogOpen(true) @@ -177,11 +196,46 @@ export function CategoriesServiceView() { const handleOpenEditDialog = (c: CategoryItem) => { setEditingCategory(c) const raw = c.name || {} - setFormNameFa(typeof raw === 'object' ? raw.fa || c.name_display : c.name_display) - setFormNameAr(typeof raw === 'object' ? raw.ar || '' : '') - setFormNameEn(typeof raw === 'object' ? raw.en || '' : '') - setFormCountryCode(c.country_info?.code || '') - setFormCityId(c.city || '') + let arVal = '' + let faVal = '' + let enVal = '' + let ruVal = '' + let idVal = '' + + if (Array.isArray(raw)) { + raw.forEach((item: any) => { + if (item.language_code === 'ar') arVal = item.name || '' + if (item.language_code === 'fa') faVal = item.name || '' + if (item.language_code === 'en') enVal = item.name || '' + if (item.language_code === 'ru') ruVal = item.name || '' + if (item.language_code === 'id') idVal = item.name || '' + }) + } else if (raw && typeof raw === 'object') { + arVal = raw.ar || '' + faVal = raw.fa || '' + enVal = raw.en || '' + ruVal = raw.ru || '' + idVal = raw.id || '' + } + + if (!arVal && !faVal && c.name_display) { + arVal = c.name_display + } + + setFormNameAr(arVal) + setFormNameFa(faVal) + setFormNameEn(enVal) + setFormNameRu(ruVal) + setFormNameId(idVal) + + // جمع‌آوری شناسه‌های شهرهای منتسب + const cityIds: number[] = [] + if (c.cities && c.cities.length > 0) { + c.cities.forEach((ct) => cityIds.push(ct.id)) + } else if (c.city) { + cityIds.push(c.city) + } + setFormSelectedCityIds(cityIds) setFormSelectedDuaIds(c.assigned_dua_ids || []) setDuaFilterSearch('') setIsDialogOpen(true) @@ -226,38 +280,33 @@ export function CategoriesServiceView() { // ذخیره فرم ایجاد/ویرایش دسته‌بندی const handleSaveCategory = async (e: React.FormEvent) => { e.preventDefault() - if (!formNameFa.trim()) { - alert('لطفاً عنوان دسته‌بندی (فارسی) را وارد کنید.') - return - } - if (!formCityId) { - alert('لطفاً شهر مربوطه را انتخاب کنید.') + if (!formNameAr.trim()) { + alert('لطفاً عنوان دسته‌بندی (عربی) را وارد کنید. زبان عربی پیش‌فرض و الزامی است.') return } setIsSaving(true) try { - const nameObj = { - fa: formNameFa.trim(), - ar: formNameAr.trim() || formNameFa.trim(), - en: formNameEn.trim() || formNameFa.trim(), + const nameObj: Record = { + ar: formNameAr.trim(), + fa: formNameFa.trim() || formNameAr.trim(), + en: formNameEn.trim(), + ru: formNameRu.trim(), + id: formNameId.trim(), + } + + const payload: UpdateCategoryInput = { + name: nameObj, + city_ids: formSelectedCityIds, + city: formSelectedCityIds[0] || null, + dua_ids: formSelectedDuaIds, } if (editingCategory) { - const payload: UpdateCategoryInput = { - name: nameObj, - city: Number(formCityId), - dua_ids: formSelectedDuaIds, - } const updated = await updateCategory(editingCategory.id, payload) setCategories((prev) => prev.map((item) => (item.id === updated.id ? updated : item))) } else { - const payload: CreateCategoryInput = { - name: nameObj, - city: Number(formCityId), - dua_ids: formSelectedDuaIds, - } - const created = await createCategory(payload) + const created = await createCategory(payload as CreateCategoryInput) setCategories((prev) => [created, ...prev]) } setIsDialogOpen(false) @@ -310,136 +359,200 @@ export function CategoriesServiceView() { )} {/* ۱. کارت‌های آمار و شاخص‌های کلیدی */} -
-
- کل دسته‌بندی‌های فعال -
- {isLoading ? : toFa(totalCategoriesCount)} +
+
+
+ کل دسته‌بندی‌های فعال +
+ {isLoading ? : toFa(totalCategoriesCount)} +
-
-
- شهرهای تحت پوشش -
- {isLoading ? : toFa(coveredCitiesCount)} +
+
-
- پیوندهای ادعیه به پوشه‌ها -
- {isLoading ? : toFa(totalAssignedDuas)} + +
+
+ شهرهای تحت پوشش +
+ {isLoading ? : toFa(coveredCitiesCount)} +
+
+
+
-
- سلسله‌مراتب ناوبری -
- مستقیم + +
+
+ پیوندهای ادعیه به پوشه‌ها +
+ {isLoading ? : toFa(totalAssignedDuas)} +
+
+
+
{/* ۲. نوار ابزار، فیلترها و دکمه افزودن */} - - - {/* فیلترها و سرچ */} -
- {/* سرچ عنوان */} -
- - setSearchTerm(e.target.value)} - placeholder="جستجوی دسته‌بندی، شهر..." - className="h-9 pr-9 text-xs bg-card border-border-soft rounded-xl" - /> -
- - {/* فیلتر کشور */} - +
+ {/* فیلترها و سرچ */} +
+ {/* سرچ عنوان */} +
+ + setSearchTerm(e.target.value)} + placeholder="جستجو در دسته‌بندی‌ها یا شهرها..." + className="h-9.5 ps-9 pe-8 text-xs bg-card border-border-soft rounded-xl w-full" + /> + {searchTerm && ( + + )} +
- {/* فیلتر شهر */} + {/* فیلتر شهر (مبنای شهر، بدون نیاز به کشور) */} +
- - {/* کلید تغییر حالت نمایش */} -
- - -
+
- {/* دکمه‌های عملیاتی */} -
+ {/* کلید پاک کردن فیلتر */} + {(searchTerm || selectedCityId !== 'ALL') && ( + )} + + {/* کلید تغییر حالت نمایش شبکه‌ای / جدولی */} +
+ + +
+ {/* دکمه پاک کردن فیلترها */} + {(searchTerm || selectedCityId !== 'ALL') && ( -
- - + )} +
+ + {/* دکمه‌های عملیاتی سمت چپ */} +
+ + + +
+
{/* ۳. لیست دسته‌بندی‌ها */} {isLoading ? (
{[1, 2, 3, 4, 5, 6].map((i) => ( - - - - + +
+
+
+ +
+ + +
+
+ +
+
+ +
+
+
+ + + +
))}
@@ -469,43 +582,40 @@ export function CategoriesServiceView() { className="group relative flex flex-col justify-between overflow-hidden border-border-soft bg-card/40 shadow-xs transition duration-200 hover:border-primary/40 hover:bg-card/80" > - {/* هدر کارت: آیکون پوشه، شناسه و شهر */} -
-
-
- -
-
-

- {cat.name_display} -

- - #{cat.id} • {cat.name?.en || ''} - -
+ {/* هدر کارت: آیکون پوشه و عنوان دسته‌بندی */} +
+
+ +
+
+

+ {cat.name?.ar || cat.name_display} +

- - - {cat.city_info?.name || 'شهر نامشخص'} -
- {/* کشور و آمار ادعیه */} -
-
- کشور منتسب: - - {cat.country_info?.name || 'نامشخص'} - -
-
- ادعیه منتسب در اپلیکیشن: - - {toFa(cat.duas_count)} دعا + {/* محدوده جغرافیایی (شهرها) */} +
+
+ + + شهر(ها): +
+ {getCategoryCities(cat).length > 0 ? ( + getCategoryCities(cat).map((ct) => ( + + {ct.flag} {ct.nameFa} + + )) + ) : ( + 🌐 عمومی / سراسری + )} +
@@ -515,10 +625,15 @@ export function CategoriesServiceView() { variant="outline" size="sm" onClick={() => handleOpenQuickAssign(cat)} - className="h-8 rounded-xl text-xs border-border-soft gap-1.5 flex-1 hover:bg-primary/10 hover:text-primary hover:border-primary/30" + className="h-8.5 rounded-xl text-xs font-medium border-border-soft gap-2 flex-1 justify-between px-3 hover:bg-primary/10 hover:text-primary hover:border-primary/30 group/btn transition" > - - مدیریت ادعیه ({toFa(cat.duas_count)}) + + + مدیریت ادعیه + + + {toFa(cat.duas_count)} +
@@ -526,7 +641,7 @@ export function CategoriesServiceView() { variant="ghost" size="sm" onClick={() => handleOpenEditDialog(cat)} - className="size-8 p-0 rounded-lg text-grey-3 hover:text-foreground hover:bg-card" + className="size-8.5 p-0 rounded-xl text-grey-3 hover:text-foreground hover:bg-card border border-transparent hover:border-border-soft" title="ویرایش دسته‌بندی" > @@ -535,7 +650,7 @@ export function CategoriesServiceView() { variant="ghost" size="sm" onClick={() => handleDeleteCategory(cat)} - className="size-8 p-0 rounded-lg text-destructive/70 hover:text-destructive hover:bg-destructive/10" + className="size-8.5 p-0 rounded-xl text-destructive/70 hover:text-destructive hover:bg-destructive/10 border border-transparent hover:border-destructive/20" title="حذف دسته‌بندی" > @@ -554,8 +669,7 @@ export function CategoriesServiceView() { شناسه عنوان دسته‌بندی (پوشه) - شهر - کشور + شهر(ها) تعداد ادعیه عملیات @@ -565,23 +679,41 @@ export function CategoriesServiceView() { #{c.id} -
{c.name_display}
-
{c.name?.en || ''}
+
{c.name?.ar || c.name_display}
+
+ {c.name?.fa && {c.name.fa}} + {c.name?.en && ({c.name.en})} +
+ + +
+ {getCategoryCities(c).length > 0 ? ( + getCategoryCities(c).map((ct) => ( + + {ct.flag} {ct.nameFa} + + )) + ) : ( + 🌐 عمومی / سراسری + )} +
- - - {c.city_info?.name || 'نامشخص'} + + + {toFa(c.duas_count)} دعا - {c.country_info?.name || 'نامشخص'} - {toFa(c.duas_count)}
@@ -589,7 +721,8 @@ export function CategoriesServiceView() { variant="ghost" size="sm" onClick={() => handleOpenEditDialog(c)} - className="size-7 p-0 rounded-lg text-grey-3 hover:text-foreground" + className="size-7.5 p-0 rounded-lg text-grey-3 hover:text-foreground hover:bg-card" + title="ویرایش" > @@ -597,7 +730,8 @@ export function CategoriesServiceView() { variant="ghost" size="sm" onClick={() => handleDeleteCategory(c)} - className="size-7 p-0 rounded-lg text-destructive/70 hover:text-destructive hover:bg-destructive/10" + className="size-7.5 p-0 rounded-lg text-destructive/70 hover:text-destructive hover:bg-destructive/10" + title="حذف" > @@ -612,158 +746,227 @@ export function CategoriesServiceView() { {/* ۴. دیالوگ ایجاد و ویرایش دسته‌بندی */} - - - + + + {editingCategory ? 'ویرایش دسته‌بندی ادعیه' : 'افزودن دسته‌بندی جدید برای شهر'} -
- {/* انتخاب کشور و شهر */} -
-
- - -
+ + {/* بدنه فرم که زیر فوتر اسکرول می‌خورد */} +
+ {/* انتخاب شهرهای مربوطه */} +
+
+ + +
-
- - +
+ {PILGRIMAGE_CITIES.map((city) => { + const isSelected = formSelectedCityIds.includes(city.id) + return ( + + ) + })} +
+

+ امکان انتخاب چند شهر برای هر دسته‌بندی وجود دارد. در صورت انتخاب نکردن شهر، دسته‌بندی به عنوان عمومی/سراسری لحاظ می‌شود. +

-
- - {/* عنوان فارسی */} -
- - setFormNameFa(e.target.value)} - required - className="text-xs bg-card/60 border-border-soft rounded-xl" - /> -
- {/* عنوان عربی و انگلیسی */} -
-
- + {/* ۱. عنوان دسته‌بندی به عربی (زبان پیش‌فرض و الزامی) */} +
+
+ + + پیش‌فرض و الزامی + +
setFormNameAr(e.target.value)} - className="text-xs bg-card/60 border-border-soft rounded-xl" - /> -
-
- - setFormNameEn(e.target.value)} - className="text-xs bg-card/60 border-border-soft rounded-xl font-mono" - dir="ltr" + required + dir="rtl" + className="text-xs bg-card border-border-soft rounded-xl focus:border-primary" />
-
- {/* تخصیص اولیه ادعیه به این دسته‌بندی */} -
-
-