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í users", 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; } /** * Cập nhật nội dung tab Hồ sơ với thông tin người dùng */ function updateProfileTabContent() { const username = localStorage.getItem('username'); const role = localStorage.getItem('role'); if (username) { const avatar = document.getElementById('profile-avatar-initials'); const userDisplay = document.getElementById('profile-username-display'); const statusDisplay = document.getElementById('profile-status-display'); const userInput = document.getElementById('profile-username'); if (avatar) avatar.innerText = username.charAt(0).toUpperCase(); if (userDisplay) userDisplay.innerText = username; if (statusDisplay) statusDisplay.innerText = role || 'Thành viên'; if (userInput) userInput.value = username; } } /** * Hàm bổ trợ định dạng ngày tháng theo múi giờ hệ thống */ function formatSystemDate(dateString) { if (!dateString) return 'N/A'; const date = new Date(dateString); try { return new Intl.DateTimeFormat(systemSettings.language === 'vi' ? 'vi-VN' : 'en-US', { timeZone: systemSettings.timezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(date); } catch (e) { // Fallback nếu timezone không hợp lệ return date.toLocaleDateString(); } } /** * Initializes the full-screen Leaflet Map */ function initMap() { // Đọc vị trí và zoom đã lưu từ localStorage const savedLat = localStorage.getItem('map-lat'); const savedLng = localStorage.getItem('map-lng'); const savedZoom = localStorage.getItem('map-zoom'); // Đảm bảo tọa độ khởi tạo luôn hợp lệ let startLat = parseFloat(savedLat); let startLng = parseFloat(savedLng); let startZoom = parseInt(savedZoom); if (isNaN(startLat)) startLat = 21.0285; if (isNaN(startLng)) startLng = 105.8542; if (isNaN(startZoom)) startZoom = 13; // Khởi tạo bản đồ với zoomControl và tắt attribution mặc định của Leaflet map = L.map('map', { zoomControl: true, attributionControl: false }).setView([startLat, startLng], startZoom); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, // Attribution sẽ được thêm thủ công bên dưới để chỉ hiển thị OpenStreetMap // attribution: '© OpenStreetMap contributors' }).addTo(map); // Khởi tạo Marker Cluster Group CHỈ dành cho Scene (Ảnh mẹ) markerClusterGroup = L.markerClusterGroup({ zoomToBoundsOnClick: false, spiderfyOnMaxZoom: true, maxClusterRadius: 50, spiderfyDistanceMultiplier: 3.5, // Tăng thêm khoảng cách để callout ảnh mẹ tách rõ ràng khi tỏa ra showCoverageOnHover: false, iconCreateFunction: function(cluster) { const childMarkers = cluster.getAllChildMarkers(); try { if (childMarkers.length > 0 && childMarkers[0].options.icon) { const childIcon = childMarkers[0].options.icon; // Trả về một DivIcon MỚI dựa trên cấu hình của con, tránh dùng chung instance gây crash render return L.divIcon({ html: childIcon.options.html, className: childIcon.options.className, iconSize: childIcon.options.iconSize, iconAnchor: childIcon.options.iconAnchor }); } } catch (e) {} return L.divIcon({ className: 'cluster-fallback', html: '
' }); } }); // Khi click chuột trái vào một cụm callout, thực hiện tách chúng ra markerClusterGroup.on('clusterclick', (a) => { a.layer.spiderfy(); }); // Thêm attribution chỉ với OpenStreetMap, không có Leaflet L.control.attribution({ prefix: false }).addAttribution('OpenStreetMap contributors').addTo(map); map.addLayer(markerClusterGroup); // Lưu vị trí bản đồ mỗi khi người dùng di chuyển hoặc zoom xong map.on('moveend', () => { const center = map.getCenter(); localStorage.setItem('map-lat', center.lat); localStorage.setItem('map-lng', center.lng); localStorage.setItem('map-zoom', map.getZoom()); }); // Event listener for right-click on map to open modal map.on('contextmenu', (e) => { // Nếu viewer 3D đang hiển thị, không thực hiện tạo scene trên bản đồ const viewerContainer = document.getElementById('viewer-container'); // Kiểm tra z-index và display để chắc chắn viewer đang ẩn if (viewerContainer && viewerContainer.style.display !== 'none') { return; } // 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'); if (!token) return; const { lat, lng } = e.latlng; openCreateSceneModal(lat, lng); }); } /** * Checks if user is logged in (via localStorage JWT) and updates UI */ function checkAuthStatus() { const token = localStorage.getItem('jwt'); const username = localStorage.getItem('username'); const role = localStorage.getItem('role'); const authGuest = document.getElementById('auth-guest'); const authLoggedIn = document.getElementById('auth-logged-in'); const avatarInitials = document.getElementById('avatar-initials'); if (token && username) { if (authGuest) authGuest.style.display = 'none'; // Hide login form authLoggedIn.style.display = 'block'; // Show welcome message and buttons avatarInitials.innerText = username.charAt(0).toUpperCase(); // Hiển thị các nút dành cho admin (Chủ sở hữu/Admin tối cao) const adminButtons = document.querySelectorAll('.dashboard-tabs .admin-only'); if (role === 'admin' || role === 'Chủ sở hữu') { adminButtons.forEach(btn => btn.style.display = 'block'); } else { adminButtons.forEach(btn => btn.style.display = 'none'); } } else { authGuest.style.display = 'block'; // Show login form authLoggedIn.style.display = 'none'; // Hide welcome message avatarInitials.innerText = '?'; document.getElementById('user-dropdown').classList.remove('show'); // Đóng dropdown nếu không đăng nhập } } /** * Handles user login */ async function handleLogin() { const errorMsg = document.getElementById('login-error-msg'); if (errorMsg) errorMsg.style.display = 'none'; const username = document.getElementById('username-input').value.trim(); const password = document.getElementById('password-input').value.trim(); if (!username || !password) { if (errorMsg) { errorMsg.innerText = 'Vui lòng nhập đầy đủ thông tin'; errorMsg.style.display = 'block'; } return; } try { const response = await fetch(`${API_BASE_URL}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await response.json(); if (!response.ok) throw new Error(data.message || 'Đăng nhập thất bại'); localStorage.setItem('jwt', data.token); localStorage.setItem('username', data.user.username); localStorage.setItem('role', data.user.role); localStorage.setItem('userId', data.user.id); checkAuthStatus(); toggleDropdown(); // Đóng dropdown sau khi đăng nhập loadScenes(); // Reload scenes to show member/private scenes // Làm sạch form document.getElementById('username-input').value = ''; document.getElementById('password-input').value = ''; } catch (error) { if (errorMsg) { errorMsg.innerText = error.message; errorMsg.style.display = 'block'; } } } /** * Chuyển đổi giữa chế độ Đăng nhập và Đăng ký trong dropdown */ window.switchAuthMode = function(mode) { const loginSection = document.getElementById('login-section'); const registerSection = document.getElementById('register-section'); const loginBtn = document.getElementById('tab-login-btn'); const registerBtn = document.getElementById('tab-register-btn'); if (mode === 'login') { loginSection.style.display = 'block'; registerSection.style.display = 'none'; loginBtn.classList.add('active'); registerBtn.classList.remove('active'); } else { loginSection.style.display = 'none'; registerSection.style.display = 'block'; loginBtn.classList.remove('active'); registerBtn.classList.add('active'); } }; /** * Handles user registration */ async function handleRegister() { const errorMsg = document.getElementById('reg-error-msg'); if (errorMsg) errorMsg.style.display = 'none'; const fullName = document.getElementById('reg-fullname').value.trim(); const email = document.getElementById('reg-email').value.trim(); const username = document.getElementById('reg-username').value.trim(); const password = document.getElementById('reg-password').value; const confirm = document.getElementById('reg-confirm').value; const agree = document.getElementById('reg-agree').checked; // Kiểm tra các trường trống if (!fullName || !email || !username || !password || !confirm) { if (errorMsg) { errorMsg.innerText = 'Vui lòng điền đầy đủ thông tin'; errorMsg.style.display = 'block'; } return; } // Kiểm tra mật khẩu khớp nhau if (password !== confirm) { if (errorMsg) { errorMsg.innerText = 'Mật khẩu xác nhận không khớp'; errorMsg.style.display = 'block'; } return; } // Kiểm tra đồng ý quy định if (!agree) { if (errorMsg) { errorMsg.innerText = 'Bạn phải đồng ý với quy định của trang'; errorMsg.style.display = 'block'; } return; } try { const response = await fetch(`${API_BASE_URL}/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fullName, email, username, password, agreedToRules: agree, role: 'Thành viên' }) }); const data = await response.json(); if (!response.ok) throw new Error(data.message || 'Đăng ký thất bại'); showNotification('Đăng ký thành công! Bạn có thể đăng nhập ngay bây giờ.', 'success'); switchAuthMode('login'); // Tự động chuyển về tab đăng nhập sau khi thành công } catch (error) { if (errorMsg) { errorMsg.innerText = error.message; errorMsg.style.display = 'block'; } } } /** * Hiển thị hộp thoại xác nhận đăng xuất */ function showLogoutConfirm() { const modal = document.getElementById('logout-confirm-modal'); if (modal) { closeDashboard(); // Đảm bảo đóng dashboard nếu đang mở modal.style.display = 'flex'; } } /** * Đóng hộp thoại xác nhận đăng xuất */ function closeLogoutConfirm() { const modal = document.getElementById('logout-confirm-modal'); if (modal) { modal.style.display = 'none'; openDashboard(); // Mở lại dashboard sau khi đóng confirm } } /** * Handles user logout */ function handleLogout() { localStorage.removeItem('jwt'); localStorage.removeItem('username'); localStorage.removeItem('role'); localStorage.removeItem('activeSceneId'); localStorage.removeItem('activeScenePrivacy'); localStorage.removeItem('activeSceneToken'); localStorage.removeItem('userId'); // Đảm bảo đóng viewer nếu đang mở if (typeof closeViewer === 'function') { closeViewer(); } checkAuthStatus(); if (document.getElementById('user-dropdown').classList.contains('show')) { toggleDropdown(); } closeLogoutConfirm(); closeDashboard(); loadScenes(); // Reload scenes to filter out private ones } /** * Opens Modal for creating a Scene and sets lat/lng inputs */ function openCreateSceneModal(lat, lng) { 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); const lang = systemSettings.language || 'vi'; const modalTitle = document.getElementById('create-scene-modal-title'); if (modalTitle) modalTitle.innerText = lang === 'vi' ? "Tạo 3D scene mới" : "Create New 3D Scene"; } /** * Closes the Create Scene Modal and removes temporary marker */ function closeModal() { document.getElementById('create-scene-modal').style.display = 'none'; if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; } document.getElementById('create-scene-form').reset(); document.getElementById('shared-with-group').style.display = 'none'; if (returnToDashboardAfterEdit) { const targetTab = dashboardReturnTab; returnToDashboardAfterEdit = false; openDashboard(); openDashboardTab(targetTab); } } /** * Toggles visibility of the shared users input based on privacy selection */ function toggleSharedUsers() { const privacy = document.getElementById('modal-privacy').value; const group = document.getElementById('shared-with-group'); if (privacy === 'shared') { group.style.display = 'block'; } else { group.style.display = 'none'; } } /** * Form submission for Scene creation (multipart/form-data) */ async function submitScene(e) { e.preventDefault(); const form = document.getElementById('create-scene-form'); const formData = new FormData(form); const sceneId = document.getElementById('modal-scene-id').value; const token = localStorage.getItem('jwt'); const url = sceneId ? `${API_BASE_URL}/scenes/${sceneId}` : `${API_BASE_URL}/scenes`; const method = sceneId ? 'PUT' : 'POST'; 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(); loadScenes(); }); } /** * Helper function to handle uploads with progress bars */ function uploadWithProgress(url, method, formData, token, prefix, callback) { const xhr = new XMLHttpRequest(); const container = document.getElementById(`${prefix}-progress-container`); const bar = document.getElementById(`${prefix}-progress-bar`); const percentText = document.getElementById(`${prefix}-progress-percent`); const statusText = document.getElementById(`${prefix}-progress-status`); if (container) container.style.display = 'block'; if (statusText) statusText.innerText = "Đang tải ảnh lên..."; xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percent = Math.round((e.loaded / e.total) * 100); if (bar) bar.style.width = percent + '%'; if (percentText) percentText.innerText = percent + '%'; if (percent === 100 && statusText) statusText.innerText = "Tải lên xong! Đang khởi tạo trên server..."; } }); xhr.addEventListener('load', () => { if (container) container.style.display = 'none'; if (xhr.status >= 200 && xhr.status < 300) { callback(JSON.parse(xhr.responseText)); } else { let errorMsg = 'Không thể tải lên'; try { const err = JSON.parse(xhr.responseText); errorMsg = err.message || errorMsg; } catch (e) {} showNotification('Lỗi: ' + errorMsg, 'error'); } }); xhr.addEventListener('error', () => { if (container) container.style.display = 'none'; showNotification('Lỗi kết nối mạng.', 'error'); }); xhr.open(method, url); if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`); xhr.send(formData); } /** * Loads and displays visible Scenes on the map */ async function loadScenes() { try { const token = localStorage.getItem('jwt'); const headers = {}; if (token) { 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}`, { method: 'GET', headers }); console.log(`[API Response] /scenes status: ${response.status}`); if (!response.ok) throw new Error('Failed to load scenes'); const scenes = await response.json(); console.log(`[Data] Nhận được ${scenes.length} scenes từ server`); if (!Array.isArray(scenes)) return; // Xóa sạch các layers cũ trước khi nạp mới markerClusterGroup.clearLayers(); const markersToAdd = []; const activeSceneId = localStorage.getItem('activeSceneId'); 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 độ scenes.forEach((scene) => { // 1. Kiểm tra tọa độ an toàn - Ngăn chặn treo map do NaN const latNum = Number(scene.gps?.lat ?? scene.lat); const lngNum = Number(scene.gps?.lng ?? scene.lng); if (isNaN(latNum) || isNaN(lngNum)) { console.error(`Bỏ qua Scene "${scene.name || scene.title}" do tọa độ lỗi:`, scene); return; } // 2. Logic lọc Ảnh mẹ: Sửa lỗi typo coordKey (dùng latNum 2 lần) const coordKey = `${latNum.toFixed(6)},${lngNum.toFixed(6)}`; if (seenCoordinates.has(coordKey)) return; seenCoordinates.add(coordKey); // 3. Truy cập Asset an toàn const assetId = scene.assetId?._id || scene.assetId; if (!assetId) return; // Bỏ qua nếu không có ảnh liên kết 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 calloutIcon = L.divIcon({ className: 'custom-scene-marker', html: `Đ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}')`; card.innerHTML = ` `; 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; closeDashboard(); // Mặc định truyền false cho isChild, logic backend sẽ xử lý cascade privacy sau openEditMetadataModal(scene, false); }; // Xử lý nút Xóa (Sẽ được hoàn thiện ở Bước 4) document.getElementById(`delete-scene-${scene._id}`).onclick = () => { dashboardReturnTab = 'my-scenes'; returnToDashboardAfterEdit = true; closeDashboard(); deleteScene(scene._id); }; }); } catch (e) { listContainer.innerHTML = `Lỗi: ${e.message}
`; } } /** * Tải danh sách người dùng dành cho Admin tối cao */ async function loadAdminUsers() { const token = localStorage.getItem('jwt'); const container = document.getElementById('admin-users-list'); if (!container) return; container.innerHTML = 'Đang tải danh sách người dùng...
'; try { const res = await fetch(`${API_BASE_URL}/admin/users`, { headers: { 'Authorization': `Bearer ${token}` } }); const users = await res.json(); if (!res.ok) throw new Error(users.message); let html = `| Họ tên | Username | Quyền hạn | Reset Password | Thao tác | |
|---|---|---|---|---|---|
| ${user.username} |
Lỗi: ${e.message}
`; } } window.updateUserByAdmin = async function(userId) { const token = localStorage.getItem('jwt'); const payload = { fullName: document.getElementById(`adm-fn-${userId}`).value, email: document.getElementById(`adm-em-${userId}`).value, role: document.getElementById(`adm-role-${userId}`).value, password: document.getElementById(`adm-pw-${userId}`).value }; try { const res = await fetch(`${API_BASE_URL}/admin/users/${userId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(payload) }); const data = await res.json(); if (!res.ok) throw new Error(data.message); showNotification(data.message, 'success'); loadAdminUsers(); } catch (e) { showNotification(e.message, 'error'); } }; window.deleteUserByAdmin = async function(userId) { if (!confirm('Xóa vĩnh viễn người dùng này?')) return; const token = localStorage.getItem('jwt'); try { const res = await fetch(`${API_BASE_URL}/admin/users/${userId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); if (!res.ok) throw new Error(data.message); showNotification(data.message, 'success'); loadAdminUsers(); } catch (e) { showNotification(e.message, 'error'); } }; /** * Tải và hiển thị kho ảnh/media của người dùng */ async function loadMyAssets() { const token = localStorage.getItem('jwt'); const gridContainer = document.getElementById('media-library-list'); const currentUserId = localStorage.getItem('userId'); const userRole = localStorage.getItem('role'); gridContainer.innerHTML = 'Đang tải kho ảnh...
'; try { const res = await fetch(`${API_BASE_URL}/me/assets`, { headers: { 'Authorization': `Bearer ${token}` } }); const assets = await res.json(); if (!res.ok) throw new Error(assets.message); gridContainer.innerHTML = ''; if (assets.length === 0) { gridContainer.innerHTML = 'Kho ảnh trống.
'; return; } assets.forEach(asset => { const scene = asset.linkedScene; const isTrash = !scene; const parentNames = asset.parentScenes?.map(p => p.name || p.title).join(', '); const card = document.createElement('div'); card.className = `media-card ${isTrash ? 'trash-item' : ''}`; // Build inner HTML without the onclick for edit/delete buttons let innerHtml = `${scene?.description || 'Không có mô tả'}
${parentNames ? `🔗 Liên kết từ: ${parentNames}
` : ''} Tải lên: ${formatSystemDate(asset.createdAt)}Lỗi nạp media: ${e.message}
`; } } /** * Xóa ảnh khỏi kho media */ window.deleteAsset = function(assetId) { assetIdToDelete = assetId; document.getElementById('delete-asset-confirm-modal').style.display = 'flex'; }; window.closeDeleteAssetModal = function() { document.getElementById('delete-asset-confirm-modal').style.display = 'none'; assetIdToDelete = null; }; window.confirmDeleteAsset = async function() { if (!assetIdToDelete) return; const token = localStorage.getItem('jwt'); try { const res = await fetch(`${API_BASE_URL}/assets/${assetIdToDelete}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); let data; const contentType = res.headers.get("content-type"); if (contentType && contentType.includes("application/json")) { data = await res.json(); } if (!res.ok) { throw new Error(data?.message || `Lỗi máy chủ (${res.status})`); } closeDeleteAssetModal(); showNotification(data.message || "Đã xóa thành công", 'success'); loadMyAssets(); // Nạp lại kho ảnh loadScenes(); // Nạp lại bản đồ nếu có scene bị xóa kèm theo } catch (e) { showNotification("Lỗi khi xóa: " + e.message, 'error'); } }; /** * Hiển thị Modal thông báo thành công */ window.showSuccessModal = function(message, icon = '✓') { const modal = document.getElementById('success-modal'); const msgElem = document.getElementById('success-modal-message'); const iconElem = document.getElementById('success-modal-icon'); if (modal && msgElem && iconElem) { msgElem.innerText = message; iconElem.innerText = icon; modal.style.display = 'flex'; // Tự động ẩn sau 3 giây setTimeout(() => closeSuccessModal(), 3000); } }; /** * Đóng Modal thành công (hỗ trợ click ra ngoài) */ window.closeSuccessModal = function(e) { const modal = document.getElementById('success-modal'); if (!modal) return; // Nếu nhấn từ code (không có e) hoặc click trúng overlay thì đóng if (e && e.target !== modal) return; modal.style.display = 'none'; }; /** * Hiển thị Modal thông báo lỗi hoặc cảnh báo */ window.showErrorModal = function(message, title = "Thông báo", icon = '⚠️') { const modal = document.getElementById('error-modal'); const msgElem = document.getElementById('error-modal-message'); const titleElem = document.getElementById('error-modal-title'); const iconElem = document.getElementById('error-modal-icon'); if (modal && msgElem && iconElem) { msgElem.innerText = message; iconElem.innerText = icon; if (titleElem) titleElem.innerText = title; modal.style.display = 'flex'; } }; /** * Đóng Modal lỗi (hỗ trợ click ra ngoài) */ window.closeErrorModal = function(e) { const modal = document.getElementById('error-modal'); if (!modal) return; // Nếu nhấn từ code (không có e) hoặc click trúng overlay thì đóng if (e && e.target !== modal) return; modal.style.display = 'none'; }; /** * Hàm thông báo dùng chung thay thế alert() */ window.showNotification = function(message, type = 'success') { if (type === 'success') { showSuccessModal(message, '✓'); } else if (type === 'warning') { showErrorModal(message, 'Cảnh báo', '⚠️'); } else { showErrorModal(message, 'Lỗi', '❌'); } }; window.openEditFromMedia = function(scene, isChild = false) { if (!scene || !scene._id) { showNotification("Không thể chỉnh sửa: Ảnh này không được gắn với một Scene hợp lệ.", 'error'); return; } dashboardReturnTab = 'media-library'; returnToDashboardAfterEdit = true; closeDashboard(); openEditMetadataModal(scene, isChild); }; /** * Mở Modal sửa thông tin Metadata chuyên biệt */ window.openEditMetadataModal = function(scene, isChild = false) { currentEditingScene = scene; // Lưu lại để dùng cho chia sẻ // Load dữ liệu chia sẻ hiện tại sharedUsersData = scene.sharedWith || []; sharedEmailsData = scene.sharedEmails || []; document.getElementById('edit-modal-scene-id').value = scene._id; document.getElementById('edit-modal-title').value = scene.name || scene.title || ''; document.getElementById('edit-modal-description').value = scene.description || ''; const lat = scene.gps?.lat || scene.lat; const lng = scene.gps?.lng || scene.lng; document.getElementById('edit-modal-lat').value = lat; document.getElementById('edit-modal-lng').value = lng; // Xử lý logic Privacy cho Cảnh con const privacySelect = document.getElementById('edit-modal-privacy'); const childInfo = document.getElementById('edit-child-privacy-info'); if (isChild) { privacySelect.value = scene.privacy; privacySelect.disabled = true; childInfo.style.display = 'block'; } else { privacySelect.value = scene.privacy; privacySelect.disabled = false; childInfo.style.display = 'none'; } handleEditPrivacyChange(); // Cập nhật hiển thị nút bánh răng document.getElementById('edit-scene-metadata-modal').style.display = 'flex'; // Khởi tạo Mini Map tại vị trí hiện tại của Scene setTimeout(() => initEditSceneMiniMap(lat, lng), 100); }; function closeEditMetadataModal() { document.getElementById('edit-scene-metadata-modal').style.display = 'none'; if (returnToDashboardAfterEdit) { const targetTab = dashboardReturnTab; returnToDashboardAfterEdit = false; openDashboard(); openDashboardTab(targetTab); } } function initEditSceneMiniMap(lat, lng) { if (editMiniMap) { editMiniMap.setView([lat, lng], 16); if (editMiniMapMarker) editMiniMapMarker.setLatLng([lat, lng]); editMiniMap.invalidateSize(); return; } editMiniMap = L.map('edit-mini-map').setView([lat, lng], 16); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(editMiniMap); editMiniMapMarker = L.marker([lat, lng], { draggable: true }).addTo(editMiniMap); editMiniMap.on('click', (e) => { const { lat, lng } = e.latlng; editMiniMapMarker.setLatLng([lat, lng]); document.getElementById('edit-modal-lat').value = lat.toFixed(6); document.getElementById('edit-modal-lng').value = lng.toFixed(6); }); } /** * Xử lý khi thay đổi Dropdown Privacy trong Modal sửa */ window.handleEditPrivacyChange = function() { const privacy = document.getElementById('edit-modal-privacy').value; const settingsBtn = document.getElementById('btn-edit-privacy-settings'); const isChild = document.getElementById('edit-modal-privacy').disabled; if (!isChild && (privacy === 'member' || privacy === 'shared')) { settingsBtn.style.display = 'block'; } else { settingsBtn.style.display = 'none'; } }; /** * Mở modal cài đặt chi tiết dựa trên loại Privacy */ window.openPrivacySettingsModal = function() { const privacy = document.getElementById('edit-modal-privacy').value; if (privacy === 'member') { renderSharedList(); document.getElementById('share-member-modal').style.display = 'flex'; } else if (privacy === 'shared' || privacy === 'public') { showShareLink(currentEditingScene); } }; /** * Hiển thị modal lấy link chia sẻ (dùng cho cả khách và chủ sở hữu) * @param {Object} scene - Đối tượng Scene cần lấy link */ window.showShareLink = function(scene) { if (!scene) return; // Lưu lại scene đang tương tác để các logic phụ trợ hoạt động đồng bộ currentEditingScene = scene; // Trỏ link vào endpoint /api/share/ để hỗ trợ Open Graph (ảnh thumbnail Facebook/Zalo) const baseUrl = window.location.origin + '/api/share/'; const token = scene.shareToken || ''; document.getElementById('shared-link-input').value = `${baseUrl}${scene._id}${token ? '?token=' + token : ''}`; document.getElementById('share-link-modal').style.display = 'flex'; }; /** * Tìm kiếm người dùng để chia sẻ */ window.searchUsersToShare = async function(query) { const dropdown = document.getElementById('search-results-dropdown'); if (!query || query.length < 2) { dropdown.style.display = 'none'; return; } const token = localStorage.getItem('jwt'); try { const res = await fetch(`${API_BASE_URL}/users/search?q=${encodeURIComponent(query)}`, { headers: { 'Authorization': `Bearer ${token}` } }); const users = await res.json(); dropdown.innerHTML = ''; if (users.length > 0) { users.forEach(user => { const item = document.createElement('div'); item.className = 'search-item'; item.innerText = `${user.username} (${user.email})`; item.onclick = () => addMemberToShare(user); dropdown.appendChild(item); }); dropdown.style.display = 'block'; } else { // Nếu không tìm thấy user, cho phép thêm email thủ công if (query.includes('@')) { dropdown.innerHTML = `