import React, { useState } from 'react'; import { format, differenceInMinutes, parseISO } from 'date-fns'; import { CheckCircle2, Circle, Clock, MapPin, AlertCircle, Zap, Navigation, Edit2, Trash2, Plus, List } from 'lucide-react'; import { useTourStore } from './useTourStore.js'; import { ConfirmModal } from './ConfirmModal.js'; 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 }: { onAddLocation?: (legId: string) => void, onEditLocation?: (location: any) => void }) => { const { currentTour, legs, optimizeRouting, userRole, addLeg, updateLeg, deleteLeg, initializeLegs, deleteLocation } = useTourStore(); const [confirmState, setConfirmState] = useState<{ open: boolean; title?: string; message?: string; onConfirm?: () => void }>({ open: false }); const toggleComplete = async (locationId: string) => { // Gọi API PATCH /api/v1/locations/:id để cập nhật trạng thái console.log("Toggle status for location:", locationId); }; 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 () => { const countStr = window.prompt("Chuyến đi này bạn muốn chia làm bao nhiêu chặng?", "3"); const count = parseInt(countStr || "0"); if (count > 0 && currentTour) { await initializeLegs(currentTour.id, count); } }; const handleEditLeg = async (leg: any) => { const note = window.prompt("Sửa ghi chú chặng:", leg.note || ""); if (note !== null) { await updateLeg(leg.id, { note }); } }; const handleDeleteLeg = async (legId: string) => { setConfirmState({ open: true, title: 'Xóa chặng', message: 'Bạn có chắc chắn muốn xóa chặng này?', onConfirm: async () => { try { await deleteLeg(legId); } catch (err: any) { alert(err.message); } finally { setConfirmState({ open: false }); } }, }); }; const handleDeleteLocation = async (id: string) => { setConfirmState({ open: true, title: 'Xóa địa điểm', message: 'Bạn có chắc chắn muốn xóa địa điểm này?', onConfirm: async () => { try { await deleteLocation(id); } catch (err: any) { alert(err.message); } finally { setConfirmState({ open: false }); } }, }); }; 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 (
{/* Leg Header */}
{leg.sequence} {leg.note || `Chi tiết Chặng ${leg.sequence}`}
{prevLegLastLoc && (
Tiếp nối từ {prevLegLastLoc.name}
)}
{['OWNER', 'MANAGER'].includes(userRole || '') && ( <> )} {leg.totalDistance !== undefined && (
{leg.totalDistance} km
~ {formatTravelTime(Math.round((leg.totalDistance / 35) * 60))}
)} {totalDwellMinutes > 0 && (
Dừng: {formatTravelTime(totalDwellMinutes)}
)}
{['OWNER', 'MANAGER'].includes(userRole || '') && legs.length > 0 && ( )}
{/* Vertical Line for the whole leg */} {/* Mở rộng đường kẻ xuống dưới (bottom-[-3rem]) để nối liền với chặng tiếp theo */}
{leg.locations.map((location, idx) => { // Logic quan trọng: Gán điểm cuối chặng này nối với điểm đầu chặng sau const nextLocation = leg.locations[idx + 1] || legs[legIdx + 1]?.locations[0]; const distanceToNext = nextLocation ? calculateDistance(location.latitude, location.longitude, nextLocation.latitude, nextLocation.longitude) : null; const averageSpeed = 35; // km/h const travelTimeMinutes = distanceToNext ? Math.round((distanceToNext / averageSpeed) * 60) : null; const dwellMinutes = (location.plannedStart && location.plannedEnd) ? differenceInMinutes(parseISO(location.plannedEnd), parseISO(location.plannedStart)) : null; const locationExpense = leg.expenses?.find((e: any) => e.locationId === location.id); const isStartPoint = legs[0]?.id === leg.id && idx === 0; const isEndPoint = legs[legs.length - 1]?.id === leg.id && idx === leg.locations.length - 1; return (
{/* Timeline Node */}
{/* Card Content */}
{isStartPoint && ( Điểm bắt đầu )} {isEndPoint && ( Điểm kết thúc )}

{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.category})
{locationExpense.description && (
Dịch vụ: {locationExpense.description}
)} {locationExpense.note && (
{locationExpense.note}
)} {locationExpense.paidBy && (
Đã thanh toán: {locationExpense.paidBy.name}
)}
)}
{location.plannedStart ? format(parseISO(location.plannedStart), 'HH:mm') : '--:--'}
{location.status === 'COMPLETED' && location.actualStart && (
Thực tế: {format(parseISO(location.actualStart), 'HH:mm')}
)} {['OWNER', 'MANAGER'].includes(userRole || '') && !isStartPoint && !isEndPoint && (
)}
{/* Logic tính toán độ lệch thời gian */}
{distanceToNext !== null && travelTimeMinutes !== null && (
{distanceToNext.toFixed(2)} km ~ {formatTravelTime(travelTimeMinutes)}
)}
); })}
); }) )} {/* Actions at the bottom of the list */} {['OWNER', 'MANAGER'].includes(userRole || '') && (
)}
confirmState.onConfirm?.()} onCancel={() => setConfirmState({ open: false })} />
); };