From 1933b58f8494deb23eda965fe8fbf4244a373580 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 17 Jun 2026 17:29:40 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20l=E1=BB=97i=20bong=20b=C3=B3ng=20Tour=20?= =?UTF-8?q?v=E1=BA=ABn=20xu=E1=BA=A5t=20hi=E1=BB=87n=20v=C3=A0=20=C4=91i?= =?UTF-8?q?=E1=BB=83m=20b=E1=BA=AFt=20=C4=91=E1=BA=A7u=20l=C3=A0=20=C4=91i?= =?UTF-8?q?=E1=BB=83m=20xu=E1=BA=A5t=20ph=C3=A1t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/AddLocationModal.tsx | 42 ++++++++++++------- frontend/src/components/ItineraryTimeline.tsx | 28 +++++++++++-- frontend/src/pages/TourDetailPage.tsx | 7 +++- 3 files changed, 57 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/AddLocationModal.tsx b/frontend/src/components/AddLocationModal.tsx index 509bf99..bbd2018 100644 --- a/frontend/src/components/AddLocationModal.tsx +++ b/frontend/src/components/AddLocationModal.tsx @@ -61,7 +61,7 @@ const MapPicker = ({ onPick, center }: { onPick: (latlng: L.LatLng) => void, cen ); }; -export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editingLocation, isPublicView = false, onSuccess }: { isOpen: boolean, onClose: () => void, tourId: string, initialLegId?: string, editingLocation?: any, isPublicView?: boolean, onSuccess?: () => void }) => { +export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editingLocation, isStartPoint = false, isPublicView = false, onSuccess }: { isOpen: boolean, onClose: () => void, tourId: string, initialLegId?: string, editingLocation?: any, isStartPoint?: boolean, isPublicView?: boolean, onSuccess?: () => void }) => { const [formData, setFormData] = useState({ name: '', address: '', @@ -85,7 +85,7 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin const searchTimeout = useRef(null); // Gom các selector lại để giảm số lượng Hook gọi nội bộ và tăng hiệu năng - const { legs, addLocation, updateLocation, mapCenter, currentTour, userRole } = useTourStore(); + const { legs, addLocation, updateLocation, updateTourStartPoint, mapCenter, currentTour, userRole } = useTourStore(); const notify = useNotification(); // 1. Luôn khai báo Hook ở cấp cao nhất, không đặt code logic/return giữa các Hook @@ -153,8 +153,10 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin // 2. Thực hiện các tính toán và hàm xử lý const currentLegId = formData.legId || (legs.length > 0 ? legs[0].id : ''); const targetLeg = legs.find(l => l.id === currentLegId); - const titleText = editingLocation ? `Sửa địa điểm: ${editingLocation.name}` : (initialLegId ? `Thêm địa điểm cho ${targetLeg?.note || `Chặng ${targetLeg?.sequence}`}` : 'Thêm địa điểm mới'); - const buttonText = editingLocation ? 'Cập nhật thay đổi' : (initialLegId ? 'Xác nhận thêm vào chặng' : 'Thêm địa điểm'); + const titleText = isStartPoint ? 'Thiết lập Điểm xuất phát' : + editingLocation ? `Sửa địa điểm: ${editingLocation.name}` : + (initialLegId ? `Thêm địa điểm cho ${targetLeg?.note || `Chặng ${targetLeg?.sequence}`}` : 'Thêm địa điểm mới'); + const buttonText = isStartPoint ? 'Xác nhận Điểm xuất phát' : editingLocation ? 'Cập nhật thay đổi' : (initialLegId ? 'Xác nhận thêm vào chặng' : 'Thêm địa điểm'); const handleSearchLocation = (query: string) => { setFormData(prev => ({ ...prev, name: query })); @@ -281,20 +283,28 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin e.preventDefault(); setIsLoading(true); try { - const payload: any = { - ...formData, - expenseAmount: formData.expenseAmount.replace(/\./g, ''), // Loại bỏ dấu chấm trước khi gửi - legId: currentLegId, - latitude: parseFloat(formData.latitude as any), - longitude: parseFloat(formData.longitude as any), - }; - - if (editingLocation) { - await updateLocation(editingLocation.id, payload); + if (isStartPoint) { + await updateTourStartPoint(tourId, { + name: formData.name || "Điểm xuất phát", + latitude: parseFloat(formData.latitude as any), + longitude: parseFloat(formData.longitude as any), + }); } else { - await addLocation(tourId, payload); + const payload: any = { + ...formData, + expenseAmount: formData.expenseAmount.replace(/\./g, ''), // Loại bỏ dấu chấm trước khi gửi + legId: currentLegId, + latitude: parseFloat(formData.latitude as any), + longitude: parseFloat(formData.longitude as any), + }; + + if (editingLocation) { + await updateLocation(editingLocation.id, payload); + } else { + await addLocation(tourId, payload); + } } - notify({ title: 'Thành công', message: editingLocation ? 'Đã cập nhật địa điểm.' : 'Đã thêm địa điểm mới.', type: 'success' }); + notify({ title: 'Thành công', message: isStartPoint ? 'Đã thiết lập điểm xuất phát.' : editingLocation ? 'Đã cập nhật địa điểm.' : 'Đã thêm địa điểm mới.', type: 'success' }); onSuccess?.(); // Gọi callback onSuccess sau khi thành công onClose(); } catch (error) { diff --git a/frontend/src/components/ItineraryTimeline.tsx b/frontend/src/components/ItineraryTimeline.tsx index 1729c49..ea3d857 100644 --- a/frontend/src/components/ItineraryTimeline.tsx +++ b/frontend/src/components/ItineraryTimeline.tsx @@ -45,7 +45,7 @@ export const ItineraryTimeline = ({ onSuccess, isPublicView = false }: { - onAddLocation?: (legId: string) => void, + onAddLocation?: (legId: string, isStart?: boolean) => void, onEditLocation?: (location: any) => void, onQuickNote?: (name: string) => void, onSuccess?: () => void, @@ -301,6 +301,27 @@ export const ItineraryTimeline = ({
+ {/* 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 => loc.plannedStart && new Date(loc.plannedStart).getTime() === 0) && ( +
+
+
+ +
+
+ +
+ )} + {leg.locations.map((location, idx) => { // Tìm vị trí của điểm này trong toàn bộ hành trình const globalIdx = allLocations.findIndex(loc => loc.id === location.id); @@ -316,8 +337,9 @@ export const ItineraryTimeline = ({ 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; + // 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; return (
diff --git a/frontend/src/pages/TourDetailPage.tsx b/frontend/src/pages/TourDetailPage.tsx index cdd0340..969da16 100644 --- a/frontend/src/pages/TourDetailPage.tsx +++ b/frontend/src/pages/TourDetailPage.tsx @@ -207,6 +207,7 @@ export const TourDetailPage = ({ const [isAddLocationOpen, setIsAddLocationOpen] = useState(false); const [isAddMemberOpen, setIsAddMemberOpen] = useState(false); const [isAddPhotoOpen, setIsAddPhotoOpen] = useState(false); + const [isStartPointAction, setIsStartPointAction] = useState(false); const [targetLegId, setTargetLegId] = useState(null); const [editingLocation, setEditingLocation] = useState(null); const [selectedMember, setSelectedMember] = useState(null); @@ -1014,14 +1015,16 @@ export const TourDetailPage = ({
{viewMode === 'timeline' ? ( - { + { setTargetLegId(legId); setEditingLocation(null); + setIsStartPointAction(!!isStart); setIsAddLocationOpen(true); }} onEditLocation={(loc) => { setEditingLocation(loc); setTargetLegId(loc.legId); setMapCenter([loc.latitude, loc.longitude]); + setIsStartPointAction(false); setIsAddLocationOpen(true); }} onQuickNote={(locName: string) => handleQuickNote(locName)} @@ -1505,6 +1508,7 @@ export const TourDetailPage = ({ onClick={() => { setTargetLegId(null); // Reset khi nhấn nút floating tổng quát setEditingLocation(null); + setIsStartPointAction(false); if (activeTab === 'plan') setIsAddLocationOpen(true); if (activeTab === 'photo' && canUploadPhoto) setIsAddPhotoOpen(true); }} @@ -1535,6 +1539,7 @@ export const TourDetailPage = ({ isOpen={isAddLocationOpen} onClose={() => setIsAddLocationOpen(false)} initialLegId={targetLegId || undefined} + isStartPoint={isStartPointAction} editingLocation={editingLocation} tourId={currentTour.id} isPublicView={isPublicView} // Pass isPublicView