From 0e1013bd0b237fd53a17ce5291057fe41f43d7e6 Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Thu, 17 Sep 2026 08:27:49 +0330 Subject: [PATCH] feat: add Zayer Guide service feature with location management UI and API integration --- src/features/services/ServicesPage.tsx | 107 +-- .../components/CreateLocationDialog.tsx | 425 ++++++++++ .../components/LocationDetailSheet.tsx | 251 ++++++ .../components/ZayerGuideServiceView.tsx | 782 ++++++++++++++++++ .../services/services/zayer-guide-api.ts | 465 +++++++++++ src/features/services/types/zayer-guide.ts | 108 +++ 6 files changed, 2032 insertions(+), 106 deletions(-) create mode 100644 src/features/services/components/CreateLocationDialog.tsx create mode 100644 src/features/services/components/LocationDetailSheet.tsx create mode 100644 src/features/services/components/ZayerGuideServiceView.tsx create mode 100644 src/features/services/services/zayer-guide-api.ts create mode 100644 src/features/services/types/zayer-guide.ts 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 ( + + + +
+
+ +
+
+ + {editingLocation ? `ویرایش مکان: ${editingLocation.title}` : 'افزودن مکان جدید به راهنمای زائر'} + + + فیلدها منطبق بر ساختار رسمی مدل پایگاه‌داده و وب‌سرویس CityGuide + +
+
+
+ +
+ {validationError && ( +
+ + {validationError} +
+ )} + + {/* ۱. عنوان و وضعیت فعال بودن */} +
+
+ + setTitle(e.target.value)} + placeholder="مثلاً: حرم مطهر امام حسین (ع) یا بیمارستان سفیر" + className="text-xs bg-surface-base" + required + /> +
+ +
+ +
+ + + {isActive ? 'فعال در سامانه' : 'غیرفعال'} + +
+
+
+ + {/* ۲. دسته‌بندی و شهر/کشور مقصد */} +
+
+ + +
+ +
+ + +
+
+ + {/* ۳. آدرس و شماره تماس */} +
+
+ + setAddress(e.target.value)} + placeholder="مثلاً: کربلای معلی، شارع الحسین (ع)، جنب باب القبله" + className="text-xs bg-surface-base" + /> +
+ +
+ + setPhoneNumber(e.target.value)} + placeholder="+964 780 000 0000" + dir="ltr" + className="text-xs bg-surface-base font-mono" + /> +
+
+ + {/* ۴. شرح و توضیحات مکان */} +
+ +