You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1616 lines
76 KiB
1616 lines
76 KiB
import React, { useState, useEffect, useMemo } from 'react'
|
|
import { useParams, useNavigate, useSearchParams, Link } from 'react-router-dom'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
} from '@/components/ui/dialog'
|
|
import DeleteConfirmDialog from '@/components/shared/DeleteConfirmDialog'
|
|
import { Ic } from '@/icons'
|
|
import { toFa } from '@/lib/utils'
|
|
import { useTranslation } from '@/i18n'
|
|
import { Shimmer } from '@/components/shared/shimmer_frontend'
|
|
import type {
|
|
CustomTripDestinationItem,
|
|
CustomTripHotelItem,
|
|
CustomTripTransportRelationItem,
|
|
CustomTripOriginItem,
|
|
} from './types'
|
|
import {
|
|
fetchCustomTripDestinations,
|
|
fetchCustomTripOrigins,
|
|
fetchCustomTripHotels,
|
|
fetchCustomTripTransportRelations,
|
|
saveCustomTripDestination,
|
|
deleteCustomTripDestination,
|
|
saveCustomTripHotel,
|
|
deleteCustomTripHotel,
|
|
saveCustomTripTransportRelation,
|
|
deleteCustomTripTransportRelation,
|
|
} from './services/custom-trip-api'
|
|
|
|
const PRESET_DEST_COUNTRIES = [
|
|
{ name: 'عراق', flag: '🇮🇶' },
|
|
{ name: 'ایران', flag: '🇮🇷' },
|
|
{ name: 'عربستان سعودی', flag: '🇸🇦' },
|
|
{ name: 'سوریه', flag: '🇸🇾' },
|
|
]
|
|
|
|
const COMMON_AMENITIES = [
|
|
{ id: 'wifi', name: 'اینترنت وایفای رایگان' },
|
|
{ id: 'restaurant', name: 'رستوران ایرانی و بوفه باز' },
|
|
{ id: 'shrine_transfer', name: 'ترانسفر حرم مطهر رایگان' },
|
|
{ id: 'airport_transfer', name: 'ترانسفر فرودگاهی اختصاصی' },
|
|
{ id: 'laundry', name: 'لاندری و خدمات خشکشویی' },
|
|
{ id: 'wheelchair', name: 'خدمات ویژه ویلچر و توانیابان' },
|
|
{ id: 'doctor', name: 'پزشک و خدمات درمانی مقیم' },
|
|
{ id: 'safety_box', name: 'صندوق امانات دیجیتال در اتاق' },
|
|
]
|
|
|
|
export default function CustomTripDestinationDetailPage() {
|
|
const { t } = useTranslation()
|
|
const { id } = useParams<{ id: string }>()
|
|
const navigate = useNavigate()
|
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
|
|
// Default active tab is 'info' (مشخصات و اطلاعات شهر)
|
|
const currentTab = searchParams.get('tab') || 'info'
|
|
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [destination, setDestination] = useState<CustomTripDestinationItem | null>(null)
|
|
const [allOrigins, setAllOrigins] = useState<CustomTripOriginItem[]>([])
|
|
const [allHotels, setAllHotels] = useState<CustomTripHotelItem[]>([])
|
|
const [allRelations, setAllRelations] = useState<CustomTripTransportRelationItem[]>([])
|
|
|
|
// --- TAB 1: DESTINATION INFO FORM STATE ---
|
|
const [destName, setDestName] = useState('')
|
|
const [destNameEn, setDestNameEn] = useState('')
|
|
const [destCountry, setDestCountry] = useState('عراق')
|
|
const [destFlag, setDestFlag] = useState('🇮🇶')
|
|
const [destDescription, setDestDescription] = useState('')
|
|
const [destHolyPlaces, setDestHolyPlaces] = useState('')
|
|
const [destIsActive, setDestIsActive] = useState(true)
|
|
const [isDestSavedSuccess, setIsDestSavedSuccess] = useState(false)
|
|
const [isDestDeleteOpen, setIsDestDeleteOpen] = useState(false)
|
|
|
|
// --- TAB 2: HOTELS STATE ---
|
|
const [hotelSearch, setHotelSearch] = useState('')
|
|
const [hotelStarsFilter, setHotelStarsFilter] = useState<'all' | '5' | '4' | '3'>('all')
|
|
const [hotelViewMode, setHotelViewMode] = useState<'cards' | 'table'>('cards')
|
|
const [isHotelFormOpen, setIsHotelFormOpen] = useState(false)
|
|
const [editingHotel, setEditingHotel] = useState<CustomTripHotelItem | null>(null)
|
|
const [hotelToDelete, setHotelToDelete] = useState<CustomTripHotelItem | null>(null)
|
|
|
|
// Hotel form fields
|
|
const [hotelFormName, setHotelFormName] = useState('')
|
|
const [hotelFormStars, setHotelFormStars] = useState<number>(5)
|
|
const [hotelFormDistance, setHotelFormDistance] = useState('')
|
|
const [hotelFormScore, setHotelFormScore] = useState<number>(4.8)
|
|
const [hotelFormDescription, setHotelFormDescription] = useState('')
|
|
const [hotelFormGuideNote, setHotelFormGuideNote] = useState('')
|
|
const [hotelFormAmenities, setHotelFormAmenities] = useState<string[]>([])
|
|
const [hotelFormCustomAmenity, setHotelFormCustomAmenity] = useState('')
|
|
const [hotelFormPriceAdult, setHotelFormPriceAdult] = useState<number>(100)
|
|
const [hotelFormPriceChild, setHotelFormPriceChild] = useState<number>(50)
|
|
const [hotelFormIsActive, setHotelFormIsActive] = useState(true)
|
|
|
|
// --- TAB 3: TRANSPORTS STATE ---
|
|
const [transportSearch, setTransportSearch] = useState('')
|
|
const [transportMethodFilter, setTransportMethodFilter] = useState<'all' | 'flight' | 'train' | 'road'>('all')
|
|
const [transportViewMode, setTransportViewMode] = useState<'cards' | 'table'>('cards')
|
|
const [isTransportFormOpen, setIsTransportFormOpen] = useState(false)
|
|
const [editingTransport, setEditingTransport] = useState<CustomTripTransportRelationItem | null>(null)
|
|
const [transportToDelete, setTransportToDelete] = useState<CustomTripTransportRelationItem | null>(null)
|
|
|
|
// Transport form fields
|
|
const [transportFormFromCity, setTransportFormFromCity] = useState('')
|
|
const [transportFormFromCountry, setTransportFormFromCountry] = useState('')
|
|
const [transportFormHasAirplane, setTransportFormHasAirplane] = useState(true)
|
|
const [transportFormAirplaneDetails, setTransportFormAirplaneDetails] = useState('')
|
|
const [transportFormHasTrain, setTransportFormHasTrain] = useState(false)
|
|
const [transportFormTrainDetails, setTransportFormTrainDetails] = useState('')
|
|
const [transportFormHasRoad, setTransportFormHasRoad] = useState(false)
|
|
const [transportFormRoadDetails, setTransportFormRoadDetails] = useState('')
|
|
const [transportFormGuideNote, setTransportFormGuideNote] = useState('')
|
|
const [transportFormIsActive, setTransportFormIsActive] = useState(true)
|
|
|
|
// Load all initial data
|
|
useEffect(() => {
|
|
let isMounted = true
|
|
async function load() {
|
|
setIsLoading(true)
|
|
try {
|
|
const [dests, origs, hotels, rels] = await Promise.all([
|
|
fetchCustomTripDestinations(),
|
|
fetchCustomTripOrigins(),
|
|
fetchCustomTripHotels(),
|
|
fetchCustomTripTransportRelations(),
|
|
])
|
|
|
|
if (isMounted) {
|
|
setAllOrigins(origs)
|
|
setAllHotels(hotels)
|
|
setAllRelations(rels)
|
|
|
|
const target = dests.find(
|
|
(d) =>
|
|
String(d.id) === String(id) ||
|
|
d.name === String(id) ||
|
|
(d.nameEn && d.nameEn.toLowerCase() === String(id).toLowerCase())
|
|
)
|
|
if (target) {
|
|
setDestination(target)
|
|
// Init Tab 1 Form
|
|
setDestName(target.name)
|
|
setDestNameEn(target.nameEn || '')
|
|
setDestCountry(target.country)
|
|
setDestFlag(target.flag || '📍')
|
|
setDestDescription(target.description || '')
|
|
setDestHolyPlaces((target.holyPlaces || []).join('، '))
|
|
setDestIsActive(target.isActive)
|
|
} else if (dests.length > 0) {
|
|
const fallback = dests[0]
|
|
setDestination(fallback)
|
|
setDestName(fallback.name)
|
|
setDestNameEn(fallback.nameEn || '')
|
|
setDestCountry(fallback.country)
|
|
setDestFlag(fallback.flag || '📍')
|
|
setDestDescription(fallback.description || '')
|
|
setDestHolyPlaces((fallback.holyPlaces || []).join('، '))
|
|
setDestIsActive(fallback.isActive)
|
|
}
|
|
}
|
|
} finally {
|
|
if (isMounted) setIsLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
return () => {
|
|
isMounted = false
|
|
}
|
|
}, [id])
|
|
|
|
// Destination-specific hotels
|
|
const destHotels = useMemo(() => {
|
|
if (!destination) return []
|
|
return allHotels.filter(
|
|
(h) =>
|
|
h.cityName === destination.name ||
|
|
destination.name.includes(h.cityName) ||
|
|
h.cityName.includes(destination.name)
|
|
)
|
|
}, [allHotels, destination])
|
|
|
|
// Filtered hotels
|
|
const filteredHotels = useMemo(() => {
|
|
return destHotels.filter((h) => {
|
|
const matchStars = hotelStarsFilter === 'all' || String(h.stars) === hotelStarsFilter
|
|
const term = hotelSearch.trim().toLowerCase()
|
|
const matchSearch =
|
|
!term ||
|
|
h.name.toLowerCase().includes(term) ||
|
|
(h.distanceToShrine && h.distanceToShrine.toLowerCase().includes(term)) ||
|
|
(h.description && h.description.toLowerCase().includes(term)) ||
|
|
(h.guideNote && h.guideNote.toLowerCase().includes(term)) ||
|
|
h.amenities.some((a) => a.toLowerCase().includes(term))
|
|
return matchStars && matchSearch
|
|
})
|
|
}, [destHotels, hotelStarsFilter, hotelSearch])
|
|
|
|
// Destination-specific transport relations
|
|
const destRelations = useMemo(() => {
|
|
if (!destination) return []
|
|
return allRelations.filter(
|
|
(r) =>
|
|
r.toCity === destination.name ||
|
|
destination.name.includes(r.toCity) ||
|
|
r.toCity.includes(destination.name)
|
|
)
|
|
}, [allRelations, destination])
|
|
|
|
// Filtered relations
|
|
const filteredRelations = useMemo(() => {
|
|
return destRelations.filter((rel) => {
|
|
const matchMethod =
|
|
transportMethodFilter === 'all' ||
|
|
(transportMethodFilter === 'flight' && rel.hasAirplane) ||
|
|
(transportMethodFilter === 'train' && rel.hasTrain) ||
|
|
(transportMethodFilter === 'road' && rel.hasRoad)
|
|
|
|
const term = transportSearch.trim().toLowerCase()
|
|
const matchSearch =
|
|
!term ||
|
|
rel.fromCity.toLowerCase().includes(term) ||
|
|
rel.fromCountry.toLowerCase().includes(term) ||
|
|
(rel.airplaneDetails && rel.airplaneDetails.toLowerCase().includes(term)) ||
|
|
(rel.trainDetails && rel.trainDetails.toLowerCase().includes(term)) ||
|
|
(rel.roadDetails && rel.roadDetails.toLowerCase().includes(term)) ||
|
|
(rel.guideNote && rel.guideNote.toLowerCase().includes(term))
|
|
|
|
return matchMethod && matchSearch
|
|
})
|
|
}, [destRelations, transportMethodFilter, transportSearch])
|
|
|
|
const handleTabChange = (tabKey: string) => {
|
|
setSearchParams({ tab: tabKey })
|
|
}
|
|
|
|
// --- TAB 1: SAVE DESTINATION HANDLER ---
|
|
const handleSaveDestinationInfo = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!destination || !destName.trim()) return
|
|
|
|
const places = destHolyPlaces
|
|
.split(/[،,]/)
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
|
|
const updated: CustomTripDestinationItem = {
|
|
...destination,
|
|
name: destName.trim(),
|
|
nameEn: destNameEn.trim() || undefined,
|
|
country: destCountry.trim(),
|
|
flag: destFlag,
|
|
description: destDescription.trim() || undefined,
|
|
holyPlaces: places.length ? places : undefined,
|
|
isActive: destIsActive,
|
|
}
|
|
|
|
await saveCustomTripDestination(updated)
|
|
setDestination(updated)
|
|
setIsDestSavedSuccess(true)
|
|
setTimeout(() => setIsDestSavedSuccess(false), 3000)
|
|
}
|
|
|
|
const handleDeleteDestinationConfirm = async () => {
|
|
if (!destination) return
|
|
await deleteCustomTripDestination(destination.id)
|
|
navigate('/custom-trip/destinations')
|
|
}
|
|
|
|
// --- TAB 2: HOTEL HANDLERS ---
|
|
const handleOpenAddHotel = () => {
|
|
setEditingHotel(null)
|
|
setHotelFormName('')
|
|
setHotelFormStars(5)
|
|
setHotelFormDistance('')
|
|
setHotelFormScore(4.8)
|
|
setHotelFormDescription('')
|
|
setHotelFormGuideNote('')
|
|
setHotelFormAmenities(['اینترنت وایفای رایگان', 'رستوران ایرانی و بوفه باز'])
|
|
setHotelFormCustomAmenity('')
|
|
setHotelFormPriceAdult(110)
|
|
setHotelFormPriceChild(55)
|
|
setHotelFormIsActive(true)
|
|
setIsHotelFormOpen(true)
|
|
}
|
|
|
|
const handleOpenEditHotel = (hotel: CustomTripHotelItem) => {
|
|
setEditingHotel(hotel)
|
|
setHotelFormName(hotel.name)
|
|
setHotelFormStars(hotel.stars)
|
|
setHotelFormDistance(hotel.distanceToShrine || '')
|
|
setHotelFormScore(hotel.score || 4.8)
|
|
setHotelFormDescription(hotel.description || '')
|
|
setHotelFormGuideNote(hotel.guideNote || '')
|
|
setHotelFormAmenities(hotel.amenities || [])
|
|
setHotelFormCustomAmenity('')
|
|
setHotelFormPriceAdult(hotel.pricePerDayAdult || 100)
|
|
setHotelFormPriceChild(hotel.pricePerDayChild || 50)
|
|
setHotelFormIsActive(hotel.isActive)
|
|
setIsHotelFormOpen(true)
|
|
}
|
|
|
|
const handleToggleHotelAmenity = (name: string) => {
|
|
setHotelFormAmenities((prev) =>
|
|
prev.includes(name) ? prev.filter((a) => a !== name) : [...prev, name]
|
|
)
|
|
}
|
|
|
|
const handleAddCustomHotelAmenity = () => {
|
|
const val = hotelFormCustomAmenity.trim()
|
|
if (!val) return
|
|
if (!hotelFormAmenities.includes(val)) {
|
|
setHotelFormAmenities((prev) => [...prev, val])
|
|
}
|
|
setHotelFormCustomAmenity('')
|
|
}
|
|
|
|
const handleSaveHotel = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!destination || !hotelFormName.trim()) return
|
|
|
|
const item: CustomTripHotelItem = {
|
|
id: editingHotel ? editingHotel.id : `hotel_${Date.now()}`,
|
|
name: hotelFormName.trim(),
|
|
cityName: destination.name,
|
|
stars: hotelFormStars,
|
|
score: hotelFormScore,
|
|
distanceToShrine: hotelFormDistance.trim() || 'فاصله استاندارد تا حرم مطهر',
|
|
amenities: hotelFormAmenities,
|
|
description: hotelFormDescription.trim() || 'هتل درجه یک و مجهز جهت اقامت زائرین گرامی.',
|
|
guideNote: hotelFormGuideNote.trim() || undefined,
|
|
pricePerDayAdult: hotelFormPriceAdult,
|
|
pricePerDayChild: hotelFormPriceChild,
|
|
isActive: hotelFormIsActive,
|
|
}
|
|
|
|
await saveCustomTripHotel(item)
|
|
setAllHotels((prev) => {
|
|
const exists = prev.some((h) => h.id === item.id)
|
|
return exists ? prev.map((h) => (h.id === item.id ? item : h)) : [item, ...prev]
|
|
})
|
|
setIsHotelFormOpen(false)
|
|
}
|
|
|
|
const handleConfirmDeleteHotel = async () => {
|
|
if (!hotelToDelete) return
|
|
await deleteCustomTripHotel(hotelToDelete.id)
|
|
setAllHotels((prev) => prev.filter((h) => h.id !== hotelToDelete.id))
|
|
setHotelToDelete(null)
|
|
}
|
|
|
|
// --- TAB 3: TRANSPORT HANDLERS ---
|
|
const handleOpenAddTransport = () => {
|
|
setEditingTransport(null)
|
|
setTransportFormFromCity(allOrigins.length > 0 ? allOrigins[0].name : 'تهران')
|
|
setTransportFormFromCountry(allOrigins.length > 0 ? allOrigins[0].country : 'ایران')
|
|
setTransportFormHasAirplane(true)
|
|
setTransportFormAirplaneDetails(`پرواز مستقیم به فرودگاه نجف / بغداد`)
|
|
setTransportFormHasTrain(false)
|
|
setTransportFormTrainDetails('')
|
|
setTransportFormHasRoad(false)
|
|
setTransportFormRoadDetails('')
|
|
setTransportFormGuideNote(`معمولاً پروازهای این مسیر به صورت روزانه یا چارتر هفتگی انجام میشود.`)
|
|
setTransportFormIsActive(true)
|
|
setIsTransportFormOpen(true)
|
|
}
|
|
|
|
const handleOpenEditTransport = (rel: CustomTripTransportRelationItem) => {
|
|
setEditingTransport(rel)
|
|
setTransportFormFromCity(rel.fromCity)
|
|
setTransportFormFromCountry(rel.fromCountry)
|
|
setTransportFormHasAirplane(rel.hasAirplane)
|
|
setTransportFormAirplaneDetails(rel.airplaneDetails || '')
|
|
setTransportFormHasTrain(rel.hasTrain)
|
|
setTransportFormTrainDetails(rel.trainDetails || '')
|
|
setTransportFormHasRoad(rel.hasRoad)
|
|
setTransportFormRoadDetails(rel.roadDetails || '')
|
|
setTransportFormGuideNote(rel.guideNote || '')
|
|
setTransportFormIsActive(rel.isActive)
|
|
setIsTransportFormOpen(true)
|
|
}
|
|
|
|
const handleSaveTransport = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!destination || !transportFormFromCity.trim()) return
|
|
|
|
const item: CustomTripTransportRelationItem = {
|
|
id: editingTransport ? editingTransport.id : `rel_${Date.now()}`,
|
|
fromCity: transportFormFromCity.trim(),
|
|
fromCountry: transportFormFromCountry.trim() || 'نامشخص',
|
|
toCity: destination.name,
|
|
toCountry: destination.country,
|
|
hasAirplane: transportFormHasAirplane,
|
|
airplaneDetails: transportFormHasAirplane ? transportFormAirplaneDetails.trim() || undefined : undefined,
|
|
hasTrain: transportFormHasTrain,
|
|
trainDetails: transportFormHasTrain ? transportFormTrainDetails.trim() || undefined : undefined,
|
|
hasRoad: transportFormHasRoad,
|
|
roadDetails: transportFormHasRoad ? transportFormRoadDetails.trim() || undefined : undefined,
|
|
guideNote: transportFormGuideNote.trim() || undefined,
|
|
isActive: transportFormIsActive,
|
|
}
|
|
|
|
await saveCustomTripTransportRelation(item)
|
|
setAllRelations((prev) => {
|
|
const exists = prev.some((r) => r.id === item.id)
|
|
return exists ? prev.map((r) => (r.id === item.id ? item : r)) : [item, ...prev]
|
|
})
|
|
setIsTransportFormOpen(false)
|
|
}
|
|
|
|
const handleConfirmDeleteTransport = async () => {
|
|
if (!transportToDelete) return
|
|
await deleteCustomTripTransportRelation(transportToDelete.id)
|
|
setAllRelations((prev) => prev.filter((r) => r.id !== transportToDelete.id))
|
|
setTransportToDelete(null)
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<Shimmer className="h-10 w-48 rounded-xl" />
|
|
<Shimmer className="h-44 w-full rounded-2xl" />
|
|
<Shimmer className="h-72 w-full rounded-2xl" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!destination) {
|
|
return (
|
|
<div className="p-8 text-center rounded-2xl bg-card border border-border/70 space-y-4">
|
|
<Ic name="alert" className="size-10 text-muted-foreground mx-auto" />
|
|
<h2 className="text-base font-bold text-foreground">شهر مقصد مورد نظر یافت نشد!</h2>
|
|
<Button onClick={() => navigate('/custom-trip/destinations')} variant="outline" className="rounded-xl">
|
|
بازگشت به فهرست شهرهای مقصد
|
|
</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-5">
|
|
{/* Top Breadcrumb Navigation */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
|
<div className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground">
|
|
<Link
|
|
to="/custom-trip/destinations"
|
|
className="flex items-center gap-1.5 hover:text-foreground font-semibold transition-colors"
|
|
>
|
|
<Ic name="arrowRight" className="size-4" />
|
|
<span>شهرهای مقصد</span>
|
|
</Link>
|
|
<span className="text-border">/</span>
|
|
<span className="font-bold text-foreground flex items-center gap-1.5">
|
|
<span>{destination.flag || '📍'}</span>
|
|
<span>{destination.name}</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<span
|
|
className={`inline-flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-xl border ${
|
|
destination.isActive
|
|
? 'bg-emerald-500/10 text-emerald-600 border-emerald-500/30'
|
|
: 'bg-muted text-muted-foreground border-border/60'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`size-2 rounded-full ${
|
|
destination.isActive ? 'bg-emerald-500 animate-pulse' : 'bg-muted-foreground'
|
|
}`}
|
|
/>
|
|
<span>{destination.isActive ? 'مقصد فعال و قابل رزرو' : 'مقصد غیرفعال'}</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabs Navigation Header: 1. Info & Description | 2. Hotels | 3. Transportation */}
|
|
<div className="flex items-center gap-2 p-1.5 rounded-2xl bg-muted/60 border border-border/70 overflow-x-auto">
|
|
{/* Tab 1: City Info & Custom Trip Description */}
|
|
<button
|
|
type="button"
|
|
onClick={() => handleTabChange('info')}
|
|
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs sm:text-sm font-semibold transition-all cursor-pointer whitespace-nowrap ${
|
|
currentTab === 'info'
|
|
? 'bg-card text-foreground shadow-xs border border-border/70 font-bold'
|
|
: 'text-muted-foreground hover:text-foreground hover:bg-card/50'
|
|
}`}
|
|
>
|
|
<Ic name="mapPin" className={`size-4 ${currentTab === 'info' ? 'text-primary' : ''}`} />
|
|
<span>مشخصات و توضیحات مقصد (Description)</span>
|
|
</button>
|
|
|
|
{/* Tab 2: Hotels */}
|
|
<button
|
|
type="button"
|
|
onClick={() => handleTabChange('hotels')}
|
|
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs sm:text-sm font-semibold transition-all cursor-pointer whitespace-nowrap ${
|
|
currentTab === 'hotels'
|
|
? 'bg-card text-foreground shadow-xs border border-border/70 font-bold'
|
|
: 'text-muted-foreground hover:text-foreground hover:bg-card/50'
|
|
}`}
|
|
>
|
|
<Ic name="bed" className={`size-4 ${currentTab === 'hotels' ? 'text-primary' : ''}`} />
|
|
<span>هتلها و اقامتگاههای مقصد</span>
|
|
<span className="px-2 py-0.5 rounded-full text-[11px] font-mono font-bold bg-muted text-muted-foreground">
|
|
{toFa(destHotels.length)}
|
|
</span>
|
|
</button>
|
|
|
|
{/* Tab 3: Transportation */}
|
|
<button
|
|
type="button"
|
|
onClick={() => handleTabChange('transports')}
|
|
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl text-xs sm:text-sm font-semibold transition-all cursor-pointer whitespace-nowrap ${
|
|
currentTab === 'transports'
|
|
? 'bg-card text-foreground shadow-xs border border-border/70 font-bold'
|
|
: 'text-muted-foreground hover:text-foreground hover:bg-card/50'
|
|
}`}
|
|
>
|
|
<Ic name="route" className={`size-4 ${currentTab === 'transports' ? 'text-primary' : ''}`} />
|
|
<span>روشها و مسیرهای حملونقل (Transportation)</span>
|
|
<span className="px-2 py-0.5 rounded-full text-[11px] font-mono font-bold bg-muted text-muted-foreground">
|
|
{toFa(destRelations.length)}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* ========================================================================= */}
|
|
{/* TAB 1: DESTINATION INFO & DESCRIPTION (صفحه اول: مشخصات و توضیحات شهر) */}
|
|
{/* ========================================================================= */}
|
|
{currentTab === 'info' && (
|
|
<div className="space-y-4">
|
|
<form onSubmit={handleSaveDestinationInfo} className="space-y-4">
|
|
<div className="rounded-2xl bg-card border border-border/70 p-5 sm:p-6 shadow-xs space-y-5">
|
|
<div className="flex items-center justify-between border-b border-border/60 pb-4">
|
|
<div>
|
|
<h2 className="text-base sm:text-lg font-black text-foreground flex items-center gap-2">
|
|
<span className="text-2xl select-none">{destFlag || '📍'}</span>
|
|
<span>اطلاعات و شناسنامه شهر {destination.name}</span>
|
|
</h2>
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
ویرایش مشخصات اصلی، موقعیت کشور، اماکن مقدسه و توضیحات شهر برای ماژول سفر اختصاصی (Custom Trip).
|
|
</p>
|
|
</div>
|
|
|
|
{isDestSavedSuccess && (
|
|
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-600 text-xs font-bold animate-fadeIn">
|
|
<Ic name="checkCircle" className="size-4" />
|
|
<span>تغییرات با موفقیت ذخیره شد</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Form Grid */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
{/* 1. Country */}
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs font-bold text-foreground block">
|
|
کشور مقصد:
|
|
</label>
|
|
<select
|
|
value={destCountry}
|
|
onChange={(e) => {
|
|
const cName = e.target.value
|
|
setDestCountry(cName)
|
|
const preset = PRESET_DEST_COUNTRIES.find((p) => p.name === cName)
|
|
if (preset) setDestFlag(preset.flag)
|
|
}}
|
|
className="w-full h-11 px-3 text-xs sm:text-sm rounded-xl bg-background border border-border/70 text-foreground font-medium focus:ring-1 focus:ring-primary focus:outline-none"
|
|
>
|
|
{PRESET_DEST_COUNTRIES.map((c) => (
|
|
<option key={c.name} value={c.name}>
|
|
{c.flag} {c.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* 2. City Name FA */}
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs font-bold text-foreground block">
|
|
نام شهر مقصد (فارسی):
|
|
</label>
|
|
<Input
|
|
value={destName}
|
|
onChange={(e) => setDestName(e.target.value)}
|
|
placeholder="مثال: کربلای معلی، نجف اشرف، مشهد مقدس..."
|
|
className="h-11 rounded-xl text-xs sm:text-sm"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* 3. City Name EN */}
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs font-bold text-foreground block">
|
|
نام لاتین شهر (انگلیسی / English):
|
|
</label>
|
|
<Input
|
|
value={destNameEn}
|
|
onChange={(e) => setDestNameEn(e.target.value)}
|
|
placeholder="e.g. Karbala, Najaf, Mashhad..."
|
|
className="h-11 rounded-xl text-xs sm:text-sm font-mono"
|
|
/>
|
|
</div>
|
|
|
|
{/* 4. Active Status Toggle */}
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs font-bold text-foreground block">
|
|
وضعیت شهر در سامانه:
|
|
</label>
|
|
<button
|
|
type="button"
|
|
onClick={() => setDestIsActive(!destIsActive)}
|
|
className={`w-full h-11 px-4 rounded-xl text-xs font-bold flex items-center justify-between border transition-all cursor-pointer ${
|
|
destIsActive
|
|
? 'bg-emerald-500/10 text-emerald-600 border-emerald-500/30 hover:bg-emerald-500/20'
|
|
: 'bg-muted text-muted-foreground border-border/70 hover:bg-muted/80'
|
|
}`}
|
|
>
|
|
<span>{destIsActive ? 'شهر فعال و در دسترس در رزرو' : 'شهر غیرفعال'}</span>
|
|
<span
|
|
className={`size-2.5 rounded-full ${
|
|
destIsActive ? 'bg-emerald-500 animate-pulse' : 'bg-muted-foreground'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 5. Holy Places */}
|
|
<div className="space-y-1.5 pt-1">
|
|
<label className="text-xs font-bold text-foreground block">
|
|
اماکن مقدسه و زیارتگاههای شاخص (با ویرگول جدا کنید):
|
|
</label>
|
|
<Input
|
|
value={destHolyPlaces}
|
|
onChange={(e) => setDestHolyPlaces(e.target.value)}
|
|
placeholder="مثال: حرم مطهر امام حسین (ع)، حرم حضرت ابوالفضل العباس (ع)، تل زینبیه، مقام علیاکبر (ع)..."
|
|
className="h-11 rounded-xl text-xs sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* 6. City Description for Custom Trip (توضیح برای آن شهر که در Custom Trip در توضیحات شهر قرار داده میشود) */}
|
|
<div className="space-y-1.5 pt-1">
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-xs font-bold text-foreground block">
|
|
توضیحات و راهنمای شهر در سفر اختصاصی (Description):
|
|
</label>
|
|
<span className="text-[11px] text-muted-foreground">
|
|
این متن در اپلیکیشن هنگام انتخاب این مقصد به زائر نمایش داده میشود.
|
|
</span>
|
|
</div>
|
|
<textarea
|
|
value={destDescription}
|
|
onChange={(e) => setDestDescription(e.target.value)}
|
|
rows={4}
|
|
placeholder="شرح کوتاه اماکن مقدسه، شرایط سفر، فصول مناسب زیارت و ویژگیهای این شهر زیارتی..."
|
|
className="w-full p-3.5 text-xs sm:text-sm rounded-xl bg-background border border-border/70 text-foreground leading-relaxed focus:ring-1 focus:ring-primary focus:outline-none resize-y"
|
|
/>
|
|
</div>
|
|
|
|
{/* Save Footer Bar */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-3 border-t border-border/60">
|
|
<Button
|
|
type="submit"
|
|
className="h-10 px-6 rounded-xl font-bold text-xs sm:text-sm bg-primary text-white hover:bg-primary/90 shadow-xs"
|
|
>
|
|
<Ic name="check" className="size-4" />
|
|
<span>ذخیره و ثبت تغییرات شهر مقصد</span>
|
|
</Button>
|
|
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={() => setIsDestDeleteOpen(true)}
|
|
className="h-10 px-4 rounded-xl text-xs font-bold text-rose-600 hover:bg-rose-500/10 hover:text-rose-700"
|
|
>
|
|
<Ic name="trash" className="size-4" />
|
|
<span>حذف این شهر مقصد</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
{/* Delete Destination Dialog */}
|
|
<DeleteConfirmDialog
|
|
open={isDestDeleteOpen}
|
|
onOpenChange={setIsDestDeleteOpen}
|
|
title={`حذف شهر مقصد ${destination.name}`}
|
|
description={`آیا از حذف کامل شهر مقصد «${destination.name}» اطمینان دارید؟ تمام هتلها و مسیرهای مرتبط با آن نیز حذف خواهند شد.`}
|
|
onConfirm={handleDeleteDestinationConfirm}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* ========================================================================= */}
|
|
{/* TAB 2: HOTELS (هتلها و اقامتگاههای مقصد) */}
|
|
{/* ========================================================================= */}
|
|
{currentTab === 'hotels' && (
|
|
<div className="space-y-4">
|
|
{/* Action & Filter Bar */}
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3 p-3.5 rounded-2xl bg-card border border-border/70 shadow-xs">
|
|
<div className="flex items-center gap-2 flex-1 flex-wrap">
|
|
<div className="relative flex-1 min-w-[220px]">
|
|
<Ic
|
|
name="search"
|
|
className="absolute start-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground"
|
|
/>
|
|
<Input
|
|
value={hotelSearch}
|
|
onChange={(e) => setHotelSearch(e.target.value)}
|
|
placeholder="جستجوی نام هتل، فاصله تا حرم، راهنما یا امکانات..."
|
|
className="ps-9 h-10 rounded-xl bg-background text-xs sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* Stars Filter */}
|
|
<div className="flex items-center gap-1 p-1 bg-muted/60 rounded-xl border border-border/50 text-xs">
|
|
{(['all', '5', '4', '3'] as const).map((s) => (
|
|
<button
|
|
key={s}
|
|
type="button"
|
|
onClick={() => setHotelStarsFilter(s)}
|
|
className={`px-3 py-1.5 rounded-lg font-medium transition-all cursor-pointer ${
|
|
hotelStarsFilter === s
|
|
? 'bg-card text-foreground shadow-2xs font-bold'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
{s === 'all' ? 'همه ستارهها' : `${toFa(s)} ستاره`}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 justify-end">
|
|
{/* View Mode Toggle */}
|
|
<div className="flex items-center p-1 bg-muted/60 rounded-xl border border-border/50">
|
|
<button
|
|
type="button"
|
|
onClick={() => setHotelViewMode('cards')}
|
|
className={`p-1.5 rounded-lg transition-all cursor-pointer ${
|
|
hotelViewMode === 'cards'
|
|
? 'bg-card text-foreground shadow-2xs'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
title="نمایش کارتی"
|
|
>
|
|
<Ic name="grid" className="size-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setHotelViewMode('table')}
|
|
className={`p-1.5 rounded-lg transition-all cursor-pointer ${
|
|
hotelViewMode === 'table'
|
|
? 'bg-card text-foreground shadow-2xs'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
title="نمایش جدولی"
|
|
>
|
|
<Ic name="list" className="size-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={handleOpenAddHotel}
|
|
className="rounded-xl h-10 text-xs sm:text-sm font-bold gap-1.5 bg-primary text-white hover:bg-primary/90 shadow-xs"
|
|
>
|
|
<Ic name="plus" className="size-4" />
|
|
<span>افزودن هتل به {destination.name}</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hotels Content */}
|
|
{filteredHotels.length === 0 ? (
|
|
<div className="p-8 text-center rounded-2xl bg-card border border-border/70 space-y-3">
|
|
<Ic name="bed" className="size-8 text-muted-foreground mx-auto" />
|
|
<p className="text-sm font-medium text-foreground">
|
|
هیچ هتلی برای شهر {destination.name} یافت نشد.
|
|
</p>
|
|
<Button onClick={handleOpenAddHotel} variant="outline" className="rounded-xl text-xs">
|
|
+ افزودن اولین هتل برای این شهر
|
|
</Button>
|
|
</div>
|
|
) : hotelViewMode === 'cards' ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{filteredHotels.map((hotel) => (
|
|
<div
|
|
key={hotel.id}
|
|
className="rounded-2xl bg-card border border-border/70 p-4 sm:p-5 hover:border-primary/40 transition-all space-y-3 shadow-2xs flex flex-col justify-between"
|
|
>
|
|
<div className="space-y-3">
|
|
{/* Header */}
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<div className="flex items-center gap-1.5">
|
|
<h3 className="text-base font-bold text-foreground">{hotel.name}</h3>
|
|
<div className="flex items-center text-amber-500">
|
|
{Array.from({ length: hotel.stars }).map((_, i) => (
|
|
<span key={i} className="text-xs">★</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground mt-0.5 flex items-center gap-1">
|
|
<Ic name="mapPin" className="size-3 text-primary" />
|
|
<span>{hotel.cityName}</span>
|
|
<span className="text-border">•</span>
|
|
<span className="font-mono text-emerald-600 dark:text-emerald-400 font-bold">
|
|
امتیاز {toFa(hotel.score)} از ۵
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleOpenEditHotel(hotel)}
|
|
className="size-8 p-0 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
title="ویرایش هتل"
|
|
>
|
|
<Ic name="pencil" className="size-3.5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setHotelToDelete(hotel)}
|
|
className="size-8 p-0 rounded-lg hover:bg-rose-500/10 text-muted-foreground hover:text-rose-600"
|
|
title="حذف هتل"
|
|
>
|
|
<Ic name="trash" className="size-3.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Distance to shrine */}
|
|
<div className="p-2.5 rounded-xl bg-muted/50 border border-border/50 flex items-center gap-2 text-xs">
|
|
<span className="text-base select-none">🕌</span>
|
|
<div>
|
|
<span className="font-semibold text-foreground">نزدیکی به حرم: </span>
|
|
<span className="text-muted-foreground">{hotel.distanceToShrine}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hotel Description (توضیحات هتل) */}
|
|
{hotel.description && (
|
|
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-2">
|
|
{hotel.description}
|
|
</p>
|
|
)}
|
|
|
|
{/* Guide Note (فیلد توضیحی راهنما: جدا از توضیحات هتل) */}
|
|
{hotel.guideNote && (
|
|
<div className="p-2.5 rounded-xl bg-amber-500/10 border border-amber-500/20 text-xs space-y-1">
|
|
<div className="flex items-center gap-1.5 font-bold text-amber-700 dark:text-amber-400">
|
|
<Ic name="sparkles" className="size-3.5" />
|
|
<span>راهنما و تجربه اقامت در این هتل:</span>
|
|
</div>
|
|
<p className="text-muted-foreground leading-relaxed">
|
|
{hotel.guideNote}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Amenities */}
|
|
{hotel.amenities && hotel.amenities.length > 0 && (
|
|
<div className="flex items-center gap-1.5 flex-wrap pt-1">
|
|
{hotel.amenities.map((amenity, aIdx) => (
|
|
<span
|
|
key={aIdx}
|
|
className="text-[11px] px-2 py-0.5 rounded-lg bg-card border border-border/70 text-muted-foreground font-medium"
|
|
>
|
|
{amenity}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Pricing Footer */}
|
|
<div className="pt-3 border-t border-border/50 flex items-center justify-between text-xs">
|
|
<span className="text-muted-foreground">نرخ هر شب:</span>
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex items-baseline gap-1">
|
|
<span className="text-muted-foreground text-[11px]">بزرگسال:</span>
|
|
<span className="font-mono font-bold text-foreground">
|
|
${toFa(hotel.pricePerDayAdult)}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-baseline gap-1">
|
|
<span className="text-muted-foreground text-[11px]">کودک:</span>
|
|
<span className="font-mono font-bold text-foreground">
|
|
${toFa(hotel.pricePerDayChild)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
/* Table View */
|
|
<div className="overflow-hidden rounded-2xl border border-border/70 bg-card shadow-xs">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-xs sm:text-sm text-start">
|
|
<thead className="bg-muted/40 text-muted-foreground border-b border-border/60 font-medium">
|
|
<tr>
|
|
<th className="px-4 py-3.5 text-start">نام هتل</th>
|
|
<th className="px-4 py-3.5 text-center">ستاره</th>
|
|
<th className="px-4 py-3.5 text-start">فاصله تا حرم مطهر</th>
|
|
<th className="px-4 py-3.5 text-start">توضیحات و راهنمای اقامت</th>
|
|
<th className="px-4 py-3.5 text-center">نرخ هر شب</th>
|
|
<th className="px-4 py-3.5 text-center">وضعیت</th>
|
|
<th className="px-4 py-3.5 text-center">عملیات</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-border/50">
|
|
{filteredHotels.map((hotel) => (
|
|
<tr key={hotel.id} className="hover:bg-muted/20 transition-colors">
|
|
<td className="px-4 py-3.5 font-bold text-foreground">
|
|
<div>{hotel.name}</div>
|
|
<span className="text-[11px] font-mono text-muted-foreground">
|
|
امتیاز {toFa(hotel.score)}/۵
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3.5 text-center">
|
|
<span className="text-amber-500 font-bold">
|
|
{toFa(hotel.stars)} ★
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3.5 text-xs text-muted-foreground max-w-xs">
|
|
{hotel.distanceToShrine}
|
|
</td>
|
|
<td className="px-4 py-3.5 text-xs max-w-sm space-y-1">
|
|
<div className="text-muted-foreground line-clamp-1">{hotel.description}</div>
|
|
{hotel.guideNote && (
|
|
<div className="text-[11px] text-amber-600 dark:text-amber-400 line-clamp-1 font-medium">
|
|
💡 راهنما: {hotel.guideNote}
|
|
</div>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-3.5 text-center font-mono">
|
|
<div className="font-bold text-foreground">${toFa(hotel.pricePerDayAdult)}</div>
|
|
<div className="text-[10px] text-muted-foreground">کودک: ${toFa(hotel.pricePerDayChild)}</div>
|
|
</td>
|
|
<td className="px-4 py-3.5 text-center">
|
|
<span
|
|
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold ${
|
|
hotel.isActive
|
|
? 'bg-emerald-500/10 text-emerald-600'
|
|
: 'bg-muted text-muted-foreground'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`size-1.5 rounded-full ${
|
|
hotel.isActive ? 'bg-emerald-500' : 'bg-muted-foreground'
|
|
}`}
|
|
/>
|
|
{hotel.isActive ? 'فعال' : 'غیرفعال'}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3.5 text-center">
|
|
<div className="flex items-center justify-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleOpenEditHotel(hotel)}
|
|
className="size-8 p-0 rounded-lg hover:bg-muted"
|
|
>
|
|
<Ic name="pencil" className="size-3.5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setHotelToDelete(hotel)}
|
|
className="size-8 p-0 rounded-lg hover:bg-rose-500/10 text-rose-600"
|
|
>
|
|
<Ic name="trash" className="size-3.5" />
|
|
</Button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Delete Hotel Dialog */}
|
|
<DeleteConfirmDialog
|
|
open={!!hotelToDelete}
|
|
onOpenChange={(open) => !open && setHotelToDelete(null)}
|
|
title="حذف هتل"
|
|
description={`آیا از حذف هتل «${hotelToDelete?.name || ''}» اطمینان دارید؟`}
|
|
onConfirm={handleConfirmDeleteHotel}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* ========================================================================= */}
|
|
{/* TAB 3: TRANSPORTATION (روشها و مسیرهای حملونقل) */}
|
|
{/* ========================================================================= */}
|
|
{currentTab === 'transports' && (
|
|
<div className="space-y-4">
|
|
{/* Action & Filter Bar */}
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3 p-3.5 rounded-2xl bg-card border border-border/70 shadow-xs">
|
|
<div className="flex items-center gap-2 flex-1 flex-wrap">
|
|
<div className="relative flex-1 min-w-[220px]">
|
|
<Ic
|
|
name="search"
|
|
className="absolute start-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground"
|
|
/>
|
|
<Input
|
|
value={transportSearch}
|
|
onChange={(e) => setTransportSearch(e.target.value)}
|
|
placeholder="جستجوی شهر مبدأ، جزئیات خطوط یا راهنمای مسیر..."
|
|
className="ps-9 h-10 rounded-xl bg-background text-xs sm:text-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* Method Filter */}
|
|
<div className="flex items-center gap-1 p-1 bg-muted/60 rounded-xl border border-border/50 text-xs">
|
|
{(
|
|
[
|
|
{ id: 'all', label: 'همه روشها' },
|
|
{ id: 'flight', label: '✈️ هوایی' },
|
|
{ id: 'train', label: '🚆 ریلی' },
|
|
{ id: 'road', label: '🚌 جادهای' },
|
|
] as const
|
|
).map((m) => (
|
|
<button
|
|
key={m.id}
|
|
type="button"
|
|
onClick={() => setTransportMethodFilter(m.id)}
|
|
className={`px-3 py-1.5 rounded-lg font-medium transition-all cursor-pointer ${
|
|
transportMethodFilter === m.id
|
|
? 'bg-card text-foreground shadow-2xs font-bold'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
}`}
|
|
>
|
|
{m.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 justify-end">
|
|
<Button
|
|
onClick={handleOpenAddTransport}
|
|
className="rounded-xl h-10 text-xs sm:text-sm font-bold gap-1.5 bg-primary text-white hover:bg-primary/90 shadow-xs"
|
|
>
|
|
<Ic name="plus" className="size-4" />
|
|
<span>تعریف مسیر مبدأ به {destination.name}</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Transports Cards */}
|
|
{filteredRelations.length === 0 ? (
|
|
<div className="p-8 text-center rounded-2xl bg-card border border-border/70 space-y-3">
|
|
<Ic name="route" className="size-8 text-muted-foreground mx-auto" />
|
|
<p className="text-sm font-medium text-foreground">
|
|
هیچ مسیر حملونقلی به مقصد {destination.name} ثبت نشده است.
|
|
</p>
|
|
<Button onClick={handleOpenAddTransport} variant="outline" className="rounded-xl text-xs">
|
|
+ افزودن اولین مسیر ارتباطی به این مقصد
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{filteredRelations.map((rel) => (
|
|
<div
|
|
key={rel.id}
|
|
className="rounded-2xl bg-card border border-border/70 p-4 sm:p-5 hover:border-primary/40 transition-all space-y-3.5 shadow-2xs"
|
|
>
|
|
{/* Route Header */}
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center gap-1.5 font-bold text-foreground text-sm sm:text-base">
|
|
<span>{rel.fromCity}</span>
|
|
<Ic name="arrowLeft" className="size-3.5 text-primary" />
|
|
<span className="text-primary">{destination.name}</span>
|
|
</div>
|
|
<span className="text-[11px] px-2 py-0.5 rounded-md bg-muted text-muted-foreground">
|
|
{rel.fromCountry}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleOpenEditTransport(rel)}
|
|
className="size-8 p-0 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
title="ویرایش مسیر"
|
|
>
|
|
<Ic name="pencil" className="size-3.5" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setTransportToDelete(rel)}
|
|
className="size-8 p-0 rounded-lg hover:bg-rose-500/10 text-muted-foreground hover:text-rose-600"
|
|
title="حذف مسیر"
|
|
>
|
|
<Ic name="trash" className="size-3.5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Guide Note for Route (فیلد توضیحی راهنمای مسیر) */}
|
|
{rel.guideNote && (
|
|
<div className="p-2.5 rounded-xl bg-sky-500/10 border border-sky-500/20 text-xs space-y-1">
|
|
<div className="flex items-center gap-1.5 font-bold text-sky-700 dark:text-sky-300">
|
|
<Ic name="route" className="size-3.5" />
|
|
<span>راهنما و شرایط سفر در این مسیر:</span>
|
|
</div>
|
|
<p className="text-muted-foreground leading-relaxed">
|
|
{rel.guideNote}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Transport Methods Status & Details */}
|
|
<div className="space-y-2 pt-1">
|
|
{/* Airplane */}
|
|
<div
|
|
className={`p-2.5 rounded-xl border transition-all ${
|
|
rel.hasAirplane
|
|
? 'bg-sky-500/5 border-sky-500/30'
|
|
: 'bg-muted/30 border-border/40 opacity-60'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-base select-none">✈️</span>
|
|
<span className="text-xs font-bold text-foreground">پرواز هوایی</span>
|
|
{rel.hasAirplane && (
|
|
<span className="text-[10px] px-1.5 py-0.2 rounded bg-sky-500/10 text-sky-600 font-semibold">
|
|
فعال
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{rel.hasAirplane && rel.airplaneDetails && (
|
|
<p className="text-xs text-muted-foreground mt-1.5 ps-6">
|
|
{rel.airplaneDetails}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Train */}
|
|
<div
|
|
className={`p-2.5 rounded-xl border transition-all ${
|
|
rel.hasTrain
|
|
? 'bg-emerald-500/5 border-emerald-500/30'
|
|
: 'bg-muted/30 border-border/40 opacity-60'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-base select-none">🚆</span>
|
|
<span className="text-xs font-bold text-foreground">قطار ریلی</span>
|
|
{rel.hasTrain && (
|
|
<span className="text-[10px] px-1.5 py-0.2 rounded bg-emerald-500/10 text-emerald-600 font-semibold">
|
|
فعال
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{rel.hasTrain && rel.trainDetails && (
|
|
<p className="text-xs text-muted-foreground mt-1.5 ps-6">
|
|
{rel.trainDetails}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Road */}
|
|
<div
|
|
className={`p-2.5 rounded-xl border transition-all ${
|
|
rel.hasRoad
|
|
? 'bg-amber-500/5 border-amber-500/30'
|
|
: 'bg-muted/30 border-border/40 opacity-60'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-base select-none">🚌</span>
|
|
<span className="text-xs font-bold text-foreground">اتوبوس و ون VIP (جادهای)</span>
|
|
{rel.hasRoad && (
|
|
<span className="text-[10px] px-1.5 py-0.2 rounded bg-amber-500/10 text-amber-600 font-semibold">
|
|
فعال
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{rel.hasRoad && rel.roadDetails && (
|
|
<p className="text-xs text-muted-foreground mt-1.5 ps-6">
|
|
{rel.roadDetails}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Delete Transport Dialog */}
|
|
<DeleteConfirmDialog
|
|
open={!!transportToDelete}
|
|
onOpenChange={(open) => !open && setTransportToDelete(null)}
|
|
title="حذف مسیر حملونقل"
|
|
description={`آیا از حذف مسیر «${transportToDelete?.fromCity} به ${destination.name}» مطمئن هستید؟`}
|
|
onConfirm={handleConfirmDeleteTransport}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* ========================================================================= */}
|
|
{/* MODAL: ADD / EDIT HOTEL */}
|
|
{/* ========================================================================= */}
|
|
<Dialog open={isHotelFormOpen} onOpenChange={setIsHotelFormOpen}>
|
|
<DialogContent className="max-w-xl rounded-2xl p-6 max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base font-bold text-foreground flex items-center gap-2">
|
|
<Ic name="bed" className="size-5 text-primary" />
|
|
{editingHotel ? 'ویرایش مشخصات هتل' : `افزودن هتل جدید به ${destination.name}`}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-xs text-muted-foreground">
|
|
اطلاعات، درجه ستاره، فاصله تا حرم، توضیحات هتل و فیلد راهنمای اقامت را ثبت کنید.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSaveHotel} className="space-y-4 my-2 text-xs sm:text-sm">
|
|
{/* Locked City Name */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-foreground block mb-1">شهر مقصد:</label>
|
|
<div className="h-10 px-3 flex items-center rounded-xl bg-muted/70 border border-border/70 text-foreground font-bold">
|
|
{destination.flag} {destination.name} ({destination.country})
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hotel Name */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-foreground block mb-1">
|
|
نام هتل (فارسی و انگلیسی):
|
|
</label>
|
|
<Input
|
|
value={hotelFormName}
|
|
onChange={(e) => setHotelFormName(e.target.value)}
|
|
placeholder="مثال: هتل بارون کربلا (Baron Hotel Karbala)"
|
|
className="h-10 rounded-xl"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Stars & Score */}
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="text-xs font-semibold text-foreground block mb-1">
|
|
درجه هتل (تعداد ستاره):
|
|
</label>
|
|
<select
|
|
value={hotelFormStars}
|
|
onChange={(e) => setHotelFormStars(Number(e.target.value))}
|
|
className="w-full h-10 px-3 text-xs sm:text-sm rounded-xl bg-background border border-border/70 text-foreground font-medium focus:ring-1 focus:ring-primary focus:outline-none"
|
|
>
|
|
<option value={5}>۵ ستاره VIP (لوکس)</option>
|
|
<option value={4}>۴ ستاره عالی</option>
|
|
<option value={3}>۳ ستاره اقتصادی</option>
|
|
<option value={2}>۲ ستاره</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs font-semibold text-foreground block mb-1">
|
|
امتیاز رضایت (از ۵):
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
step="0.1"
|
|
min="1"
|
|
max="5"
|
|
value={hotelFormScore}
|
|
onChange={(e) => setHotelFormScore(Number(e.target.value))}
|
|
className="h-10 rounded-xl font-mono"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Distance to Shrine */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-foreground block mb-1">
|
|
فاصله و نزدیکی به حرم مطهر:
|
|
</label>
|
|
<Input
|
|
value={hotelFormDistance}
|
|
onChange={(e) => setHotelFormDistance(e.target.value)}
|
|
placeholder="مثال: ۴۰۰ متر تا باب بغداد حرم مطهر (ترانسفر رایگان هر ۱۰ دقیقه)"
|
|
className="h-10 rounded-xl"
|
|
/>
|
|
</div>
|
|
|
|
{/* Full Description (توضیحات خود هتل) */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-foreground block mb-1">
|
|
توضیحات و مشخصات هتل (Description):
|
|
</label>
|
|
<textarea
|
|
value={hotelFormDescription}
|
|
onChange={(e) => setHotelFormDescription(e.target.value)}
|
|
placeholder="شرح امکانات، بوفه، چشمانداز اتاقها و شرایط پذیرش..."
|
|
rows={2}
|
|
className="w-full p-3 text-xs sm:text-sm rounded-xl bg-background border border-border/70 text-foreground focus:ring-1 focus:ring-primary focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
{/* Guide Note (فیلد توضیحی راهنما: جدا از توضیحات هتل) */}
|
|
<div className="p-3 rounded-xl bg-amber-500/5 border border-amber-500/20 space-y-1.5">
|
|
<label className="text-xs font-bold text-amber-800 dark:text-amber-300 flex items-center gap-1.5">
|
|
<Ic name="sparkles" className="size-3.5" />
|
|
<span>فیلد توضیحی راهنما (Guide Note - راهنمای تجربی مسافران):</span>
|
|
</label>
|
|
<p className="text-[11px] text-muted-foreground">
|
|
توضیحی تکمیلی و راهنمایی برای این هتل (مثلاً: «این هتل در ایام ولادت شلوغ است»، یا نحوه ارائه خدمات به خانوادهها).
|
|
</p>
|
|
<textarea
|
|
value={hotelFormGuideNote}
|
|
onChange={(e) => setHotelFormGuideNote(e.target.value)}
|
|
placeholder="مثال: هتل بارون در کربلا معمولاً ترانسفر سریع و لاندری رایگان شبانهروزی دارد..."
|
|
rows={2}
|
|
className="w-full p-2.5 text-xs rounded-xl bg-background border border-border/70 text-foreground focus:ring-1 focus:ring-primary focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
{/* Amenities Checkboxes */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-foreground block mb-1.5">
|
|
امکانات و خدمات هتل:
|
|
</label>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 p-2.5 rounded-xl bg-muted/30 border border-border/60 max-h-36 overflow-y-auto">
|
|
{COMMON_AMENITIES.map((amenity) => {
|
|
const isChecked = hotelFormAmenities.includes(amenity.name)
|
|
return (
|
|
<label
|
|
key={amenity.id}
|
|
className="flex items-center gap-2 cursor-pointer text-xs text-foreground p-1 hover:bg-card rounded-lg"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={isChecked}
|
|
onChange={() => handleToggleHotelAmenity(amenity.name)}
|
|
className="rounded border-border text-primary focus:ring-primary"
|
|
/>
|
|
<span>{amenity.name}</span>
|
|
</label>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Add Custom Amenity */}
|
|
<div className="flex items-center gap-2 mt-2">
|
|
<Input
|
|
value={hotelFormCustomAmenity}
|
|
onChange={(e) => setHotelFormCustomAmenity(e.target.value)}
|
|
placeholder="افزودن امکانات دیگر..."
|
|
className="h-9 rounded-xl text-xs"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleAddCustomHotelAmenity}
|
|
className="rounded-xl h-9 text-xs"
|
|
>
|
|
افزودن
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Daily Pricing */}
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="text-xs font-semibold text-foreground block mb-1">
|
|
نرخ روزانه بزرگسال (دلار $):
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
min="0"
|
|
value={hotelFormPriceAdult}
|
|
onChange={(e) => setHotelFormPriceAdult(Number(e.target.value))}
|
|
className="h-10 rounded-xl font-mono"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="text-xs font-semibold text-foreground block mb-1">
|
|
نرخ روزانه کودک (دلار $):
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
min="0"
|
|
value={hotelFormPriceChild}
|
|
onChange={(e) => setHotelFormPriceChild(Number(e.target.value))}
|
|
className="h-10 rounded-xl font-mono"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Active Status */}
|
|
<div className="flex items-center justify-between p-3 rounded-xl bg-muted/40 border border-border/70">
|
|
<span className="text-xs font-semibold text-foreground">وضعیت هتل (فعال در سامانه):</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setHotelFormIsActive(!hotelFormIsActive)}
|
|
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all cursor-pointer ${
|
|
hotelFormIsActive
|
|
? 'bg-emerald-500 text-white'
|
|
: 'bg-muted text-muted-foreground'
|
|
}`}
|
|
>
|
|
{hotelFormIsActive ? 'فعال' : 'غیرفعال'}
|
|
</button>
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0 pt-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setIsHotelFormOpen(false)}
|
|
className="rounded-xl text-xs"
|
|
>
|
|
انصراف
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
className="rounded-xl text-xs font-bold bg-primary text-white hover:bg-primary/90"
|
|
>
|
|
{editingHotel ? 'ذخیره تغییرات هتل' : 'ثبت نهایی هتل'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* ========================================================================= */}
|
|
{/* MODAL: ADD / EDIT TRANSPORT ROUTE */}
|
|
{/* ========================================================================= */}
|
|
<Dialog open={isTransportFormOpen} onOpenChange={setIsTransportFormOpen}>
|
|
<DialogContent className="max-w-lg rounded-2xl p-6 max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base font-bold text-foreground flex items-center gap-2">
|
|
<Ic name="route" className="size-5 text-primary" />
|
|
{editingTransport ? 'ویرایش مسیر حملونقل' : `تعریف مسیر حملونقل جدید به ${destination.name}`}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-xs text-muted-foreground">
|
|
مشخصات وسایل نقلیه، خطوط و فیلد توضیحی راهنمای مسیر را ثبت کنید.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSaveTransport} className="space-y-4 my-2 text-xs sm:text-sm">
|
|
{/* Origin City */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-foreground block mb-1">شهر مبدأ:</label>
|
|
<select
|
|
value={transportFormFromCity}
|
|
onChange={(e) => {
|
|
const cityName = e.target.value
|
|
setTransportFormFromCity(cityName)
|
|
const orig = allOrigins.find((o) => o.name === cityName)
|
|
if (orig) setTransportFormFromCountry(orig.country)
|
|
}}
|
|
className="w-full h-10 px-3 text-xs sm:text-sm rounded-xl bg-background border border-border/70 text-foreground font-medium focus:ring-1 focus:ring-primary focus:outline-none"
|
|
>
|
|
{allOrigins.map((orig) => (
|
|
<option key={orig.id} value={orig.name}>
|
|
{orig.flag} {orig.name} ({orig.country})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Destination Locked */}
|
|
<div>
|
|
<label className="text-xs font-semibold text-foreground block mb-1">شهر مقصد (قفلشده):</label>
|
|
<div className="h-10 px-3 flex items-center rounded-xl bg-muted/70 border border-border/70 text-foreground font-bold">
|
|
{destination.flag} {destination.name} ({destination.country})
|
|
</div>
|
|
</div>
|
|
|
|
{/* Guide Note for Route (فیلد توضیحی راهنمای مسیر) */}
|
|
<div className="p-3 rounded-xl bg-sky-500/5 border border-sky-500/20 space-y-1.5">
|
|
<label className="text-xs font-bold text-sky-800 dark:text-sky-300 flex items-center gap-1.5">
|
|
<Ic name="route" className="size-3.5" />
|
|
<span>فیلد توضیحی راهنمای این مسیر (مثلاً نحوه معمول پروازها یا ترانسفر):</span>
|
|
</label>
|
|
<textarea
|
|
value={transportFormGuideNote}
|
|
onChange={(e) => setTransportFormGuideNote(e.target.value)}
|
|
placeholder="مثال: پرواز مشهد به نجف معمولاً به صورت روزانه انجام میشود..."
|
|
rows={2}
|
|
className="w-full p-2.5 text-xs rounded-xl bg-background border border-border/70 text-foreground focus:ring-1 focus:ring-primary focus:outline-none"
|
|
/>
|
|
</div>
|
|
|
|
{/* Airplane Toggle & Details */}
|
|
<div className="p-3 rounded-xl border border-border/70 space-y-2">
|
|
<label className="flex items-center gap-2 cursor-pointer font-bold text-foreground text-xs">
|
|
<input
|
|
type="checkbox"
|
|
checked={transportFormHasAirplane}
|
|
onChange={(e) => setTransportFormHasAirplane(e.target.checked)}
|
|
className="rounded border-border text-primary focus:ring-primary"
|
|
/>
|
|
<span>✈️ پرواز هوایی در این مسیر فعال است</span>
|
|
</label>
|
|
{transportFormHasAirplane && (
|
|
<Input
|
|
value={transportFormAirplaneDetails}
|
|
onChange={(e) => setTransportFormAirplaneDetails(e.target.value)}
|
|
placeholder="مثال: پرواز مستقیم سلامایر یا ایرانایر"
|
|
className="h-9 rounded-xl text-xs"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Train Toggle & Details */}
|
|
<div className="p-3 rounded-xl border border-border/70 space-y-2">
|
|
<label className="flex items-center gap-2 cursor-pointer font-bold text-foreground text-xs">
|
|
<input
|
|
type="checkbox"
|
|
checked={transportFormHasTrain}
|
|
onChange={(e) => setTransportFormHasTrain(e.target.checked)}
|
|
className="rounded border-border text-primary focus:ring-primary"
|
|
/>
|
|
<span>🚆 قطار ریلی در این مسیر فعال است</span>
|
|
</label>
|
|
{transportFormHasTrain && (
|
|
<Input
|
|
value={transportFormTrainDetails}
|
|
onChange={(e) => setTransportFormTrainDetails(e.target.value)}
|
|
placeholder="مثال: قطارهای ۵ ستاره فدک یا خط بصره - کربلا"
|
|
className="h-9 rounded-xl text-xs"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Road Toggle & Details */}
|
|
<div className="p-3 rounded-xl border border-border/70 space-y-2">
|
|
<label className="flex items-center gap-2 cursor-pointer font-bold text-foreground text-xs">
|
|
<input
|
|
type="checkbox"
|
|
checked={transportFormHasRoad}
|
|
onChange={(e) => setTransportFormHasRoad(e.target.checked)}
|
|
className="rounded border-border text-primary focus:ring-primary"
|
|
/>
|
|
<span>🚌 حملونقل جادهای (اتوبوس VIP و ون اختصاصی)</span>
|
|
</label>
|
|
{transportFormHasRoad && (
|
|
<Input
|
|
value={transportFormRoadDetails}
|
|
onChange={(e) => setTransportFormRoadDetails(e.target.value)}
|
|
placeholder="مثال: ترانسفر زمینی از گذرگاه مرزی مهران یا شلمچه"
|
|
className="h-9 rounded-xl text-xs"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Active Switch */}
|
|
<div className="flex items-center justify-between p-3 rounded-xl bg-muted/40 border border-border/70">
|
|
<span className="text-xs font-semibold text-foreground">وضعیت مسیر (فعال در سامانه):</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setTransportFormIsActive(!transportFormIsActive)}
|
|
className={`px-3 py-1.5 rounded-lg text-xs font-bold transition-all cursor-pointer ${
|
|
transportFormIsActive
|
|
? 'bg-emerald-500 text-white'
|
|
: 'bg-muted text-muted-foreground'
|
|
}`}
|
|
>
|
|
{transportFormIsActive ? 'فعال' : 'غیرفعال'}
|
|
</button>
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0 pt-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => setIsTransportFormOpen(false)}
|
|
className="rounded-xl text-xs"
|
|
>
|
|
انصراف
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
className="rounded-xl text-xs font-bold bg-primary text-white hover:bg-primary/90"
|
|
>
|
|
{editingTransport ? 'ذخیره تغییرات مسیر' : 'ثبت مسیر حملونقل'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
)
|
|
}
|