feat: cho phép lấy thời gian và địa điểm hiện tại để gán cho điểm

This commit is contained in:
2026-06-16 10:55:18 +07:00
parent 9e26aabce6
commit bc49e081c6
2 changed files with 102 additions and 18 deletions
+65 -2
View File
@@ -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 } from 'lucide-react';
import { X, MapPin, Loader2, Clock, Map as MapIcon, Navigation } from 'lucide-react';
import { useTourStore } from '@/store/useTourStore.js';
import { MapContainer, TileLayer, Marker, useMapEvents, useMap } from 'react-leaflet';
import L from 'leaflet';
@@ -80,7 +80,7 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
const [isLoading, setIsLoading] = useState(false);
// 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 } = useTourStore();
const { legs, addLocation, updateLocation, mapCenter, currentTour, userRole } = useTourStore();
// 1. Luôn khai báo Hook ở cấp cao nhất, không đặt code logic/return giữa các Hook
useEffect(() => {
@@ -147,6 +147,58 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
} catch (e) {}
};
const handleUseCurrentLocation = () => {
if (!navigator.geolocation) {
alert("Trình duyệt hoặc thiết bị của bạn không hỗ trợ định vị GPS.");
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;
}
alert(errorMessage);
},
{
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
@@ -200,6 +252,17 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
</div>
</div>
{/* 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') && (
<button
type="button"
onClick={handleUseCurrentLocation}
className="w-full mb-6 py-4 bg-indigo-50 hover:bg-indigo-100 text-indigo-600 rounded-2xl flex items-center justify-center gap-2 text-xs font-black uppercase tracking-widest border border-indigo-100 transition-all active:scale-95 shadow-sm"
>
<Navigation className="w-4 h-4 fill-current" /> Sử dụng vị trí & thời gian hiện tại
</button>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-bold text-gray-700 mb-1">Tên đa điểm</label>