import React, { useState, useEffect, useRef, useMemo } from 'react'; import { format, parseISO } from 'date-fns'; import { X, MapPin, Loader2, Clock, Map as MapIcon, Navigation, Search } from 'lucide-react'; import { useTourStore } from '@/store/useTourStore'; import { useNotification } from '@/hooks/useNotification'; import { MapContainer, TileLayer, Marker, useMapEvents, useMap } from 'react-leaflet'; import L from 'leaflet'; // Cấu hình Icon mặc định để tránh crash Marker trong Modal delete (L.Icon.Default.prototype as any)._getIconUrl; L.Icon.Default.mergeOptions({ iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png', iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', }); // Component Helper xử lý việc chọn vị trí trên mini map bằng chuột phải const MapPicker = ({ onPick, center }: { onPick: (latlng: L.LatLng) => void, center: [number, number] }) => { const [menuPos, setMenuPos] = useState<{ x: number, y: number, latlng: L.LatLng } | null>(null); const menuRef = useRef(null); const map = useMap(); useMapEvents({ contextmenu: (e) => { L.DomEvent.preventDefault(e.originalEvent); L.DomEvent.stopPropagation(e.originalEvent); setMenuPos({ x: e.containerPoint.x, y: e.containerPoint.y, latlng: e.latlng }); }, click: () => setMenuPos(null), dragstart: () => setMenuPos(null), }); useEffect(() => { if (menuPos && menuRef.current) { L.DomEvent.disableClickPropagation(menuRef.current); } }, [menuPos]); useEffect(() => { map.setView(center, map.getZoom()); }, [center]); return ( <> {menuPos && (
)} ); }; export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editingLocation, isStartPoint = false, isEndPoint = false, isPublicView = false, onSuccess }: { isOpen: boolean, onClose: () => void, tourId: string, initialLegId?: string, editingLocation?: any, isStartPoint?: boolean, isEndPoint?: boolean, isPublicView?: boolean, onSuccess?: () => void }) => { const [formData, setFormData] = useState({ name: '', address: '', latitude: 10.7769, longitude: 106.7009, type: 'VISIT', legId: '', note: '', expenseAmount: '', expenseCategory: 'OTHER', expenseDescription: '', expenseNote: '', paidById: '', plannedStart: '', plannedEnd: '' }); const [isLoading, setIsLoading] = useState(false); const [searchResults, setSearchResults] = useState([]); const [hasNoResults, setHasNoResults] = useState(false); 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, updateTourStartPoint, updateTourEndPoint, 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 useEffect(() => { if (isOpen) { if (editingLocation) { const leg = legs.find(l => l.id === editingLocation.legId); const expense = leg?.expenses?.find((e: any) => e.locationId === editingLocation.id); setFormData({ name: editingLocation.name || '', address: editingLocation.address || '', latitude: editingLocation.latitude, longitude: editingLocation.longitude, type: editingLocation.type || 'VISIT', legId: editingLocation.legId || '', note: editingLocation.note || '', expenseAmount: expense?.amount ? Number(expense.amount).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".") : '', expenseCategory: expense?.category || 'OTHER', expenseDescription: expense?.description || '', expenseNote: expense?.note || '', paidById: expense?.paidById || '', plannedStart: isStartPoint ? (editingLocation.plannedEnd ? editingLocation.plannedEnd.slice(0, 16) : '') : (editingLocation.plannedStart ? editingLocation.plannedStart.slice(0, 16) : ''), plannedEnd: editingLocation.plannedEnd ? editingLocation.plannedEnd.slice(0, 16) : '' }); } else { setFormData(prev => ({ ...prev, name: '', address: '', legId: initialLegId || (legs.length > 0 ? legs[0].id : ''), note: '', expenseAmount: '', expenseCategory: 'OTHER', expenseDescription: '', expenseNote: '', paidById: '', plannedStart: '', plannedEnd: '' })); } } }, [initialLegId, editingLocation, isOpen]); // Memoize tọa độ để tránh việc bản đồ tự động reset tâm khi re-render (ví dụ khi gõ tìm kiếm) const currentCoords = useMemo<[number, number]>( () => [formData.latitude, formData.longitude], [formData.latitude, formData.longitude] ); // 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 trước/hiện tại làm tham chiếu tiếp nối) useEffect(() => { if (isOpen && !editingLocation && formData.legId && !formData.name) { const selectedLeg = legs.find(l => l.id === formData.legId); if (selectedLeg) { if (selectedLeg.locations && selectedLeg.locations.length > 0) { // Di chuyển đến địa điểm cuối cùng của chặng hiện tại để 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 { // Chặng trống -> Lấy địa điểm cuối của chặng trước làm tọa độ tiếp nối const currentLegIdx = legs.findIndex(l => l.id === formData.legId); const prevLeg = currentLegIdx > 0 ? legs[currentLegIdx - 1] : null; if (prevLeg && prevLeg.locations && prevLeg.locations.length > 0) { const lastLoc = prevLeg.locations[prevLeg.locations.length - 1]; setFormData(prev => ({ ...prev, latitude: lastLoc.latitude, longitude: lastLoc.longitude })); } else { // Nếu không có chặng trước hoặc chặng trước trống, 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] })); } } } } }, [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 : ''); const targetLeg = legs.find(l => l.id === currentLegId); const titleText = isStartPoint ? 'Thiết lập Điểm xuất phát' : isEndPoint ? 'Thiết lập Điểm kết thúc' : 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' : isEndPoint ? 'Xác nhận Điểm kết thúc' : 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); setHasNoResults(false); return; } setIsSearching(true); setHasNoResults(false); searchTimeout.current = setTimeout(async () => { try { // Loại bỏ countrycodes=vn để tìm kiếm rộng hơn, thêm namedetails=1 để lấy tên chính xác const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=15&addressdetails=1&namedetails=1&accept-language=vi`, { headers: { 'Accept-Language': 'vi' } }); const data = await res.json(); setSearchResults(data); setHasNoResults(data.length === 0); } 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); // Ưu tiên lấy tên từ namedetails nếu có, nếu không lấy phần đầu của display_name const locationName = result.namedetails?.name || result.display_name.split(',')[0]; setFormData(prev => ({ ...prev, name: locationName, address: result.display_name, latitude: lat, longitude: lon })); setSearchResults([]); }; const handlePickLocation = async (latlng: L.LatLng) => { setFormData(prev => ({ ...prev, latitude: latlng.lat, longitude: latlng.lng })); // Tự động lấy tên địa điểm từ tọa độ vừa chọn try { const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng.lat}&lon=${latlng.lng}`); const data = await res.json(); const addr = data.address; const detectedName = addr.amenity || addr.building || addr.historic || addr.tourist || addr.shop || addr.office || addr.leisure || addr.attraction || addr.road || addr.neighbourhood || addr.suburb || data.display_name?.split(',')[0] || ""; setFormData(prev => ({ ...prev, name: detectedName || prev.name, address: data.display_name || prev.address })); } catch (e) {} }; const handleUseCurrentLocation = () => { if (!navigator.geolocation) { notify({ title: 'Thông báo', message: "Trình duyệt hoặc thiết bị của bạn không hỗ trợ định vị GPS.", type: 'info' }); return; } // Kiểm tra môi trường Secure Context (HTTPS) - Bắt buộc cho Geolocation trên Mobile if (!window.isSecureContext) { alert("Tính năng định vị GPS yêu cầu kết nối bảo mật (HTTPS). Nếu bạn đang truy cập qua địa chỉ IP, vui lòng sử dụng HTTPS hoặc Localhost."); return; } navigator.geolocation.getCurrentPosition( async (pos) => { const latlng = L.latLng(pos.coords.latitude, pos.coords.longitude); const now = new Date(); const formattedTime = format(now, "yyyy-MM-dd'T'HH:mm"); setFormData(prev => ({ ...prev, latitude: latlng.lat, longitude: latlng.lng, plannedStart: formattedTime })); // Tự động thực hiện reverse geocoding để lấy tên địa điểm và địa chỉ handlePickLocation(latlng); }, (err) => { let errorMessage = "Không thể lấy vị trí: "; switch(err.code) { case err.PERMISSION_DENIED: errorMessage += "Bạn đã từ chối quyền truy cập vị trí."; break; case err.POSITION_UNAVAILABLE: errorMessage += "Thông tin vị trí không khả dụng."; break; case err.TIMEOUT: errorMessage += "Hết thời gian chờ yêu cầu định vị."; break; default: errorMessage += err.message; } notify({ title: 'Lỗi định vị', message: errorMessage, type: 'error' }); }, { enableHighAccuracy: true, // Ưu tiên dùng GPS thay vì Wifi/Cell tower timeout: 10000, // Chờ tối đa 10 giây maximumAge: 0 // Không dùng vị trí cũ trong cache } ); }; // 3. Early return phải nằm SAU tất cả các khai báo Hook if (!isOpen || isPublicView) return null; // Do not render if public view const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); try { 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), plannedEnd: formData.plannedStart, }); } else if (isEndPoint) { await updateTourEndPoint(tourId, { name: formData.name || "Điểm kết thúc", latitude: parseFloat(formData.latitude as any), longitude: parseFloat(formData.longitude as any), plannedStart: formData.plannedStart, }); } else { 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: isStartPoint ? 'Đã thiết lập điểm xuất phát.' : isEndPoint ? 'Đã thiết lập điểm kết thúc.' : 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) { notify({ title: 'Lỗi', message: 'Lỗi khi lưu địa điểm', type: 'error' }); } finally { setIsLoading(false); } }; return (

{titleText}

{/* Mini Map Picker */}
{/* Map Search Bar Overlay - Tích hợp tìm kiếm trực tiếp trên bản đồ */}
handleSearchLocation(e.target.value)} /> {isSearching ? ( ) : formData.name && ( )}
{/* Dropdown kết quả tìm kiếm ngay trong khung bản đồ */} {(searchResults.length > 0 || hasNoResults) && (
{hasNoResults ? (
Không tìm thấy địa điểm phù hợp...
) : ( searchResults.map((result, idx) => ( )) )}
)}
CHUỘT PHẢI ĐỂ CHỌN VỊ TRÍ
{/* Nút lấy vị trí và thời gian hiện tại - Chỉ dành cho OWNER/MANAGER khi thêm mới */} {!editingLocation && (userRole === 'OWNER' || userRole === 'MANAGER') && ( )}
setFormData({...formData, name: e.target.value})} />
setFormData({...formData, address: e.target.value})} />