const API_BASE_URL = '/api'; // Sử dụng đường dẫn tương đối để tránh lỗi CORS/Hostname
let map;
let tempMarker = null;
let markerClusterGroup;
let currentSceneId = null;
let previousSceneId = null;
let miniMap = null;
let miniMapMarker = null;
let systemSettings = { timezone: 'Asia/Ho_Chi_Minh', language: 'vi' };
let returnToDashboardAfterEdit = false;
let assetIdToDelete = null;
let sceneIdToDelete = null;
let dashboardReturnTab = 'media-library';
let editMiniMap = null;
let editMiniMapMarker = null;
let currentEditingScene = null; // Lưu object scene đang sửa để quản lý chia sẻ
let sharedUsersData = []; // [{id, username, email}]
let sharedEmailsData = []; // [email]
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
try {
console.log("--- Bắt đầu khởi tạo Frontend ---");
// 0. Kiểm tra tham số URL để truy cập trực tiếp
const urlParams = new URLSearchParams(window.location.search);
const urlSceneId = urlParams.get('sceneId');
const urlToken = urlParams.get('token');
// Ưu tiên nạp cấu hình hệ thống trước
fetchSystemSettings();
if (document.getElementById('map')) {
console.log("1. Đang khởi tạo bản đồ Leaflet...");
initMap();
}
// Chạy tuần tự để tránh xung đột luồng xử lý
checkAuthStatus(); // 2. Kiểm tra đăng nhập
// 3. Xử lý logic vào thẳng Scene hoặc khôi phục trang
if (urlSceneId) {
console.log(`[Direct Access] Opening scene ${urlSceneId} from URL`);
openScene(urlSceneId, urlToken ? 'shared' : null, urlToken);
} else {
restoreActiveScene();
}
// Đả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();
}
} catch (error) {
console.error("Ứng dụng không thể khởi tạo:", error);
}
});
/**
* Lấy cấu hình hệ thống từ Backend
*/
async function fetchSystemSettings() {
try {
const res = await fetch(`${API_BASE_URL}/system/settings`);
if (res.ok) {
systemSettings = await res.json();
applySystemSettings();
}
} catch (e) {
console.warn("Không thể nạp cấu hình hệ thống, dùng mặc định.");
}
}
/**
* Áp dụng cấu hình Múi giờ và Ngôn ngữ vào UI
*/
function applySystemSettings() {
console.log(`[System] Applying settings: Language=${systemSettings.language}, Timezone=${systemSettings.timezone}`);
// 1. Cập nhật nhãn (Labels) dựa trên ngôn ngữ
const translations = {
vi: {
brand: "Bản đồ Tour 3D Ảo",
login: "Đăng nhập / Đăng ký",
profile: "Quản lý hồ sơ",
logout: "Đăng xuất",
dashboardTitle: "Bảng điều khiển người dùng",
tabProfile: "Hồ sơ",
tabScenes: "Quản lí scene",
tabMedia: "Quản lí ảnh và media",
tabUsers: "Quản lí người dùng",
tabSystem: "Cài đặt hệ thống"
},
en: {
brand: "Virtual 3D Tour Map",
login: "Login / Register",
profile: "Manage Profile",
logout: "Logout",
dashboardTitle: "User Dashboard",
tabProfile: "Profile",
tabScenes: "My Scenes",
tabMedia: "Media Library",
tabUsers: "User Management",
tabSystem: "System Settings"
}
};
const lang = systemSettings.language || 'vi';
const t = translations[lang];
// Cập nhật các phần tử cố định
const brandH1 = document.querySelector('.app-brand h1');
if (brandH1) brandH1.innerText = t.brand;
const dashboardH2 = document.querySelector('.dashboard-content h2');
if (dashboardH2) dashboardH2.innerText = t.dashboardTitle;
// Cập nhật các nút Tab
const tabButtons = document.querySelectorAll('.dashboard-tabs .tab-btn');
if (tabButtons.length >= 3) {
tabButtons[0].innerText = t.tabProfile;
tabButtons[1].innerText = t.tabScenes;
tabButtons[2].innerText = t.tabMedia;
if (tabButtons[3]) tabButtons[3].innerText = t.tabUsers;
if (tabButtons[4]) tabButtons[4].innerText = t.tabSystem;
}
const profileBtn = document.querySelector('button[onclick="openDashboard()"]');
if (profileBtn) profileBtn.innerText = t.profile;
const logoutBtn = document.querySelector('button[onclick="handleLogout()"]');
if (logoutBtn) logoutBtn.innerText = t.logout;
}
/**
* Hàm định dạng dung lượng file cho Frontend
*/
function formatBytes(bytes, decimals = 2) {
if (!bytes || bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
/**
* Tải và hiển thị thống kê các tệp tin lớn nhất
*/
async function loadMediaStats() {
const token = localStorage.getItem('jwt');
const statsContainer = document.getElementById('media-library-stats');
if (!statsContainer) return;
try {
const res = await fetch(`${API_BASE_URL}/me/assets/top-large`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const topFiles = await res.json();
if (topFiles && topFiles.length > 0) {
let html = `
`,
iconSize: [64, 64],
iconAnchor: [32, 76] // Căn giữa ngang, đáy mũi tên tại tọa độ lat/lng
});
const marker = L.marker([latNum, lngNum], {
icon: calloutIcon,
title: sceneName
});
// Tạo nội dung thông tin khi Hover (Tooltip)
const createdDate = formatSystemDate(scene.assetId?.createdAt);
const tooltipContent = `
${sceneName}
${scene.description ? `${scene.description} ` : ''}
Người tạo: ${scene.createdBy ? scene.createdBy.username : 'Ẩn danh'} Ngày tạo: ${createdDate}
`;
// Gán Tooltip cho sự kiện Hover
marker.bindTooltip(tooltipContent, {
direction: 'top',
offset: [0, -70],
className: 'custom-scene-tooltip'
});
// Sự kiện Click chuột trái: Vào thẳng trình xem 360
marker.on('click', () => {
openScene(scene._id, scene.privacy, scene.shareToken || '');
});
marker.on('contextmenu', (e) => {
if (e.originalEvent) {
L.DomEvent.stop(e.originalEvent);
}
const currentUserId = localStorage.getItem('userId');
const userRole = localStorage.getItem('role');
// Hỗ trợ cả schema cũ (owner) và mới (createdBy)
const ownerId = scene.createdBy?._id || scene.createdBy || scene.owner?._id || scene.owner;
// Phân quyền: Admin hoặc Chủ sở hữu Scene
const isAdmin = userRole === 'admin' || userRole === 'Chủ sở hữu';
const isOwner = currentUserId && ownerId && ownerId.toString() === currentUserId.toString();
if (isAdmin || isOwner) {
handleEditDeleteScene(scene);
} else if (scene.privacy === 'public') {
// Cho phép bất kỳ ai (kể cả khách) lấy link chia sẻ của scene công khai
showShareLink(scene);
} else {
showNotification("Bạn không có quyền chỉnh sửa scene này.", 'warning');
}
});
markersToAdd.push(marker);
});
// Thêm danh sách marker đã lọc vào group
markerClusterGroup.addLayers(markersToAdd);
} catch (error) {
console.error('Error loading scenes:', error);
}
}
/**
* Handles Edit/Delete options for a scene
*/
async function handleEditDeleteScene(scene) {
const modal = document.getElementById('action-choice-modal');
const title = document.getElementById('action-modal-title');
const editBtn = document.getElementById('btn-edit-action');
const editPrivacyBtn = document.getElementById('btn-edit-privacy-action');
const deleteBtn = document.getElementById('btn-delete-action');
const shareBtn = document.getElementById('btn-share-action');
title.innerText = `Scene: ${scene.title}`;
modal.style.display = 'flex';
// Hành động Lấy link chia sẻ trực tiếp
shareBtn.onclick = () => {
closeActionModal();
showShareLink(scene);
};
// Hành động Chỉnh sửa privacy
editPrivacyBtn.onclick = () => {
returnToDashboardAfterEdit = false;
closeActionModal();
// Sử dụng thuộc tính isChildScene từ backend để quyết định quyền chỉnh sửa
openEditMetadataModal(scene, scene.isChildScene);
};
// Gán sự kiện cho nút Sửa
editBtn.onclick = () => {
returnToDashboardAfterEdit = false;
closeActionModal();
openEditMetadataModal(scene, scene.isChildScene);
};
// Gán sự kiện cho nút Xóa
deleteBtn.onclick = () => {
returnToDashboardAfterEdit = false; // Đảm bảo không mở dashboard nếu xóa từ map
closeActionModal();
deleteScene(scene._id);
};
}
/**
* Closes the Action Choice Modal
*/
function closeActionModal() {
document.getElementById('action-choice-modal').style.display = 'none';
}
/**
* Mở modal xác nhận xóa scene
*/
window.deleteScene = async function(sceneId, sceneData = null) { // Thêm sceneData để tránh fetch lại
sceneIdToDelete = sceneId;
// Tạm thời đóng dashboard và lưu trạng thái để mở lại sau
dashboardReturnTab = 'my-scenes';
returnToDashboardAfterEdit = true;
closeDashboard();
const confirmModal = document.getElementById('delete-scene-confirm-modal');
const confirmMessageElem = document.getElementById('delete-scene-confirm-message'); // Giả định có element này trong HTML
if (!confirmModal || !confirmMessageElem) {
console.error("Delete confirmation modal elements not found.");
return;
}
const token = localStorage.getItem('jwt');
if (!token) {
showNotification('Vui lòng đăng nhập để thực hiện thao tác này.', 'warning');
return;
}
let sceneToConfirm = sceneData;
if (!sceneToConfirm) {
try {
// Fetch scene details nếu chưa có sẵn (ví dụ: xóa từ bản đồ)
const response = await fetch(`${API_BASE_URL}/scenes/${sceneId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
sceneToConfirm = await response.json();
if (!response.ok) throw new Error(sceneToConfirm.message || 'Failed to fetch scene details for deletion.');
} catch (error) {
showNotification("Không thể chuẩn bị xóa scene: " + error.message, 'error');
return;
}
}
let message = '';
if (sceneToConfirm.isChildScene) {
message = `Bạn đang xóa cảnh "${sceneToConfirm.name || sceneToConfirm.title}". Cảnh này là một phần của tour khác. Việc xóa sẽ chỉ gỡ bỏ cảnh này và các liên kết đến nó. Các cảnh cha sẽ không bị ảnh hưởng. Bạn có chắc chắn muốn xóa?`;
} else {
message = `Bạn đang xóa cảnh "${sceneToConfirm.name || sceneToConfirm.title}". Cảnh này có thể là cảnh gốc hoặc cảnh không có liên kết đến. Việc xóa sẽ gỡ bỏ cảnh này VÀ TẤT CẢ CÁC CẢNH CON liên kết với nó trong tour. Thao tác này không thể hoàn tác. Bạn có chắc chắn muốn xóa?`;
}
confirmMessageElem.innerText = message;
confirmModal.style.display = 'flex';
};
window.closeDeleteSceneModal = function() {
document.getElementById('delete-scene-confirm-modal').style.display = 'none';
sceneIdToDelete = null;
if (returnToDashboardAfterEdit) {
const targetTab = dashboardReturnTab;
returnToDashboardAfterEdit = false;
openDashboard();
openDashboardTab(targetTab);
}
};
window.confirmDeleteScene = async function() {
if (!sceneIdToDelete) return;
const token = localStorage.getItem('jwt');
try {
const response = await fetch(`${API_BASE_URL}/scenes/${sceneIdToDelete}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
if (!response.ok) throw new Error(data.message || 'Failed to delete scene');
closeDeleteSceneModal();
showSuccessModal(data.message || 'Scene đã được xóa vĩnh viễn');
loadScenes();
if (document.getElementById('tab-my-scenes').classList.contains('active')) {
loadMyScenes();
}
} catch (error) {
showNotification("Lỗi khi xóa: " + error.message, 'error');
}
};
/**
* Fetches secure scene details and triggers the Panorama viewer
*/
async function openScene(sceneId, privacy, shareToken, force = false, initialPitch = 0, initialYaw = 0) {
// Nếu đang xem chính scene này và không yêu cầu làm mới (force), không cần nạp lại
if (!force && currentSceneId === sceneId && document.getElementById('viewer-container').style.display === 'block') {
return;
}
try {
const token = localStorage.getItem('jwt');
const headers = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
console.log(`[Viewer] Đang mở scene: ${sceneId}`);
let url = `${API_BASE_URL}/scenes/${sceneId}`;
if (privacy === 'shared' && shareToken) {
url += `?token=${shareToken}`;
}
// Lưu trạng thái Scene hiện tại để khôi phục sau khi reload trang
localStorage.setItem('activeSceneId', sceneId);
localStorage.setItem('activeScenePrivacy', privacy || '');
localStorage.setItem('activeSceneToken', shareToken || '');
// Nạp đồng thời Scene và danh sách Hotspots từ Collection riêng
const [sceneRes, hotspotsRes] = await Promise.all([
fetch(url, { method: 'GET', headers }),
fetch(`${API_BASE_URL}/hotspots/${sceneId}`, { method: 'GET', headers })
]);
const scene = await sceneRes.json();
const hotspots = await hotspotsRes.json();
console.log("DEBUG: Hotspots raw data from API:", hotspots);
if (!sceneRes.ok) throw new Error(scene.message || 'Failed to fetch scene details');
// Lấy ID người tạo (createdBy) để phân quyền chuột phải trong viewer
const sceneOwnerId = scene.createdBy?._id || scene.createdBy || scene.owner?._id || scene.owner;
// Tự động focus bản đồ vào vị trí của Scene
if (map) {
map.flyTo([scene.gps?.lat || scene.lat, scene.gps?.lng || scene.lng], 16);
}
// Cập nhật tọa độ vào các input ẩn để hỗ trợ GPS inheritance cho hotspot khi tải ảnh mới
document.getElementById('modal-lat').value = scene.gps?.lat || scene.lat;
document.getElementById('modal-lng').value = scene.gps?.lng || scene.lng;
// Cập nhật lịch sử di chuyển để hỗ trợ tạo hotspot ngược tự động
if (currentSceneId && currentSceneId !== sceneId) {
previousSceneId = currentSceneId;
}
currentSceneId = sceneId;
// Kiểm tra an toàn assetId (hỗ trợ cả dạng Object và String ID)
const assetId = scene.assetId?._id || scene.assetId;
if (!assetId) throw new Error("Dữ liệu hình ảnh của cảnh này bị lỗi hoặc chưa xử lý xong.");
let secureImageUrl = `${API_BASE_URL}/assets/view/${assetId}`;
// Ưu tiên JWT token nếu đang đăng nhập, nếu không thì dùng shareToken
if (token) {
secureImageUrl += `?token=${token}`;
} else if (privacy === 'shared' && scene.shareToken) {
secureImageUrl += `?token=${scene.shareToken}`;
}
// Initialize 3D Viewer with secure, referer-protected image stream
initPanoramaViewer(secureImageUrl, hotspots || [], sceneOwnerId, initialPitch, initialYaw);
// Sau khi mở thành công từ URL trực tiếp, xóa tham số để làm sạch thanh địa chỉ (URL chuyên nghiệp)
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('sceneId')) {
window.history.replaceState({}, document.title, "/");
}
} catch (error) {
if (typeof closeViewer === 'function') closeViewer();
localStorage.removeItem('activeSceneId');
localStorage.removeItem('activeScenePrivacy');
localStorage.removeItem('activeSceneToken');
// Kiểm tra nếu đang truy cập qua link trực tiếp (URL có sceneId) mà gặp lỗi (do xóa token hoặc token không hợp lệ)
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('sceneId')) {
showNotification("Bạn không có quyền truy cập hoặc liên kết chia sẻ đã hết hạn. Quay về bản đồ công cộng.", 'error');
// Xóa toàn bộ tham số URL và tải lại trang để làm mới trạng thái (về trang chủ dành cho khách)
window.history.replaceState({}, document.title, "/");
location.reload();
return;
}
showNotification(error.message, 'error');
}
}
/**
* Khôi phục Scene đang xem từ localStorage sau khi reload trang
*/
function restoreActiveScene() {
const savedSceneId = localStorage.getItem('activeSceneId');
if (savedSceneId) {
const savedPrivacy = localStorage.getItem('activeScenePrivacy');
const savedToken = localStorage.getItem('activeSceneToken');
const savedPitch = parseFloat(localStorage.getItem('activeScenePitch')) || 0;
const savedYaw = parseFloat(localStorage.getItem('activeSceneYaw')) || 0;
openScene(savedSceneId, savedPrivacy, savedToken, false, savedPitch, savedYaw);
}
}
/**
* Xử lý việc tạo hotspot sau khi click chuột phải trong trình xem 360
* @param {number} pitch - Tọa độ dọc (-90 đến 90)
* @param {number} yaw - Tọa độ ngang (-180 đến 180)
* @param {Object} existingHotspot - Thông tin hotspot cũ nếu có
*/
window.handleHotspotCreation = async function(pitch, yaw, existingHotspot = null) {
const token = localStorage.getItem('jwt');
if (!token) {
showNotification('Vui lòng đăng nhập để thực hiện thao tác này.', 'warning');
return;
}
const modal = document.getElementById('hotspot-modal');
const form = document.getElementById('hotspot-form');
// Hiển thị Modal TRƯỚC để các logic UI (như Mini Map) tính toán được kích thước
modal.style.display = 'flex';
// Reset form và gán tọa độ
form.reset();
document.getElementById('hs-pitch').value = pitch;
document.getElementById('hs-yaw').value = yaw;
document.getElementById('hs-id').value = existingHotspot ? existingHotspot._id : '';
document.getElementById('hotspot-modal-title').innerText = existingHotspot ? 'Cập nhật điểm điều hướng' : 'Thêm điểm điều hướng mới';
// Reset UI states
document.querySelector('input[name="hsLinkType"][value="existing"]').checked = true;
window.toggleHSLinkType('existing');
document.querySelector('input[name="hsGPSMode"][value="map"]').checked = true;
window.toggleHSGPSMode('map');
// Lấy danh sách Scene có sẵn để đổ vào dropdown
try {
const res = await fetch(`${API_BASE_URL}/scenes`, { headers: { 'Authorization': `Bearer ${token}` } });
const scenes = await res.json();
const select = document.getElementById('hs-target-id');
select.innerHTML = '';
scenes.forEach(s => {
if (s._id !== currentSceneId) { // Không liên kết tới chính nó
select.innerHTML += ``;
}
});
// QUAN TRỌNG: Chỉ điền dữ liệu hotspot cũ SAU KHI dropdown đã được nạp đầy đủ options
if (existingHotspot) {
document.getElementById('hs-title').value = existingHotspot.title || '';
document.getElementById('hs-desc').value = existingHotspot.description || '';
if (existingHotspot.target_scene_id) {
select.value = existingHotspot.target_scene_id;
}
}
} catch (e) { console.error("Lỗi nạp danh sách scene:", e); }
// Xử lý sự kiện submit form
form.onsubmit = async (e) => {
e.preventDefault();
const formData = new FormData(form);
const linkType = formData.get('hsLinkType');
if (linkType === 'upload') {
const file = document.getElementById('hs-panorama-file').files[0];
if (!file) {
showNotification('Vui lòng chọn ảnh panorama.', 'warning');
return;
}
const gpsMode = formData.get('hsGPSMode');
let lat = 0, lng = 0;
if (gpsMode === 'inherit') {
// Ép kiểu về Number khi lấy từ input
lat = Number(document.getElementById('modal-lat').value);
lng = Number(document.getElementById('modal-lng').value);
} else {
lat = Number(document.getElementById('hs-lat').value);
lng = Number(document.getElementById('hs-lng').value);
if (!lat || !lng) {
showNotification('Vui lòng chọn vị trí GPS.', 'warning');
return;
}
}
const sceneData = new FormData();
sceneData.append('panorama', file);
sceneData.append('title', formData.get('title'));
sceneData.append('lat', lat); // FormData sẽ convert sang string, Backend cần ép kiểu lại
sceneData.append('lng', lng);
sceneData.append('privacy', 'public');
uploadWithProgress(`${API_BASE_URL}/scenes`, 'POST', sceneData, token, 'hs', async (sceneRes) => {
await saveHotspotToDB(pitch, yaw, formData.get('title'), formData.get('description'), sceneRes.scene._id, existingHotspot?._id);
closeHotspotModal();
});
return;
}
const finalTargetId = formData.get('targetSceneId');
if (!finalTargetId) {
showNotification('Vui lòng chọn cảnh để liên kết.', 'warning');
return;
}
await saveHotspotToDB(pitch, yaw, formData.get('title'), formData.get('description'), finalTargetId, existingHotspot?._id);
modal.style.display = 'none';
};
};
/**
* Đóng Modal biên tập Hotspot
*/
function closeHotspotModal() {
document.getElementById('hotspot-modal').style.display = 'none';
}
/**
* Khởi tạo hoặc cập nhật Mini Map trong Hotspot Modal
*/
function initHSMiniMap() {
const pLat = parseFloat(document.getElementById('modal-lat').value) || 21.0285;
const pLng = parseFloat(document.getElementById('modal-lng').value) || 105.8542;
if (miniMap) {
miniMap.setView([pLat, pLng], 15);
updateHSMiniMapMarker(pLat, pLng);
// Fix lỗi vỡ tiles của Leaflet trong Modal
setTimeout(() => miniMap.invalidateSize(), 200);
return;
}
miniMap = L.map('hs-mini-map').setView([pLat, pLng], 15);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(miniMap);
miniMap.on('click', (e) => {
const { lat, lng } = e.latlng;
updateHSMiniMapMarker(lat, lng);
});
updateHSMiniMapMarker(pLat, pLng);
setTimeout(() => miniMap.invalidateSize(), 200);
}
function updateHSMiniMapMarker(lat, lng) {
if (miniMapMarker) miniMap.removeLayer(miniMapMarker);
miniMapMarker = L.marker([lat, lng]).addTo(miniMap);
document.getElementById('hs-lat').value = lat.toFixed(6);
document.getElementById('hs-lng').value = lng.toFixed(6);
}
window.toggleHSLinkType = function(type) {
document.getElementById('hs-section-existing').style.display = type === 'existing' ? 'block' : 'none';
document.getElementById('hs-section-upload').style.display = type === 'upload' ? 'block' : 'none';
if (type === 'upload') {
const gpsMode = document.querySelector('input[name="hsGPSMode"]:checked')?.value || 'map';
window.toggleHSGPSMode(gpsMode);
}
};
window.toggleHSGPSMode = function(mode) {
document.getElementById('hs-map-selector').style.display = mode === 'map' ? 'block' : 'none';
document.getElementById('hs-manual-gps').style.display = mode === 'manual' ? 'block' : 'none';
if (mode === 'map') initHSMiniMap();
};
/**
* Chuyển đổi tab cũ (giữ lại để tương thích nếu cần)
*/
function switchHSTab(tabName) {
if (tabName === 'select') {
window.toggleHSLinkType('existing');
} else {
window.toggleHSLinkType('upload');
}
}
/**
* Ẩn/hiện input nhập GPS thủ công
*/
function toggleManualGPS() {
const mode = document.getElementById('hs-gps-mode').value;
const manualDiv = document.getElementById('hs-manual-gps');
manualDiv.style.display = mode === 'manual' ? 'block' : 'none';
}
/**
* Gửi dữ liệu lưu Hotspot lên Backend
*/
async function saveHotspotToDB(pitch, yaw, title, description, targetSceneId, hotspotId) {
const token = localStorage.getItem('jwt');
// Gọi đúng API create hoặc update tùy vào trạng thái
const url = hotspotId ? `${API_BASE_URL}/hotspots/update/${hotspotId}` : `${API_BASE_URL}/hotspots/create`;
const method = hotspotId ? 'PUT' : 'POST';
try {
const body = {
title,
description,
target_scene_id: targetSceneId,
coordinates: {
pitch: Number(pitch),
yaw: Number(yaw)
}
};
// Nếu tạo mới, cần gửi kèm ID của scene hiện tại làm parent
if (!hotspotId) {
body.parent_scene_id = currentSceneId;
}
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(body)
});
const data = await response.json();
if (!response.ok) throw new Error(data.message || 'Lỗi khi lưu hotspot');
showNotification('Lưu điểm điều hướng thành công!', 'success');
// Buộc nạp lại để cập nhật danh sách hotspot mới
openScene(currentSceneId, localStorage.getItem('activeScenePrivacy'), localStorage.getItem('activeSceneToken'), true);
} catch (error) {
console.error(error);
showNotification(error.message, 'error');
}
}
/**
* Công cụ dọn dẹp toàn bộ dữ liệu (Chỉ dùng cho nhà phát triển)
* Gọi lệnh: systemReset() từ trình duyệt
*/
window.systemReset = async function() {
if (!confirm("CẢNH BÁO: Thao tác này sẽ xóa sạch TOÀN BỘ scene và ảnh trên server. Bạn có chắc chắn?")) return;
const token = localStorage.getItem('jwt');
try {
const response = await fetch(`${API_BASE_URL}/maintenance/reset-all`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
if (response.ok) {
localStorage.clear(); // Xóa sạch token, vị trí map, active scene
showNotification(data.message, 'success');
location.reload();
} else {
throw new Error(data.message);
}
} catch (e) {
showNotification("Lỗi reset: " + e.message, 'error');
}
};
/**
* Mở Menu tùy chọn cho Hotspot (Sửa/Xóa)
*/
window.openHotspotMenu = function(hotspot) {
const modal = document.getElementById('hotspot-action-modal');
const editBtn = document.getElementById('btn-hs-edit');
const deleteBtn = document.getElementById('btn-hs-delete');
modal.style.display = 'flex';
// Hành động Sửa: Mở form biên tập với dữ liệu cũ
editBtn.onclick = () => {
closeHotspotActionModal();
window.handleHotspotCreation(hotspot.pitch, hotspot.yaw, hotspot);
};
// Hành động Xóa: Xác nhận và gọi API xóa
deleteBtn.onclick = async () => {
if (confirm('Bạn có chắc chắn muốn xóa điểm điều hướng này?')) {
closeHotspotActionModal();
await deleteHotspot(hotspot._id);
}
};
};
/**
* Đóng Modal tùy chọn Hotspot
*/
function closeHotspotActionModal() {
document.getElementById('hotspot-action-modal').style.display = 'none';
}
/**
* Xóa Hotspot thông qua API
*/
async function deleteHotspot(hotspotId) {
const token = localStorage.getItem('jwt');
try {
// Gọi đúng API delete hotspot độc lập
const response = await fetch(`${API_BASE_URL}/hotspots/delete/${hotspotId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.message || 'Lỗi xóa hotspot');
}
showNotification('Đã xóa điểm điều hướng.', 'success');
// Refresh lại scene hiện tại để cập nhật viewer
openScene(currentSceneId, null, null, true);
} catch (e) {
showNotification(e.message, 'error');
}
}
/**
* Toggles the visibility of the user dropdown menu.
*/
function toggleDropdown() {
document.getElementById('user-dropdown').classList.toggle('show');
}
/**
* Opens the user dashboard overlay.
*/
function openDashboard() {
const username = localStorage.getItem('username');
const role = localStorage.getItem('role');
if (username) {
document.getElementById('sidebar-avatar').innerText = username.charAt(0).toUpperCase();
document.getElementById('sidebar-username').innerText = username;
document.getElementById('sidebar-status').innerText = role || 'Thành viên';
}
document.getElementById('dashboard-overlay').style.display = 'flex';
document.getElementById('user-dropdown').classList.remove('show'); // Close dropdown
// Mở tab profile mặc định khi mở dashboard
openDashboardTab('profile');
}
/**
* Closes the user dashboard overlay.
*/
function closeDashboard() {
document.getElementById('dashboard-overlay').style.display = 'none';
}
/**
* Tải danh sách scene của chính người dùng đăng nhập
*/
async function loadMyScenes() {
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 = '
Đang tải danh sách...
';
try {
const res = await fetch(`${API_BASE_URL}/me/scenes`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const scenes = await res.json();
if (!res.ok) throw new Error(scenes.message);
listContainer.innerHTML = '';
if (scenes.length === 0) {
listContainer.innerHTML = '
Bạn chưa tạo scene nào.
';
return;
}
scenes.forEach(scene => {
const assetId = scene.assetId?._id || scene.assetId;
let thumbUrl = `${API_BASE_URL}/assets/view/${assetId}`;
if (token) thumbUrl += `?token=${token}`;
const card = document.createElement('div');
card.className = 'scene-card';
card.style.backgroundImage = `url('${thumbUrl}')`;
// Logic hiển thị badge trạng thái
let statusBadge = '';
if (scene.status === 'processing') {
statusBadge = '⏳ Đang xử lý 8K...';
} else if (scene.status === 'failed') {
statusBadge = '❌ Lỗi xử lý';
}
card.innerHTML = `