import { useState, useEffect, useMemo } from 'react'; import { format, differenceInMinutes, parseISO } from 'date-fns'; import { CheckCircle2, Circle, Clock, MapPin, AlertCircle, Zap, Navigation, Edit2, Trash2, Plus, List, X, Calendar as CalendarIcon, AlignLeft, MessageSquare, FileText, Flag, ChevronDown } from 'lucide-react'; import { useTourStore } from '@/store/useTourStore'; import { useConfirm } from '@/hooks/useConfirm'; import { useNotification } from '@/hooks/useNotification'; import { CommentModal } from '@/components/CommentModal'; const TimeVariance = ({ planned, actual }: { planned: string, actual: string | null }) => { if (!actual) return null; const diff = differenceInMinutes(parseISO(actual), parseISO(planned)); const isLate = diff > 0; return (
{isLate ? : } {isLate ? `Trễ ${diff} phút` : diff === 0 ? 'Đúng giờ' : `Sớm ${Math.abs(diff)} phút`}
); }; const calculateDistance = (lat1: number, lon1: number, lat2: number, lon2: number) => { const p = 0.017453292519943295; // Math.PI / 180 const c = Math.cos; const a = 0.5 - c((lat2 - lat1) * p) / 2 + c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p)) / 2; return 12742 * Math.asin(Math.sqrt(a)); // 2 * R; R = 6371 km }; const formatTravelTime = (minutes: number) => { if (minutes < 60) return `${minutes} phút`; const hours = Math.floor(minutes / 60); const mins = minutes % 60; return mins > 0 ? `${hours} giờ ${mins} phút` : `${hours} giờ`; }; export const ItineraryTimeline = ({ onAddLocation, onEditLocation, onQuickNote, onNavigate, onSuccess, isPublicView = false }: { onAddLocation?: (legId: string, isStart?: boolean, isEnd?: boolean) => void, onEditLocation?: (location: any) => void, onQuickNote?: (data: { legId: string; location: any; leg: any }) => void, onNavigate?: (location: any) => void, onSuccess?: () => void, isPublicView?: boolean }) => { // Tối ưu hóa selectors để chỉ lắng nghe những thay đổi cần thiết const currentTour = useTourStore(state => state.currentTour); const legs = useTourStore(state => state.legs); const userRole = useTourStore(state => state.userRole); const addLeg = useTourStore(state => state.addLeg); const updateLeg = useTourStore(state => state.updateLeg); // Removed: const { isPublicView } = useTourStore(state => state); // isPublicView is passed as a prop const deleteLeg = useTourStore(state => state.deleteLeg); const initializeLegs = useTourStore(state => state.initializeLegs); const deleteLocation = useTourStore(state => state.deleteLocation); // Khai báo logic canEdit để sử dụng trong toàn bộ component const canEdit = isPublicView ? false : ['OWNER', 'MANAGER'].includes(userRole || ''); const confirm = useConfirm(); const notify = useNotification(); const [isLegCountModalOpen, setIsLegCountModalOpen] = useState(false); const [tempLegCount, setTempLegCount] = useState(3); const [isCommentModalOpen, setIsCommentModalOpen] = useState(false); const [commentLocationId, setCommentLocationId] = useState(''); const [commentLocationName, setCommentLocationName] = useState(''); // State to track single expanded stage (exclusive single-expansion mode) const [expandedStageId, setExpandedStageId] = useState(legs.length > 0 ? legs[0]?.id : null); const toggleStageExpanded = (legId: string) => { // Exclusive mode: if clicking the same stage, close it. Otherwise, open only the clicked one. setExpandedStageId(prevId => prevId === legId ? null : legId); }; // Tối ưu hóa: Cập nhật UI ngay lập tức bằng cách can thiệp vào State của Store const handleCommentIncrement = (locationId: string) => { const currentLegs = useTourStore.getState().legs; const updatedLegs = currentLegs.map((leg: any) => ({ ...leg, locations: leg.locations.map((loc: any) => loc.id === locationId ? { ...loc, _count: { ...loc._count, comments: (loc._count?.comments || 0) + 1 } } : loc ) })); // Dùng setState của Zustand để cập nhật một phần dữ liệu useTourStore.setState({ legs: updatedLegs }); }; const handleCommentDecrement = (locationId: string) => { const currentLegs = useTourStore.getState().legs; const updatedLegs = currentLegs.map((leg: any) => ({ ...leg, locations: leg.locations.map((loc: any) => loc.id === locationId ? { ...loc, _count: { ...loc._count, comments: Math.max(0, (loc._count?.comments || 1) - 1) } } : loc ) })); useTourStore.setState({ legs: updatedLegs }); }; // State cho Modal sửa chặng const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [editingLegData, setEditingLegData] = useState({ id: '', note: '', description: '', startDate: '', endDate: '' }); const handleStatusClick = (location: any) => { if (onNavigate) { onNavigate(location); } }; const handleAddLeg = async () => { const note = window.prompt("Nhập ghi chú cho chặng mới:", `Chặng ${legs.length + 1}`); if (note && currentTour) { await addLeg(currentTour.id, { note }); } }; const handleDeclareLegs = async () => { setTempLegCount(legs.length > 0 ? legs.length : 3); setIsLegCountModalOpen(true); }; const confirmDeclareLegs = async () => { if (tempLegCount > 0 && tempLegCount <= 20 && currentTour) { await initializeLegs(currentTour.id, tempLegCount); } setIsLegCountModalOpen(false); }; const handleEditLeg = async (leg: any) => { setEditingLegData({ id: leg.id, note: leg.note || "", description: leg.description || "", startDate: leg.startDate ? leg.startDate.split('T')[0] : "", endDate: leg.endDate ? leg.endDate.split('T')[0] : "" }); setIsEditModalOpen(true); }; const saveLegEdit = async () => { if (editingLegData.id) { await updateLeg(editingLegData.id, { note: editingLegData.note, description: editingLegData.description, startDate: editingLegData.startDate || null, endDate: editingLegData.endDate || null }); setIsEditModalOpen(false); } }; const handleDeleteLeg = async (legId: string) => { const isConfirmed = await confirm({ title: 'Xóa chặng', message: 'Bạn có chắc chắn muốn xóa chặng này?' }); if (isConfirmed) { try { await deleteLeg(legId); onSuccess?.(); } catch (err: any) { notify({ title: 'Lỗi', message: err.message, type: 'error' }); } } }; const handleDeleteLocation = async (id: string) => { const isConfirmed = await confirm({ title: 'Xóa địa điểm', message: 'Bạn có chắc chắn muốn xóa địa điểm này?' }); if (isConfirmed) { try { await deleteLocation(id); onSuccess?.(); } catch (err: any) { notify({ title: 'Lỗi', message: err.message, type: 'error' }); } } }; // Tạo mảng phẳng tất cả địa điểm để tính toán quãng đường liên tục giữa các chặng const allLocations = useMemo(() => legs.flatMap(l => l.locations), [legs]); useEffect(() => { console.log("ItineraryTimeline: Legs updated", legs); // Initialize first leg as expanded when legs change if (legs.length > 0 && !expandedStageId) { setExpandedStageId(legs[0].id); } }, [legs]); return (
{legs.length === 0 ? (

Chưa có chặng nào trong lộ trình.

) : ( legs.map((leg, legIdx) => { const totalDwellMinutes = leg.locations.reduce((acc: number, loc: any) => { if (loc.plannedStart && loc.plannedEnd) { return acc + differenceInMinutes(parseISO(loc.plannedEnd), parseISO(loc.plannedStart)); } return acc; }, 0); // Xác định địa điểm cuối cùng của chặng trước đó để hiển thị tính liên tục const prevLegLastLoc = legIdx > 0 ? legs[legIdx - 1]?.locations.slice(-1)[0] : null; return (
{/* Folder header row - clickable to toggle exclusive expansion */}
toggleStageExpanded(leg.id)} className="folder-header-row animate-in fade-in slide-in-from-bottom-4 duration-300 hover:bg-gray-50/50 transition-colors" >
{leg.sequence}
{leg.note || `Chi tiết Chặng ${leg.sequence}`} {leg.startDate && ( {format(parseISO(leg.startDate), 'dd/MM/yyyy')} {leg.endDate && leg.endDate !== leg.startDate && ` - ${format(parseISO(leg.endDate), 'dd/MM/yyyy')}`} )}
{prevLegLastLoc && (
Tiếp nối từ {prevLegLastLoc.name}
)}
{/* Expand/Collapse indicator button */}
{/* Action Buttons Row - Below stage title */}
{canEdit && ( <> )} {leg.totalDistance !== undefined && (
{leg.totalDistance} km
~ {formatTravelTime(Math.round((leg.totalDistance / 35) * 60))}
)} {totalDwellMinutes > 0 && (
Dừng: {formatTravelTime(totalDwellMinutes)}
)}
{/* Scrollable content body with proper z-index layering */}
{/* Folder child content box - Grid accordion for exclusive expansion */}
{/* Vertical Line for the whole leg - Dynamic height */}
{/* Nút thêm nhanh "Điểm xuất phát" cho Chặng 1 nếu chưa có */} {legIdx === 0 && !leg.locations.some((loc: any) => loc.plannedStart && new Date(loc.plannedStart).getTime() === 0) && (
)} {/* Nút thêm nhanh "Điểm kết thúc" cho Chặng cuối nếu chưa có */} {legIdx === legs.length - 1 && !legs.some((l: any) => l.locations.some((loc: any) => loc.plannedEnd && new Date(loc.plannedEnd).getTime() === 0)) && (
)} {leg.locations.map((location: any) => { // Tìm vị trí của điểm này trong toàn bộ hành trình const globalIdx = allLocations.findIndex((loc: any) => loc.id === location.id); const prevLocation = globalIdx > 0 ? allLocations[globalIdx - 1] : null; const distanceFromPrev = prevLocation ? calculateDistance(prevLocation.latitude, prevLocation.longitude, location.latitude, location.longitude) : null; const locationExpense = leg.expenses?.find((e: any) => e.locationId === location.id); // Nhận diện điểm mốc dựa trên timestamp đặc biệt (0) thay vì chỉ số mảng const isStartPoint = location.plannedStart && new Date(location.plannedStart).getTime() === 0; const isEndPoint = location.plannedEnd && new Date(location.plannedEnd).getTime() === 0; const plannedTimeStr = isStartPoint ? location.plannedEnd : location.plannedStart; const hasValidPlannedTime = plannedTimeStr && new Date(plannedTimeStr).getTime() !== 0; const dwellMinutes = (location.plannedStart && location.plannedEnd && !isStartPoint && !isEndPoint) ? differenceInMinutes(parseISO(location.plannedEnd), parseISO(location.plannedStart)) : null; return (
{/* Timeline Node */}
{/* Card Content */}
onNavigate?.(location)} className={`flex-1 bg-white p-4 rounded-xl border transition-all duration-200 cursor-pointer ${ location.status === 'COMPLETED' ? 'border-green-100 bg-green-50/30' : 'border-gray-100 shadow-sm hover:shadow-md' }`} >
{isStartPoint && ( Điểm bắt đầu )} {isEndPoint && ( Điểm kết thúc )}

{ e.stopPropagation(); onNavigate?.(location); }} className={`font-semibold text-lg cursor-pointer hover:text-blue-600 transition-colors select-none ${location.status === 'COMPLETED' ? 'text-gray-500 line-through' : 'text-gray-800'}`} > {location.name}

{location.address}
{location.note && (
{location.note}
)} {dwellMinutes !== null && (
Thời gian dừng: {formatTravelTime(dwellMinutes)}
)} {locationExpense && (
Chi phí: {Number(locationExpense.amount).toLocaleString()}đ
{locationExpense.description && (
Dịch vụ: {locationExpense.description}
)} {locationExpense.note && (
{locationExpense.note}
)} {locationExpense.paidBy && (
Đã thanh toán: {locationExpense.paidBy.name}
)}
)}
e.stopPropagation()}>
{onQuickNote && !isPublicView && ( )}
{hasValidPlannedTime ? format(parseISO(plannedTimeStr), 'HH:mm') : '--:--'}
{location.status === 'COMPLETED' && location.actualStart && (
Thực tế: {format(parseISO(location.actualStart), 'HH:mm')}
)} {canEdit && ( // Allow editing and deleting of all locations if user has edit permissions
)}
{/* Logic tính toán độ lệch thời gian */}
{/* Hiển thị quãng đường di chuyển từ điểm trước ĐẾN điểm hiện tại */} {distanceFromPrev !== null && distanceFromPrev > 0 && (
+{distanceFromPrev.toFixed(1)} km từ {prevLocation?.name.split(',')[0]}
)}
); })}
{/* End of ml-2 wrapper */}
{/* End of child-nodes-list */}
{/* End of folder-child-content-box */}
{/* End of child-nodes-list-wrapper */}
); }) )} {/* Actions at the bottom of the list */} {canEdit && ( // Only show these buttons if canEdit
)} {/* Modal Khai báo số chặng (Popover) */} {isLegCountModalOpen && (
setIsLegCountModalOpen(false)} />

Số chặng lộ trình

Bạn muốn chia chuyến đi này thành bao nhiêu chặng nhỏ? (Tối đa 20 chặng)

{tempLegCount}
)} {/* Modal Chỉnh sửa Chặng (Popover) */} {isEditModalOpen && (
setIsEditModalOpen(false)} />

Chỉnh sửa Chặng

setEditingLegData({ ...editingLegData, note: e.target.value })} placeholder="VD: Ngày 1: Khởi hành" className="w-full px-5 py-4 bg-gray-50 dark:bg-slate-800 border border-gray-100 dark:border-slate-700 rounded-2xl focus:ring-2 focus:ring-blue-500 outline-none transition-all text-sm text-gray-800 dark:text-white" />