From bc49e081c6fefd733239b1cc983e57509fb95630 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Tue, 16 Jun 2026 10:55:18 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20cho=20ph=C3=A9p=20l=E1=BA=A5y=20th?= =?UTF-8?q?=E1=BB=9Di=20gian=20v=C3=A0=20=C4=91=E1=BB=8Ba=20=C4=91i?= =?UTF-8?q?=E1=BB=83m=20hi=E1=BB=87n=20t=E1=BA=A1i=20=C4=91=E1=BB=83=20g?= =?UTF-8?q?=C3=A1n=20cho=20=C4=91i=E1=BB=83m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/AddLocationModal.tsx | 67 +++++++++++++++++++- frontend/vite.config.ts | 53 +++++++++++----- 2 files changed, 102 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/AddLocationModal.tsx b/frontend/src/components/AddLocationModal.tsx index 13fe966..7e47a70 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 } 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 + {/* 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') && ( + + )} +
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index fb182df..52da095 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,23 +1,44 @@ -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({ - plugins: [react()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, - }, - server: { - port: 3002, - host: true, - proxy: { - '/api': { - target: 'http://127.0.0.1:3001', - changeOrigin: true, +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: { + '@': path.resolve(__dirname, './src'), }, }, - }, + 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, + }, + }, + }, + }; }); \ No newline at end of file