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:
@@ -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>
|
||||
|
||||
+23
-2
@@ -1,9 +1,22 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
export default defineConfig(({ mode }) => {
|
||||
// Nạp các biến môi trường từ thư mục gốc
|
||||
const env = loadEnv(mode, path.resolve(__dirname, '..'), '');
|
||||
|
||||
// Cấu hình HTTPS nếu tìm thấy file chứng chỉ (ví dụ đặt tại thư mục gốc của dự án)
|
||||
const httpsConfig = fs.existsSync('../key.pem') && fs.existsSync('../cert.pem')
|
||||
? {
|
||||
key: fs.readFileSync('../key.pem'),
|
||||
cert: fs.readFileSync('../cert.pem'),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -13,11 +26,19 @@ export default defineConfig({
|
||||
server: {
|
||||
port: 3002,
|
||||
host: true,
|
||||
// Cho phép các host từ file .env
|
||||
allowedHosts: env.ALLOWED_HOSTS ? env.ALLOWED_HOSTS.split(',') : true,
|
||||
https: httpsConfig,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:3001',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/socket.io': {
|
||||
target: 'http://127.0.0.1:3001',
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user