Chỉnh sửa giao diện Tour Dashboard

This commit is contained in:
2026-06-13 22:16:27 +07:00
parent 5e3f10631f
commit c7db2c3ed8
11 changed files with 244 additions and 47 deletions
+24 -4
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import { MapContainer, TileLayer, Marker, Popup, useMap, useMapEvents } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { useTourStore } from './useTourStore.js';
@@ -25,8 +25,21 @@ function RecenterMap({ position }: { position: [number, number] }) {
return null;
}
// Component Helper để theo dõi sự di chuyển của người dùng trên bản đồ
function MapTracker() {
const setMapCenter = useTourStore(state => state.setMapCenter);
useMapEvents({
moveend: (e) => {
const map = e.target;
const center = map.getCenter();
setMapCenter([center.lat, center.lng]);
},
});
return null;
}
export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: () => void, onLogout?: () => void, user?: any, onViewTour: (id: string) => void }) => {
const { publicTours, fetchPublicTours, fetchTour, updateTour, deleteTour } = useTourStore();
const { publicTours, fetchPublicTours, fetchTour, updateTour, deleteTour, mapCenter, setMapCenter } = useTourStore();
const [userPos, setUserPos] = useState<[number, number]>([10.7769, 106.7009]); // Mặc định TP.HCM
const [isAdminModalOpen, setIsAdminModalOpen] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
@@ -34,7 +47,11 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: ()
useEffect(() => {
fetchPublicTours();
navigator.geolocation.getCurrentPosition(
(pos) => setUserPos([pos.coords.latitude, pos.coords.longitude]),
(pos) => {
const posArray: [number, number] = [pos.coords.latitude, pos.coords.longitude];
setUserPos(posArray);
setMapCenter(posArray);
},
() => console.log("Không thể lấy vị trí người dùng")
);
}, []);
@@ -111,12 +128,15 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: ()
</div>
</div>
<MapContainer center={userPos} zoom={13} className="h-full w-full">
<MapContainer center={mapCenter} zoom={13} className="h-full w-full">
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; OpenStreetMap contributors'
/>
{/* Theo dõi di chuyển bản đồ */}
<MapTracker />
{/* Tự động di chuyển bản đồ đến vị trí người dùng khi tìm thấy tọa độ */}
<RecenterMap position={userPos} />
+125 -21
View File
@@ -15,21 +15,37 @@ import {
Quote,
Plus,
List,
Map as MapIconLucide
Map as MapIconLucide,
MapPin,
Flag
} from 'lucide-react';
import L from 'leaflet';
// Định nghĩa kiểu dữ liệu cho Địa điểm để khớp với Schema Prisma
type LocationType = 'MOVE' | 'VISIT' | 'REST' | 'EAT';
// Menu ngữ cảnh cho bản đồ
const MapContextMenu = ({ onAction }: { onAction: (action: string, latlng: L.LatLng) => void }) => {
const [menuPos, setMenuPos] = useState<{ x: number, y: number, latlng: L.LatLng } | null>(null);
const { legs } = useTourStore();
const { legs, setMapCenter } = useTourStore();
useMapEvents({
contextmenu: (e) => {
// Ngăn menu mặc định của trình duyệt hiện lên.
// Điều này là cần thiết để menu tùy chỉnh của Leaflet có thể tương tác được.
// Nếu không có, menu của trình duyệt sẽ đè lên và chặn các sự kiện click.
if (e.originalEvent) {
L.DomEvent.preventDefault(e.originalEvent);
}
setMenuPos({ x: e.containerPoint.x, y: e.containerPoint.y, latlng: e.latlng });
},
click: () => setMenuPos(null),
dragstart: () => setMenuPos(null)
dragstart: () => setMenuPos(null),
moveend: (e) => {
const map = e.target;
const center = map.getCenter();
setMapCenter([center.lat, center.lng]);
}
});
if (!menuPos) return null;
@@ -40,10 +56,10 @@ const MapContextMenu = ({ onAction }: { onAction: (action: string, latlng: L.Lat
style={{ top: menuPos.y, left: menuPos.x }}
>
<button onClick={() => { onAction('START', menuPos.latlng); setMenuPos(null); }} className="w-full text-left px-4 py-2 hover:bg-blue-50 text-sm font-bold text-gray-700 flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500" /> Bắt đu từ đây
<div className="w-2 h-2 rounded-full bg-blue-600" /> Bắt đu từ đây
</button>
<button onClick={() => { onAction('END', menuPos.latlng); setMenuPos(null); }} className="w-full text-left px-4 py-2 hover:bg-blue-50 text-sm font-bold text-gray-700 flex items-center gap-2 border-b border-gray-50">
<div className="w-2 h-2 rounded-full bg-red-500" /> Kết thúc đây
<button onClick={() => { onAction('END', menuPos.latlng); setMenuPos(null); }} className="w-full text-left px-4 py-2 hover:bg-green-50 text-sm font-bold text-gray-700 flex items-center gap-2 border-b border-gray-50">
<div className="w-2 h-2 rounded-full bg-green-500" /> Kết thúc đây
</button>
<div className="px-4 py-2 text-[10px] font-black text-gray-400 uppercase tracking-widest">Thêm vào chặng</div>
{legs.map(leg => (
@@ -63,7 +79,7 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
const [activeTab, setActiveTab] = useState<'plan' | 'expense' | 'photo' | 'settings'>('plan');
const [viewMode, setViewMode] = useState<'map' | 'timeline'>('timeline');
const [isAddLocationOpen, setIsAddLocationOpen] = useState(false);
const { currentTour, fetchTour, fetchPublicTours, publicTours, userRole, legs, addLocation } = useTourStore();
const { currentTour, fetchTour, fetchPublicTours, publicTours, userRole, legs, addLocation, mapCenter } = useTourStore();
useEffect(() => {
const loadData = async () => {
@@ -82,33 +98,61 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
}
}, [publicTours, currentTour, fetchTour]);
// Hàm xử lý các hành động từ Context Menu của bản đồ
const handleMapAction = async (action: string, latlng: L.LatLng) => {
if (!currentTour || legs.length === 0) return;
// Đảm bảo có tour và ít nhất một chặng để ghim
const currentLegs = useTourStore.getState().legs;
if (!currentTour || currentLegs.length === 0) {
alert("Tour chưa có chặng nào. Vui lòng tạo chặng trước khi thêm địa điểm.");
return;
}
let targetLegId = legs[0].id; // Mặc định là chặng đầu
let targetLegId = currentLegs[0].id; // Mặc định là chặng đầu
let defaultName = "Địa điểm mới";
let locationType: LocationType = 'VISIT'; // Mặc định là tham quan
if (action === 'START') defaultName = "Điểm bắt đầu";
if (action === 'START') {
defaultName = "Điểm bắt đầu";
locationType = 'MOVE'; // Điểm bắt đầu thường liên quan đến di chuyển
}
if (action === 'END') {
targetLegId = legs[legs.length - 1].id;
targetLegId = currentLegs[currentLegs.length - 1].id;
defaultName = "Điểm kết thúc";
locationType = 'MOVE'; // Điểm kết thúc cũng liên quan đến di chuyển
}
if (action.startsWith('ADD_TO_LEG_')) {
targetLegId = action.replace('ADD_TO_LEG_', '');
// Loại địa điểm mặc định vẫn là VISIT nếu thêm vào chặng cụ thể
}
const name = window.prompt("Nhập tên địa điểm:", defaultName);
// Tự động lấy tên địa điểm từ tọa độ (Reverse Geocoding)
let detectedName = "";
try {
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng.lat}&lon=${latlng.lng}`);
const data = await res.json();
detectedName = data.display_name?.split(',')[0] || "";
} catch (e) {}
const name = window.prompt("Xác nhận tên địa điểm:", detectedName || defaultName);
if (name) {
await addLocation(currentTour.id, {
name,
address: '', // Gửi chuỗi rỗng cho địa chỉ nếu không có
latitude: latlng.lat,
longitude: latlng.lng,
legId: targetLegId,
type: 'VISIT'
type: locationType as any,
plannedStart: null, // Gửi null cho thời gian nếu không có
plannedEnd: null, // Gửi null cho thời gian nếu không có
});
}
};
// Xác định Điểm xuất phát và Điểm kết thúc hiển thị dưới widget tài chính
const startPoint = legs[0]?.locations[0];
const lastLeg = legs[legs.length - 1];
const endPoint = lastLeg?.locations[lastLeg.locations.length - 1];
// Định nghĩa các tab dựa trên quyền hạn (RBAC) từ store
const tabs = [
{ id: 'plan', label: 'Lộ trình', icon: MapIcon, visible: true },
@@ -150,17 +194,27 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
</div>
{/* Tour Header Info */}
<div className="relative h-64 w-full bg-blue-900">
<div className="relative min-h-[480px] w-full bg-blue-900 flex flex-col justify-end">
<img
src={tourInfo.coverImage}
className="w-full h-full object-cover opacity-60"
className="absolute inset-0 w-full h-full object-cover opacity-60"
alt="Tour Cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
<div className="absolute bottom-16 left-0 right-0 p-6 text-white">
<div className="max-w-2xl mx-auto space-y-2">
<div className="relative z-10 p-6 text-white pt-28 pb-20">
<div className="max-w-2xl mx-auto space-y-4">
<h2 className="text-3xl font-black tracking-tight drop-shadow-md">{tourInfo.title}</h2>
{/* Dòng tóm tắt Lộ trình */}
<div className="mt-3 text-sm font-bold text-blue-100 bg-blue-800/30 backdrop-blur-sm px-4 py-2 rounded-xl border border-white/5 inline-block max-w-full truncate">
<span className="text-white/60 mr-1">Lộ trình:</span>
<span>Điểm xuất phát: </span>
<span className="text-white">{startPoint?.name || '...'}</span>
<span className="mx-2 text-white/40">-</span>
<span>Điểm kết thúc: </span>
<span className="text-white">{endPoint?.name || '...'}</span>
</div>
<div className="flex flex-wrap gap-4 text-sm font-medium opacity-90">
<div className="flex items-center bg-black/20 backdrop-blur-sm px-3 py-1 rounded-full border border-white/10">
<Calendar className="w-4 h-4 mr-1.5" />
@@ -172,6 +226,8 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
</div>
</div>
{/* Member Avatars Stack */}
<div className="flex items-center gap-2 mt-4">
<div className="flex -space-x-3">
@@ -216,6 +272,32 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
</div>
</div>
{/* Khối hiển thị Điểm đầu & Điểm cuối (Dưới Financial Quick-View) */}
<div className="max-w-2xl mx-auto mt-4 px-4 grid grid-cols-1 sm:grid-cols-2 gap-3 animate-in fade-in slide-in-from-top-2 duration-500">
{startPoint && (
<div className="bg-white p-4 rounded-2xl border border-blue-50 flex items-center gap-3 shadow-sm hover:shadow-md transition-shadow">
<div className="w-10 h-10 rounded-xl bg-blue-100 flex items-center justify-center text-blue-600 shadow-inner">
<MapPin className="w-5 h-5" />
</div>
<div className="overflow-hidden">
<p className="text-[10px] font-black text-blue-400 uppercase tracking-widest mb-0.5">Điểm xuất phát</p>
<p className="text-sm font-bold text-gray-800 truncate">{startPoint.name}</p>
</div>
</div>
)}
{endPoint && (
<div className="bg-white p-4 rounded-2xl border border-green-50 flex items-center gap-3 shadow-sm hover:shadow-md transition-shadow">
<div className="w-10 h-10 rounded-xl bg-green-100 flex items-center justify-center text-green-600 shadow-inner">
<Flag className="w-5 h-5" />
</div>
<div className="overflow-hidden">
<p className="text-[10px] font-black text-green-400 uppercase tracking-widest mb-0.5">Điểm kết thúc</p>
<p className="text-sm font-bold text-gray-800 truncate">{endPoint.name}</p>
</div>
</div>
)}
</div>
{/* Main Content Area - Điều chỉnh max-width khi ở chế độ bản đồ */}
<div className={`${activeTab === 'plan' && viewMode === 'map' ? 'w-full' : 'max-w-2xl mx-auto'} mt-6 px-4 pb-24`}>
{/* Tab Switcher */}
@@ -262,7 +344,7 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
<ItineraryTimeline />
) : (
<div className="h-[60vh] w-full rounded-3xl overflow-hidden shadow-xl border-4 border-white relative">
<MapContainer center={[10.7769, 106.7009]} zoom={13} className="h-full w-full">
<MapContainer center={mapCenter} zoom={13} className="h-full w-full">
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<MapContextMenu onAction={handleMapAction} />
@@ -272,14 +354,36 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
return <Polyline key={leg.id} positions={positions as any} color="#3b82f6" weight={3} dashArray="5, 10" />;
})}
{legs.flatMap(l => l.locations).map((loc: any) => (
<Marker key={loc.id} position={[loc.latitude, loc.longitude]}>
{legs.flatMap(l => l.locations).map((loc: any) => {
const isStart = startPoint?.id === loc.id;
const isEnd = endPoint?.id === loc.id;
let customIcon = undefined;
if (isStart) {
customIcon = L.divIcon({
className: 'custom-marker',
html: `<div class="w-5 h-5 bg-blue-600 rounded-full border-2 border-white shadow-lg"></div>`,
iconSize: [20, 20],
iconAnchor: [10, 10]
});
} else if (isEnd) {
customIcon = L.divIcon({
className: 'custom-marker',
html: `<div class="w-5 h-5 bg-green-500 rounded-full border-2 border-white shadow-lg"></div>`,
iconSize: [20, 20],
iconAnchor: [10, 10]
});
}
return (
<Marker key={loc.id} position={[loc.latitude, loc.longitude]} icon={customIcon}>
<Popup>
<div className="font-bold">{loc.name}</div>
<div className="text-xs text-gray-500">{loc.type}</div>
</Popup>
</Marker>
))}
);
})}
</MapContainer>
<div className="absolute top-4 left-4 z-[1000] bg-white/90 backdrop-blur-md p-3 rounded-2xl text-[10px] font-bold text-gray-500 shadow-lg border border-white">
Mẹo: Nhấn giữ (Mobile) hoặc Chuột phải đ ghim đa điểm
+19 -4
View File
@@ -1,6 +1,6 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useEffect, useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import { MapContainer, TileLayer, Marker, Popup, useMap, useMapEvents } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { useTourStore } from './useTourStore.js';
@@ -21,14 +21,29 @@ function RecenterMap({ position }) {
}, [position, map]);
return null;
}
function MapTracker() {
const setMapCenter = useTourStore(state => state.setMapCenter);
useMapEvents({
moveend: (e) => {
const map = e.target;
const center = map.getCenter();
setMapCenter([center.lat, center.lng]);
},
});
return null;
}
export const ExploreMap = ({ onBack, onLogout, user, onViewTour }) => {
const { publicTours, fetchPublicTours, fetchTour, updateTour, deleteTour } = useTourStore();
const { publicTours, fetchPublicTours, fetchTour, updateTour, deleteTour, mapCenter, setMapCenter } = useTourStore();
const [userPos, setUserPos] = useState([10.7769, 106.7009]);
const [isAdminModalOpen, setIsAdminModalOpen] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
useEffect(() => {
fetchPublicTours();
navigator.geolocation.getCurrentPosition((pos) => setUserPos([pos.coords.latitude, pos.coords.longitude]), () => console.log("Không thể lấy vị trí người dùng"));
navigator.geolocation.getCurrentPosition((pos) => {
const posArray = [pos.coords.latitude, pos.coords.longitude];
setUserPos(posArray);
setMapCenter(posArray);
}, () => console.log("Không thể lấy vị trí người dùng"));
}, []);
const handleEditTour = async (tour) => {
const newTitle = window.prompt("Nhập tên mới cho Tour:", tour.title);
@@ -51,7 +66,7 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }) => {
}
}
};
return (_jsxs("div", { className: "h-screen w-full relative", children: [_jsx("button", { onClick: onBack, className: "absolute top-6 left-6 z-[1000] bg-white p-3 rounded-full shadow-xl hover:bg-gray-50 transition-all", children: _jsx(X, { className: "w-6 h-6 text-gray-800" }) }), onLogout && (_jsxs("button", { onClick: onLogout, className: "absolute top-6 right-6 z-[1000] bg-white px-4 py-3 rounded-2xl shadow-xl hover:bg-red-50 hover:text-red-600 transition-all flex items-center gap-2 font-bold text-gray-700", children: [_jsx(LogOut, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "\u0110\u0103ng xu\u1EA5t" })] })), user?.isAdmin && (_jsxs("button", { onClick: () => setIsAdminModalOpen(true), className: "absolute top-6 right-40 z-[1000] bg-blue-600 px-4 py-3 rounded-2xl shadow-xl hover:bg-blue-700 text-white transition-all flex items-center gap-2 font-bold", children: [_jsx(Settings, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "Qu\u1EA3n l\u00FD h\u1EC7 th\u1ED1ng" })] })), user && (_jsxs("button", { onClick: () => setIsCreateModalOpen(true), className: "absolute top-6 right-80 z-[1000] bg-green-600 px-4 py-3 rounded-2xl shadow-xl hover:bg-green-700 text-white transition-all flex items-center gap-2 font-bold", children: [_jsx(Navigation, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "T\u1EA1o Tour m\u1EDBi" })] })), _jsx("div", { className: "absolute top-6 left-20 z-[1000] bg-white/90 backdrop-blur-md px-6 py-3 rounded-2xl shadow-xl border border-white/20 hidden sm:block", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Navigation, { className: "w-4 h-4 text-blue-600" }), _jsx("span", { className: "font-bold text-gray-800", children: "\u0110ang kh\u00E1m ph\u00E1 khu v\u1EF1c c\u1EE7a b\u1EA1n" })] }) }), _jsxs(MapContainer, { center: userPos, zoom: 13, className: "h-full w-full", children: [_jsx(TileLayer, { url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", attribution: '\u00A9 OpenStreetMap contributors' }), _jsx(RecenterMap, { position: userPos }), publicTours.map((tour) => {
return (_jsxs("div", { className: "h-screen w-full relative", children: [_jsx("button", { onClick: onBack, className: "absolute top-6 left-6 z-[1000] bg-white p-3 rounded-full shadow-xl hover:bg-gray-50 transition-all", children: _jsx(X, { className: "w-6 h-6 text-gray-800" }) }), onLogout && (_jsxs("button", { onClick: onLogout, className: "absolute top-6 right-6 z-[1000] bg-white px-4 py-3 rounded-2xl shadow-xl hover:bg-red-50 hover:text-red-600 transition-all flex items-center gap-2 font-bold text-gray-700", children: [_jsx(LogOut, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "\u0110\u0103ng xu\u1EA5t" })] })), user?.isAdmin && (_jsxs("button", { onClick: () => setIsAdminModalOpen(true), className: "absolute top-6 right-40 z-[1000] bg-blue-600 px-4 py-3 rounded-2xl shadow-xl hover:bg-blue-700 text-white transition-all flex items-center gap-2 font-bold", children: [_jsx(Settings, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "Qu\u1EA3n l\u00FD h\u1EC7 th\u1ED1ng" })] })), user && (_jsxs("button", { onClick: () => setIsCreateModalOpen(true), className: "absolute top-6 right-80 z-[1000] bg-green-600 px-4 py-3 rounded-2xl shadow-xl hover:bg-green-700 text-white transition-all flex items-center gap-2 font-bold", children: [_jsx(Navigation, { className: "w-5 h-5" }), _jsx("span", { className: "hidden sm:inline", children: "T\u1EA1o Tour m\u1EDBi" })] })), _jsx("div", { className: "absolute top-6 left-20 z-[1000] bg-white/90 backdrop-blur-md px-6 py-3 rounded-2xl shadow-xl border border-white/20 hidden sm:block", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Navigation, { className: "w-4 h-4 text-blue-600" }), _jsx("span", { className: "font-bold text-gray-800", children: "\u0110ang kh\u00E1m ph\u00E1 khu v\u1EF1c c\u1EE7a b\u1EA1n" })] }) }), _jsxs(MapContainer, { center: mapCenter, zoom: 13, className: "h-full w-full", children: [_jsx(TileLayer, { url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", attribution: '\u00A9 OpenStreetMap contributors' }), _jsx(MapTracker, {}), _jsx(RecenterMap, { position: userPos }), publicTours.map((tour) => {
const location = tour.legs?.[0]?.locations?.[0];
if (!location)
return null;
+1 -1
View File
File diff suppressed because one or more lines are too long
+64 -14
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -4,6 +4,7 @@ interface TourState {
publicTours: any[];
userRole: string | null;
activeLegId: string | null;
mapCenter: [number, number];
setTour: (tour: any) => void;
updateLegs: (legs: any[]) => void;
createTour: (tourData: any) => Promise<any>;
@@ -15,6 +16,7 @@ interface TourState {
addLocation: (tourId: string, locationData: any) => Promise<void>;
optimizeRouting: (legId: string) => Promise<void>;
setActiveLegId: (id: string | null) => void;
setMapCenter: (pos: [number, number]) => void;
fetchTour: (id: string) => Promise<void>;
fetchPublicTours: () => Promise<void>;
}
+2
View File
@@ -5,9 +5,11 @@ export const useTourStore = create((set, get) => ({
publicTours: [],
userRole: null,
activeLegId: null,
mapCenter: [10.7769, 106.7009],
setTour: (tour) => set({ currentTour: tour }),
updateLegs: (legs) => set({ legs }),
setActiveLegId: (id) => set({ activeLegId: id }),
setMapCenter: (pos) => set({ mapCenter: pos }),
fetchTour: async (id) => {
const API_BASE = `http://${window.location.hostname}:3001`;
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`);
+1 -1
View File
File diff suppressed because one or more lines are too long
+4
View File
@@ -6,6 +6,7 @@ interface TourState {
publicTours: any[];
userRole: string | null;
activeLegId: string | null;
mapCenter: [number, number];
setTour: (tour: any) => void;
updateLegs: (legs: any[]) => void;
createTour: (tourData: any) => Promise<any>;
@@ -17,6 +18,7 @@ interface TourState {
addLocation: (tourId: string, locationData: any) => Promise<void>;
optimizeRouting: (legId: string) => Promise<void>;
setActiveLegId: (id: string | null) => void;
setMapCenter: (pos: [number, number]) => void;
fetchTour: (id: string) => Promise<void>;
fetchPublicTours: () => Promise<void>;
}
@@ -27,9 +29,11 @@ export const useTourStore = create<TourState>((set, get) => ({
publicTours: [],
userRole: null,
activeLegId: null,
mapCenter: [10.7769, 106.7009],
setTour: (tour) => set({ currentTour: tour }),
updateLegs: (legs) => set({ legs }),
setActiveLegId: (id) => set({ activeLegId: id }),
setMapCenter: (pos) => set({ mapCenter: pos }),
fetchTour: async (id: string) => {
const API_BASE = `http://${window.location.hostname}:3001`;
const response = await fetch(`${API_BASE}/api/v1/tours/${id}`);