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.
 
 
 

831 lines
40 KiB

import React, { useState, useEffect, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
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 { Skeleton } from '@/components/ui/skeleton'
import { fetchTours, fetchTourItineraries } from './services/tour-api'
import type { TourListItem, TourItineraryItem } from './types'
import { TourStatusBadge, CountryFlagBadge } from './components/TourStatusBadge'
import FilterSelect from '@/components/shared/FilterSelect'
import { Shimmer } from '@/components/shared/Shimmer'
import { toFa } from '@/lib/utils'
interface TourWithItineraries {
tour: TourListItem
itineraries: TourItineraryItem[]
isLoadingItinerary: boolean
itineraryError: string | null
hoursToStart: number
isCurrentlyRunning: boolean
isImminent: boolean
currentStepIndex: number
progressPercent: number
timelineStatus: 'TRAVELING' | 'IMMINENT' | 'UPCOMING' | 'FINISHED'
}
export default function OngoingToursPage() {
const navigate = useNavigate()
const [tours, setTours] = useState<TourListItem[]>([])
const [itinerariesMap, setItinerariesMap] = useState<Record<number, { data: TourItineraryItem[]; loading: boolean; error: string | null }>>({})
const [isLoading, setIsLoading] = useState(true)
const [isRefreshing, setIsRefreshing] = useState(false)
const [error, setError] = useState<string | null>(null)
// فیلترها و جستجو
const [activeTab, setActiveTab] = useState<'ALL' | 'TRAVELING' | 'IMMINENT'>('ALL')
const [searchTerm, setSearchTerm] = useState('')
const [countryFilter, setCountryFilter] = useState('ALL')
const [expandedTourIds, setExpandedTourIds] = useState<Record<number, boolean>>({})
// لود فهرست همه تورها
const loadToursData = async (showRefresh = false) => {
if (showRefresh) setIsRefreshing(true)
else setIsLoading(true)
setError(null)
try {
const res = await fetchTours({ limit: 100 })
const rawTours = res.results || []
setTours(rawTours)
// باز کردن پیش‌فرض کارت اولین تورهای در حال اجرا
const initialExpanded: Record<number, boolean> = {}
rawTours.forEach((t) => {
initialExpanded[t.id] = true
})
setExpandedTourIds(initialExpanded)
} catch (err: any) {
setError(err.message || 'خطا در برقراری ارتباط با وب‌سرویس تورها')
} finally {
setIsLoading(false)
setIsRefreshing(false)
}
}
useEffect(() => {
loadToursData()
}, [])
// محاسبه وضعیت زمانی و تطبیق منطق ۲۴ تا ۴۸ ساعت مانده به شروع و تورهای در حال اجرا
const processedTours: TourWithItineraries[] = useMemo(() => {
const now = new Date()
const nowMs = now.getTime()
return tours
.map((tour) => {
// ساعت شروع: ابتدای روز؛ ساعت پایان: انتهای روز
const startMs = new Date(tour.started_at).setHours(0, 0, 0, 0)
const endMs = new Date(tour.ended_at).setHours(23, 59, 59, 999)
const hoursToStart = Math.round((startMs - nowMs) / (1000 * 3600))
const isCurrentlyRunning = (nowMs >= startMs && nowMs <= endMs) || tour.trip_status === 'TRAVELING'
// نزدیک به شروع: بین ۰ تا ۴۸ ساعت مانده به شروع
const isImminent = !isCurrentlyRunning && hoursToStart > 0 && hoursToStart <= 48
let timelineStatus: 'TRAVELING' | 'IMMINENT' | 'UPCOMING' | 'FINISHED' = 'UPCOMING'
if (isCurrentlyRunning) {
timelineStatus = 'TRAVELING'
} else if (isImminent) {
timelineStatus = 'IMMINENT'
} else if (nowMs > endMs) {
timelineStatus = 'FINISHED'
}
const itState = itinerariesMap[tour.id]
const itineraries = itState?.data || []
const isLoadingItinerary = itState?.loading || false
const itineraryError = itState?.error || null
// تعیین مرحله جاری سفر (Current Itinerary Step)
let currentStepIndex = -1
if (itineraries.length > 0) {
// بررسی اول: آیا مرحله‌ای صراحتاً وضعیت 'In Progress' یا ۲ دارد؟
const explicitCurrent = itineraries.findIndex(
(it) => it.status === 'In Progress' || it.status === 2 || it.status === '2'
)
if (explicitCurrent !== -1) {
currentStepIndex = explicitCurrent
} else {
// بررسی دوم: محاسبه بر اساس زمان حال و تاریخ هر مرحله
let foundByTime = -1
for (let i = 0; i < itineraries.length; i++) {
const itStart = itineraries[i].started_at ? new Date(itineraries[i].started_at).getTime() : 0
const itEnd = itineraries[i].ended_at
? new Date(itineraries[i].ended_at!).getTime()
: itStart + 24 * 3600 * 1000
if (nowMs >= itStart && nowMs <= itEnd) {
foundByTime = i
break
}
}
if (foundByTime !== -1) {
currentStepIndex = foundByTime
} else if (isCurrentlyRunning) {
// اگر در حال سفر است ولی زمان دقیق ثبت نشده، اولین مرحله در انتظار یا ابتدا را انتخاب کن
const firstNotStarted = itineraries.findIndex(
(it) => it.status === 'Not Started' || it.status === 3 || it.status === '3'
)
currentStepIndex = firstNotStarted !== -1 ? firstNotStarted : 0
} else if (isImminent) {
// تور به زودی شروع می‌شود، مرحله ۱ مرحله ورودی است
currentStepIndex = 0
}
}
}
// درصد پیشرفت تور
let progressPercent = 0
if (itineraries.length > 0) {
if (timelineStatus === 'FINISHED') {
progressPercent = 100
} else if (currentStepIndex >= 0) {
progressPercent = Math.round(((currentStepIndex + 1) / itineraries.length) * 100)
}
} else if (isCurrentlyRunning) {
const totalDuration = Math.max(1, endMs - startMs)
const elapsed = Math.max(0, nowMs - startMs)
progressPercent = Math.min(100, Math.round((elapsed / totalDuration) * 100))
}
return {
tour,
itineraries,
isLoadingItinerary,
itineraryError,
hoursToStart,
isCurrentlyRunning,
isImminent,
currentStepIndex,
progressPercent,
timelineStatus,
}
})
.filter((item) => {
// معیار قرار گرفتن در این صفحه:
// ۱. تور در حال حاضر TRAVELING باشد یا تاریخ امروز در بازه شروع و پایان باشد
// ۲. یا زمان شروع آن در بازه نزدیک (۲۴ الی ۴۸ ساعت آینده) باشد
return item.isCurrentlyRunning || item.isImminent || item.tour.trip_status === 'TRAVELING'
})
}, [tours, itinerariesMap])
// لود خودکار Itineraryهای تورهای در حال برگزاری
useEffect(() => {
if (processedTours.length === 0) return
processedTours.forEach((item) => {
const tourId = item.tour.id
if (!itinerariesMap[tourId]) {
setItinerariesMap((prev) => ({
...prev,
[tourId]: { data: [], loading: true, error: null },
}))
fetchTourItineraries(tourId)
.then((res) => {
setItinerariesMap((prev) => ({
...prev,
[tourId]: { data: res.results || [], loading: false, error: null },
}))
})
.catch((err: any) => {
setItinerariesMap((prev) => ({
...prev,
[tourId]: {
data: [],
loading: false,
error: err.message || 'خطا در دریافت برنامه سفر',
},
}))
})
}
})
}, [processedTours])
// فیلتر و جستجو
const filteredList = useMemo(() => {
return processedTours.filter((item) => {
// فیلتر تب وضعیت زمانی
if (activeTab === 'TRAVELING' && !item.isCurrentlyRunning) return false
if (activeTab === 'IMMINENT' && !item.isImminent) return false
// فیلتر کشور
if (countryFilter !== 'ALL' && item.tour.destination_country !== countryFilter) return false
// جستجو
if (searchTerm.trim()) {
const query = searchTerm.toLowerCase()
const matchTitle = item.tour.title.toLowerCase().includes(query)
const matchSlug = item.tour.slug.toLowerCase().includes(query)
if (!matchTitle && !matchSlug) return false
}
return true
})
}, [processedTours, activeTab, countryFilter, searchTerm])
// شاخص‌های آماری بالای صفحه
const stats = useMemo(() => {
const totalOngoing = processedTours.length
const currentlyTraveling = processedTours.filter((t) => t.isCurrentlyRunning).length
const startingSoon = processedTours.filter((t) => t.isImminent).length
const distinctCountries = new Set(processedTours.map((t) => t.tour.destination_country).filter(Boolean)).size
return { totalOngoing, currentlyTraveling, startingSoon, distinctCountries }
}, [processedTours])
const toggleExpand = (tourId: number) => {
setExpandedTourIds((prev) => ({
...prev,
[tourId]: !prev[tourId],
}))
}
// قالب‌بندی زمان و تاریخ به فرمت خوانا
const formatDateTime = (dateStr?: string | null) => {
if (!dateStr) return '-'
try {
const d = new Date(dateStr)
return d.toLocaleDateString('fa-IR', {
month: 'long',
day: 'numeric',
weekday: 'short',
})
} catch {
return dateStr
}
}
return (
<div className="space-y-6 pt-2 pb-14">
{/* سربرگ صفحه تورهای در حال برگزاری */}
<div className="flex flex-col gap-4 rounded-2xl border border-border-soft bg-card/40 p-6 shadow-sm sm:flex-row sm:items-center sm:justify-between backdrop-blur-xs">
<div className="space-y-1">
<div className="flex items-center gap-2.5">
<div className="flex size-9 items-center justify-center rounded-xl bg-amber-500/10 text-amber-500">
<Ic name="calendar" className="size-5" />
</div>
<h1 className="text-xl font-black text-foreground">تورهای در حال برگزاری</h1>
<Badge
variant="outline"
className="border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400 font-bold"
>
{stats.totalOngoing} تور فعال و در جریان
</Badge>
</div>
<p className="text-xs text-grey-3">
پایش زنده وضعیت سفرهای جاری، زمان‌بندی روزانه (Itinerary)، موقعیت مرحله فعلی و وضعیت مسافران
</p>
</div>
<div className="flex flex-wrap items-center gap-2.5">
{/* لینک سریع به لیست همه تورها */}
<Button
variant="outline"
size="sm"
onClick={() => navigate('/tours')}
className="border-border-soft hover:bg-card/70 text-xs font-bold gap-1.5"
>
<Ic name="map" className="size-3.5 text-primary" />
مدیریت تمام تورها
</Button>
{/* دکمه بروزرسانی زنده */}
<Button
variant="outline"
size="sm"
onClick={() => loadToursData(true)}
disabled={isRefreshing}
className="border-border-soft hover:bg-card/70 text-xs font-bold gap-1.5"
>
<Ic name="refresh" className={`size-3.5 ${isRefreshing ? 'animate-spin' : ''}`} />
بروزرسانی داده‌ها
</Button>
</div>
</div>
{/* کارت‌های شاخص‌های کلیدی آماری (KPIs) */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3.5">
<Card className="border-border-soft bg-card/40 shadow-xs">
<CardContent className="p-4 flex items-center justify-between">
<div>
<p className="text-[11px] text-muted-foreground font-medium">کل تورهای در جریان</p>
{isLoading ? (
<Skeleton className="mt-2 h-7 w-12 rounded-lg" />
) : (
<p className="text-xl font-black mt-1 font-mono text-foreground">{toFa(stats.totalOngoing)}</p>
)}
</div>
<div className="size-9 rounded-xl bg-muted/60 flex items-center justify-center text-muted-foreground">
<Ic name="calendar" className="size-4" />
</div>
</CardContent>
</Card>
<Card className="border-border-soft bg-card/40 shadow-xs">
<CardContent className="p-4 flex items-center justify-between">
<div>
<p className="text-[11px] text-muted-foreground font-medium">هم‌اکنون در حال سفر</p>
{isLoading ? (
<Skeleton className="mt-2 h-7 w-12 rounded-lg" />
) : (
<p className="text-xl font-black mt-1 font-mono text-sky-500">
{toFa(stats.currentlyTraveling)}
</p>
)}
</div>
<div className="size-9 rounded-xl bg-sky-500/10 text-sky-500 flex items-center justify-center">
<span className="relative flex size-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
<span className="relative inline-flex rounded-full size-2 bg-sky-500"></span>
</span>
</div>
</CardContent>
</Card>
<Card className="border-border-soft bg-card/40 shadow-xs">
<CardContent className="p-4 flex items-center justify-between">
<div>
<p className="text-[11px] text-muted-foreground font-medium">نزدیک به شروع (۲۴-۴۸ ساعت)</p>
{isLoading ? (
<Skeleton className="mt-2 h-7 w-12 rounded-lg" />
) : (
<p className="text-xl font-black mt-1 font-mono text-emerald-600 dark:text-emerald-400">
{toFa(stats.startingSoon)}
</p>
)}
</div>
<div className="size-9 rounded-xl bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center">
<Ic name="clock" className="size-4" />
</div>
</CardContent>
</Card>
<Card className="border-border-soft bg-card/40 shadow-xs">
<CardContent className="p-4 flex items-center justify-between">
<div>
<p className="text-[11px] text-muted-foreground font-medium">مقاصد در حال میزبانی</p>
{isLoading ? (
<Skeleton className="mt-2 h-7 w-12 rounded-lg" />
) : (
<p className="text-xl font-black mt-1 font-mono text-foreground">{toFa(stats.distinctCountries)}</p>
)}
</div>
<div className="size-9 rounded-xl bg-muted/60 flex items-center justify-center text-muted-foreground">
<Ic name="globe" className="size-4" />
</div>
</CardContent>
</Card>
</div>
{/* نوار فیلتر و تب‌های زمانی */}
<div className="flex flex-col gap-3 rounded-2xl border border-border-soft bg-card/40 p-4 sm:flex-row sm:items-center sm:justify-between shadow-xs">
{/* تب‌های وضعیت زمانی */}
<div className="flex flex-wrap items-center gap-1.5">
<Button
variant={activeTab === 'ALL' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setActiveTab('ALL')}
className={`text-xs font-bold rounded-xl h-8 px-3 ${
activeTab === 'ALL' ? 'bg-primary/15 text-primary border border-primary/20' : 'text-grey-3'
}`}
>
همه تورهای در جریان ({toFa(processedTours.length)})
</Button>
<Button
variant={activeTab === 'TRAVELING' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setActiveTab('TRAVELING')}
className={`text-xs font-bold rounded-xl h-8 px-3 ${
activeTab === 'TRAVELING' ? 'bg-sky-500/15 text-sky-500 border border-sky-500/20' : 'text-grey-3'
}`}
>
هم‌اکنون در حال سفر ({toFa(stats.currentlyTraveling)})
</Button>
<Button
variant={activeTab === 'IMMINENT' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => setActiveTab('IMMINENT')}
className={`text-xs font-bold rounded-xl h-8 px-3 ${
activeTab === 'IMMINENT'
? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20'
: 'text-grey-3'
}`}
>
نزدیک به شروع ۲۴-۴۸ ساعت ({toFa(stats.startingSoon)})
</Button>
</div>
{/* فیلتر مقصد و جستجو */}
<div className="flex flex-wrap items-center gap-2">
<div className="relative min-w-[200px] flex-1 sm:w-64 sm:flex-none">
<Ic name="search" className="absolute right-3 top-1/2 -translate-y-1/2 size-3.5 text-grey-3" />
<Input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="جستجوی عنوان یا اسلاگ تور..."
className="h-8 pr-8 text-xs bg-surface-base border-border-soft"
/>
</div>
<FilterSelect
value={countryFilter}
onChange={setCountryFilter}
options={[
{ value: 'ALL', label: 'همه مقاصد' },
{ value: 'IQ', label: '🇮🇶 عراق' },
{ value: 'IR', label: '🇮🇷 ایران' },
{ value: 'SA', label: '🇸🇦 عربستان' },
{ value: 'SY', label: '🇸🇾 سوریه' },
]}
allValue="ALL"
size="sm"
className="w-32"
/>
</div>
</div>
{/* وضعیت لودینگ استاندارد Shimmer */}
{isLoading ? (
<Shimmer withCard cardHeaderTitle="تورهای در حال برگزاری" cardHeaderIcon="calendar" rows={6} />
) : error ? (
/* وضعیت خطا */
<div className="rounded-2xl border border-rose-500/20 bg-rose-500/5 p-12 text-center text-rose-500">
<Ic name="alert" className="mx-auto size-10 mb-3" />
<h3 className="text-base font-bold">خطا در بارگذاری تورهای در حال برگزاری</h3>
<p className="mt-1 text-xs text-grey-3 max-w-md mx-auto">{error}</p>
<Button
variant="outline"
size="sm"
onClick={() => loadToursData()}
className="mt-5 border-rose-500/30 text-rose-500 hover:bg-rose-500/10"
>
تلاش مجدد
</Button>
</div>
) : filteredList.length === 0 ? (
/* وضعیت خالی */
<div className="rounded-2xl border border-dashed border-border-soft bg-card/20 p-16 text-center">
<div className="mx-auto flex size-14 items-center justify-center rounded-2xl bg-primary/10 text-primary">
<Ic name="calendar" className="size-7" />
</div>
<h3 className="mt-4 text-sm font-bold text-foreground">توری در حال برگزاری یا نزدیک به شروع یافت نشد</h3>
<p className="mt-1 text-xs text-grey-3 max-w-md mx-auto">
در بازه زمانی جاری (هم‌اکنون یا ۲۴ تا ۴۸ ساعت آینده)، هیچ توری مطابق با فیلترهای انتخابی شما در حال
برگزاری نیست.
</p>
<Button
variant="outline"
size="sm"
onClick={() => navigate('/tours')}
className="mt-5 border-border-soft text-xs font-bold"
>
مشاهده فهرست همه تورها
</Button>
</div>
) : (
/* فهرست کارت‌های تورهای در حال برگزاری با برنامه‌های سفر گام‌به‌گام (Itinerary Stepper) */
<div className="space-y-6">
{filteredList.map((item) => {
const {
tour,
itineraries,
isLoadingItinerary,
itineraryError,
isCurrentlyRunning,
isImminent,
hoursToStart,
currentStepIndex,
progressPercent,
} = item
const isExpanded = Boolean(expandedTourIds[tour.id])
const imgUrl =
tour.image?.image_url?.md ||
tour.image?.image_url?.medium ||
tour.image?.image_url?.lg ||
tour.image?.image_url?.large ||
tour.image?.image_url?.original ||
'/placeholder.jpg'
return (
<div
key={tour.id}
className="overflow-hidden rounded-2xl border border-border-soft bg-card/40 shadow-xs transition hover:border-primary/40 backdrop-blur-xs"
>
{/* بخش ۱: سربرگ کارت تور و مشخصات کلیدی */}
<div className="p-5 sm:p-6 border-b border-border-soft bg-card/60">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
{/* تصویر، عنوان و کشور */}
<div className="flex items-start gap-4 min-w-0">
<div className="relative size-20 sm:size-24 shrink-0 overflow-hidden rounded-2xl border border-border-soft bg-surface-base shadow-2xs">
<img
src={imgUrl}
alt={tour.title}
className="h-full w-full object-cover"
onError={(e) => {
;(e.target as HTMLImageElement).src =
'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200" fill="%2318181b"><rect width="200" height="200"/><text x="50%" y="50%" fill="%2371717a" dominant-baseline="middle" text-anchor="middle" font-size="12" font-family="sans-serif">Aqila</text></svg>'
}}
/>
</div>
<div className="min-w-0 space-y-1.5 flex-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-mono font-bold text-grey-3">#{tour.id}</span>
<CountryFlagBadge countryCode={tour.destination_country} />
{/* وضعیت زمانی زنده */}
{isCurrentlyRunning ? (
<span className="inline-flex items-center gap-1.5 rounded-full bg-sky-500/15 px-2.5 py-0.5 text-xs font-bold text-sky-500">
<span className="relative flex size-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75"></span>
<span className="relative inline-flex rounded-full size-2 bg-sky-500"></span>
</span>
هم‌اکنون در حال برگزاری
</span>
) : isImminent ? (
<span className="inline-flex items-center gap-1.5 rounded-full bg-amber-500/15 px-2.5 py-0.5 text-xs font-bold text-amber-600 dark:text-amber-400">
<Ic name="clock" className="size-3" />
آماده‌سازی شروع ({toFa(Math.max(1, hoursToStart))} ساعت مانده)
</span>
) : (
<TourStatusBadge status={tour.trip_status} type="trip" />
)}
</div>
<h2 className="text-base sm:text-lg font-black text-foreground leading-snug">
{tour.title}
</h2>
<div className="flex flex-wrap items-center gap-3 text-xs text-grey-3 font-mono" dir="ltr">
<div className="flex items-center gap-1">
<Ic name="calendar" className="size-3.5 text-primary" />
<span>
{tour.started_at} ~ {tour.ended_at}
</span>
</div>
<span>|</span>
<div className="flex items-center gap-1">
<Ic name="tag" className="size-3.5 text-grey-3" />
<span>{tour.slug}</span>
</div>
</div>
</div>
</div>
{/* دکمه‌ها و خلاصه پیشرفت مرحله */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 lg:self-center shrink-0">
{/* نوار کوچک پیشرفت مرحله */}
{itineraries.length > 0 && (
<div className="rounded-xl border border-border-soft bg-surface-base p-2.5 min-w-[200px] space-y-1.5">
<div className="flex items-center justify-between text-xs">
<span className="text-grey-3 text-[11px]">وضعیت اجرای برنامه:</span>
<span className="font-bold text-primary font-mono">{toFa(progressPercent)}%</span>
</div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-border-soft">
<div
className="h-full rounded-full bg-gradient-to-r from-primary to-amber-500 transition-all duration-500"
style={{ width: `${progressPercent}%` }}
/>
</div>
<div className="text-[10px] text-grey-3 truncate">
{currentStepIndex >= 0 ? (
<>
مرحله فعلی: <strong className="text-foreground">{toFa(currentStepIndex + 1)}</strong> از{' '}
<strong className="text-foreground">{toFa(itineraries.length)}</strong>
</>
) : (
`${toFa(itineraries.length)} مرحله ثبت‌شده`
)}
</div>
</div>
)}
{/* دکمه باز/بسته کردن تایم‌لاین Itinerary */}
<Button
variant="secondary"
size="sm"
onClick={() => toggleExpand(tour.id)}
className="rounded-xl text-xs font-bold gap-1.5 h-10 px-4"
>
<Ic name="list" className="size-4 text-primary" />
<span>{isExpanded ? 'بستن برنامه سفر' : 'مشاهده برنامه سفر (Itinerary)'}</span>
<Ic
name={isExpanded ? 'chevUp' : 'chevDown'}
className="size-3.5 text-grey-3 transition-transform"
/>
</Button>
</div>
</div>
</div>
{/* بخش ۲: بدنه Itineraryها و تایم‌لاین زنده مراحل سفر */}
{isExpanded && (
<div className="p-5 sm:p-6 space-y-6">
{/* اطلاعات مرحله جاری (Current Active Step Callout) */}
{itineraries.length > 0 && currentStepIndex >= 0 && (
<div className="relative overflow-hidden rounded-2xl border border-primary/30 bg-gradient-to-r from-primary/10 via-amber-500/5 to-transparent p-4 sm:p-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="relative flex size-2.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
<span className="relative inline-flex rounded-full size-2.5 bg-primary"></span>
</span>
<span className="text-xs font-black text-primary">
مرحله جاری تور (هم‌اکنون در حال برگزاری):
</span>
</div>
<h3 className="text-sm sm:text-base font-black text-foreground">
{toFa(currentStepIndex + 1)}. {itineraries[currentStepIndex]?.title}
</h3>
<p className="text-xs text-grey-2 leading-relaxed max-w-3xl">
{itineraries[currentStepIndex]?.summary}
</p>
</div>
<div className="text-start sm:text-end shrink-0 text-xs font-mono text-grey-3">
<div className="font-bold text-foreground">
{formatDateTime(itineraries[currentStepIndex]?.started_at)}
</div>
{itineraries[currentStepIndex]?.ended_at && (
<div className="text-[11px]">
تا {formatDateTime(itineraries[currentStepIndex]?.ended_at)}
</div>
)}
</div>
</div>
</div>
)}
{/* وضعیت لودینگ Itinerary */}
{isLoadingItinerary ? (
<Shimmer rows={3} withCard={false} />
) : itineraryError ? (
<div className="rounded-xl border border-rose-500/20 bg-rose-500/5 p-4 text-center text-xs text-rose-500">
{itineraryError}
</div>
) : itineraries.length === 0 ? (
<div className="rounded-xl border border-dashed border-border-soft p-8 text-center text-grey-3">
<Ic name="calendar" className="mx-auto size-7 mb-2 opacity-50" />
<p className="text-xs">برنامه زمان‌بندی روزانه‌ای (Itinerary) برای این تور ثبت نشده است.</p>
</div>
) : (
/* تایم‌لاین عمودی مراحل سفر (Vertical Stepper Timeline) */
<div className="relative space-y-6 before:absolute before:right-4 before:top-3 before:bottom-3 before:w-0.5 before:bg-border-soft">
{itineraries.map((step, idx) => {
const isCurrent = idx === currentStepIndex
const isPast = currentStepIndex >= 0 ? idx < currentStepIndex : false
// وضعیت این مرحله: Completed / In Progress / Not Started
const isCompleted =
step.status === 'Completed' ||
step.status === 1 ||
step.status === '1' ||
isPast
const isInProgress =
step.status === 'In Progress' ||
step.status === 2 ||
step.status === '2' ||
isCurrent
return (
<div key={step.id || idx} className="relative flex items-start gap-4 sm:gap-6 pr-0">
{/* آیکون / نشانگر دایره‌ای تایم‌لاین */}
<div className="relative z-10 flex size-8 shrink-0 items-center justify-center rounded-full border-2 transition-all">
{isCompleted ? (
<div className="flex size-8 items-center justify-center rounded-full bg-emerald-500 text-white shadow-xs">
<Ic name="check" className="size-4" />
</div>
) : isInProgress ? (
<div className="flex size-8 items-center justify-center rounded-full bg-primary text-white ring-4 ring-primary/20 shadow-md">
<span className="text-xs font-black font-mono">{toFa(idx + 1)}</span>
</div>
) : (
<div className="flex size-8 items-center justify-center rounded-full bg-surface-base border-border-soft text-grey-3">
<span className="text-xs font-bold font-mono">{toFa(idx + 1)}</span>
</div>
)}
</div>
{/* کارت محتوای مرحله سفر */}
<div
className={`flex-1 rounded-2xl border p-4 sm:p-5 transition ${
isInProgress
? 'border-primary/40 bg-card/90 shadow-md ring-1 ring-primary/15'
: isCompleted
? 'border-border-soft bg-card/40 opacity-95'
: 'border-border-soft bg-card/20 opacity-80'
}`}
>
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-bold text-grey-3">
گام {toFa(idx + 1)} از {toFa(itineraries.length)}
</span>
{/* برچسب وضعیت مرحله */}
{isInProgress ? (
<Badge className="bg-primary text-white font-bold text-[11px] gap-1 shadow-2xs">
<span className="relative flex size-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span>
<span className="relative inline-flex rounded-full size-1.5 bg-white"></span>
</span>
مرحله جاری (In Progress)
</Badge>
) : isCompleted ? (
<Badge
variant="outline"
className="border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 font-bold text-[11px]"
>
تکمیل شده (Completed)
</Badge>
) : (
<Badge
variant="outline"
className="border-border-soft bg-surface-base text-grey-3 font-medium text-[11px]"
>
در انتظار شروع (Not Started)
</Badge>
)}
</div>
<h4 className="text-sm sm:text-base font-black text-foreground">
{step.title}
</h4>
</div>
{/* تاریخ و زمان مرحله */}
<div className="text-start sm:text-end text-xs font-mono text-grey-3 bg-surface-base px-2.5 py-1.5 rounded-xl border border-border-soft shrink-0">
<div className="flex items-center gap-1 font-bold text-foreground">
<Ic name="calendar" className="size-3.5 text-primary" />
<span>{formatDateTime(step.started_at)}</span>
</div>
{step.ended_at && (
<div className="text-[10px] text-grey-3 mt-0.5" dir="ltr">
الی {formatDateTime(step.ended_at)}
</div>
)}
</div>
</div>
{/* خلاصه و توضیحات مرحله */}
{step.summary && (
<p className="mt-3 text-xs leading-relaxed text-grey-2 whitespace-pre-line">
{step.summary}
</p>
)}
{/* تصاویر مرحله */}
{step.images && step.images.length > 0 && (
<div className="mt-4 space-y-1.5">
<span className="text-[11px] font-bold text-grey-3">تصاویر این مرحله:</span>
<div className="flex gap-2.5 overflow-x-auto pb-1 no-scrollbar">
{step.images.map((img, imgIdx) => (
<div
key={img.id || imgIdx}
className="relative size-20 shrink-0 overflow-hidden rounded-xl border border-border-soft bg-surface-base shadow-2xs"
>
<img
src={
img.image_url?.sm ||
img.image_url?.small ||
img.image_url?.md ||
img.image_url?.medium ||
img.image_url?.original ||
'/placeholder.jpg'
}
alt=""
className="h-full w-full object-cover"
/>
</div>
))}
</div>
</div>
)}
</div>
</div>
)
})}
</div>
)}
</div>
)}
</div>
)
})}
</div>
)}
</div>
)
}