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.
 
 
 

237 lines
9.3 KiB

import * as React from 'react'
import { useTranslation } from '@/i18n'
import { formatDualDate } from '@/lib/date-utils'
import type { UserTourItem } from '../types'
import { fetchUserTours } from '../services/users-api'
import { Ic } from '@/icons'
interface UserToursTabProps {
userId: number
}
type TimelineFilter = 'all' | 'active' | 'upcoming' | 'past'
export function UserToursTab({ userId }: UserToursTabProps) {
const { t, language } = useTranslation()
const [timeline, setTimeline] = React.useState<TimelineFilter>('all')
const [loading, setLoading] = React.useState(true)
const [tours, setTours] = React.useState<UserTourItem[]>([])
const loadTours = React.useCallback(async () => {
setLoading(true)
try {
const res = await fetchUserTours(userId, timeline)
setTours(res.results || [])
} catch (err) {
console.error('Failed to load user tours:', err)
setTours([])
} finally {
setLoading(false)
}
}, [userId, timeline])
React.useEffect(() => {
loadTours()
}, [loadTours])
const formatDate = (isoDate: string | null | undefined) => {
if (!isoDate) return '—'
return formatDualDate(isoDate, language as any)
}
const getOrderStatusBadge = (status: string) => {
switch (status) {
case 'APPROVED':
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
{t('billing.statusApproved')}
</span>
)
case 'PENDING':
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-500/10 text-amber-600 dark:text-amber-400">
<span className="w-1.5 h-1.5 rounded-full bg-amber-500" />
{t('billing.statusPending')}
</span>
)
case 'AWAITING_PAYMENT':
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-500/10 text-blue-600 dark:text-blue-400">
<span className="w-1.5 h-1.5 rounded-full bg-blue-500" />
{t('billing.statusAwaiting')}
</span>
)
case 'REJECTED':
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-rose-500/10 text-rose-600 dark:text-rose-400">
<span className="w-1.5 h-1.5 rounded-full bg-rose-500" />
{t('billing.statusRejected')}
</span>
)
case 'EXPIRED':
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-slate-500/10 text-slate-600 dark:text-slate-400">
<span className="w-1.5 h-1.5 rounded-full bg-slate-500" />
{t('billing.statusExpired')}
</span>
)
default:
return (
<span className="inline-flex px-2 py-0.5 rounded-full text-xs font-medium bg-muted text-muted-foreground">
{status}
</span>
)
}
}
const getTripStatusBadge = (tripStatus: string | null | undefined) => {
if (!tripStatus) return null
let colorClass = 'bg-muted text-muted-foreground'
if (tripStatus === 'TRAVELING') {
colorClass = 'bg-primary/10 text-primary font-semibold'
} else if (tripStatus === 'FINISHED' || tripStatus === 'COMPLETED') {
colorClass = 'bg-muted/60 text-muted-foreground'
} else if (tripStatus === 'PENDING') {
colorClass = 'bg-amber-500/10 text-amber-600 dark:text-amber-400'
}
return (
<span className={`inline-flex px-2 py-0.5 rounded-full text-[11px] ${colorClass}`}>
{tripStatus}
</span>
)
}
return (
<div className="space-y-4 sm:space-y-6">
{/* ساب‌تب‌های کپسولی برای فیلتر وضعیت‌های زمانی تور */}
<div className="flex flex-wrap items-center gap-2 p-1.5 rounded-2xl bg-muted/40 border border-border/40 w-fit">
<button
type="button"
onClick={() => setTimeline('all')}
className={`px-3 py-1.5 rounded-xl text-xs sm:text-sm font-semibold transition-all ${
timeline === 'all'
? 'bg-card text-foreground shadow-xs border border-border/60'
: 'text-muted-foreground hover:text-foreground'
}`}
>
{t('users.toursAll')}
</button>
<button
type="button"
onClick={() => setTimeline('active')}
className={`px-3 py-1.5 rounded-xl text-xs sm:text-sm font-semibold transition-all ${
timeline === 'active'
? 'bg-card text-primary shadow-xs border border-border/60'
: 'text-muted-foreground hover:text-foreground'
}`}
>
{t('users.toursActive')}
</button>
<button
type="button"
onClick={() => setTimeline('upcoming')}
className={`px-3 py-1.5 rounded-xl text-xs sm:text-sm font-semibold transition-all ${
timeline === 'upcoming'
? 'bg-card text-foreground shadow-xs border border-border/60'
: 'text-muted-foreground hover:text-foreground'
}`}
>
{t('users.toursUpcoming')}
</button>
<button
type="button"
onClick={() => setTimeline('past')}
className={`px-3 py-1.5 rounded-xl text-xs sm:text-sm font-semibold transition-all ${
timeline === 'past'
? 'bg-card text-foreground shadow-xs border border-border/60'
: 'text-muted-foreground hover:text-foreground'
}`}
>
{t('users.toursPast')}
</button>
</div>
{/* محتوای تورها */}
{loading ? (
<div className="space-y-3">
{[1, 2, 3].map((i) => (
<div
key={i}
className="h-24 rounded-2xl bg-muted/40 border border-border/40 animate-pulse"
/>
))}
</div>
) : tours.length === 0 ? (
<div className="p-8 text-center bg-card border border-border/60 rounded-2xl">
<div className="mx-auto w-10 h-10 rounded-full bg-muted/50 flex items-center justify-center text-muted-foreground mb-3">
<Ic name="map" className="w-5 h-5" />
</div>
<p className="text-sm font-medium text-foreground mb-1">
{t('users.noToursFound')}
</p>
<p className="text-xs text-muted-foreground">
{t('billing.noFactorsFilter')}
</p>
</div>
) : (
<div className="grid grid-cols-1 gap-3 sm:gap-4">
{tours.map((order) => (
<div
key={order.id}
className="bg-card border border-border/60 rounded-2xl p-4 sm:p-5 shadow-xs hover:border-border transition-all flex flex-col md:flex-row md:items-center justify-between gap-4"
>
{/* سمت راست: عنوان و جزئیات */}
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-2">
<h4 className="text-sm sm:text-base font-bold text-foreground">
{order.tour_title || `سفارش #${order.id}`}
</h4>
{getOrderStatusBadge(order.status)}
{getTripStatusBadge(order.tour_trip_status)}
</div>
<div className="flex flex-wrap items-center gap-3 sm:gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1 font-mono">
<Ic name="calendar" className="w-3.5 h-3.5 text-muted-foreground/70" />
<span>
{formatDate(order.tour_started_at)} الی {formatDate(order.tour_ended_at)}
</span>
</div>
<div className="flex items-center gap-1 font-mono">
<span>ثبت:</span>
<span>{formatDate(order.created?.slice(0, 10))}</span>
</div>
</div>
</div>
{/* سمت چپ: قیمت و وضعیت مالی */}
<div className="flex items-center justify-between md:flex-col md:items-end gap-2 border-t md:border-t-0 pt-3 md:pt-0 border-border/40">
<div className="text-end">
<span className="block text-[11px] text-muted-foreground">
{t('users.tourPrice')}
</span>
<span className="text-base sm:text-lg font-mono font-bold text-foreground">
${Number(order.total_price || 0).toLocaleString()}
</span>
</div>
<div>
<span
className={`inline-flex px-2 py-0.5 rounded-full text-xs font-medium ${
order.is_paid
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
: 'bg-amber-500/10 text-amber-600 dark:text-amber-400'
}`}
>
{order.is_paid ? 'پرداخت شده' : 'پرداخت نشده'}
</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
)
}