diff --git a/backend/models/Setting.js b/backend/models/Setting.js new file mode 100644 index 0000000..4f6ce42 --- /dev/null +++ b/backend/models/Setting.js @@ -0,0 +1,15 @@ +const mongoose = require('mongoose'); + +const settingSchema = new mongoose.Schema({ + timezone: { + type: String, + default: 'Asia/Ho_Chi_Minh' + }, + language: { + type: String, + enum: ['vi', 'en'], + default: 'vi' + } +}, { timestamps: true }); + +module.exports = mongoose.model('Setting', settingSchema); \ No newline at end of file diff --git a/backend/routes/apiRoutes.js b/backend/routes/apiRoutes.js index 102c910..c6608f6 100644 --- a/backend/routes/apiRoutes.js +++ b/backend/routes/apiRoutes.js @@ -8,6 +8,7 @@ const User = require('../models/User'); const Asset = require('../models/Asset'); const Scene = require('../models/Scene'); const Hotspot = require('../models/Hotspot'); // Giả định bạn đã tạo model mới +const Setting = require('../models/Setting'); const { protect, optionalAuth } = require('../middlewares/authMiddleware'); const { verifyReferer, setNoCacheHeaders } = require('../middlewares/securityMiddleware'); @@ -515,6 +516,115 @@ router.delete('/scenes/:id', protect, async (req, res) => { } }); +/** + * @route GET /api/me/profile + * @desc Lấy thông tin hồ sơ người dùng hiện tại + * @access Private + */ +router.get('/me/profile', protect, async (req, res) => { + try { + const user = await User.findById(req.user._id).select('-password'); + res.json(user); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +/** + * @route PUT /api/me/profile + * @desc Cập nhật hồ sơ (đổi tên, mật khẩu) + * @access Private + */ +router.put('/me/profile', protect, async (req, res) => { + try { + const user = await User.findById(req.user._id); + if (!user) return res.status(404).json({ message: 'User not found' }); + + if (req.body.username) user.username = req.body.username; + if (req.body.password) user.password = req.body.password; // Hook pre-save sẽ tự hash + + await user.save(); + res.json({ + message: 'Hồ sơ đã được cập nhật', + user: { id: user._id, username: user.username, role: user.role } + }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +/** + * @route GET /api/me/scenes + * @desc Lấy danh sách các cảnh mẹ do người dùng tạo + * @access Private + */ +router.get('/me/scenes', protect, async (req, res) => { + try { + const scenes = await Scene.find({ createdBy: req.user._id }) + .populate('assetId') + .sort({ createdAt: -1 }); + res.json(scenes); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +/** + * @route GET /api/me/assets + * @desc Lấy danh sách media của người dùng + * @access Private + */ +router.get('/me/assets', protect, async (req, res) => { + try { + const assets = await Asset.find({ uploadedBy: req.user._id }).sort({ createdAt: -1 }); + res.json(assets); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +/** + * @route GET /api/admin/users + * @desc Lấy toàn bộ danh sách người dùng (Chỉ Admin) + * @access Private (Admin) + */ +router.get('/admin/users', protect, async (req, res) => { + if (req.user.role !== 'Chủ sở hữu') { + return res.status(403).json({ message: 'Bạn không có quyền truy cập quản trị' }); + } + try { + const users = await User.find({}).select('-password').sort({ createdAt: -1 }); + res.json(users); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +/** + * @route GET/PUT /api/system/settings + * @desc Quản lý thiết lập hệ thống + * @access Private (Admin) + */ +router.get('/system/settings', optionalAuth, async (req, res) => { + try { + let settings = await Setting.findOne(); + if (!settings) settings = await Setting.create({}); + res.json(settings); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + +router.put('/system/settings', protect, async (req, res) => { + if (req.user.role !== 'Chủ sở hữu') return res.status(403).json({ message: 'Forbidden' }); + try { + const settings = await Setting.findOneAndUpdate({}, req.body, { new: true, upsert: true }); + res.json(settings); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}); + /** * @route POST /api/maintenance/reset-all * @desc Wipe all scenes, assets, and physical files (DANGEROUS: For dev reset only) diff --git a/frontend/css/style.css b/frontend/css/style.css index 0495a73..1b2195b 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -17,54 +17,6 @@ html, body { z-index: 1; } -/* Floating panels on top of the map */ -.floating-panel { - position: absolute; - top: 20px; - right: 20px; - background: rgba(255, 255, 255, 0.95); - padding: 15px; - border-radius: 8px; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15); - z-index: 1000; - width: 280px; -} - -.floating-panel h3 { - margin-bottom: 10px; - font-size: 16px; - color: #333; -} - -.floating-panel input { - width: 100%; - padding: 8px; - margin-bottom: 8px; - border: 1px solid #ccc; - border-radius: 4px; -} - -.btn-group { - display: flex; - gap: 10px; -} - -.btn-group button, .floating-panel button { - flex: 1; - padding: 8px; - background-color: #007bff; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-weight: bold; - transition: background-color 0.2s; -} - -.btn-group button:hover, .floating-panel button:hover { - background-color: #0056b3; -} - /* Modal Styling */ .modal { display: none; @@ -92,14 +44,18 @@ html, body { .close-btn { position: absolute; top: 15px; - right: 20px; - font-size: 24px; + right: 25px; + font-size: 30px; cursor: pointer; - color: #666; + color: #fff; /* Chuyển sang màu trắng để nổi bật trên nền tối */ + z-index: 5000; /* Đảm bảo nằm trên cùng của tất cả các pane */ + transition: all 0.2s ease; + text-shadow: 0 0 10px rgba(0,0,0,0.5); } .close-btn:hover { - color: #000; + color: #ff4d4d; /* Chuyển sang màu đỏ khi di chuột để dễ nhận biết */ + transform: scale(1.1); } .form-group { @@ -176,10 +132,331 @@ html, body { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); - display: flex; align-items: center; justify-content: center; + align-items: center; justify-content: center; z-index: 4000; } +/* Top Bar */ +#top-bar { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 60px; /* Fixed height */ + background: rgba(20, 20, 20, 0.8); /* Màu tối transparent */ + color: #fff; + box-shadow: 0 2px 15px rgba(0, 0, 0, 0.4); + z-index: 1500; /* Above map, below modals */ + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 20px; +} + +.app-brand { + display: flex; + align-items: center; + gap: 10px; +} + +.app-brand h1 { + font-size: 20px; + color: #fff; + margin: 0; +} + +#user-controls { + position: relative; +} + +#user-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: #007bff; + color: white; + display: flex; + justify-content: center; + align-items: center; + font-weight: bold; + font-size: 18px; + cursor: pointer; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} + +#user-dropdown { + display: none; /* Hidden by default */ + position: absolute; + top: 50px; /* Below the avatar */ + right: -20px; /* Canh sát mép màn hình, bù trừ padding của top-bar */ + background: rgba(30, 30, 30, 0.98); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); + border-radius: 8px; + padding: 10px 0; /* Remove horizontal padding to let hover cover full width */ + width: auto; /* Tự động điều chỉnh theo nội dung */ + z-index: 1600; /* Above top-bar */ + border: 1px solid rgba(255, 255, 255, 0.1); + min-width: 180px; /* Đảm bảo không quá nhỏ */ +} + +#user-dropdown.show { + display: block; +} + +#user-dropdown h3 { + margin: 10px 20px; + font-size: 16px; + color: #fff; /* Giữ màu trắng cho các tiêu đề khác trong dropdown nếu có */ + /* Nếu đây là tiêu đề "Welcome", nó sẽ bị ẩn bởi quy tắc bên dưới */ +} + +#user-dropdown input { + width: calc(100% - 40px); + margin: 0 20px 10px 20px; + padding: 8px; + border: 1px solid #444; + background: #222; + color: #fff; + border-radius: 4px; +} + +#user-dropdown .btn-group { + display: flex; + flex-direction: column; /* Sắp xếp theo cột */ + gap: 0; +} + +#user-dropdown .btn-group button, #user-dropdown button { + width: 100%; + padding: 10px 15px; /* Giảm padding để hộp nhỏ gọn hơn */ + background-color: transparent; /* Không sử dụng background button */ + color: #ccc; + text-align: left; /* Căn lề trái giống menu text */ + border: none; + border-radius: 0; + cursor: pointer; + font-weight: 500; + font-size: 14px; + transition: all 0.2s ease; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +#user-dropdown .btn-group button:hover, #user-dropdown button:hover { + background-color: rgba(255, 255, 255, 0.1); /* Hiệu ứng hover nhẹ */ + color: #fff; +} + +#user-dropdown button:last-child { + border-bottom: none; +} + +/* Dashboard Overlay */ +#dashboard-overlay { + /* Inherits .modal-overlay styles */ + z-index: 4500; /* Above all other modals */ +} + +.dashboard-content { + /* Các thuộc tính đã được sửa ở lần trước */ + max-width: 800px !important; /* Mở rộng chiều rộng tối đa lên 1800px */ + width: 95% !important; + height: 85vh; + overflow: hidden; /* Cắt bỏ phần tràn nếu có */ + position: relative; /* Làm mốc cho nút close-btn absolute */ + display: flex !important; + flex-direction: row !important; /* Ép buộc luôn nằm ngang */ + flex-wrap: nowrap; /* Ngăn chặn các thành phần nhảy xuống hàng dọc */ + gap: 0; /* Loại bỏ khoảng hở giữa 2 pane để tạo khối thống nhất */ + background: transparent !important; /* Loại bỏ nền chung của panel */ + box-shadow: none !important; /* Loại bỏ bóng chung */ + padding: 0; +} + +.dashboard-tabs { + /* Các thuộc tính đã được sửa ở lần trước */ + display: flex; + flex-direction: column; /* Danh mục menu dọc */ + background: rgba(30, 30, 30, 0.9); + backdrop-filter: blur(10px); + width: 220px; + flex-shrink: 0; /* Không cho phép menu bị thu nhỏ */ + border-radius: 15px 0 0 15px; /* Chỉ bo góc bên trái */ + padding: 20px 10px; + gap: 2px; /* Khoảng cách hẹp giữa các dòng menu */ + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + height: 100%; /* Đảm bảo sidebar chiếm toàn bộ chiều cao của dashboard-content */ + overflow-y: auto; /* Cho phép cuộn nếu nội dung sidebar quá dài */ +} + +.dashboard-tabs .tab-btn { + /* Các thuộc tính đã được sửa ở lần trước */ + padding: 12px 15px; + border: none; + border-radius: 10px; + background: none; + cursor: pointer; + font-weight: 400; + color: #aaa; + text-align: left; + transition: all 0.3s ease; + flex: none; /* Ngăn button thay đổi kích thước theo chiều cao của dashboard */ +} + +.dashboard-tabs .tab-btn.active { + /* Các thuộc tính đã được sửa ở lần trước */ + color: #fff; + background: rgba(255, 255, 255, 0.1); + box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.05); +} + +.dashboard-tabs .tab-btn:hover:not(.active) { + background: rgba(255, 255, 255, 0.05); + color: #fff; +} + +#dashboard-tab-content { + flex: 1; /* Quan trọng: Giúp phần nội dung chiếm toàn bộ không gian còn lại bên phải */ + min-width: 0; /* Ngăn chặn nội dung bên trong đẩy bung layout */ + height: 100%; + display: block; + overflow-y: auto; +} + +.dashboard-tab-pane { + display: none !important; /* Ẩn tuyệt đối các tab không hoạt động */ + width: 100%; + height: 100%; /* Đảm bảo mỗi tab pane chiếm toàn bộ chiều cao của #dashboard-tab-content */ + background: rgba(30, 30, 30, 0.9); /* Đồng nhất màu tối với sidebar */ + color: #fff; /* Chuyển màu chữ sang trắng cho dark mode */ + backdrop-filter: blur(5px); + border-radius: 0 15px 15px 0; /* Chỉ bo góc bên phải */ + padding: 30px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + overflow-y: auto; + border: 1px solid rgba(255, 255, 255, 0.1); + border-left: 1px solid rgba(255, 255, 255, 0.05); /* Đường ngăn cách mờ giữa 2 pane */ +} + +.dashboard-tab-pane.active { + display: block !important; /* Chỉ tab active mới được phép hiển thị */ +} + +/* Điều chỉnh các phần tử bên trong tab pane cho phù hợp với nền tối */ +.dashboard-tab-pane h3 { + color: #fff; + margin-bottom: 20px; +} + +.dashboard-tab-pane label { + color: #ccc; +} + +/* Dashboard List Styles */ +.dashboard-list { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 10px; +} + +.dashboard-item { + background: rgba(255, 255, 255, 0.05); + padding: 15px; + border-radius: 10px; + display: flex; + justify-content: space-between; + align-items: center; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: background 0.2s; +} + +.dashboard-item:hover { + background: rgba(255, 255, 255, 0.08); +} + +.item-info { + flex: 1; /* Chiếm toàn bộ không gian bên trái */ + display: flex; + flex-direction: column; + gap: 4px; + padding-right: 20px; /* Khoảng cách an toàn với các nút bấm */ +} + +.item-info strong { color: #fff; font-size: 16px; } +.item-info span { color: #888; font-size: 13px; } + +.item-actions { display: flex; gap: 10px; flex-shrink: 0; } +.item-actions button { + padding: 8px 16px; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + font-size: 13px; + transition: opacity 0.2s; +} + +.edit-btn { background: #007bff; color: white; } +.delete-btn { background: #dc3545; color: white; } +.item-actions button:hover { opacity: 0.9; } + +/* Tùy chỉnh các dòng trong Sidebar */ +.sidebar-user-header { + display: flex; + align-items: center; + gap: 12px; + padding: 0 10px 10px 10px; +} + +.avatar-circle { + width: 45px; + height: 45px; + border-radius: 50%; + background: #007bff; + color: white; + display: flex; + justify-content: center; + align-items: center; + font-weight: 600; + font-size: 20px; +} + +.user-name-text { + font-size: 16px; + font-weight: 600; + color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.user-status-text { + font-size: 13px; + color: #888; + padding: 0 10px 15px 10px; +} + +.sidebar-divider { + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.1); + margin: 5px 10px 15px 10px; +} + +.dashboard-tabs .tab-btn.logout-item { + margin-top: 10px; + color: #ff4d4d; +} + +.dashboard-tabs .tab-btn.logout-item:hover { + background: rgba(255, 77, 77, 0.1); + color: #ff4d4d; +} + +.admin-only { + /* Style for admin-specific elements, or hide by default */ + display: none; +} .modal-content { background: #fff; padding: 20px; diff --git a/frontend/favicon.ico b/frontend/favicon.ico new file mode 100644 index 0000000..0a42af9 Binary files /dev/null and b/frontend/favicon.ico differ diff --git a/frontend/index.html b/frontend/index.html index dfb08c7..e0ce405 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -19,20 +19,111 @@
- -
-
-

Login / Register

- - -
- - + +
+
+

Virtual 3D Tour Map

+
+
+
+ ? +
+
- + + + diff --git a/frontend/js/main_map.js b/frontend/js/main_map.js index d640334..8e03e59 100644 --- a/frontend/js/main_map.js +++ b/frontend/js/main_map.js @@ -7,11 +7,15 @@ let currentSceneId = null; let previousSceneId = null; let miniMap = null; let miniMapMarker = null; +let systemSettings = { timezone: 'Asia/Ho_Chi_Minh', language: 'vi' }; // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { try { console.log("--- Bắt đầu khởi tạo Frontend ---"); + // Ư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(); @@ -22,17 +26,140 @@ document.addEventListener('DOMContentLoaded', () => { // Đảm bảo map đã sẵn sàng trước khi nạp data if (map) { - loadScenes().then(() => { - console.log("4. Đang chuẩn bị khôi phục Scene cũ (nếu có)..."); - // Chỉ khôi phục khi bản đồ đã nạp xong các marker - setTimeout(restoreActiveScene, 500); - }); + // 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) { + document.getElementById('profile-avatar-initials').innerText = username.charAt(0).toUpperCase(); + document.getElementById('profile-username-display').innerText = username; + document.getElementById('profile-status-display').innerText = role || 'Thành viên'; // Hiển thị vai trò làm trạng thái + } +} + +/** + * 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) { + document.getElementById('profile-avatar-initials').innerText = username.charAt(0).toUpperCase(); + document.getElementById('profile-username-display').innerText = username; + document.getElementById('profile-status-display').innerText = role || 'Thành viên'; // Hiển thị vai trò làm trạng thái + } +} + +/** + * 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 */ @@ -50,12 +177,14 @@ function initMap() { if (isNaN(startLat)) startLat = 21.0285; if (isNaN(startLng)) startLng = 105.8542; if (isNaN(startZoom)) startZoom = 13; - - map = L.map('map', { zoomControl: true }).setView([startLat, startLng], startZoom); + + // 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: '© OpenStreetMap contributors' + // 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ẹ) @@ -88,6 +217,9 @@ function initMap() { 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 @@ -127,14 +259,25 @@ function checkAuthStatus() { const authGuest = document.getElementById('auth-guest'); const authLoggedIn = document.getElementById('auth-logged-in'); + const avatarInitials = document.getElementById('avatar-initials'); + if (token && username) { - authGuest.style.display = 'none'; - authLoggedIn.style.display = 'block'; - document.getElementById('logged-username').innerText = username; - document.getElementById('logged-role').innerText = role || 'Thành viên'; + authGuest.style.display = 'none'; // Hide login form + authLoggedIn.style.display = 'block'; // Show welcome message and buttons + avatarInitials.innerText = username.charAt(0).toUpperCase(); + + // Chỉ hiển thị các NÚT BẤM menu admin trong sidebar + const adminButtons = document.querySelectorAll('.dashboard-tabs .admin-only'); + if (role === 'Chủ sở hữu' || role === 'admin') { + adminButtons.forEach(btn => btn.style.display = 'block'); + } else { + adminButtons.forEach(btn => btn.style.display = 'none'); + } } else { - authGuest.style.display = 'block'; - authLoggedIn.style.display = 'none'; + 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 } } @@ -166,6 +309,7 @@ async function handleLogin() { localStorage.setItem('userId', data.user.id); checkAuthStatus(); + toggleDropdown(); // Đóng dropdown sau khi đăng nhập loadScenes(); // Reload scenes to show member/private scenes alert('Logged in successfully!'); } catch (error) { @@ -219,6 +363,7 @@ function handleLogout() { } checkAuthStatus(); + toggleDropdown(); // Đóng dropdown sau khi đăng xuất loadScenes(); // Reload scenes to filter out private ones alert('Logged out successfully'); } @@ -411,7 +556,7 @@ async function loadScenes() { }); // Tạo nội dung thông tin khi Hover (Tooltip) - const createdDate = scene.assetId?.createdAt ? new Date(scene.assetId.createdAt).toLocaleDateString('vi-VN') : 'N/A'; + const createdDate = formatSystemDate(scene.assetId?.createdAt); const tooltipContent = `
${sceneName}
@@ -518,6 +663,7 @@ function openEditSceneModal(scene) { * Deletes a scene via API */ async function deleteScene(sceneId) { + if (!confirm('Bạn có chắc chắn muốn xóa scene này?')) return; const token = localStorage.getItem('jwt'); try { const response = await fetch(`${API_BASE_URL}/scenes/${sceneId}`, { @@ -527,6 +673,7 @@ async function deleteScene(sceneId) { if (!response.ok) throw new Error('Failed to delete scene'); alert('Scene deleted successfully'); loadScenes(); + if (document.getElementById('tab-my-scenes').classList.contains('active')) loadMyScenes(); } catch (error) { alert(error.message); } @@ -939,3 +1086,119 @@ async function deleteHotspot(hotspotId) { alert(e.message); } } + +/** + * 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'); + 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 item = document.createElement('div'); + item.className = 'dashboard-item'; + item.innerHTML = ` +
+ ${scene.name || scene.title} + Quyền: ${scene.privacy} - Ngày tạo: ${formatSystemDate(scene.createdAt)} +
+
+ + +
+ `; + listContainer.appendChild(item); + // Gán sự kiện sửa bằng code để truyền object scene an toàn + document.getElementById(`edit-${scene._id}`).onclick = () => openEditSceneModal(scene); + }); + } catch (e) { + listContainer.innerHTML = `

Lỗi: ${e.message}

`; + } +} + +/** + * Opens a specific tab within the dashboard. + * @param {string} tabName - The ID of the tab pane to open (e.g., 'profile', 'my-scenes'). + */ +function openDashboardTab(tabName) { + // Ẩn tất cả các tab pane + document.querySelectorAll('.dashboard-tab-pane').forEach(pane => { + pane.classList.remove('active'); + }); + // Bỏ active khỏi tất cả các nút tab + document.querySelectorAll('.dashboard-tabs .tab-btn').forEach(btn => { + btn.classList.remove('active'); + }); + + // Hiển thị tab pane được chọn + const selectedPane = document.getElementById(`tab-${tabName}`); + if (selectedPane) { + selectedPane.classList.add('active'); + // Nếu là tab profile, cập nhật nội dung + if (tabName === 'profile') { + updateProfileTabContent(); + } + if (tabName === 'my-scenes') { + loadMyScenes(); + } + } + + // Đánh dấu nút tab được chọn là active + const selectedTabButton = document.querySelector(`.dashboard-tabs .tab-btn[onclick="openDashboardTab('${tabName}')"]`); + if (selectedTabButton) { + selectedTabButton.classList.add('active'); + } + + // Cập nhật avatar initials khi mở dashboard + const username = localStorage.getItem('username'); + if (username) { + document.getElementById('avatar-initials').innerText = username.charAt(0).toUpperCase(); + } +}