import React, { useState, useEffect, useMemo } from 'react' import { useNavigate } from 'react-router-dom' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Ic } from '@/icons' import { toFa, toEn, formatAmount } from '@/lib/utils' import { formatDualDate } from '@/lib/date-utils' import { UniversalDatePicker } from '@/components/shared/UniversalDatePicker' import { useTranslation } from '@/i18n' import { useBillingBadgeStore } from '@/stores/billing-badge' import { toast } from '@/stores/toast' import type { CustomTripOrderItem, CustomTripOrderStatus, CustomTripLeg, CustomTripOriginItem, CustomTripDestinationItem, CustomTripHotelItem, } from '../types' import { updateCustomTripOrder, fetchCustomTripOrigins, fetchCustomTripDestinations, fetchCustomTripHotels, } from '../services/custom-trip-api' export interface CustomTripOrderReviewDialogProps { order: CustomTripOrderItem | null open: boolean onOpenChange: (open: boolean) => void onOrderUpdated: (updated: CustomTripOrderItem) => void getStatusBadge: (status: CustomTripOrderStatus) => React.ReactNode origins?: CustomTripOriginItem[] destinations?: CustomTripDestinationItem[] hotels?: CustomTripHotelItem[] } function addDaysIso(dateStr: string, days: number): string { const clean = (dateStr || '').slice(0, 10) const d = new Date(`${clean}T00:00:00Z`) if (Number.isNaN(d.getTime())) { const fallback = new Date(Date.now() + 7 * 86400000) fallback.setUTCDate(fallback.getUTCDate() + days) return fallback.toISOString().slice(0, 10) } d.setUTCDate(d.getUTCDate() + days) return d.toISOString().slice(0, 10) } function diffDaysIso(startStr: string, endStr: string): number { const s = new Date(`${(startStr || '').slice(0, 10)}T00:00:00Z`) const e = new Date(`${(endStr || '').slice(0, 10)}T00:00:00Z`) if (Number.isNaN(s.getTime()) || Number.isNaN(e.getTime())) return 0 return Math.round((e.getTime() - s.getTime()) / 86400000) } function getDefaultBaseDate(createdStr?: string): string { const d = createdStr ? new Date(createdStr) : new Date() const today = new Date() today.setHours(0, 0, 0, 0) if (Number.isNaN(d.getTime()) || d.getTime() < today.getTime()) { const nextWeek = new Date(Date.now() + 7 * 86400000) return `${nextWeek.getFullYear()}-${String(nextWeek.getMonth() + 1).padStart(2, '0')}-${String(nextWeek.getDate()).padStart(2, '0')}` } d.setUTCDate(d.getUTCDate() + 7) return d.toISOString().slice(0, 10) } function normalizeLegsChain( origin: string, legs: CustomTripLeg[], fallbackStartDate?: string ): CustomTripLeg[] { const destLegs = legs.filter((l) => !l.isReturn) const existingReturn = legs.find((l) => l.isReturn) if (destLegs.length === 0) { destLegs.push({ fromCity: origin, toCity: '', transportMethod: 'پرواز (هوایی)', transportMethodCode: 1, hotelId: null, hotelName: '', stayNights: 3, mealPlan: 'FULL_BOARD', departureDate: fallbackStartDate || getDefaultBaseDate(), departureTime: '09:00', isReturn: false, }) } let curDate = destLegs[0]?.departureDate || destLegs[0]?.checkIn || fallbackStartDate || getDefaultBaseDate() const normalizedDestLegs: CustomTripLeg[] = destLegs.map((leg, idx) => { const fromCity = idx === 0 ? origin || leg.fromCity : destLegs[idx - 1]?.toCity || leg.fromCity const stayNights = Math.max(1, Number(leg.stayNights) || 3) const legDep = leg.departureDate && leg.departureDate >= curDate ? leg.departureDate : curDate const depDate = idx === 0 ? (leg.departureDate || curDate) : legDep const checkIn = depDate const checkOut = addDaysIso(depDate, stayNights) curDate = checkOut return { ...leg, fromCity, transportMethodCode: (leg.transportMethodCode || 1) as 1 | 2 | 3, transportMethod: leg.transportMethod || (leg.transportMethodCode === 2 ? 'قطار (ریلی)' : leg.transportMethodCode === 3 ? 'زمینی (اتوبوس / ون VIP)' : 'پرواز (هوایی)'), stayNights, mealPlan: leg.mealPlan || 'FULL_BOARD', departureDate: depDate, departureTime: leg.departureTime || '09:00', checkIn, checkOut, isReturn: false, } }) const lastDestLegWithCity = [...normalizedDestLegs].reverse().find((l) => Boolean(l.toCity)) const lastDestCity = lastDestLegWithCity ? lastDestLegWithCity.toCity : (normalizedDestLegs[normalizedDestLegs.length - 1]?.fromCity || origin) const retMethodCode = (existingReturn?.transportMethodCode || 1) as 1 | 2 | 3 let returnToCity = existingReturn?.toCity || origin || '' if (returnToCity && lastDestCity && returnToCity.trim() === lastDestCity.trim()) { if (origin && origin.trim() !== lastDestCity.trim()) { returnToCity = origin } else { returnToCity = '' } } const returnLeg: CustomTripLeg = { fromCity: lastDestCity, toCity: returnToCity, transportMethodCode: retMethodCode, transportMethod: existingReturn?.transportMethod || (retMethodCode === 2 ? 'قطار (ریلی)' : retMethodCode === 3 ? 'زمینی (اتوبوس / ون VIP)' : 'پرواز (هوایی)'), transportId: existingReturn?.transportId ?? null, hotelId: null, hotelName: '', stayNights: 0, mealPlan: '', departureDate: existingReturn?.departureDate && existingReturn.departureDate >= curDate ? existingReturn.departureDate : curDate, departureTime: existingReturn?.departureTime || '18:00', checkIn: null, checkOut: null, isReturn: true, } return [...normalizedDestLegs, returnLeg] } export function CustomTripOrderReviewDialog({ order, open, onOpenChange, onOrderUpdated, getStatusBadge, origins = [], destinations = [], hotels = [], }: CustomTripOrderReviewDialogProps) { const { t, language } = useTranslation() const navigate = useNavigate() const [editOriginCity, setEditOriginCity] = useState('') const [editLegs, setEditLegs] = useState([]) const [editPrice, setEditPrice] = useState(0) const [editNotes, setEditNotes] = useState('') const [isSavingStage1, setIsSavingStage1] = useState(false) const [isTransferringStage2, setIsTransferringStage2] = useState(false) const [isRejecting, setIsRejecting] = useState(false) const [scrollToLegIndex, setScrollToLegIndex] = useState(null) const [localOrigins, setLocalOrigins] = useState(origins) const [localDestinations, setLocalDestinations] = useState(destinations) const [localHotels, setLocalHotels] = useState(hotels) useEffect(() => { if (origins && origins.length > 0) { setLocalOrigins(origins) } else { fetchCustomTripOrigins().then(setLocalOrigins).catch(() => {}) } if (destinations && destinations.length > 0) { setLocalDestinations(destinations) } else { fetchCustomTripDestinations().then(setLocalDestinations).catch(() => {}) } if (hotels && hotels.length > 0) { setLocalHotels(hotels) } else { fetchCustomTripHotels().then(setLocalHotels).catch(() => {}) } }, [origins, destinations, hotels]) useEffect(() => { if (scrollToLegIndex !== null) { const timer = setTimeout(() => { const el = document.getElementById(`leg-card-${scrollToLegIndex}`) if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }) const select = el.querySelector('select') if (select) { select.focus() } } setScrollToLegIndex(null) }, 120) return () => clearTimeout(timer) } }, [scrollToLegIndex]) // Drag & Drop state for reordering destination legs const [draggedLegIdx, setDraggedLegIdx] = useState(null) const [dragOverLegIdx, setDragOverLegIdx] = useState(null) useEffect(() => { if (order) { const orig = order.originCity || '' const defaultStart = getDefaultBaseDate(order.created) setEditOriginCity(orig) const initialLegs: CustomTripLeg[] = order.legs && order.legs.length > 0 ? order.legs.map((l) => ({ ...l, transportMethodCode: (l.transportMethodCode || 1) as 1 | 2 | 3, stayNights: l.isReturn ? 0 : l.stayNights || 3, mealPlan: l.isReturn ? '' : l.mealPlan || 'FULL_BOARD', departureTime: l.departureTime || (l.isReturn ? '18:00' : '09:00'), })) : (order.destinations || []).map((dest, idx) => ({ fromCity: idx === 0 ? orig : order.destinations[idx - 1], toCity: dest, transportMethod: 'پرواز (هوایی)', transportMethodCode: 1, hotelId: null, hotelName: '', stayNights: 3, mealPlan: 'FULL_BOARD', departureDate: idx === 0 ? defaultStart : null, departureTime: '09:00', isReturn: false, })) setEditLegs(normalizeLegsChain(orig, initialLegs, defaultStart)) setEditPrice(order.finalPrice ?? 0) setEditNotes(order.adminNotes || '') } }, [order]) if (!order) return null const isAlreadyInvoiced = Boolean(order.factorId) && order.status !== 'PENDING' const destOnlyLegs = editLegs.filter((l) => !l.isReturn) const destLegsCount = destOnlyLegs.length // Validation: Mandatory fields const isPriceValid = Boolean(editPrice && !isNaN(editPrice) && Number(editPrice) > 0) const isOriginValid = Boolean(editOriginCity && editOriginCity.trim().length > 0) const hasValidDestLegs = destLegsCount > 0 && destOnlyLegs.every((l) => Boolean(l.toCity && l.toCity.trim().length > 0 && Number(l.stayNights || 0) >= 1)) const isFormValid = isPriceValid && isOriginValid && hasValidDestLegs const handleOriginChange = (newOrigin: string) => { setEditOriginCity(newOrigin) setEditLegs((prev) => { const updated = prev.map((leg) => { if (leg.isReturn && (!leg.toCity || leg.toCity === editOriginCity)) { return { ...leg, toCity: newOrigin } } return leg }) return normalizeLegsChain(newOrigin, updated) }) } const handleUpdateLeg = (index: number, patch: Partial) => { setEditLegs((prev) => { const copy = prev.map((item, idx) => (idx === index ? { ...item, ...patch } : item)) // If user edited departureDate on a leg after index 0, adjust previous leg's stayNights if valid so dates remain sequential if (patch.departureDate && index > 0) { const prevLeg = copy[index - 1] if (prevLeg && !prevLeg.isReturn && prevLeg.departureDate) { const diff = diffDaysIso(prevLeg.departureDate, patch.departureDate) if (diff >= 1 && diff <= 60) { copy[index - 1] = { ...prevLeg, stayNights: diff } } } } return normalizeLegsChain(editOriginCity, copy) }) } const handleMoveLeg = (index: number, direction: 'up' | 'down') => { const destOnly = editLegs.filter((l) => !l.isReturn) const retLeg = editLegs.find((l) => l.isReturn) if (index < 0 || index >= destOnly.length) return const targetIdx = direction === 'up' ? index - 1 : index + 1 if (targetIdx < 0 || targetIdx >= destOnly.length) return const firstStartDate = destOnly[0]?.departureDate || getDefaultBaseDate(order.created) const copy = [...destOnly] const temp = copy[index] copy[index] = copy[targetIdx] copy[targetIdx] = temp if (copy[0]) { copy[0] = { ...copy[0], departureDate: firstStartDate } } setEditLegs(normalizeLegsChain(editOriginCity, retLeg ? [...copy, retLeg] : copy, firstStartDate)) } const handleDragStart = (e: React.DragEvent, index: number) => { if (editLegs[index]?.isReturn) { e.preventDefault() return } setDraggedLegIdx(index) e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData('text/plain', String(index)) } const handleDragOver = (e: React.DragEvent, index: number) => { if (draggedLegIdx === null || editLegs[index]?.isReturn) return e.preventDefault() e.dataTransfer.dropEffect = 'move' if (dragOverLegIdx !== index) { setDragOverLegIdx(index) } } const handleDrop = (e: React.DragEvent, dropIndex: number) => { e.preventDefault() if (draggedLegIdx === null || draggedLegIdx === dropIndex || editLegs[dropIndex]?.isReturn) { setDraggedLegIdx(null) setDragOverLegIdx(null) return } const destOnly = editLegs.filter((l) => !l.isReturn) const retLeg = editLegs.find((l) => l.isReturn) if (draggedLegIdx >= destOnly.length || dropIndex >= destOnly.length) { setDraggedLegIdx(null) setDragOverLegIdx(null) return } const firstStartDate = destOnly[0]?.departureDate || getDefaultBaseDate(order.created) const reordered = [...destOnly] const [moved] = reordered.splice(draggedLegIdx, 1) reordered.splice(dropIndex, 0, moved) if (reordered[0]) { reordered[0] = { ...reordered[0], departureDate: firstStartDate } } setEditLegs( normalizeLegsChain(editOriginCity, retLeg ? [...reordered, retLeg] : reordered, firstStartDate) ) setDraggedLegIdx(null) setDragOverLegIdx(null) } const handleDragEnd = () => { setDraggedLegIdx(null) setDragOverLegIdx(null) } const handleAddLeg = () => { setEditLegs((prev) => { const destOnly = prev.filter((l) => !l.isReturn) const retLeg = prev.find((l) => l.isReturn) const lastDestCity = destOnly.length > 0 ? destOnly[destOnly.length - 1].toCity : editOriginCity const newDestLeg: CustomTripLeg = { fromCity: lastDestCity || editOriginCity, toCity: '', // Must be blank/empty when added as requested transportMethod: 'پرواز (هوایی)', transportMethodCode: 1, hotelId: null, hotelName: '', stayNights: 3, mealPlan: 'FULL_BOARD', departureDate: null, departureTime: '09:00', isReturn: false, } const newIndex = destOnly.length setScrollToLegIndex(newIndex) const next = retLeg ? [...destOnly, newDestLeg, retLeg] : [...destOnly, newDestLeg] return normalizeLegsChain(editOriginCity, next) }) } const handleRemoveLeg = (index: number) => { const legToRemove = editLegs[index] if (!legToRemove || legToRemove.isReturn || destLegsCount <= 1) return setEditLegs((prev) => { const next = prev.filter((_, idx) => idx !== index) return normalizeLegsChain(editOriginCity, next) }) } // تاریخ استاندارد امروز برای جلوگیری از انتخاب تاریخ گذشته در هتل و پرواز const todayIso = useMemo(() => { const n = new Date() return `${n.getFullYear()}-${String(n.getMonth() + 1).padStart(2, '0')}-${String(n.getDate()).padStart(2, '0')}` }, []) // محاسبه حداقل تاریخ مجاز برای هر مرحله از مسیر (عدم انتخاب تاریخ گذشته) const getLegMinDate = (idx: number): string => { if (idx === 0) return todayIso const prevLeg = editLegs[idx - 1] const prevCheckOut = (prevLeg?.checkOut || prevLeg?.departureDate || '').slice(0, 10) if (prevCheckOut && prevCheckOut > todayIso) return prevCheckOut return todayIso } // حداقل تاریخ مجاز برای پرواز / مسیر بازگشت const returnMinDate = useMemo(() => { const destLegs = editLegs.filter((l) => !l.isReturn) const lastDest = destLegs[destLegs.length - 1] const lastCheckOut = (lastDest?.checkOut || lastDest?.departureDate || '').slice(0, 10) if (lastCheckOut && lastCheckOut > todayIso) return lastCheckOut return todayIso }, [editLegs, todayIso]) const validateItinerary = (): boolean => { if (!editOriginCity.trim()) { toast.error('لطفاً شهر مبدأ سفر را مشخص نمایید.') return false } const destLegs = editLegs.filter((l) => !l.isReturn) if (destLegs.length === 0) { toast.error('حداقل یک مقصد برای برنامه سفر باید ثبت شده باشد.') return false } for (let i = 0; i < destLegs.length; i++) { if (!destLegs[i].toCity || !destLegs[i].toCity.trim()) { toast.error(`${t('customTrip.emptyDestinationError') || 'لطفاً نام شهر مقصد را مشخص نمایید.'} (${toFa(i + 1)})`) const el = document.getElementById(`leg-card-${i}`) el?.scrollIntoView({ behavior: 'smooth', block: 'center' }) el?.querySelector('select')?.focus() return false } } const retLeg = editLegs.find((l) => l.isReturn) if (retLeg && (!retLeg.toCity || !retLeg.toCity.trim())) { toast.error(t('customTrip.selectReturnDestinationPlaceholder') || 'لطفاً شهر مقصد در مسیر بازگشت را انتخاب نمایید.') const retEl = document.getElementById(`leg-card-${editLegs.length - 1}`) retEl?.scrollIntoView({ behavior: 'smooth', block: 'center' }) retEl?.querySelector('select')?.focus() return false } const seenRoutes = new Set() const seenDestCities = new Set() for (let i = 0; i < editLegs.length; i++) { const leg = editLegs[i] const legFromCity = ( leg.fromCity || (i === 0 ? editOriginCity : editLegs[i - 1]?.toCity) || editOriginCity ).trim() const legToCity = (leg.toCity || '').trim() if (!legToCity) continue // Rule 1: No leg can have fromCity == toCity if (legFromCity === legToCity) { toast.error( `${t('customTrip.sameOriginAndDestinationError') || 'شهر مبدأ و مقصد در یک مرحله نمی‌تواند یکسان باشد.'} (${leg.isReturn ? 'مسیر بازگشت' : `مرحله ${toFa(i + 1)}`}: ${legToCity})` ) const el = document.getElementById(`leg-card-${i}`) el?.scrollIntoView({ behavior: 'smooth', block: 'center' }) el?.querySelector('select')?.focus() return false } // Rule 2: No duplicate route (fromCity -> toCity) anywhere in the itinerary const routeKey = `${legFromCity}-->${legToCity}` if (seenRoutes.has(routeKey)) { toast.error( `${t('customTrip.duplicateRouteError') || 'این مسیر (مبدأ به مقصد) قبلاً در مراحل سفر ثبت شده است و نمی‌تواند تکرار شود.'} (${legFromCity} به ${legToCity})` ) const el = document.getElementById(`leg-card-${i}`) el?.scrollIntoView({ behavior: 'smooth', block: 'center' }) el?.querySelector('select')?.focus() return false } seenRoutes.add(routeKey) // Rules for intermediate destination legs: if (!leg.isReturn) { // Origin city cannot be intermediate destination if (legToCity === editOriginCity.trim()) { toast.error( `${t('customTrip.originAsDestinationError') || 'شهر مبدأ سفر نمی‌تواند به عنوان مقصد میانی انتخاب شود؛ مسیر بازگشت برای برگشت به این شهر اختصاص دارد.'} (${legToCity})` ) const el = document.getElementById(`leg-card-${i}`) el?.scrollIntoView({ behavior: 'smooth', block: 'center' }) return false } // No duplicate intermediate destinations if (seenDestCities.has(legToCity)) { toast.error( `${t('customTrip.duplicateDestinationError') || 'هیچ یک از شهرهای مقصد میانی نمی‌تواند تکراری باشد.'} (${legToCity})` ) const el = document.getElementById(`leg-card-${i}`) el?.scrollIntoView({ behavior: 'smooth', block: 'center' }) return false } seenDestCities.add(legToCity) } } // اعتبارسنجی عدم انتخاب تاریخ گذشته برای تمامی پروازها، هتل‌ها و مسیرهای سفر for (let i = 0; i < editLegs.length; i++) { const leg = editLegs[i] const depDate = (leg.departureDate || '').slice(0, 10) if (!depDate) { toast.error( t('customTrip.emptyDepartureDateError') || `لطفاً تاریخ ${leg.isReturn ? 'بازگشت' : `مرحله ${toFa(i + 1)}`} را مشخص نمایید.` ) const el = document.getElementById(leg.isReturn ? 'leg-card-return' : `leg-card-${i}`) el?.scrollIntoView({ behavior: 'smooth', block: 'center' }) return false } if (depDate < todayIso) { toast.error( t('customTrip.pastDateNotAllowedError') || 'امکان انتخاب تاریخ گذشته وجود ندارد. لطفاً تاریخی از امروز به بعد را انتخاب نمایید.' ) const el = document.getElementById(leg.isReturn ? 'leg-card-return' : `leg-card-${i}`) el?.scrollIntoView({ behavior: 'smooth', block: 'center' }) return false } } return true } // Stage 1: Save modifications & keep in Custom Trip Submitted Orders (no Factor created) const handleSaveStage1Review = async () => { if (!isFormValid) { if (!isPriceValid) { toast.error(t('customTrip.priceRequiredNotice') || 'تعیین مبلغ نهایی فاکتور الزامی است.') } else { toast.error(t('customTrip.requiredFieldsAlert') || 'لطفاً تمامی موارد اجباری را تکمیل نمایید.') } return } if (!validateItinerary()) return setIsSavingStage1(true) try { const updated = await updateCustomTripOrder(order.id, { status: isAlreadyInvoiced ? order.status : 'PENDING', priceEstimate: Number(editPrice), adminNotes: editNotes, originCity: editOriginCity, legs: editLegs, transferToBilling: false, }) onOrderUpdated(updated) toast.success(t('customTrip.stage1SavedToast')) } catch (err: any) { toast.error(err?.message || 'خطا در ذخیره تغییرات بررسی سفارش') } finally { setIsSavingStage1(false) } } // Stage 2: Finalize order items & invoice and transfer to Financial Invoices (Billing) const handleFinalizeAndTransferToBilling = async () => { if (!isFormValid) { if (!isPriceValid) { toast.error(t('customTrip.priceRequiredNotice') || 'تعیین مبلغ نهایی فاکتور الزامی است.') } else { toast.error(t('customTrip.requiredFieldsAlert') || 'لطفاً تمامی موارد اجباری را تکمیل نمایید.') } return } if (!validateItinerary()) return const finalAmount = Number(editPrice) setIsTransferringStage2(true) try { const updated = await updateCustomTripOrder(order.id, { status: 'AWAITING_PAYMENT', priceEstimate: finalAmount, adminNotes: editNotes, originCity: editOriginCity, legs: editLegs, transferToBilling: true, }) onOrderUpdated(updated) useBillingBadgeStore.setState({ lastFetched: null }) useBillingBadgeStore.getState().fetchCounts() toast.success(t('customTrip.stage2TransferredToast')) onOpenChange(false) const targetFactor = updated.factorNumber || updated.factorId || order.factorNumber || order.factorId if (targetFactor) { navigate(`/billing/${targetFactor}`) } else { navigate('/billing') } } catch (err: any) { toast.error(err?.message || 'خطا در تأیید نهایی و صدور صورت‌حساب مالی') } finally { setIsTransferringStage2(false) } } const handleRejectOrder = async () => { setIsRejecting(true) try { const updated = await updateCustomTripOrder(order.id, { status: 'REJECTED', adminNotes: editNotes || 'رد شده پس از بررسی در بخش سفر اختصاصی', }) onOrderUpdated(updated) useBillingBadgeStore.setState({ lastFetched: null }) useBillingBadgeStore.getState().fetchCounts() onOpenChange(false) } catch (err: any) { toast.error(err?.message || 'خطا در رد سفارش') } finally { setIsRejecting(false) } } return ( {/* Header */}
{t('customTrip.orderDetailTitle')} #{toFa(order.id)} {order.trackingCode && ( {order.trackingCode} )}
{getStatusBadge(order.status)} {isAlreadyInvoiced ? ( {t('customTrip.invoiceTransferredBadge')} (#{toFa(order.factorNumber || order.factorId || '')}) ) : ( {t('customTrip.notInvoicedYetBadge')} )}
{t('customTrip.orderDetailDesc')}
{/* Scrollable Body */}
{/* Two-Stage Workflow Banner */}

{isAlreadyInvoiced ? t('customTrip.stage2Title') : t('customTrip.stage1Title')}

{isAlreadyInvoiced ? t('customTrip.stage2Desc') : t('customTrip.stage1Desc')}

{isAlreadyInvoiced && ( )}
{/* Customer Info Card */}
{t('customTrip.passengerName')}: {order.user.fullName || order.user.username}
{t('customTrip.contactPhone')}: {order.user.phoneNumber || '—'}
{t('customTrip.passengers')}: {toFa(order.adultsCount)} {t('customTrip.adults')} {order.childrenCount > 0 && `، ${toFa(order.childrenCount)} ${t('customTrip.children')}`} {order.infantsCount > 0 && `، ${toFa(order.infantsCount)} ${t('customTrip.infants')}`}
{/* Origin City Selector */}
{t('customTrip.originCity')}:
{localOrigins.length > 0 ? ( ) : ( handleOriginChange(e.target.value)} className="h-9.5 text-xs sm:text-sm rounded-xl" /> )}
{/* Editable Itinerary Legs (Drag & Drop, Return Leg, Sequential Dates & Ticket Times) */}

برنامه و مقاصد سفر:

برای جابجایی ترتیب مقاصد، کارت‌ها را بکشید و رها کنید.

{editLegs.map((leg, index) => { const isReturnLeg = Boolean(leg.isReturn) const cityHotels = isReturnLeg ? [] : localHotels.filter( (h) => h.cityName === leg.toCity || leg.toCity.includes(h.cityName) || h.cityName.includes(leg.toCity) ) const isDragging = draggedLegIdx === index const isDragOver = dragOverLegIdx === index && draggedLegIdx !== index return (
handleDragStart(e, index)} onDragOver={(e) => handleDragOver(e, index)} onDrop={(e) => handleDrop(e, index)} onDragEnd={handleDragEnd} className={`p-4 sm:p-5 rounded-2xl border transition-all space-y-3.5 ${ isReturnLeg ? 'border-primary/40 bg-primary/5 shadow-2xs' : isDragOver ? 'border-primary ring-2 ring-primary/30 bg-primary/10' : isDragging ? 'opacity-50 border-dashed border-primary/60 bg-muted/40' : 'border-border/80 bg-card shadow-2xs' }`} > {/* Top Row: Drag Handle, Sequence Badge, Route Chain & Actions */}
{!isReturnLeg && ( )} {toFa(index + 1)}
{leg.fromCity || (isReturnLeg ? (destLegsCount > 0 ? editLegs[destLegsCount - 1]?.toCity || editOriginCity : editOriginCity) : editOriginCity)} ← {isReturnLeg ? (leg.toCity && leg.toCity.trim() !== (leg.fromCity || '').trim() ? leg.toCity : `[${t('customTrip.pendingDestinationChoice') || 'در انتظار تعیین مقصد'}]`) : leg.toCity || `[${t('customTrip.pendingDestinationChoice') || 'در انتظار تعیین مقصد'}]`}
{/* Concise non-wrapping summary badges */} {isReturnLeg ? (
{t('customTrip.returnLegBadge') || 'مسیر بازگشت'} {leg.transportMethodCode === 2 ? '🚆 قطار' : leg.transportMethodCode === 3 ? '🚌 زمینی' : '✈️ پرواز'}
) : (
{leg.transportMethodCode === 2 ? '🚆 قطار' : leg.transportMethodCode === 3 ? '🚌 زمینی' : '✈️ پرواز'} {leg.stayNights ? ( {toFa(leg.stayNights)} شب اقامت ) : null}
)}
{!isReturnLeg ? (
{destLegsCount > 1 && ( )}
) : (
پایان سفر و بازگشت
)}
{/* Editable Fields Grid */} {isReturnLeg ? ( /* Return Leg: Clean 2-Row + Notice Layout */
{/* Row 1: Return Route & Transport Method (3 equal columns on md+) */}
{/* 1. Return Origin City */}
{leg.fromCity || (destLegsCount > 0 ? editLegs[destLegsCount - 1]?.toCity || editOriginCity : editOriginCity)} آخرین مقصد
{/* 2. Return Destination City */}
{(() => { const retLegFromCity = (leg.fromCity || (destLegsCount > 0 ? editLegs[destLegsCount - 1]?.toCity || editOriginCity : editOriginCity) || editOriginCity).trim() const retSelectedValue = leg.toCity && leg.toCity.trim() !== retLegFromCity ? leg.toCity : '' return ( ) })()}
{/* 3. Return Transport Method */}
{/* Row 2: Return Departure Schedule (8 cols Date + 4 cols Time) */}
{/* Return Departure Date */}
handleUpdateLeg(index, { departureDate: iso }) } allowedModes={['islamic', 'gregorian']} defaultMode="islamic" compact={false} minDate={returnMinDate} placeholder={t('customTrip.selectDepartureDatePlaceholder')} className="w-full" />
{/* Return Departure Time */}
handleUpdateLeg(index, { departureTime: e.target.value }) } className="h-10 text-xs font-mono text-center rounded-xl px-3 dir-ltr font-bold text-foreground" />
{/* Row 3: Return Notice */}
مسیر پایانی بازگشت است و نیازی به رزرو هتل اقامتی ندارد.
) : ( /* Destination Leg Fields: 3 Clean Rows (Route, Schedule, Hotel & Stay Range) */
{/* Row 1: Journey Route & Transport (3 equal columns on md+) */}
{/* 1. Origin City */}
{index === 0 ? ( ) : (
{leg.fromCity || editLegs[index - 1]?.toCity || editOriginCity} مرحله قبل
)}
{/* 2. Destination City */}
{/* 3. Transport Method */}
{/* Row 2: Departure Schedule (8 cols Date + 4 cols Time) */}
{/* 4. Departure Date (8 cols on md+) */}
handleUpdateLeg(index, { departureDate: iso }) } allowedModes={['islamic', 'gregorian']} defaultMode="islamic" compact={false} minDate={getLegMinDate(index)} placeholder={t('customTrip.selectDepartureDatePlaceholder')} className="w-full" />
{/* 5. Departure Time (4 cols on md+) */}
handleUpdateLeg(index, { departureTime: e.target.value }) } className="h-10 text-xs font-mono text-center rounded-xl px-3 dir-ltr font-bold text-foreground" />
{/* Row 3: Hotel & Accommodation */}
{/* 1. Hotel Selection (md: 6 cols) */}
{cityHotels.length > 0 ? ( ) : ( handleUpdateLeg(index, { hotelName: e.target.value })} placeholder={t('customTrip.customHotelPlaceholder') || 'نام هتل...'} className="h-10 text-xs rounded-xl" /> )}
{/* 2. Stay Nights (md: 3 cols) */}
handleUpdateLeg(index, { stayNights: Math.max(1, Number(e.target.value) || 1), }) } className="h-10 text-xs font-mono text-center rounded-xl px-2 pe-8 font-bold" /> شب
{/* 3. Meal Plan (md: 3 cols) */}
{/* 4. Dedicated Stay Date Range Bar (Full Width) */}
{t('customTrip.stayDateRange') || 'بازه اقامت در هتل'}: {toFa(leg.stayNights || 1)} {t('customTrip.nightsCountLabel') || 'شب'}
{t('customTrip.checkInLabel') || 'ورود'}: {leg.checkIn ? leg.checkIn.slice(0, 10) : '—'}
←
{t('customTrip.checkOutLabel') || 'خروج'}: {leg.checkOut ? leg.checkOut.slice(0, 10) : '—'}
)}
) })}
{/* Passengers & Passport Images */} {order.passengers && order.passengers.length > 0 && (

{t('customTrip.passengersList')}:

{order.passengers.map((pax, idx) => (
{toFa(idx + 1)}
{`${pax.firstName} ${pax.lastName}`.trim() || '—'} {pax.passengerType === 'INFANT' ? t('customTrip.infants') : pax.passengerType === 'CHILD' ? t('customTrip.children') : t('customTrip.adults')}
{pax.passportNumber && ( {t('customTrip.passportNumber')}:{' '} {pax.passportNumber} )} {pax.phoneNumber && ( {pax.phoneNumber} )}
{pax.passportImage ? ( {t('customTrip.viewPassportImage')} ) : ( {t('customTrip.noPassportImage')} )}
))}
)} {/* User Special Request */} {order.detail && (
{t('customTrip.specialRequest')}:

{order.detail}

)} {/* Final Invoice Price & Admin Notes */}
0 ? formatAmount(editPrice) : ''} onChange={(e) => { const raw = toEn(e.target.value).replace(/[^0-9]/g, '') setEditPrice(raw ? Number(raw) : 0) }} className={`h-10 rounded-xl font-mono font-bold text-start text-sm transition-colors ${ !isPriceValid ? 'border-rose-500/60 focus:ring-rose-500/30 bg-rose-500/5' : 'border-border/70 focus:ring-primary' }`} placeholder={t('customTrip.pricePlaceholder')} />
{!isPriceValid ? (

{t('customTrip.priceRequiredNotice')}

) : (

{t('customTrip.priceAfterInvoiceNotice')}

)}