Vào thằng Tour Dashboard khi click vào Tour callout

This commit is contained in:
2026-06-14 10:01:26 +07:00
parent b3519c94cd
commit bf63510980
9 changed files with 190 additions and 243 deletions
+15 -44
View File
@@ -1,10 +1,12 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import React, { useEffect, useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap, useMapEvents } from 'react-leaflet';
import { MapContainer, TileLayer, Marker, useMap, useMapEvents } from 'react-leaflet';
import _MarkerClusterGroup from 'react-leaflet-cluster';
const MarkerClusterGroup = _MarkerClusterGroup.default || _MarkerClusterGroup;
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { useTourStore } from './useTourStore.js';
import { X, Navigation, Image as ImageIcon, LogOut, Settings, Edit2, Trash2 } from 'lucide-react';
import { X, Navigation, LogOut, Settings } from 'lucide-react';
import { UserManagementModal } from './UserManagementModal.js';
import { CreateTourModal } from './CreateTourModal.js';
const DefaultIcon = L.icon({
@@ -36,7 +38,7 @@ function MapTracker() {
return null;
}
export const ExploreMap = ({ onBack, onLogout, user, onViewTour }) => {
const { publicTours, fetchPublicTours, fetchTour, updateTour, deleteTour, mapCenter, setMapCenter } = useTourStore();
const { publicTours, fetchPublicTours, setMapCenter } = useTourStore();
const [initialViewState] = useState(() => {
const saved = localStorage.getItem('map_view_state');
if (saved) {
@@ -66,36 +68,14 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }) => {
setMapCenter(initialViewState.center);
}
}, []);
const handleEditTour = async (tour) => {
const newTitle = window.prompt("Nhập tên mới cho Tour:", tour.title);
if (newTitle && newTitle !== tour.title) {
try {
await updateTour(tour.id, { title: newTitle });
}
catch (err) {
alert(err.message);
}
}
};
const handleDeleteTour = async (id) => {
if (window.confirm("Bạn có chắc chắn muốn xóa Tour này và toàn bộ dữ liệu liên quan?")) {
try {
await deleteTour(id);
}
catch (err) {
alert(err.message);
}
}
};
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: mapZoom, 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 startLoc = tour.legs?.[0]?.locations?.[0];
if (!startLoc)
return null;
const lastLeg = tour.legs?.[tour.legs.length - 1];
const endLoc = lastLeg?.locations?.[lastLeg.locations.length - 1];
const tourImage = tour.photos?.[0]?.imageUrl || `https://picsum.photos/seed/${tour.id}/200/200`;
const commonPopup = (_jsx(Popup, { className: "custom-popup", children: _jsxs("div", { className: "p-1 max-w-[200px]", children: [_jsx("img", { src: tourImage, className: "w-full h-32 object-cover rounded-lg mb-2" }), _jsx("h4", { className: "font-bold text-gray-900 leading-tight mb-1", children: tour.title }), _jsxs("button", { onClick: () => onViewTour(tour.id), className: "text-xs font-bold text-blue-600 flex items-center gap-1", children: ["Xem chi ti\u1EBFt h\u00ECnh \u1EA3nh ", _jsx(ImageIcon, { className: "w-3 h-3" })] }), user && (_jsxs("div", { className: "flex gap-2 mt-3 pt-3 border-t border-gray-100", children: [_jsxs("button", { onClick: () => handleEditTour(tour), className: "flex-1 flex items-center justify-center gap-1 py-1.5 bg-blue-50 text-blue-600 rounded-lg text-[10px] font-bold hover:bg-blue-100 transition-colors", children: [_jsx(Edit2, { className: "w-3 h-3" }), " S\u1EEDa"] }), _jsxs("button", { onClick: () => handleDeleteTour(tour.id), className: "flex-1 flex items-center justify-center gap-1 py-1.5 bg-red-50 text-red-600 rounded-lg text-[10px] font-bold hover:bg-red-100 transition-colors", children: [_jsx(Trash2, { className: "w-3 h-3" }), " X\u00F3a"] })] }))] }) }));
return (_jsxs(React.Fragment, { children: [_jsx(Marker, { position: [startLoc.latitude, startLoc.longitude], icon: L.divIcon({
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: mapZoom, className: "h-full w-full", preferCanvas: true, children: [_jsx(TileLayer, { url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", attribution: '\u00A9 OpenStreetMap contributors' }), _jsx(MapTracker, {}), _jsx(RecenterMap, { position: userPos }), _jsx(MarkerClusterGroup, { chunkedLoading: true, children: publicTours.map((tour) => {
const startLoc = tour.legs?.[0]?.locations?.[0];
if (!startLoc)
return null;
const tourImage = tour.photos?.[0]?.imageUrl || `https://picsum.photos/seed/${tour.id}/200/200`;
return (_jsx(React.Fragment, { children: _jsx(Marker, { position: [startLoc.latitude, startLoc.longitude], eventHandlers: {
click: () => onViewTour(tour.id)
}, icon: L.divIcon({
className: 'custom-bubble',
html: `
<div class="relative group">
@@ -109,17 +89,8 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour }) => {
`,
iconSize: [48, 48],
iconAnchor: [24, 24]
}), children: commonPopup }), endLoc && (endLoc.latitude !== startLoc.latitude || endLoc.longitude !== startLoc.longitude) && (_jsx(Marker, { position: [endLoc.latitude, endLoc.longitude], icon: L.divIcon({
className: 'custom-end-marker',
html: `
<div class="w-8 h-8 bg-green-600 rounded-full border-2 border-white shadow-lg flex items-center justify-center text-xs font-black text-white hover:scale-110 transition-transform">
E
</div>
`,
iconSize: [32, 32],
iconAnchor: [16, 16]
}), children: commonPopup }))] }, tour.id));
})] }), _jsx(UserManagementModal, { isOpen: isAdminModalOpen, onClose: () => setIsAdminModalOpen(false) }), _jsx(CreateTourModal, { isOpen: isCreateModalOpen, onClose: () => setIsCreateModalOpen(false), onSuccess: (tour) => {
}) }) }, tour.id));
}) })] }), _jsx(UserManagementModal, { isOpen: isAdminModalOpen, onClose: () => setIsAdminModalOpen(false) }), _jsx(CreateTourModal, { isOpen: isCreateModalOpen, onClose: () => setIsCreateModalOpen(false), onSuccess: (tour) => {
fetchTour(tour.id);
onViewTour(tour.id);
} })] }));
+1 -1
View File
File diff suppressed because one or more lines are too long
+9 -7
View File
@@ -5,6 +5,8 @@ import { ExpenseManager } from './ExpenseManager.js';
import { useTourStore } from './useTourStore.js';
import { AddLocationModal } from './AddLocationModal.js';
import { MapContainer, TileLayer, Marker, Popup, useMapEvents, Polyline } from 'react-leaflet';
import _MarkerClusterGroup from 'react-leaflet-cluster';
const MarkerClusterGroup = _MarkerClusterGroup.default || _MarkerClusterGroup;
import { useMap } from 'react-leaflet';
import { Map as MapIcon, Wallet, Image as ImageIcon, Calendar, Users, ChevronLeft, Settings, Quote, Plus, List, Map as MapIconLucide, MapPin, Flag } from 'lucide-react';
import L from 'leaflet';
@@ -215,7 +217,7 @@ export const TourDetailPage = ({ onBack }) => {
{ id: 'plan', label: 'Lộ trình', icon: MapIcon, visible: true },
{ id: 'expense', label: 'Chi phí', icon: Wallet, visible: ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '') },
{ id: 'photo', label: 'Ảnh', icon: ImageIcon, visible: true },
{ id: 'settings', label: 'Cài đặt', icon: Settings, visible: ['OWNER', 'MANAGER'].includes(userRole || '') },
{ id: 'settings', label: 'Cài đặt', icon: Settings, visible: userRole === 'OWNER' },
].filter(t => t.visible);
const hasFinanceAccess = ['OWNER', 'MANAGER', 'MEMBER'].includes(userRole || '');
const travelQuotes = [
@@ -237,12 +239,12 @@ export const TourDetailPage = ({ onBack }) => {
: 'text-gray-400 hover:text-gray-500 hover:bg-gray-50'}`, children: [_jsx(tab.icon, { className: `w-4 h-4 mr-2 ${activeTab === tab.id ? 'scale-110' : ''}` }), tab.label] }, tab.id))) }), _jsxs("div", { className: "transition-opacity duration-300", children: [activeTab === 'plan' && (_jsxs("div", { className: "animate-in fade-in slide-in-from-bottom-2", children: [_jsx("div", { className: "flex justify-center mb-6", children: _jsxs("div", { className: "bg-gray-100 p-1 rounded-2xl flex gap-1", children: [_jsxs("button", { onClick: () => setViewMode('timeline'), className: `flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all ${viewMode === 'timeline' ? 'bg-white shadow-sm text-blue-600' : 'text-gray-500'}`, children: [_jsx(List, { className: "w-3.5 h-3.5" }), " Danh s\u00E1ch"] }), _jsxs("button", { onClick: () => setViewMode('map'), className: `flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all ${viewMode === 'map' ? 'bg-white shadow-sm text-blue-600' : 'text-gray-500'}`, children: [_jsx(MapIconLucide, { className: "w-3.5 h-3.5" }), " B\u1EA3n \u0111\u1ED3"] })] }) }), viewMode === 'timeline' ? (_jsx(ItineraryTimeline, { onAddLocation: (legId) => {
setTargetLegId(legId);
setIsAddLocationOpen(true);
} })) : (_jsxs("div", { className: "h-[60vh] w-full rounded-3xl overflow-hidden shadow-xl border-4 border-white relative", children: [_jsxs(MapContainer, { center: initialViewState?.center || mapCenter, zoom: mapZoom, className: "h-full w-full", children: [_jsx(TileLayer, { url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" }), _jsx(MapContextMenu, { onAction: handleMapAction }), _jsx(MapTourBounds, { locations: allLocations }), allLocations.length > 1 && (_jsx(Polyline, { positions: allLocations.map(l => [l.latitude, l.longitude]), color: "#3b82f6", weight: 3, dashArray: "5, 10" })), legs.flatMap(l => l.locations).map((loc) => {
const isStart = startPoint?.id === loc.id;
const isEnd = endPoint?.id === loc.id;
const icon = isStart ? START_ICON : isEnd ? END_ICON : VISIT_ICON;
return (_jsx(Marker, { position: [loc.latitude, loc.longitude], icon: icon, children: _jsxs(Popup, { children: [_jsx("div", { className: "font-bold", children: loc.name }), _jsx("div", { className: "text-xs text-gray-500", children: loc.type })] }) }, loc.id));
})] }), _jsx("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", children: "M\u1EB9o: Nh\u1EA5n gi\u1EEF (Mobile) ho\u1EB7c Chu\u1ED9t ph\u1EA3i \u0111\u1EC3 ghim \u0111\u1ECBa \u0111i\u1EC3m" })] }))] })), activeTab === 'expense' && (_jsx("div", { className: "animate-in fade-in slide-in-from-bottom-4", children: _jsx(ExpenseManager, {}) })), activeTab === 'photo' && (_jsx("div", { className: "grid grid-cols-3 gap-1.5 animate-in fade-in", children: [1, 2, 3, 4, 5, 6].map((i) => (_jsxs("div", { className: "aspect-square bg-gray-200 rounded-xl overflow-hidden relative group border border-white", children: [_jsx("div", { className: "absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" }), _jsx("img", { src: `https://picsum.photos/seed/${i + 10}/400/400`, alt: "Tour photo", className: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" })] }, i))) })), activeTab === 'settings' && (_jsxs("div", { className: "p-8 text-center bg-white rounded-3xl border border-dashed border-gray-200 animate-in zoom-in-95", children: [_jsx(Settings, { className: "w-12 h-12 text-gray-300 mx-auto mb-4" }), _jsx("p", { className: "text-gray-500 font-medium", children: "T\u00EDnh n\u0103ng qu\u1EA3n l\u00FD th\u00E0nh vi\u00EAn \u0111ang \u0111\u01B0\u1EE3c c\u1EADp nh\u1EADt..." })] }))] })] }), _jsx("div", { className: "fixed bottom-6 left-1/2 -translate-x-1/2 z-40", children: _jsx("button", { onClick: () => {
} })) : (_jsxs("div", { className: "h-[60vh] w-full rounded-3xl overflow-hidden shadow-xl border-4 border-white relative", children: [_jsxs(MapContainer, { center: initialViewState?.center || mapCenter, zoom: mapZoom, className: "h-full w-full", preferCanvas: true, children: [_jsx(TileLayer, { url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" }), _jsx(MapContextMenu, { onAction: handleMapAction }), _jsx(MapTourBounds, { locations: allLocations }), allLocations.length > 1 && (_jsx(Polyline, { positions: allLocations.map(l => [l.latitude, l.longitude]), color: "#3b82f6", weight: 3, dashArray: "5, 10", smoothFactor: 1.5 })), _jsx(MarkerClusterGroup, { chunkedLoading: true, children: legs.flatMap(l => l.locations).map((loc) => {
const isStart = startPoint?.id === loc.id;
const isEnd = endPoint?.id === loc.id;
const icon = isStart ? START_ICON : isEnd ? END_ICON : VISIT_ICON;
return (_jsx(Marker, { position: [loc.latitude, loc.longitude], icon: icon, children: _jsxs(Popup, { children: [_jsx("div", { className: "font-bold", children: loc.name }), _jsx("div", { className: "text-xs text-gray-500", children: loc.type })] }) }, loc.id));
}) })] }), _jsx("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", children: "M\u1EB9o: Nh\u1EA5n gi\u1EEF (Mobile) ho\u1EB7c Chu\u1ED9t ph\u1EA3i \u0111\u1EC3 ghim \u0111\u1ECBa \u0111i\u1EC3m" })] }))] })), activeTab === 'expense' && (_jsx("div", { className: "animate-in fade-in slide-in-from-bottom-4", children: _jsx(ExpenseManager, {}) })), activeTab === 'photo' && (_jsx("div", { className: "grid grid-cols-3 gap-1.5 animate-in fade-in", children: [1, 2, 3, 4, 5, 6].map((i) => (_jsxs("div", { className: "aspect-square bg-gray-200 rounded-xl overflow-hidden relative group border border-white", children: [_jsx("div", { className: "absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" }), _jsx("img", { src: `https://picsum.photos/seed/${i + 10}/400/400`, alt: "Tour photo", className: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" })] }, i))) })), activeTab === 'settings' && (_jsxs("div", { className: "p-8 text-center bg-white rounded-3xl border border-dashed border-gray-200 animate-in zoom-in-95", children: [_jsx(Settings, { className: "w-12 h-12 text-gray-300 mx-auto mb-4" }), _jsx("p", { className: "text-gray-500 font-medium", children: "T\u00EDnh n\u0103ng qu\u1EA3n l\u00FD th\u00E0nh vi\u00EAn \u0111ang \u0111\u01B0\u1EE3c c\u1EADp nh\u1EADt..." })] }))] })] }), _jsx("div", { className: "fixed bottom-6 left-1/2 -translate-x-1/2 z-40", children: _jsx("button", { onClick: () => {
setTargetLegId(null);
if (activeTab === 'plan')
setIsAddLocationOpen(true);
+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