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
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>
|
|
)
|
|
}
|