Refactor giai đoạn 1: test các tính năng vừa thay đổi như tour, scene...

This commit is contained in:
2026-06-10 21:58:45 +07:00
parent 3f1b31b233
commit 358a98b21b
31 changed files with 1391 additions and 638 deletions
+48 -3
View File
@@ -772,6 +772,32 @@ html, body {
z-index: 1000;
}
.processing-overlay {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
font-size: 10px;
border-radius: 5px;
text-align: center;
}
.processing-overlay .spinner-icon {
font-size: 18px;
margin-bottom: 4px;
display: inline-block;
animation: fa-spin 2s linear infinite;
}
@keyframes fa-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.scene-callout img {
width: 100%;
height: 100%;
@@ -987,6 +1013,7 @@ html, body {
border: 1px solid rgba(255, 255, 255, 0.1);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
display: flex;
flex-direction: column;
justify-content: flex-end;
@@ -997,12 +1024,14 @@ html, body {
.scene-card:hover {
transform: translateY(-5px);
border-color: rgba(0, 123, 255, 0.5);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.5);
}
.scene-card-overlay {
padding: 15px;
background: linear-gradient(to top, rgba(0,0,0,0.95) 20%, rgba(0,0,0,0.6) 70%, transparent 100%);
background: linear-gradient(to top, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.7) 60%, rgba(0,0,0,0.2) 90%, transparent 100%);
color: #fff;
width: 100%;
}
.scene-card-info strong {
@@ -1010,6 +1039,7 @@ html, body {
font-size: 16px;
margin-bottom: 4px;
color: #fff;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
}
.scene-card-info .scene-desc {
@@ -1020,17 +1050,32 @@ html, body {
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}
.scene-card-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
gap: 12px;
font-size: 11px;
color: #aaa;
color: #eee;
margin-bottom: 10px;
}
.scene-card-meta span {
display: flex;
align-items: center;
gap: 4px;
background: rgba(0, 0, 0, 0.3);
padding: 2px 6px;
border-radius: 4px;
}
.tour-card {
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
/* --- Edit Metadata Modal (Dark Theme) --- */
#edit-scene-metadata-modal .modal-content {
background: rgba(30, 30, 30, 0.95);
+40
View File
@@ -208,6 +208,45 @@
</div>
</div>
<!-- Modal for Creating Tour -->
<div id="create-tour-modal" class="modal">
<div class="modal-content">
<span class="close-btn" onclick="closeTourModal()">&times;</span>
<h2 id="create-tour-modal-title">Tạo Tour 3D mới</h2>
<form id="create-tour-form" onsubmit="submitTour(event)">
<input type="hidden" id="tour-id">
<div class="form-group">
<label>Vị trí tọa độ:</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="tour-lat" readonly>
<input type="text" id="tour-lng" readonly>
</div>
</div>
<div class="form-group">
<label for="tour-name">Tên Tour:</label>
<input type="text" id="tour-name" required placeholder="Ví dụ: Tour tham quan văn phòng">
</div>
<div class="form-group">
<label for="tour-description">Mô tả Tour:</label>
<textarea id="tour-description" rows="3" placeholder="Mô tả ngắn gọn về tour này..."></textarea>
</div>
<div class="form-group">
<label for="tour-privacy">Quyền riêng tư:</label>
<select id="tour-privacy">
<option value="public">Công khai (Mọi người)</option>
<option value="private">Riêng tư (Chỉ mình tôi)</option>
<option value="member">Thành viên (Cần đăng nhập)</option>
<option value="shared">Chia sẻ (Qua link)</option>
</select>
</div>
<div class="modal-footer" style="padding: 0; border: none; background: transparent;">
<button type="button" class="cancel-btn" onclick="closeTourModal()">Hủy bỏ</button>
<button type="submit" class="save-btn">Tạo Tour & Tiếp tục</button>
</div>
</form>
</div>
</div>
<!-- Modal for Creating Scene -->
<div id="create-scene-modal" class="modal">
<div class="modal-content">
@@ -216,6 +255,7 @@
<form id="create-scene-form" onsubmit="submitScene(event)">
<!-- Hidden field for editing existing scene -->
<input type="hidden" id="modal-scene-id" name="sceneId">
<input type="hidden" id="modal-tour-id" name="tourId">
<div class="form-group">
<label>Selected Coordinates:</label>
<div style="display: flex; gap: 10px;">
+287 -76
View File
@@ -12,6 +12,7 @@ let returnToDashboardAfterEdit = false;
let assetIdToDelete = null;
let sceneIdToDelete = null;
let dashboardReturnTab = 'media-library';
let processingPollingInterval = null;
let editMiniMap = null;
let editMiniMapMarker = null;
let currentEditingScene = null; // Lưu object scene đang sửa để quản lý chia sẻ
@@ -55,8 +56,10 @@ document.addEventListener('DOMContentLoaded', () => {
// Đảm bảo map đã sẵn sàng trước khi nạp data
if (map) {
// Chỉ nạp danh sách Scene để vẽ marker lên bản đồ
loadScenes();
// Nạp marker kèm theo token từ URL nếu có (dành cho Guest xem tour shared)
const urlParams = new URLSearchParams(window.location.search);
const urlToken = urlParams.get('token');
loadScenes(urlToken);
}
} catch (error) {
console.error("Ứng dụng không thể khởi tạo:", error);
@@ -93,10 +96,11 @@ function applySystemSettings() {
logout: "Đăng xuất",
dashboardTitle: "Bảng điều khiển người dùng",
tabProfile: "Hồ sơ",
tabScenes: "Quản lí scene",
tabScenes: "Quản lí tour",
tabMedia: "Quản lí ảnh và media",
tabUsers: "Quản lí người dùng",
tabSystem: "Cài đặt hệ thống"
tabSystem: "Cài đặt hệ thống",
btnRecalculate: "Tính toán lại vị trí Tour"
},
en: {
brand: "Virtual 3D Tour Map",
@@ -105,10 +109,11 @@ function applySystemSettings() {
logout: "Logout",
dashboardTitle: "User Dashboard",
tabProfile: "Profile",
tabScenes: "My Scenes",
tabScenes: "My Tours",
tabMedia: "Media Library",
tabUsers: "User Management",
tabSystem: "System Settings"
tabSystem: "System Settings",
btnRecalculate: "Recalculate Tour Centers"
}
};
@@ -137,6 +142,9 @@ function applySystemSettings() {
const logoutBtn = document.querySelector('button[onclick="handleLogout()"]');
if (logoutBtn) logoutBtn.innerText = t.logout;
const recalculateBtn = document.getElementById('btn-recalculate-tours');
if (recalculateBtn) recalculateBtn.innerText = t.btnRecalculate;
}
/**
@@ -442,11 +450,11 @@ function initMap() {
}
// Cho phép bất kỳ người dùng nào đã đăng nhập tạo Scene mới trên bản đồ
const token = localStorage.getItem('jwt');
const token = localStorage.getItem('jwt'); // Kiểm tra token để đảm bảo người dùng đã đăng nhập
if (!token) return;
const { lat, lng } = e.latlng;
openCreateSceneModal(lat, lng);
openCreateTourModal(lat, lng); // Mở modal tạo Tour thay vì Scene
});
}
@@ -671,29 +679,150 @@ function handleLogout() {
}
/**
* Opens Modal for creating a Scene and sets lat/lng inputs
* Mở Modal để tạo Tour mới và điền sẵn tọa độ
*/
function openCreateSceneModal(lat, lng) {
function openCreateTourModal(lat, lng) {
const token = localStorage.getItem('jwt');
if (!token) {
showNotification('Vui lòng đăng nhập trước để tạo Tour.', 'error');
return;
}
// Đặt marker tạm thời trên bản đồ
if (tempMarker) map.removeLayer(tempMarker);
tempMarker = L.marker([lat, lng]).addTo(map);
const tourIdInput = document.getElementById('tour-id');
if (tourIdInput) tourIdInput.value = '';
document.getElementById('create-tour-modal').style.display = 'flex';
document.getElementById('tour-name').value = '';
document.getElementById('tour-description').value = '';
document.getElementById('tour-privacy').value = 'private';
document.getElementById('tour-lat').value = lat.toFixed(6);
document.getElementById('tour-lng').value = lng.toFixed(6);
const lang = systemSettings.language || 'vi';
const modalTitle = document.getElementById('create-tour-modal-title');
if (modalTitle) modalTitle.innerText = lang === 'vi' ? "Tạo Tour 3D mới" : "Create New 3D Tour";
}
/**
* Đóng Modal tạo Tour
*/
function closeTourModal() {
document.getElementById('create-tour-modal').style.display = 'none';
if (tempMarker) {
map.removeLayer(tempMarker);
tempMarker = null;
}
document.getElementById('create-tour-form').reset();
if (returnToDashboardAfterEdit) {
const targetTab = dashboardReturnTab;
returnToDashboardAfterEdit = false;
openDashboard();
openDashboardTab(targetTab);
}
}
/**
* Mở Modal để chỉnh sửa thông tin Tour
*/
function openEditTourModal(tour) {
const token = localStorage.getItem('jwt');
if (!token) return;
dashboardReturnTab = 'my-scenes';
returnToDashboardAfterEdit = true;
closeDashboard();
const tourIdInput = document.getElementById('tour-id');
if (tourIdInput) tourIdInput.value = tour._id;
document.getElementById('create-tour-modal').style.display = 'flex';
document.getElementById('tour-name').value = tour.name || '';
document.getElementById('tour-description').value = tour.description || '';
document.getElementById('tour-privacy').value = tour.privacy || 'private';
document.getElementById('tour-lat').value = (tour.location?.lat || 0).toFixed(6);
document.getElementById('tour-lng').value = (tour.location?.lng || 0).toFixed(6);
const lang = systemSettings.language || 'vi';
const modalTitle = document.getElementById('create-tour-modal-title');
if (modalTitle) modalTitle.innerText = lang === 'vi' ? "Chỉnh sửa Tour" : "Edit Tour";
}
/**
* Gửi dữ liệu tạo Tour lên Backend
*/
async function submitTour(e) {
e.preventDefault();
const token = localStorage.getItem('jwt');
const tourId = document.getElementById('tour-id')?.value;
const name = document.getElementById('tour-name').value.trim();
const description = document.getElementById('tour-description').value.trim();
const privacy = document.getElementById('tour-privacy').value;
const lat = document.getElementById('tour-lat').value;
const lng = document.getElementById('tour-lng').value;
const url = tourId ? `${API_BASE_URL}/tours/${tourId}` : `${API_BASE_URL}/tours`;
const method = tourId ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ name, description, privacy, lat, lng })
});
const data = await res.json();
if (!res.ok) throw new Error(data.message);
// 1. Đóng modal Tour ngay lập tức để giải phóng giao diện
closeTourModal();
// 2. Hiển thị thông báo
showNotification(tourId ? 'Tour đã được cập nhật thành công!' : 'Tour đã được tạo thành công!', 'success');
// 3. Làm mới bản đồ để cập nhật các marker (đặc biệt quan trọng khi di chuyển vị trí Tour)
loadScenes();
if (!tourId) {
// Nếu là Tour mới, tự động chuyển sang Modal tạo Scene để upload ảnh
openCreateSceneModal(Number(lat), Number(lng), data.tour._id); // Mở modal tạo Scene, truyền tourId
} else {
// Nếu đang trong Dashboard, cập nhật lại danh sách hiển thị
if (document.getElementById('dashboard-overlay').style.display === 'flex') {
loadMyTours();
}
}
} catch (err) {
showNotification("Lỗi: " + err.message, 'error');
}
}
/**
* Opens Modal for creating a Scene and sets lat/lng inputs
* @param {number} lat - Vĩ độ
* @param {number} lng - Kinh độ
* @param {string|null} tourId - ID của Tour cha (nếu có)
*/
function openCreateSceneModal(lat, lng, tourId = null) {
returnToDashboardAfterEdit = false;
const token = localStorage.getItem('jwt');
if (!token) {
showNotification('Please log in first to create a 3D scene.', 'error');
return;
}
// Place a temporary marker on the map
if (tempMarker) map.removeLayer(tempMarker);
tempMarker = L.marker([lat, lng]).addTo(map);
document.getElementById('create-scene-modal').style.display = 'flex';
document.getElementById('modal-scene-id').value = '';
document.getElementById('modal-lat').value = lat.toFixed(6);
document.getElementById('modal-lng').value = lng.toFixed(6);
// [FIX] Đảm bảo xóa tourId cũ khi tạo từ Map để Scene này trở thành Tour Gốc (Root)
const tourIdInput = document.getElementById('modal-tour-id');
if (tourIdInput) tourIdInput.value = '';
localStorage.removeItem('activeTourId');
if (tourIdInput) tourIdInput.value = tourId || ''; // Điền tourId nếu có
if (tourId) localStorage.setItem('activeTourId', tourId); // Lưu tourId vào localStorage
else localStorage.removeItem('activeTourId'); // Xóa nếu không có tourId (tạo cảnh độc lập)
const lang = systemSettings.language || 'vi';
const modalTitle = document.getElementById('create-scene-modal-title');
@@ -704,6 +833,7 @@ function openCreateSceneModal(lat, lng) {
* Closes the Create Scene Modal and removes temporary marker
*/
function closeModal() {
document.getElementById('create-tour-modal').style.display = 'none'; // Đảm bảo đóng cả modal Tour
document.getElementById('create-scene-modal').style.display = 'none';
if (tempMarker) {
map.removeLayer(tempMarker);
@@ -746,6 +876,7 @@ async function submitScene(e) {
const url = sceneId ? `${API_BASE_URL}/scenes/${sceneId}` : `${API_BASE_URL}/scenes`;
const method = sceneId ? 'PUT' : 'POST';
formData.append('tourId', document.getElementById('modal-tour-id').value); // Đảm bảo tourId được gửi
uploadWithProgress(url, method, formData, token, 'create', () => {
showNotification(sceneId ? 'Scene đang được cập nhật ngầm!' : 'Scene đã được tạo! Ảnh đang được xử lý 8K...', 'success');
closeModal();
@@ -801,8 +932,9 @@ function uploadWithProgress(url, method, formData, token, prefix, callback) {
/**
* Loads and displays visible Scenes on the map
* @param {string|null} urlToken - Token từ URL chia sẻ (nếu có)
*/
async function loadScenes() {
async function loadScenes(urlToken = null) {
try {
const token = localStorage.getItem('jwt');
const headers = {};
@@ -810,10 +942,10 @@ async function loadScenes() {
headers['Authorization'] = `Bearer ${token}`;
}
// Thêm timestamp để tránh lỗi 304 hang do cache trình duyệt
const timestamp = new Date().getTime();
console.log(`3.1 Đang gửi yêu cầu lấy danh sách Scene (ts: ${timestamp})...`);
const response = await fetch(`${API_BASE_URL}/scenes?_=${timestamp}`, {
let url = `${API_BASE_URL}/scenes?_=${new Date().getTime()}`;
if (urlToken) url += `&token=${urlToken}`;
const response = await fetch(url, {
method: 'GET',
headers
});
@@ -829,6 +961,7 @@ async function loadScenes() {
markerClusterGroup.clearLayers();
const markersToAdd = [];
const activeSceneId = localStorage.getItem('activeSceneId');
let foundProcessing = 0;
const seenCoordinates = new Set(); // Dùng để lọc "Ảnh mẹ" (1 marker per location)
// Chỉ lặp qua danh sách Scene mẹ, lọc bỏ các hotspots trùng tọa độ
@@ -853,16 +986,28 @@ async function loadScenes() {
const sceneName = scene.name || scene.title || "Untitled Scene";
let thumbUrl = `${API_BASE_URL}/assets/view/${assetId}`;
if (token) thumbUrl += `?token=${token}`;
else if (scene.privacy === 'shared' && scene.shareToken) thumbUrl += `?token=${scene.shareToken}`;
const isProcessing = scene.status === 'processing';
if (isProcessing) foundProcessing++;
let thumbHtml = '';
if (isProcessing) {
thumbHtml = `<div class="processing-overlay">
<div class="spinner-icon">⏳</div>
<div style="font-size: 8px;">Đang nén 8K</div>
</div>`;
} else {
let thumbUrl = `${API_BASE_URL}/assets/view/${assetId}`;
if (token) thumbUrl += `?token=${token}`;
else if (scene.privacy === 'shared' && scene.shareToken) thumbUrl += `?token=${scene.shareToken}`;
thumbHtml = `<img src="${thumbUrl}" alt="${sceneName}">`;
}
const calloutIcon = L.divIcon({
className: 'custom-scene-marker',
className: `custom-scene-marker ${isProcessing ? 'is-processing' : ''}`,
html: `
<div class="scene-callout">
<div class="scene-img-wrapper">
<img src="${thumbUrl}" alt="${sceneName}">
${thumbHtml}
</div>
</div>`,
iconSize: [64, 64],
@@ -927,6 +1072,18 @@ async function loadScenes() {
// Thêm danh sách marker đã lọc vào group
markerClusterGroup.addLayers(markersToAdd);
// Quản lý việc tự động cập nhật bản đồ khi có scene đang xử lý
if (foundProcessing > 0) {
if (!processingPollingInterval) {
processingPollingInterval = setInterval(() => loadScenes(), 5000);
}
} else {
if (processingPollingInterval) {
clearInterval(processingPollingInterval);
processingPollingInterval = null;
}
}
} catch (error) {
console.error('Error loading scenes:', error);
}
@@ -1061,7 +1218,7 @@ window.confirmDeleteScene = async function() {
loadScenes();
if (document.getElementById('tab-my-scenes').classList.contains('active')) {
loadMyScenes();
loadMyTours();
}
} catch (error) {
showNotification("Lỗi khi xóa: " + error.message, 'error');
@@ -1103,6 +1260,16 @@ async function openScene(sceneId, privacy, shareToken, force = false, initialPit
const scene = await sceneRes.json();
const hotspots = await hotspotsRes.json();
// Ngăn chặn mở Scene nếu ảnh chưa xử lý xong hoặc lỗi
if (scene.status === 'processing') {
showNotification("Cảnh này đang được nén chất lượng 8K. Vui lòng quay lại sau vài giây.", 'warning');
return;
}
if (scene.status === 'failed') {
showNotification("Lỗi xử lý ảnh 8K. Vui lòng upload lại ảnh cho cảnh này.", 'error');
return;
}
// [TOUR ID] Luôn cập nhật activeTourId theo Scene hiện tại để đảm bảo các cảnh con/hotspot mới
// được gán đúng vào Tour gốc của cảnh đang xem, tránh sử dụng ID cũ/lỗi từ phiên trước.
@@ -1583,83 +1750,96 @@ function closeDashboard() {
}
/**
* Tải danh sách scene của chính người dùng đăng nhập
* Tải danh sách Tour của người dùng hoặc các Tour họ có quyền truy cập
*/
async function loadMyScenes() {
async function loadMyTours() {
const token = localStorage.getItem('jwt');
const listContainer = document.getElementById('my-scenes-list');
// Chuyển sang grid để đồng bộ với media library
listContainer.className = 'dashboard-grid';
listContainer.innerHTML = '<p>Đang tải danh sách...</p>';
try {
const res = await fetch(`${API_BASE_URL}/me/scenes`, {
const res = await fetch(`${API_BASE_URL}/tours`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const scenes = await res.json();
if (!res.ok) throw new Error(scenes.message);
const tours = await res.json();
if (!res.ok) throw new Error(tours.message);
listContainer.innerHTML = '';
if (scenes.length === 0) {
listContainer.innerHTML = '<p>Bạn chưa tạo scene nào.</p>';
if (tours.length === 0) {
listContainer.innerHTML = '<p>Bạn chưa có Tour nào. Hãy tạo một Tour mới từ bản đồ!</p>';
return;
}
scenes.forEach(scene => {
const assetId = scene.assetId?._id || scene.assetId;
let thumbUrl = `${API_BASE_URL}/assets/view/${assetId}`;
if (token) thumbUrl += `?token=${token}`;
tours.forEach(tour => {
const card = document.createElement('div');
card.className = 'scene-card';
card.style.backgroundImage = `url('${thumbUrl}')`;
card.className = 'scene-card tour-card';
// Logic hiển thị badge trạng thái
let statusBadge = '';
if (scene.status === 'processing') {
statusBadge = '<span class="status-badge processing">⏳ Đang xử lý 8K...</span>';
} else if (scene.status === 'failed') {
statusBadge = '<span class="status-badge failed">❌ Lỗi xử lý</span>';
// Hiển thị thumbnail dựa trên rootSceneId (đã được populate assetId từ backend)
const rootScene = tour.rootSceneId;
const assetId = rootScene ? (rootScene.assetId?._id || rootScene.assetId) : null;
if (assetId) {
let thumbUrl = `${API_BASE_URL}/assets/view/${assetId}`;
if (token) thumbUrl += `?token=${token}`;
card.style.backgroundImage = `url('${thumbUrl}')`;
} else {
card.style.backgroundColor = '#1a1a1a';
}
card.style.borderLeft = `5px solid ${tour.privacy === 'public' ? '#28a745' : '#ffc107'}`;
card.innerHTML = `
<div class="scene-card-overlay">
<div class="scene-card-info">
<strong>${scene.name || scene.title}</strong>
<p class="scene-desc">${scene.description || 'Không có mô tả'}</p>
<strong style="color:#00d4ff;"><i class="fas fa-route"></i> ${tour.name}</strong>
<p class="scene-desc">${tour.description || 'Không có mô tả cho tour này'}</p>
<div class="scene-card-meta">
<span>🔒 ${scene.privacy}</span>
<span>👤 ${scene.createdBy?.username || 'Bạn'}</span>
<span>📅 ${formatSystemDate(scene.createdAt)}</span>
<span>👁️ ${scene.views || 0} lượt xem</span>
<span>🔒 ${tour.privacy.toUpperCase()}</span>
<span>👤 ${tour.createdBy?.username || 'N/A'}</span>
<span>🖼️ ${tour.scenes?.length || 0} cảnh</span>
<span>📅 ${formatSystemDate(tour.createdAt)}</span>
</div>
${statusBadge}
</div>
<div class="media-actions" style="border: none; padding: 0;">
<button class="edit-btn-small" id="edit-scene-${scene._id}" ${scene.status === 'processing' ? 'disabled style="opacity:0.5; cursor:not-allowed;"' : ''}>Sửa</button>
<button class="delete-btn-small" id="delete-scene-${scene._id}">Xóa</button>
<button class="edit-btn-small" id="view-stats-${scene._id}" style="background: #6f42c1;">Thống kê</button>
<button class="edit-btn-small" id="edit-tour-${tour._id}" style="background:#007bff">Sửa</button>
<button class="delete-btn-small" id="delete-tour-${tour._id}">Xóa</button>
<button class="edit-btn-small" id="view-tour-${tour._id}" style="background:#28a745">Xem</button>
</div>
</div>
`;
listContainer.appendChild(card);
// Xử lý nút Sửa: Logic đóng dashboard -> mở modal -> quay lại dashboard
document.getElementById(`edit-scene-${scene._id}`).onclick = () => {
dashboardReturnTab = 'my-scenes';
returnToDashboardAfterEdit = true;
// Nút Sửa Tour
document.getElementById(`edit-tour-${tour._id}`).onclick = () => {
openEditTourModal(tour);
};
// Nút Xóa Tour: Gọi API xóa Tour (bao gồm xóa cascade các scene bên trong)
document.getElementById(`delete-tour-${tour._id}`).onclick = async () => {
if (confirm(`Bạn có chắc muốn xóa Tour "${tour.name}" và toàn bộ ${tour.scenes?.length || 0} cảnh bên trong?`)) {
try {
const res = await fetch(`${API_BASE_URL}/tours/${tour._id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.ok) {
showNotification("Đã xóa Tour thành công", "success");
loadMyTours();
loadScenes();
}
} catch (e) { showNotification("Lỗi xóa tour", "error"); }
}
};
// Nút Xem Tour: Bay tới vị trí và mở cảnh khởi đầu
document.getElementById(`view-tour-${tour._id}`).onclick = () => {
closeDashboard();
openEditMetadataModal(scene, scene.isChildScene);
};
// Xử lý nút Xóa
document.getElementById(`delete-scene-${scene._id}`).onclick = async () => {
await deleteScene(scene._id, scene); // Truyền đối tượng scene đầy đủ
};
// Xử lý nút Thống kê
document.getElementById(`view-stats-${scene._id}`).onclick = () => {
showViewStatsModal(scene._id, scene.name || scene.title);
if (tour.location) {
map.flyTo([tour.location.lat, tour.location.lng], 16);
}
if (tour.rootSceneId) {
openScene(tour.rootSceneId, tour.privacy, tour.shareToken);
}
};
});
} catch (e) {
@@ -2415,6 +2595,30 @@ async function updateSystemSettings(e) {
}
}
/**
* Admin: Kích hoạt tính năng tính toán lại trung tâm cho toàn bộ Tour
*/
window.recalculateAllTourCenters = async function() {
const token = localStorage.getItem('jwt');
if (!confirm("Bạn có chắc chắn muốn tính toán lại tọa độ trung tâm cho TOÀN BỘ Tour trong hệ thống? Việc này có thể mất một chút thời gian nếu dữ liệu lớn.")) return;
try {
showNotification("Đang xử lý tính toán lại...", "success");
const res = await fetch(`${API_BASE_URL}/tours/recalculate-all`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
const data = await res.json();
if (!res.ok) throw new Error(data.message);
showSuccessModal(data.message);
} catch (e) {
showNotification("Lỗi thực hiện: " + e.message, 'error');
}
};
let viewStatsChartInstance = null; // Biến để lưu instance của Chart.js
/**
@@ -2517,7 +2721,7 @@ function openDashboardTab(tabName) {
updateProfileTabContent();
}
if (tabName === 'my-scenes') {
loadMyScenes();
loadMyTours();
}
if (tabName === 'media-library') {
loadMediaStats();
@@ -2526,6 +2730,13 @@ function openDashboardTab(tabName) {
if (tabName === 'user-management') {
loadAdminUsers();
}
if (tabName === 'system-settings') {
// Cập nhật giá trị hiện tại vào form cấu hình hệ thống
const tzInput = document.getElementById('sys-timezone');
const langInput = document.getElementById('sys-language');
if (tzInput) tzInput.value = systemSettings.timezone || 'Asia/Ho_Chi_Minh';
if (langInput) langInput.value = systemSettings.language || 'vi';
}
}
// Đánh dấu nút tab được chọn là active