From 0a584a13c7a4abdbc967c204cfd9cf29d2cb6500 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Tue, 16 Jun 2026 11:37:21 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20t=C3=ACm=20ki=E1=BA=BFm=20=C4=91?= =?UTF-8?q?=E1=BB=8Ba=20=C4=91i=E1=BB=83m=20tr=C3=AAn=20b=E1=BA=A3n=20?= =?UTF-8?q?=C4=91=E1=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/AddLocationModal.tsx | 105 +++++++++++++++++-- 1 file changed, 97 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/AddLocationModal.tsx b/frontend/src/components/AddLocationModal.tsx index 7e47a70..76b4c0d 100644 --- a/frontend/src/components/AddLocationModal.tsx +++ b/frontend/src/components/AddLocationModal.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { format, parseISO } from 'date-fns'; -import { X, MapPin, Loader2, Clock, Map as MapIcon, Navigation } from 'lucide-react'; +import { X, MapPin, Loader2, Clock, Map as MapIcon, Navigation, Search } from 'lucide-react'; import { useTourStore } from '@/store/useTourStore.js'; import { MapContainer, TileLayer, Marker, useMapEvents, useMap } from 'react-leaflet'; import L from 'leaflet'; @@ -78,6 +78,9 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin plannedEnd: '' }); const [isLoading, setIsLoading] = useState(false); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + 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(); @@ -118,11 +121,25 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin } }, [initialLegId, editingLocation, isOpen]); + // Tự động cập nhật vị trí bản đồ theo chặng được chọn (Lấy điểm cuối của chặng đó làm tham chiếu) useEffect(() => { - if (isOpen && !formData.name) { - setFormData(prev => ({ ...prev, latitude: mapCenter[0], longitude: mapCenter[1] })); + if (isOpen && !editingLocation && formData.legId && !formData.name) { + const selectedLeg = legs.find(l => l.id === formData.legId); + + if (selectedLeg && selectedLeg.locations && selectedLeg.locations.length > 0) { + // Di chuyển đến địa điểm cuối cùng của chặng để người dùng thấy điểm nối tiếp + const lastLoc = selectedLeg.locations[selectedLeg.locations.length - 1]; + setFormData(prev => ({ + ...prev, + latitude: lastLoc.latitude, + longitude: lastLoc.longitude + })); + } else { + // Nếu chặng chưa có điểm nào, mặc định dùng vị trí trung tâm hiện tại của tour + setFormData(prev => ({ ...prev, latitude: mapCenter[0], longitude: mapCenter[1] })); + } } - }, [isOpen, mapCenter]); + }, [formData.legId, isOpen, editingLocation, legs, mapCenter]); // 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 : ''); @@ -130,6 +147,44 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin 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 handleSearchLocation = (query: string) => { + setFormData(prev => ({ ...prev, name: query })); + + if (searchTimeout.current) clearTimeout(searchTimeout.current); + + if (query.trim().length < 2) { + setSearchResults([]); + setIsSearching(false); + return; + } + + setIsSearching(true); + searchTimeout.current = setTimeout(async () => { + try { + const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=8&addressdetails=1&accept-language=vi`); + const data = await res.json(); + setSearchResults(data); + } catch (e) { + console.error("Lỗi tìm kiếm địa điểm:", e); + } finally { + setIsSearching(false); + } + }, 500); + }; + + const selectSearchResult = (result: any) => { + const lat = parseFloat(result.lat); + const lon = parseFloat(result.lon); + setFormData(prev => ({ + ...prev, + name: result.display_name.split(',')[0], + address: result.display_name, + latitude: lat, + longitude: lon + })); + setSearchResults([]); + }; + const handlePickLocation = async (latlng: L.LatLng) => { setFormData(prev => ({ ...prev, latitude: latlng.lat, longitude: latlng.lng })); @@ -263,11 +318,45 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin )} -
-
+ setSearchResults([])}> +
- setFormData({...formData, name: e.target.value})} /> +
+ handleSearchLocation(e.target.value)} + /> +
+ {isSearching ? ( + + ) : formData.name ? ( + + ) : ( + + )} +
+
+ + {searchResults.length > 0 && ( +
+ {searchResults.map((result, idx) => ( + + ))} +
+ )}