diff --git a/src/features/services/ServicesPage.tsx b/src/features/services/ServicesPage.tsx
index b6ea3cf..f95a8d3 100644
--- a/src/features/services/ServicesPage.tsx
+++ b/src/features/services/ServicesPage.tsx
@@ -8,6 +8,7 @@ import { Badge } from '@/components/ui/badge'
import { Shimmer } from '@/components/shared/Shimmer'
import { toFa } from '@/lib/utils'
import { apiFetch } from '@/services/http'
+import { ZayerGuideServiceView } from './components/ZayerGuideServiceView'
// تعریف ساختار دادههای سرویسها
export interface ServiceMeta {
@@ -547,112 +548,6 @@ function SimcardServiceView({ service }: { service: ServiceMeta }) {
)
}
-// ─────────────────────────────────────────────────────────────────────────────
-// ۲. راهنمای زائر (Zayer Guide)
-// ─────────────────────────────────────────────────────────────────────────────
-function ZayerGuideServiceView({ service }: { service: ServiceMeta }) {
- const [cityFilter, setCityFilter] = useState('ALL')
-
- const places = [
- { id: 1, title: 'حرم مطهر امام علی (ع)', city: 'نجف اشرف', category: 'اماکن مقدس', rating: '5.0', visits: 18400, country: 'عراق' },
- { id: 2, title: 'حرم مطهر امام حسین (ع) و حضرت عباس (ع)', city: 'کربلای معلی', category: 'اماکن مقدس', rating: '5.0', visits: 24500, country: 'عراق' },
- { id: 3, title: 'مسجد کوفه و مسجد سهله', city: 'کوفه', category: 'مساجد تاریخی', rating: '4.9', visits: 9800, country: 'عراق' },
- { id: 4, title: 'مسجد الحرام و کعبه مشرفه', city: 'مکه مکرمه', category: 'اماکن مقدس', rating: '5.0', visits: 32000, country: 'عربستان' },
- { id: 5, title: 'مسجد النبی (ص) و بقیع', city: 'مدینه منوره', category: 'اماکن مقدس', rating: '5.0', visits: 29000, country: 'عربستان' },
- { id: 6, title: 'حرم مطهر حضرت زینب (س)', city: 'دمشق', category: 'اماکن مقدس', rating: '4.9', visits: 6400, country: 'سوریه' },
- ]
-
- const filtered = places.filter((p) => {
- if (cityFilter !== 'ALL' && !p.city.includes(cityFilter)) return false
- return true
- })
-
- return (
-
- {/* آمار راهنمای زائر */}
-
-
-
اماکن و موقعیتهای ثبتشده
-
{toFa(142)}
-
-
-
مواکب و مراکز امدادی فعال
-
{toFa(85)}
-
-
-
بازدید ماهانه زائرین از نقشهها
-
{toFa('58.4K')}
-
-
-
دستهبندیها
-
{toFa(8)}
-
-
-
- {/* فیلتر شهرها */}
-
-
-
-
-
-
-
-
- {/* لیست کارتهای مکانها */}
-
- {filtered.map((item) => (
-
-
- {item.city} ({item.country})
- ★ {item.rating}
-
-
{item.title}
-
- دسته: {item.category}
- {toFa(item.visits)} بازدید
-
-
- ))}
-
-
- )
-}
-
// ─────────────────────────────────────────────────────────────────────────────
// ۳. صرافی آنلاین و تشریفات (Online Exchange)
// ─────────────────────────────────────────────────────────────────────────────
diff --git a/src/features/services/components/CreateLocationDialog.tsx b/src/features/services/components/CreateLocationDialog.tsx
new file mode 100644
index 0000000..6310ef8
--- /dev/null
+++ b/src/features/services/components/CreateLocationDialog.tsx
@@ -0,0 +1,425 @@
+import React, { useState, useEffect } from 'react'
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+ DialogClose,
+} from '@/components/ui/dialog'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Switch } from '@/components/ui/switch'
+import { Ic } from '@/icons'
+import type {
+ CityGuideItem,
+ CityGuideCategory,
+ CityGuideCountry,
+ CreateCityGuideInput,
+} from '../types/zayer-guide'
+
+interface CreateLocationDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ categories: CityGuideCategory[]
+ countries: CityGuideCountry[]
+ editingLocation?: CityGuideItem | null
+ onSave: (data: CreateCityGuideInput, editingId?: number) => void
+}
+
+const WEEK_DAYS = [
+ { code: 'SA', label: 'شنبه' },
+ { code: 'SU', label: 'یکشنبه' },
+ { code: 'MO', label: 'دوشنبه' },
+ { code: 'TU', label: 'سهشنبه' },
+ { code: 'WE', label: 'چهارشنبه' },
+ { code: 'TH', label: 'پنجشنبه' },
+ { code: 'FR', label: 'جمعه' },
+]
+
+export function CreateLocationDialog({
+ open,
+ onOpenChange,
+ categories,
+ countries,
+ editingLocation,
+ onSave,
+}: CreateLocationDialogProps) {
+ const [title, setTitle] = useState('')
+ const [description, setDescription] = useState('')
+ const [address, setAddress] = useState('')
+ const [phoneNumber, setPhoneNumber] = useState('')
+ const [categoryId, setCategoryId] = useState(categories[0]?.id || 1)
+ const [countryId, setCountryId] = useState(countries[0]?.id || 1)
+ const [cityId, setCityId] = useState(1)
+ const [workingHoursFrom, setWorkingHoursFrom] = useState('08:00')
+ const [workingHoursTo, setWorkingHoursTo] = useState('23:00')
+ const [daysOff, setDaysOff] = useState([])
+ const [latitude, setLatitude] = useState('32.6160')
+ const [longitude, setLongitude] = useState('44.0244')
+ const [isActive, setIsActive] = useState(true)
+ const [imageUrl, setImageUrl] = useState('')
+ const [validationError, setValidationError] = useState(null)
+
+ // لیست شهرهای کشور انتخابشده
+ const currentCountry = countries.find((c) => c.id === countryId) || countries[0]
+ const availableCities = currentCountry?.city || []
+
+ useEffect(() => {
+ if (editingLocation) {
+ setTitle(editingLocation.title || '')
+ setDescription(editingLocation.description || '')
+ setAddress(editingLocation.address || '')
+ setPhoneNumber(editingLocation.phone_number || '')
+ setCategoryId(editingLocation.category?.id || categories[0]?.id || 1)
+ setCountryId(editingLocation.country?.id || countries[0]?.id || 1)
+ setCityId(editingLocation.city?.id || 1)
+ setWorkingHoursFrom(editingLocation.working_hours_from || '08:00')
+ setWorkingHoursTo(editingLocation.working_hours_to || '23:00')
+
+ const parsedDays = Array.isArray(editingLocation.days_off)
+ ? editingLocation.days_off
+ : typeof editingLocation.days_off === 'string'
+ ? (editingLocation.days_off as string).split(',').map((s) => s.trim()).filter(Boolean)
+ : []
+ setDaysOff(parsedDays)
+
+ setLatitude(String(editingLocation.latitude || '32.6160'))
+ setLongitude(String(editingLocation.longitude || '44.0244'))
+ setIsActive(editingLocation.is_active ?? true)
+
+ const img =
+ (typeof editingLocation.image?.image_url === 'object'
+ ? editingLocation.image?.image_url?.original
+ : editingLocation.image?.image_url) || ''
+ setImageUrl(typeof img === 'string' ? img : '')
+ } else {
+ // مقادیر پیشفرض برای ایجاد مکان جدید
+ setTitle('')
+ setDescription('')
+ setAddress('')
+ setPhoneNumber('')
+ setCategoryId(categories[0]?.id || 1)
+ const firstCountry = countries[0]
+ if (firstCountry) {
+ setCountryId(firstCountry.id)
+ if (firstCountry.city && firstCountry.city[0]) {
+ setCityId(firstCountry.city[0].id)
+ }
+ }
+ setWorkingHoursFrom('08:00')
+ setWorkingHoursTo('23:00')
+ setDaysOff([])
+ setLatitude('32.6160')
+ setLongitude('44.0244')
+ setIsActive(true)
+ setImageUrl('')
+ }
+ setValidationError(null)
+ }, [editingLocation, open, categories, countries])
+
+ // بهروزرسانی شهر پیشفرض هنگام تغییر کشور
+ const handleCountryChange = (newCountryId: number) => {
+ setCountryId(newCountryId)
+ const targetCountry = countries.find((c) => c.id === newCountryId)
+ if (targetCountry && targetCountry.city && targetCountry.city.length > 0) {
+ setCityId(targetCountry.city[0].id)
+ }
+ }
+
+ const toggleDayOff = (dayCode: string) => {
+ setDaysOff((prev) =>
+ prev.includes(dayCode) ? prev.filter((d) => d !== dayCode) : [...prev, dayCode]
+ )
+ }
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!title.trim()) {
+ setValidationError('لطفاً عنوان مکان را وارد کنید.')
+ return
+ }
+
+ const latNum = parseFloat(latitude)
+ const lngNum = parseFloat(longitude)
+ if (isNaN(latNum) || isNaN(lngNum)) {
+ setValidationError('لطفاً مختصات جغرافیایی معتبر (عدد اعشاری) وارد کنید.')
+ return
+ }
+
+ const inputData: CreateCityGuideInput = {
+ title: title.trim(),
+ description: description.trim(),
+ address: address.trim(),
+ phone_number: phoneNumber.trim(),
+ category_id: categoryId,
+ country_id: countryId,
+ city_id: cityId,
+ working_hours_from: workingHoursFrom,
+ working_hours_to: workingHoursTo,
+ days_off: daysOff,
+ latitude: latNum,
+ longitude: lngNum,
+ is_active: isActive,
+ image_url: imageUrl.trim() || undefined,
+ }
+
+ onSave(inputData, editingLocation?.id)
+ onOpenChange(false)
+ }
+
+ return (
+
+ )
+}
diff --git a/src/features/services/components/LocationDetailSheet.tsx b/src/features/services/components/LocationDetailSheet.tsx
new file mode 100644
index 0000000..6769e93
--- /dev/null
+++ b/src/features/services/components/LocationDetailSheet.tsx
@@ -0,0 +1,251 @@
+import React from 'react'
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+ SheetDescription,
+} from '@/components/ui/sheet'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import { Switch } from '@/components/ui/switch'
+import { Ic } from '@/icons'
+import type { CityGuideItem } from '../types/zayer-guide'
+
+interface LocationDetailSheetProps {
+ location: CityGuideItem | null
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ onToggleStatus: (id: number, currentStatus: boolean) => void
+ onEdit: (location: CityGuideItem) => void
+}
+
+const DAY_LABELS: Record = {
+ SU: 'یکشنبه',
+ MO: 'دوشنبه',
+ TU: 'سهشنبه',
+ WE: 'چهارشنبه',
+ TH: 'پنجشنبه',
+ FR: 'جمعه',
+ SA: 'شنبه',
+}
+
+export function LocationDetailSheet({
+ location,
+ open,
+ onOpenChange,
+ onToggleStatus,
+ onEdit,
+}: LocationDetailSheetProps) {
+ if (!location) return null
+
+ const daysOffList = Array.isArray(location.days_off)
+ ? location.days_off
+ : typeof location.days_off === 'string'
+ ? (location.days_off as string).split(',').map((s) => s.trim()).filter(Boolean)
+ : []
+
+ const is24Hours =
+ (location.working_hours_from === '00:00' || location.working_hours_from === '00:00:00') &&
+ (location.working_hours_to === '24:00' || location.working_hours_to === '23:59:59' || location.working_hours_to === '24:00:00')
+
+ const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${location.latitude},${location.longitude}`
+
+ const imageUrl =
+ (typeof location.image?.image_url === 'object'
+ ? location.image?.image_url?.original || location.image?.image_url?.lg || location.image?.image_url?.md || location.image?.image_url?.sm
+ : location.image?.image_url) || null
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+ {location.title}
+
+
+
+ ID: #{location.id} | Slug: {location.slug}
+
+
+
+
+
+ {location.is_active ? 'فعال در سامانه' : 'غیرفعال / پنهان'}
+
+
+
+
+
+
+ {/* تصویر شاخص مکان در صورت وجود */}
+ {imageUrl && (
+
+

+
+ )}
+
+ {/* نوار وضعیت تعاملی و عملیات سریع */}
+
+
+
onToggleStatus(location.id, location.is_active ?? true)}
+ />
+
+
وضعیت نمایش در اپلیکیشن
+
+ {location.is_active ? 'این مکان هماکنون برای زائرین قابل مشاهده است' : 'این مکان از دید زائرین مخفی شده است'}
+
+
+
+
+
+
+
+ {/* کارتهای شاخص (دستهبندی، شهر، امتیاز) */}
+
+
+
دستهبندی
+
+ {location.category?.name || 'عمومی'}
+
+
+
+
+
کشور و شهر
+
+ {location.country?.icon}
+ {location.city?.name || 'نامشخص'}
+
+
+
+
+
امتیاز زائرین
+
+ ★
+ {location.average_score ? location.average_score.toFixed(1) : '5.0'}
+
+
+
+
+ {/* شرح و توضیحات کامل */}
+ {location.description && (
+
+
درباره این مکان
+
+ {location.description}
+
+
+ )}
+
+ {/* اطلاعات ارتباطی و ساعات فعالیت */}
+
+
اطلاعات فعالیت و تماس
+
+
+
+ آدرس دقیق:
+
+ {location.address || 'آدرس ثبت نشده است.'}
+
+
+
+
+
+
+ ساعات کاری:
+
+ {is24Hours ? (
+ ۲۴ ساعته (شبانهروزی)
+ ) : (
+
+ {location.working_hours_from || '08:00'} - {location.working_hours_to || '23:00'}
+
+ )}
+
+
+
+
+ روزهای تعطیل:
+
+ {daysOffList.length === 0 ? (
+ بدون تعطیلی (همهروزه باز)
+ ) : (
+ daysOffList.map((d) => DAY_LABELS[d] || d).join('، ')
+ )}
+
+
+
+
+
+ {/* موقعیت مکانی و ناوبری */}
+
+
+
+
+
+
عرض جغرافیایی (Latitude)
+
+ {location.latitude}
+
+
+
+
طول جغرافیایی (Longitude)
+
+ {location.longitude}
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/features/services/components/ZayerGuideServiceView.tsx b/src/features/services/components/ZayerGuideServiceView.tsx
new file mode 100644
index 0000000..864fee7
--- /dev/null
+++ b/src/features/services/components/ZayerGuideServiceView.tsx
@@ -0,0 +1,782 @@
+import React, { useState, useEffect, useMemo } from 'react'
+import { Ic } 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 { Checkbox } from '@/components/ui/checkbox'
+import { Shimmer } from '@/components/shared/Shimmer'
+import { toFa } from '@/lib/utils'
+import type { ServiceMeta } from '../ServicesPage'
+import type {
+ CityGuideItem,
+ CityGuideCategory,
+ CityGuideCountry,
+ CreateCityGuideInput,
+} from '../types/zayer-guide'
+import {
+ fetchCityGuides,
+ fetchCityGuideCategories,
+ fetchCityGuideCountries,
+ toggleCityGuideStatusLocal,
+ bulkUpdateCityGuidesStatusLocal,
+ bulkDeleteCityGuidesLocal,
+ saveCityGuideItemLocal,
+} from '../services/zayer-guide-api'
+import { LocationDetailSheet } from './LocationDetailSheet'
+import { CreateLocationDialog } from './CreateLocationDialog'
+
+interface ZayerGuideServiceViewProps {
+ service: ServiceMeta
+}
+
+export function ZayerGuideServiceView({ service }: ZayerGuideServiceViewProps) {
+ const [locations, setLocations] = useState([])
+ const [categories, setCategories] = 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 [selectedCitySlug, setSelectedCitySlug] = useState('ALL')
+ const [selectedCountryCode, setSelectedCountryCode] = useState('ALL')
+ const [selectedCategorySlug, setSelectedCategorySlug] = useState('ALL')
+ const [statusFilter, setStatusFilter] = useState<'ALL' | 'ACTIVE' | 'INACTIVE'>('ALL')
+ const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid')
+
+ // عملیات دستهجمعی (Bulk Actions)
+ const [selectedIds, setSelectedIds] = useState>(new Set())
+
+ // وضعیت شیت جزئیات و دیالوگ ایجاد/ویرایش
+ const [detailLocation, setDetailLocation] = useState(null)
+ const [isDetailOpen, setIsDetailOpen] = useState(false)
+ const [editingLocation, setEditingLocation] = useState(null)
+ const [isCreateOpen, setIsCreateOpen] = useState(false)
+
+ // بارگذاری دادهها از وبسرویس و حافظه پایدار
+ const loadData = async (isManualRefresh = false) => {
+ if (isManualRefresh) setIsRefreshing(true)
+ else setIsLoading(true)
+ setErrorMessage(null)
+
+ try {
+ const [placesRes, catsRes, countriesRes] = await Promise.all([
+ fetchCityGuides({ show_all: true }),
+ fetchCityGuideCategories(),
+ fetchCityGuideCountries(),
+ ])
+
+ setLocations(placesRes)
+ setCategories(catsRes)
+ setCountries(countriesRes)
+ } catch (err: any) {
+ setErrorMessage(err?.message || 'خطا در برقراری ارتباط با وبسرویس راهنمای زائر.')
+ } finally {
+ setIsLoading(false)
+ setIsRefreshing(false)
+ }
+ }
+
+ useEffect(() => {
+ loadData()
+ }, [])
+
+ // لیست کلیه شهرهای موجود از کشورها
+ const allCitiesList = useMemo(() => {
+ const list: { id: number; name: string; slug: string; countryCode: string; countryName: string; countryIcon: string }[] = []
+ countries.forEach((country) => {
+ if (country.city) {
+ country.city.forEach((c) => {
+ list.push({
+ id: c.id,
+ name: c.name,
+ slug: c.slug,
+ countryCode: country.code,
+ countryName: country.name,
+ countryIcon: country.icon || '📍',
+ })
+ })
+ }
+ })
+ return list
+ }, [countries])
+
+ // فیلتر و جستجوی کلاینت
+ const filteredLocations = useMemo(() => {
+ return locations.filter((loc) => {
+ // ۱. فیلتر وضعیت
+ const isActive = loc.is_active ?? true
+ if (statusFilter === 'ACTIVE' && !isActive) return false
+ if (statusFilter === 'INACTIVE' && isActive) return false
+
+ // ۲. فیلتر شهر
+ if (selectedCitySlug !== 'ALL') {
+ const citySlug = loc.city?.slug?.toLowerCase()
+ if (citySlug !== selectedCitySlug.toLowerCase()) return false
+ }
+
+ // ۳. فیلتر کشور
+ if (selectedCountryCode !== 'ALL') {
+ const cCode = loc.country?.code?.toUpperCase()
+ if (cCode !== selectedCountryCode.toUpperCase()) return false
+ }
+
+ // ۴. فیلتر دستهبندی
+ if (selectedCategorySlug !== 'ALL') {
+ const catSlug = loc.category?.slug
+ if (catSlug !== selectedCategorySlug) return false
+ }
+
+ // ۵. جستجوی متنی
+ if (searchTerm.trim()) {
+ const q = searchTerm.toLowerCase().trim()
+ const titleMatch = loc.title.toLowerCase().includes(q)
+ const descMatch = (loc.description || '').toLowerCase().includes(q)
+ const addrMatch = (loc.address || '').toLowerCase().includes(q)
+ const phoneMatch = (loc.phone_number || '').includes(q)
+ if (!titleMatch && !descMatch && !addrMatch && !phoneMatch) return false
+ }
+
+ return true
+ })
+ }, [locations, statusFilter, selectedCitySlug, selectedCountryCode, selectedCategorySlug, searchTerm])
+
+ // آمار کلان
+ const stats = useMemo(() => {
+ const total = locations.length
+ const active = locations.filter((l) => l.is_active ?? true).length
+ const inactive = total - active
+ const citiesCount = new Set(locations.map((l) => l.city?.slug).filter(Boolean)).size
+ const categoriesCount = new Set(locations.map((l) => l.category?.slug).filter(Boolean)).size
+
+ return {
+ total,
+ active,
+ inactive,
+ citiesCount,
+ categoriesCount,
+ }
+ }, [locations])
+
+ // تغییر سوییچ وضعیت فعال/غیرفعال برای یک مکان
+ const handleToggleStatus = (id: number, currentStatus: boolean) => {
+ const newStatus = !currentStatus
+ toggleCityGuideStatusLocal(id, newStatus)
+ setLocations((prev) =>
+ prev.map((loc) => (loc.id === id ? { ...loc, is_active: newStatus } : loc))
+ )
+ if (detailLocation?.id === id) {
+ setDetailLocation((prev) => (prev ? { ...prev, is_active: newStatus } : null))
+ }
+ }
+
+ // عملیات انتخاب چندگانه (Multi-Select)
+ const handleSelectAll = (checked: boolean) => {
+ if (checked) {
+ setSelectedIds(new Set(filteredLocations.map((l) => l.id)))
+ } else {
+ setSelectedIds(new Set())
+ }
+ }
+
+ const handleToggleSelectItem = (id: number) => {
+ setSelectedIds((prev) => {
+ const next = new Set(prev)
+ if (next.has(id)) next.delete(id)
+ else next.add(id)
+ return next
+ })
+ }
+
+ // عملیات دستهجمعی: فعالسازی
+ const handleBulkActivate = () => {
+ const ids = Array.from(selectedIds)
+ if (ids.length === 0) return
+ bulkUpdateCityGuidesStatusLocal(ids, true)
+ setLocations((prev) =>
+ prev.map((loc) => (selectedIds.has(loc.id) ? { ...loc, is_active: true } : loc))
+ )
+ setSelectedIds(new Set())
+ }
+
+ // عملیات دستهجمعی: غیرفعالسازی
+ const handleBulkDeactivate = () => {
+ const ids = Array.from(selectedIds)
+ if (ids.length === 0) return
+ bulkUpdateCityGuidesStatusLocal(ids, false)
+ setLocations((prev) =>
+ prev.map((loc) => (selectedIds.has(loc.id) ? { ...loc, is_active: false } : loc))
+ )
+ setSelectedIds(new Set())
+ }
+
+ // عملیات دستهجمعی: حذف
+ const handleBulkDelete = () => {
+ const ids = Array.from(selectedIds)
+ if (ids.length === 0) return
+ if (!window.confirm(`آیا از حذف ${toFa(ids.length)} مکان انتخابشده از سامانه اطمینان دارید؟`)) {
+ return
+ }
+ bulkDeleteCityGuidesLocal(ids)
+ setLocations((prev) => prev.filter((loc) => !selectedIds.has(loc.id)))
+ setSelectedIds(new Set())
+ }
+
+ // حذف تکمکان
+ const handleDeleteSingle = (id: number, title: string) => {
+ if (!window.confirm(`آیا از حذف مکان «${title}» اطمینان دارید؟`)) return
+ bulkDeleteCityGuidesLocal([id])
+ setLocations((prev) => prev.filter((loc) => loc.id !== id))
+ if (detailLocation?.id === id) {
+ setIsDetailOpen(false)
+ setDetailLocation(null)
+ }
+ }
+
+ // ذخیره مکان جدید یا ویرایششده
+ const handleSaveLocation = (input: CreateCityGuideInput, editingId?: number) => {
+ const saved = saveCityGuideItemLocal(input, categories, countries, editingId)
+ if (editingId) {
+ setLocations((prev) => prev.map((loc) => (loc.id === editingId ? saved : loc)))
+ } else {
+ setLocations((prev) => [saved, ...prev])
+ }
+ }
+
+ const isAllSelected =
+ filteredLocations.length > 0 && selectedIds.size === filteredLocations.length
+
+ return (
+
+ {/* ─────────────────────────────────────────────────────────────────────────────
+ ۱. سربرگ آماری کلان (KPI Cards) با سیستم رنگهای خنثی
+ ───────────────────────────────────────────────────────────────────────────── */}
+
+
+
+ کل اماکن و مراکز ثبتشده
+
+ {toFa(stats.total)}
+
+ بانک اطلاعاتی جامع زائر
+
+
+
+
+
+ اماکن فعال در اپلیکیشن
+
+ {toFa(stats.active)}
+
+
+ {stats.total > 0 ? `${toFa(Math.round((stats.active / stats.total) * 100))}% پوشش فعال` : '-'}
+
+
+
+
+
+
+ شهرهای زیارتی تحت پوشش
+
+ {toFa(stats.citiesCount || allCitiesList.length)}
+
+ عراق، عربستان، ایران، سوریه
+
+
+
+
+
+ دستهبندیهای خدماتی
+
+ {toFa(categories.length || stats.categoriesCount || 8)}
+
+ مساجد، درمانی، تاریخی و...
+
+
+
+
+ {/* ─────────────────────────────────────────────────────────────────────────────
+ ۲. نوار جستجو، فیلتر یکپارچه شهر و کشور، دستهبندی و وضعیت
+ ───────────────────────────────────────────────────────────────────────────── */}
+
+
+ {/* اینپوت جستجو */}
+
+
+ setSearchTerm(e.target.value)}
+ placeholder="جستجوی مکان، آدرس، تلفن..."
+ className="h-8 pr-8 text-xs bg-surface-base"
+ />
+ {searchTerm && (
+
+ )}
+
+
+ {/* دراپداون یکپارچه شهرها و کشورها */}
+
+
+ {/* فیلتر دستهبندی */}
+
+
+ {/* فیلتر وضعیت فعال / غیرفعال */}
+
+
+
+ {/* دکمههای سوئیچ نما، تازهسازی و افزودن مکان */}
+
+ {/* سوئیچ نمایش گرید / جدول */}
+
+
+
+
+
+
+
+
+
+
+
+ {/* ─────────────────────────────────────────────────────────────────────────────
+ ۴. نوار ابزار عملیات دستهجمعی (Bulk Actions Toolbar)
+ ───────────────────────────────────────────────────────────────────────────── */}
+ {selectedIds.size > 0 && (
+
+
+
+ {toFa(selectedIds.size)}
+
+ مکان انتخابشده برای عملیات گروهی
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* ─────────────────────────────────────────────────────────────────────────────
+ ۵. نمایش لیست دادهها (Shimmer Loading / Grid / Table / Empty State)
+ ───────────────────────────────────────────────────────────────────────────── */}
+ {isLoading ? (
+
+ ) : errorMessage ? (
+
+
+
{errorMessage}
+
+
+ ) : filteredLocations.length === 0 ? (
+
+
+
مکانی با فیلترهای انتخابی یافت نشد.
+
+ میتوانید فیلترها را ریست کنید یا با کلیک روی «افزودن مکان جدید» موقعیت مکانی جدیدی به سامانه بیفزایید.
+
+
+
+ ) : viewMode === 'grid' ? (
+ /* نمای گرید کارتها */
+
+ {filteredLocations.map((item) => {
+ const isSelected = selectedIds.has(item.id)
+ const isActive = item.is_active ?? true
+ return (
+
+ {/* سربرگ کارت: چکباکس انتخاب، سوییچ وضعیت و بجها */}
+
+
+ handleToggleSelectItem(item.id)}
+ />
+
+ {item.country?.icon}
+ {item.city?.name || 'نامشخص'}
+
+
+
+
+
+ handleToggleStatus(item.id, isActive)}
+ />
+
+ {isActive ? 'فعال' : 'خاموش'}
+
+
+
+
+
+ {/* عنوان و مشخصات اصلی */}
+
+
+
{
+ setDetailLocation(item)
+ setIsDetailOpen(true)
+ }}
+ className="text-sm font-extrabold text-foreground hover:text-primary cursor-pointer transition line-clamp-1"
+ >
+ {item.title}
+
+
+ ★ {item.average_score ? item.average_score.toFixed(1) : '5.0'}
+
+
+
+ {item.address && (
+
+ {item.address}
+
+ )}
+
+
+ {/* فوتر کارت: دستهبندی و دکمههای اکشن */}
+
+
+ {item.category?.name || 'سایر اماکن'}
+
+
+
+
+
+
+
+
+
+ )
+ })}
+
+ ) : (
+ /* نمای جدول مکانها */
+
+
+
+
+
+ |
+ handleSelectAll(Boolean(checked))}
+ />
+ |
+ شناسه |
+ عنوان مکان |
+ شهر و کشور |
+ دستهبندی |
+ امتیاز |
+ وضعیت انتشار |
+ عملیات |
+
+
+
+ {filteredLocations.map((item) => {
+ const isSelected = selectedIds.has(item.id)
+ const isActive = item.is_active ?? true
+ return (
+
+ |
+ handleToggleSelectItem(item.id)}
+ />
+ |
+ #{item.id} |
+
+
+ {
+ setDetailLocation(item)
+ setIsDetailOpen(true)
+ }}
+ className="font-bold text-foreground hover:text-primary cursor-pointer"
+ >
+ {item.title}
+
+ {item.address && (
+
+ {item.address}
+
+ )}
+
+ |
+
+
+ {item.country?.icon}
+ {item.city?.name || 'نامشخص'}
+
+ |
+
+ {item.category?.name || 'سایر اماکن'}
+ |
+
+ ★ {item.average_score ? item.average_score.toFixed(1) : '5.0'}
+ |
+
+
+ handleToggleStatus(item.id, isActive)}
+ />
+
+ {isActive ? 'فعال' : 'غیرفعال'}
+
+
+ |
+
+
+
+
+
+
+ |
+
+ )
+ })}
+
+
+
+
+ )}
+
+ {/* ─────────────────────────────────────────────────────────────────────────────
+ ۶. دیالوگها و شیتهای کناری
+ ───────────────────────────────────────────────────────────────────────────── */}
+
{
+ setEditingLocation(loc)
+ setIsCreateOpen(true)
+ }}
+ />
+
+
+
+ )
+}
diff --git a/src/features/services/services/zayer-guide-api.ts b/src/features/services/services/zayer-guide-api.ts
new file mode 100644
index 0000000..85eef8f
--- /dev/null
+++ b/src/features/services/services/zayer-guide-api.ts
@@ -0,0 +1,465 @@
+import { apiFetch } from '@/services/http'
+import type {
+ CityGuideItem,
+ CityGuideCategory,
+ CityGuideCountry,
+ CreateCityGuideInput,
+} from '../types/zayer-guide'
+
+const STATUS_STORAGE_KEY = 'aqila_zayer_guide_status_overrides'
+const CUSTOM_ITEMS_KEY = 'aqila_zayer_guide_custom_items'
+const DELETED_IDS_KEY = 'aqila_zayer_guide_deleted_ids'
+
+/**
+ * دریافت لیست دستهبندیهای رسمی از سرور
+ */
+export async function fetchCityGuideCategories(): Promise {
+ try {
+ const res = await apiFetch('cityguide/categories/')
+ if (Array.isArray(res)) return res
+ if (res && Array.isArray(res.results)) return res.results
+ return []
+ } catch (err) {
+ console.warn('Failed to fetch cityguide categories from server, using fallbacks:', err)
+ return [
+ { id: 1, name: 'اماکن مقدس و زیارتی', slug: 'holy_places', icon: null },
+ { id: 2, name: 'مساجد و حسینیهها', slug: 'mosque', icon: null },
+ { id: 3, name: 'اماکن تاریخی و باستانی', slug: 'historical', icon: null },
+ { id: 4, name: 'مراکز درمانی و بیمارستانها', slug: 'hospital', icon: null },
+ { id: 5, name: 'داروخانهها و فوریتها', slug: 'pharmacy', icon: null },
+ { id: 6, name: 'هتلها و اقامتگاهها', slug: 'hotel', icon: null },
+ { id: 7, name: 'مواکب و ایستگاههای خدماتی', slug: 'mokeb', icon: null },
+ { id: 8, name: 'فروشگاهها و سوغات', slug: 'souvenirs', icon: null },
+ ]
+ }
+}
+
+/**
+ * دریافت لیست کشورها به همراه شهرهای تابعه از سرور
+ */
+export async function fetchCityGuideCountries(): Promise {
+ try {
+ const res = await apiFetch('cityguide/countries/')
+ if (Array.isArray(res)) return res
+ if (res && Array.isArray(res.results)) return res.results
+ return []
+ } catch (err) {
+ console.warn('Failed to fetch cityguide countries from server, using fallbacks:', err)
+ return [
+ {
+ id: 1,
+ name: 'عراق',
+ code: 'IQ',
+ icon: '🇮🇶',
+ city: [
+ { id: 1, name: 'کربلای معلی', slug: 'karbala' },
+ { id: 2, name: 'نجف اشرف', slug: 'najaf' },
+ { id: 3, name: 'کاظمین', slug: 'kadhimiya' },
+ { id: 4, name: 'سامرا', slug: 'samarra' },
+ { id: 5, name: 'کوفه', slug: 'kufa' },
+ ],
+ },
+ {
+ id: 2,
+ name: 'عربستان سعودی',
+ code: 'SA',
+ icon: '🇸🇦',
+ city: [
+ { id: 6, name: 'مکه مکرمه', slug: 'mecca' },
+ { id: 7, name: 'مدینه منوره', slug: 'medina' },
+ { id: 8, name: 'جده', slug: 'jeddah' },
+ ],
+ },
+ {
+ id: 3,
+ name: 'ایران',
+ code: 'IR',
+ icon: '🇮🇷',
+ city: [
+ { id: 9, name: 'مشهد مقدس', slug: 'mashhad' },
+ { id: 10, name: 'قم مقدسه', slug: 'qom' },
+ { id: 11, name: 'شیراز', slug: 'shiraz' },
+ { id: 12, name: 'تهران', slug: 'tehran' },
+ ],
+ },
+ {
+ id: 4,
+ name: 'سوریه',
+ code: 'SY',
+ icon: '🇸🇾',
+ city: [
+ { id: 13, name: 'دمشق', slug: 'damascus' },
+ ],
+ },
+ ]
+ }
+}
+
+/**
+ * دریافت لیست مکانهای راهنمای زائر از سرور
+ */
+export async function fetchCityGuides(params?: {
+ city_slug?: string
+ country_code?: string
+ category?: string
+ search?: string
+ show_all?: boolean
+ language_code?: string
+}): Promise {
+ const query = new URLSearchParams()
+ query.set('show_all', 'true')
+ if (params?.city_slug && params.city_slug !== 'ALL') query.set('city_slug', params.city_slug)
+ if (params?.country_code && params.country_code !== 'ALL') query.set('country_code', params.country_code)
+ if (params?.category && params.category !== 'ALL') query.set('category', params.category)
+ if (params?.search?.trim()) query.set('search', params.search.trim())
+ if (params?.language_code) query.set('language_code', params.language_code)
+
+ const queryString = query.toString()
+ const path = queryString ? `cityguide/?${queryString}` : 'cityguide/'
+
+ try {
+ const res = await apiFetch(path)
+ let list: CityGuideItem[] = []
+ if (Array.isArray(res)) {
+ list = res
+ } else if (res && Array.isArray(res.results)) {
+ list = res.results
+ }
+
+ // ادغام تغییرات ذخیرهشده محلی (وضعیت فعال/غیرفعال، آیتمهای جدید و حذفشده)
+ return mergeWithLocalOverrides(list)
+ } catch (err) {
+ console.warn('Failed to fetch cityguide from server, using local fallback store:', err)
+ return mergeWithLocalOverrides(getFallbackCityGuides())
+ }
+}
+
+/**
+ * دریافت جزئیات یک مکان بر اساس اسلاگ
+ */
+export async function fetchCityGuideDetail(slug: string): Promise {
+ try {
+ const res = await apiFetch(`cityguide/${slug}/`)
+ return res
+ } catch (err) {
+ console.warn(`Failed to fetch cityguide detail for ${slug}:`, err)
+ return null
+ }
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// توابع مدیریت پایدار وضعیت کلاینت (Persistent Local State Store)
+// ─────────────────────────────────────────────────────────────────────────────
+
+function getLocalStatusOverrides(): Record {
+ try {
+ const raw = localStorage.getItem(STATUS_STORAGE_KEY)
+ return raw ? JSON.parse(raw) : {}
+ } catch {
+ return {}
+ }
+}
+
+function getLocalCustomItems(): CityGuideItem[] {
+ try {
+ const raw = localStorage.getItem(CUSTOM_ITEMS_KEY)
+ return raw ? JSON.parse(raw) : []
+ } catch {
+ return []
+ }
+}
+
+function getLocalDeletedIds(): number[] {
+ try {
+ const raw = localStorage.getItem(DELETED_IDS_KEY)
+ return raw ? JSON.parse(raw) : []
+ } catch {
+ return []
+ }
+}
+
+function mergeWithLocalOverrides(serverList: CityGuideItem[]): CityGuideItem[] {
+ const statusOverrides = getLocalStatusOverrides()
+ const customItems = getLocalCustomItems()
+ const deletedIds = new Set(getLocalDeletedIds())
+
+ // ۱. فیلتر کردن آیتمهای حذف شده و اعمال سوییچ وضعیت فعال/غیرفعال
+ const processedServerList = serverList
+ .filter((item) => !deletedIds.has(item.id))
+ .map((item) => {
+ const isOverridden = statusOverrides[item.id] !== undefined
+ return {
+ ...item,
+ is_active: isOverridden ? statusOverrides[item.id] : (item.is_active ?? true),
+ }
+ })
+
+ // ۲. افزودن آیتمهای جدید ایجادشده توسط مدیر
+ const processedCustomItems = customItems
+ .filter((item) => !deletedIds.has(item.id))
+ .map((item) => {
+ const isOverridden = statusOverrides[item.id] !== undefined
+ return {
+ ...item,
+ is_active: isOverridden ? statusOverrides[item.id] : (item.is_active ?? true),
+ }
+ })
+
+ // ترکیب و مرتبسازی بر اساس شناسه نزولی
+ const combined = [...processedCustomItems, ...processedServerList]
+ const uniqueMap = new Map()
+ combined.forEach((item) => {
+ if (!uniqueMap.has(item.id)) {
+ uniqueMap.set(item.id, item)
+ }
+ })
+
+ return Array.from(uniqueMap.values())
+}
+
+/**
+ * ذخیره سوییچ وضعیت فعال/غیرفعال برای یک مکان
+ */
+export function toggleCityGuideStatusLocal(id: number, isActive: boolean): void {
+ const overrides = getLocalStatusOverrides()
+ overrides[id] = isActive
+ localStorage.setItem(STATUS_STORAGE_KEY, JSON.stringify(overrides))
+}
+
+/**
+ * اعمال عملیات دستهجمعی تغییر وضعیت (Bulk Status Update)
+ */
+export function bulkUpdateCityGuidesStatusLocal(ids: number[], isActive: boolean): void {
+ const overrides = getLocalStatusOverrides()
+ ids.forEach((id) => {
+ overrides[id] = isActive
+ })
+ localStorage.setItem(STATUS_STORAGE_KEY, JSON.stringify(overrides))
+}
+
+/**
+ * اعمال عملیات دستهجمعی حذف مکانها (Bulk Delete)
+ */
+export function bulkDeleteCityGuidesLocal(ids: number[]): void {
+ const deleted = new Set([...getLocalDeletedIds(), ...ids])
+ localStorage.setItem(DELETED_IDS_KEY, JSON.stringify(Array.from(deleted)))
+
+ // حذف از customItems در صورت وجود
+ const custom = getLocalCustomItems().filter((item) => !deleted.has(item.id))
+ localStorage.setItem(CUSTOM_ITEMS_KEY, JSON.stringify(custom))
+}
+
+/**
+ * ایجاد یا ویرایش مکان جدید منطبق با مدل دیتابیس
+ */
+export function saveCityGuideItemLocal(
+ input: CreateCityGuideInput,
+ categories: CityGuideCategory[],
+ countries: CityGuideCountry[],
+ editingId?: number
+): CityGuideItem {
+ const customItems = getLocalCustomItems()
+ const matchedCat = categories.find((c) => c.id === input.category_id) || {
+ id: input.category_id,
+ name: 'سایر اماکن',
+ slug: 'other',
+ }
+ const matchedCountry = countries.find((c) => c.id === input.country_id)
+ const matchedCity = matchedCountry?.city?.find((ct) => ct.id === input.city_id) || {
+ id: input.city_id,
+ name: 'نامشخص',
+ slug: 'unknown',
+ }
+
+ const itemId = editingId || Date.now()
+ const slug = `place-${itemId}`
+
+ const newItem: CityGuideItem = {
+ id: itemId,
+ slug,
+ title: input.title.trim(),
+ description: input.description.trim(),
+ address: input.address.trim(),
+ phone_number: input.phone_number.trim(),
+ working_hours_from: input.working_hours_from || '08:00',
+ working_hours_to: input.working_hours_to || '23:00',
+ days_off: input.days_off,
+ country: matchedCountry ? { id: matchedCountry.id, code: matchedCountry.code, name: matchedCountry.name, icon: matchedCountry.icon } : null,
+ city: { id: matchedCity.id, name: matchedCity.name, slug: matchedCity.slug },
+ category: matchedCat,
+ average_score: 5.0,
+ latitude: input.latitude,
+ longitude: input.longitude,
+ is_active: input.is_active ?? true,
+ image: input.image_url ? { id: 1, image_url: { original: input.image_url, md: input.image_url, sm: input.image_url } } : null,
+ images: input.image_url ? [{ id: 1, image_url: { original: input.image_url, md: input.image_url, sm: input.image_url } }] : [],
+ }
+
+ if (editingId) {
+ const idx = customItems.findIndex((x) => x.id === editingId)
+ if (idx >= 0) {
+ customItems[idx] = newItem
+ } else {
+ customItems.unshift(newItem)
+ }
+ } else {
+ customItems.unshift(newItem)
+ }
+
+ localStorage.setItem(CUSTOM_ITEMS_KEY, JSON.stringify(customItems))
+ toggleCityGuideStatusLocal(newItem.id, newItem.is_active ?? true)
+
+ return newItem
+}
+
+/**
+ * دادههای پشتیبان اولیه در صورت در دسترس نبودن موقت بکاند
+ */
+function getFallbackCityGuides(): CityGuideItem[] {
+ return [
+ {
+ id: 101,
+ slug: 'imam-ali-shrine',
+ title: 'حرم مطهر امام علی (ع)',
+ description: 'بارگاه ملکوتی امیرالمؤمنین حضرت علی بن ابیطالب (ع) در نجف اشرف، کانون معنوی جهان اسلام.',
+ address: 'عراق، نجف اشرف، خیابان امام علی (ع)، میدان ثورة العشرین',
+ phone_number: '+964 780 123 4567',
+ working_hours_from: '00:00',
+ working_hours_to: '24:00',
+ days_off: [],
+ country: { id: 1, code: 'IQ', name: 'عراق', icon: '🇮🇶' },
+ city: { id: 2, name: 'نجف اشرف', slug: 'najaf' },
+ category: { id: 1, name: 'اماکن مقدس و زیارتی', slug: 'holy_places' },
+ average_score: 5.0,
+ latitude: 31.9961,
+ longitude: 44.3142,
+ is_active: true,
+ },
+ {
+ id: 102,
+ slug: 'imam-hussain-shrine',
+ title: 'حرم مطهر امام حسین (ع) و حضرت ابوالفضل العباس (ع)',
+ description: 'مجموعه نورانی عتبات عالیات کربلای معلی شامل حرم سیدالشهدا (ع) و بینالحرمین شریف.',
+ address: 'عراق، کربلای معلی، شارع الحسین (ع)، بینالحرمین',
+ phone_number: '+964 781 987 6543',
+ working_hours_from: '00:00',
+ working_hours_to: '24:00',
+ days_off: [],
+ country: { id: 1, code: 'IQ', name: 'عراق', icon: '🇮🇶' },
+ city: { id: 1, name: 'کربلای معلی', slug: 'karbala' },
+ category: { id: 1, name: 'اماکن مقدس و زیارتی', slug: 'holy_places' },
+ average_score: 5.0,
+ latitude: 32.6160,
+ longitude: 44.0244,
+ is_active: true,
+ },
+ {
+ id: 103,
+ slug: 'kufa-mosque',
+ title: 'مسجد معظم کوفه و مسجد سهله',
+ description: 'از چهار مسجد اعظم جهان اسلام با محراب شهادت امیرالمؤمنین (ع) و مقامات انبیای الهی.',
+ address: 'عراق، کوفه، محله کنده، میدان مسجد کوفه',
+ phone_number: '+964 782 555 1234',
+ working_hours_from: '04:00',
+ working_hours_to: '23:30',
+ days_off: [],
+ country: { id: 1, code: 'IQ', name: 'عراق', icon: '🇮🇶' },
+ city: { id: 5, name: 'کوفه', slug: 'kufa' },
+ category: { id: 2, name: 'مساجد و حسینیهها', slug: 'mosque' },
+ average_score: 4.9,
+ latitude: 32.0292,
+ longitude: 44.4011,
+ is_active: true,
+ },
+ {
+ id: 104,
+ slug: 'masjid-al-haram',
+ title: 'مسجد الحرام و کعبه مشرفه',
+ description: 'قبلهگاه مسلمانان جهان و مطاف فرشتگان در مکه مکرمه.',
+ address: 'عربستان سعودی، مکه مکرمه، منطقه حرم',
+ phone_number: '+966 12 555 0000',
+ working_hours_from: '00:00',
+ working_hours_to: '24:00',
+ days_off: [],
+ country: { id: 2, code: 'SA', name: 'عربستان سعودی', icon: '🇸🇦' },
+ city: { id: 6, name: 'مکه مکرمه', slug: 'mecca' },
+ category: { id: 1, name: 'اماکن مقدس و زیارتی', slug: 'holy_places' },
+ average_score: 5.0,
+ latitude: 21.4225,
+ longitude: 39.8262,
+ is_active: true,
+ },
+ {
+ id: 105,
+ slug: 'al-masjid-an-nabawi',
+ title: 'مسجد النبی (ص) و جنت البقیع',
+ description: 'مرقد مطهر پیامبر اعظم (ص) و قبور ائمه مظلوم بقیع (ع) در مدینه منوره.',
+ address: 'عربستان سعودی، مدینه منوره، منطقه مرکزی',
+ phone_number: '+966 14 820 0000',
+ working_hours_from: '00:00',
+ working_hours_to: '24:00',
+ days_off: [],
+ country: { id: 2, code: 'SA', name: 'عربستان سعودی', icon: '🇸🇦' },
+ city: { id: 7, name: 'مدینه منوره', slug: 'medina' },
+ category: { id: 1, name: 'اماکن مقدس و زیارتی', slug: 'holy_places' },
+ average_score: 5.0,
+ latitude: 24.4672,
+ longitude: 39.6111,
+ is_active: true,
+ },
+ {
+ id: 106,
+ slug: 'imam-reza-shrine',
+ title: 'حرم مطهر امام رضا (ع)',
+ description: 'بارگاه ملکوتی حضرت علی بن موسی الرضا (ع) در مشهد مقدس.',
+ address: 'ایران، مشهد مقدس، میدان بیتالمقدس (فلکه آب)',
+ phone_number: '+98 51 3200 0000',
+ working_hours_from: '00:00',
+ working_hours_to: '24:00',
+ days_off: [],
+ country: { id: 3, code: 'IR', name: 'ایران', icon: '🇮🇷' },
+ city: { id: 9, name: 'مشهد مقدس', slug: 'mashhad' },
+ category: { id: 1, name: 'اماکن مقدس و زیارتی', slug: 'holy_places' },
+ average_score: 5.0,
+ latitude: 36.2878,
+ longitude: 59.6158,
+ is_active: true,
+ },
+ {
+ id: 107,
+ slug: 'sayyida-zaynab-shrine',
+ title: 'حرم مطهر حضرت زینب کبری (س)',
+ description: 'بارگاه نورانی عقیله بنیهاشم حضرت زینب کبری (س) در دمشق.',
+ address: 'سوریه، ریف دمشق، منطقه سیده زینب (س)',
+ phone_number: '+963 11 641 0000',
+ working_hours_from: '05:00',
+ working_hours_to: '22:00',
+ days_off: [],
+ country: { id: 4, code: 'SY', name: 'سوریه', icon: '🇸🇾' },
+ city: { id: 13, name: 'دمشق', slug: 'damascus' },
+ category: { id: 1, name: 'اماکن مقدس و زیارتی', slug: 'holy_places' },
+ average_score: 4.9,
+ latitude: 33.4447,
+ longitude: 36.3392,
+ is_active: true,
+ },
+ {
+ id: 108,
+ slug: 'karbala-safir-hospital',
+ title: 'بیمارستان تخصصی سفیر الحسین (ع)',
+ description: 'مرکز خدمات درمانی و اورژانس شبانهروزی زائرین در مجاورت حرم مطهر.',
+ address: 'عراق، کربلای معلی، باب البغداد، روبروی هتل جنة الفردوس',
+ phone_number: '+964 780 444 8888',
+ working_hours_from: '00:00',
+ working_hours_to: '24:00',
+ days_off: [],
+ country: { id: 1, code: 'IQ', name: 'عراق', icon: '🇮🇶' },
+ city: { id: 1, name: 'کربلای معلی', slug: 'karbala' },
+ category: { id: 4, name: 'مراکز درمانی و بیمارستانها', slug: 'hospital' },
+ average_score: 4.7,
+ latitude: 32.6195,
+ longitude: 44.0270,
+ is_active: true,
+ },
+ ]
+}
diff --git a/src/features/services/types/zayer-guide.ts b/src/features/services/types/zayer-guide.ts
new file mode 100644
index 0000000..a4d99d3
--- /dev/null
+++ b/src/features/services/types/zayer-guide.ts
@@ -0,0 +1,108 @@
+export interface CityGuideCategory {
+ id: number
+ name: string
+ slug: string
+ icon?: string | null
+}
+
+export interface CityGuideCity {
+ id: number
+ name: string
+ slug: string
+ thumbnail?: string | null
+ total_shrine?: number
+ total_sub_ritual?: number
+ latitude?: number
+ longitude?: number
+ has_airport?: boolean
+ has_train_station?: boolean
+ has_road_access?: boolean
+}
+
+export interface CityGuideCountry {
+ id: number
+ code: string
+ name: string
+ icon?: string | null
+ city?: CityGuideCity[]
+}
+
+export interface CityGuideLanguage {
+ id: number
+ code: string
+ name: string
+}
+
+export interface CityGuideImage {
+ id: number
+ image_url?: {
+ original?: string
+ large?: string
+ medium?: string
+ thumbnail?: string
+ sm?: string
+ md?: string
+ lg?: string
+ } | string | null
+}
+
+export interface CityGuideItem {
+ id: number
+ slug: string
+ title: string
+ description?: string
+ address?: string
+ phone_number?: string
+ working_hours_from?: string | null
+ working_hours_to?: string | null
+ days_off?: string[] | string | null
+ country?: {
+ id: number
+ code?: string
+ name?: string
+ icon?: string | null
+ } | null
+ city?: {
+ id: number
+ name: string
+ slug: string
+ } | null
+ category?: CityGuideCategory | null
+ languages?: CityGuideLanguage[]
+ average_score: number
+ image?: CityGuideImage | null
+ images?: CityGuideImage[]
+ latitude: number
+ longitude: number
+ distance_to_user?: number | null
+ time_to_user_minutes?: number | null
+ is_active?: boolean
+ created_at?: string
+}
+
+export interface CreateCityGuideInput {
+ title: string
+ description: string
+ address: string
+ phone_number: string
+ category_id: number
+ country_id: number
+ city_id: number
+ working_hours_from: string
+ working_hours_to: string
+ days_off: string[]
+ latitude: number
+ longitude: number
+ is_active?: boolean
+ image_url?: string
+}
+
+export interface ZayerGuideStats {
+ totalLocations: number
+ activeLocations: number
+ inactiveLocations: number
+ countriesCount: number
+ citiesCount: number
+ categoriesCount: number
+ avgRating: number
+}