Cập nhật chế độ quản lí scene trong dashboard của người dùng
This commit is contained in:
+57
-10
@@ -568,21 +568,67 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.delete('/scenes/:id', protect, async (req, res) => {
|
router.delete('/scenes/:id', protect, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const scene = await Scene.findById(req.params.id);
|
const rootSceneId = req.params.id;
|
||||||
if (!scene || scene.createdBy.toString() !== req.user._id.toString()) {
|
const rootScene = await Scene.findById(rootSceneId);
|
||||||
return res.status(403).json({ message: 'Not authorized' });
|
|
||||||
|
if (!rootScene) {
|
||||||
|
return res.status(404).json({ message: 'Scene không tồn tại' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete physical file if exists
|
// Kiểm tra quyền: Người tạo hoặc Admin
|
||||||
const asset = await Asset.findById(scene.assetId);
|
const isAdmin = req.user.role === 'Chủ sở hữu' || req.user.role === 'admin';
|
||||||
if (asset && fs.existsSync(asset.filePath)) {
|
const isOwner = rootScene.createdBy.toString() === req.user._id.toString();
|
||||||
fs.unlinkSync(asset.filePath);
|
|
||||||
|
if (!isAdmin && !isOwner) {
|
||||||
|
return res.status(403).json({ message: 'Bạn không có quyền xóa scene này' });
|
||||||
}
|
}
|
||||||
|
|
||||||
await Asset.findByIdAndDelete(scene.assetId);
|
// 1. Tìm tất cả scene con dây chuyền (BFS)
|
||||||
await Scene.findByIdAndDelete(req.params.id);
|
let scenesToDelete = [rootSceneId.toString()];
|
||||||
|
let queue = [rootSceneId.toString()];
|
||||||
|
|
||||||
res.json({ message: 'Scene deleted successfully' });
|
while (queue.length > 0) {
|
||||||
|
const parentId = queue.shift();
|
||||||
|
const childHotspots = await Hotspot.find({ parent_scene_id: parentId });
|
||||||
|
for (const hs of childHotspots) {
|
||||||
|
if (hs.target_scene_id) {
|
||||||
|
const targetIdStr = hs.target_scene_id.toString();
|
||||||
|
if (!scenesToDelete.includes(targetIdStr)) {
|
||||||
|
scenesToDelete.push(targetIdStr);
|
||||||
|
queue.push(targetIdStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Xử lý xóa Asset và File vật lý cho toàn bộ danh sách
|
||||||
|
const scenes = await Scene.find({ _id: { $in: scenesToDelete } });
|
||||||
|
const assetIds = scenes.map(s => s.assetId).filter(id => id);
|
||||||
|
const assets = await Asset.find({ _id: { $in: assetIds } });
|
||||||
|
|
||||||
|
for (const asset of assets) {
|
||||||
|
if (asset.filePath && fs.existsSync(asset.filePath)) {
|
||||||
|
try { fs.unlinkSync(asset.filePath); } catch (e) { console.error(e); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Xóa Hotspot: Cả hotspot xuất phát từ và trỏ đến các scene bị xóa
|
||||||
|
await Hotspot.deleteMany({
|
||||||
|
$or: [
|
||||||
|
{ parent_scene_id: { $in: scenesToDelete } },
|
||||||
|
{ target_scene_id: { $in: scenesToDelete } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Xóa dữ liệu trong DB
|
||||||
|
await Asset.deleteMany({ _id: { $in: assetIds } });
|
||||||
|
await Scene.deleteMany({ _id: { $in: scenesToDelete } });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: scenesToDelete.length > 1
|
||||||
|
? `Đã xóa vĩnh viễn scene và ${scenesToDelete.length - 1} scene con liên quan.`
|
||||||
|
: 'Đã xóa scene thành công.'
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ message: error.message });
|
res.status(500).json({ message: error.message });
|
||||||
}
|
}
|
||||||
@@ -633,6 +679,7 @@ router.put('/me/profile', protect, async (req, res) => {
|
|||||||
router.get('/me/scenes', protect, async (req, res) => {
|
router.get('/me/scenes', protect, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const scenes = await Scene.find({ createdBy: req.user._id })
|
const scenes = await Scene.find({ createdBy: req.user._id })
|
||||||
|
.populate('createdBy', 'username')
|
||||||
.populate('assetId')
|
.populate('assetId')
|
||||||
.sort({ createdAt: -1 });
|
.sort({ createdAt: -1 });
|
||||||
res.json(scenes);
|
res.json(scenes);
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 MiB |
@@ -755,6 +755,59 @@ html, body {
|
|||||||
.delete-btn-small { background: #dc3545; color: white; }
|
.delete-btn-small { background: #dc3545; color: white; }
|
||||||
.media-actions button:hover { opacity: 0.8; }
|
.media-actions button:hover { opacity: 0.8; }
|
||||||
|
|
||||||
|
/* --- Scene Card Dashboard --- */
|
||||||
|
.scene-card {
|
||||||
|
position: relative;
|
||||||
|
height: 220px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
border-color: rgba(0, 123, 255, 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%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-card-info strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-card-info .scene-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #ccc;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-card-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #aaa;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Edit Metadata Modal (Dark Theme) --- */
|
/* --- Edit Metadata Modal (Dark Theme) --- */
|
||||||
#edit-scene-metadata-modal .modal-content {
|
#edit-scene-metadata-modal .modal-content {
|
||||||
background: rgba(30, 30, 30, 0.95);
|
background: rgba(30, 30, 30, 0.95);
|
||||||
|
|||||||
@@ -287,6 +287,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Scene Confirmation Modal -->
|
||||||
|
<div id="delete-scene-confirm-modal" class="modal-overlay">
|
||||||
|
<div class="modal-content action-modal-content logout-modal-dark">
|
||||||
|
<h2 style="color: #fff; margin-bottom: 10px;">Xác nhận xóa Scene</h2>
|
||||||
|
<p style="color: #ccc; margin-bottom: 25px;">Bạn có chắc chắn muốn xóa Scene này? Toàn bộ các scene con liên kết và các hotspot sẽ bị xóa vĩnh viễn khỏi hệ thống.</p>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button onclick="confirmDeleteScene()" class="delete-btn-large">Xóa vĩnh viễn</button>
|
||||||
|
<button onclick="closeDeleteSceneModal()" class="edit-btn-large" style="background: #6c757d;">Hủy bỏ</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Delete Asset Confirmation Modal -->
|
<!-- Delete Asset Confirmation Modal -->
|
||||||
<div id="delete-asset-confirm-modal" class="modal-overlay">
|
<div id="delete-asset-confirm-modal" class="modal-overlay">
|
||||||
<div class="modal-content action-modal-content logout-modal-dark">
|
<div class="modal-content action-modal-content logout-modal-dark">
|
||||||
|
|||||||
+93
-44
@@ -10,6 +10,8 @@ let miniMapMarker = null;
|
|||||||
let systemSettings = { timezone: 'Asia/Ho_Chi_Minh', language: 'vi' };
|
let systemSettings = { timezone: 'Asia/Ho_Chi_Minh', language: 'vi' };
|
||||||
let returnToDashboardAfterEdit = false;
|
let returnToDashboardAfterEdit = false;
|
||||||
let assetIdToDelete = null;
|
let assetIdToDelete = null;
|
||||||
|
let sceneIdToDelete = null;
|
||||||
|
let dashboardReturnTab = 'media-library';
|
||||||
let editMiniMap = null;
|
let editMiniMap = null;
|
||||||
let editMiniMapMarker = null;
|
let editMiniMapMarker = null;
|
||||||
let currentEditingScene = null; // Lưu object scene đang sửa để quản lý chia sẻ
|
let currentEditingScene = null; // Lưu object scene đang sửa để quản lý chia sẻ
|
||||||
@@ -139,23 +141,15 @@ function updateProfileTabContent() {
|
|||||||
const role = localStorage.getItem('role');
|
const role = localStorage.getItem('role');
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
document.getElementById('profile-avatar-initials').innerText = username.charAt(0).toUpperCase();
|
const avatar = document.getElementById('profile-avatar-initials');
|
||||||
document.getElementById('profile-username-display').innerText = username;
|
const userDisplay = document.getElementById('profile-username-display');
|
||||||
document.getElementById('profile-status-display').innerText = role || 'Thành viên'; // Hiển thị vai trò làm trạng thái
|
const statusDisplay = document.getElementById('profile-status-display');
|
||||||
}
|
const userInput = document.getElementById('profile-username');
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (avatar) avatar.innerText = username.charAt(0).toUpperCase();
|
||||||
* Cập nhật nội dung tab Hồ sơ với thông tin người dùng
|
if (userDisplay) userDisplay.innerText = username;
|
||||||
*/
|
if (statusDisplay) statusDisplay.innerText = role || 'Thành viên';
|
||||||
function updateProfileTabContent() {
|
if (userInput) userInput.value = username;
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,9 +452,10 @@ function closeModal() {
|
|||||||
document.getElementById('shared-with-group').style.display = 'none';
|
document.getElementById('shared-with-group').style.display = 'none';
|
||||||
|
|
||||||
if (returnToDashboardAfterEdit) {
|
if (returnToDashboardAfterEdit) {
|
||||||
|
const targetTab = dashboardReturnTab;
|
||||||
returnToDashboardAfterEdit = false;
|
returnToDashboardAfterEdit = false;
|
||||||
openDashboard();
|
openDashboard();
|
||||||
openDashboardTab('media-library');
|
openDashboardTab(targetTab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,11 +695,10 @@ async function handleEditDeleteScene(scene) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Gán sự kiện cho nút Xóa
|
// Gán sự kiện cho nút Xóa
|
||||||
deleteBtn.onclick = async () => {
|
deleteBtn.onclick = () => {
|
||||||
if (confirm(`Cảnh báo: Thao tác này sẽ xóa vĩnh viễn scene "${scene.title}" và tệp tin ảnh 360 liên quan. Bạn có chắc chắn?`)) {
|
returnToDashboardAfterEdit = false; // Đảm bảo không mở dashboard nếu xóa từ map
|
||||||
closeActionModal();
|
closeActionModal();
|
||||||
await deleteScene(scene._id);
|
deleteScene(scene._id);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -737,24 +731,49 @@ function openEditSceneModal(scene) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a scene via API
|
* Mở modal xác nhận xóa scene
|
||||||
*/
|
*/
|
||||||
async function deleteScene(sceneId) {
|
window.deleteScene = function(sceneId) {
|
||||||
if (!confirm('Bạn có chắc chắn muốn xóa scene này?')) return;
|
sceneIdToDelete = sceneId;
|
||||||
|
document.getElementById('delete-scene-confirm-modal').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');
|
const token = localStorage.getItem('jwt');
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/scenes/${sceneId}`, {
|
const response = await fetch(`${API_BASE_URL}/scenes/${sceneIdToDelete}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error('Failed to delete scene');
|
const data = await response.json();
|
||||||
alert('Scene deleted successfully');
|
|
||||||
|
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();
|
loadScenes();
|
||||||
if (document.getElementById('tab-my-scenes').classList.contains('active')) loadMyScenes();
|
if (document.getElementById('tab-my-scenes').classList.contains('active')) {
|
||||||
|
loadMyScenes();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(error.message);
|
alert("Lỗi khi xóa: " + error.message);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches secure scene details and triggers the Panorama viewer
|
* Fetches secure scene details and triggers the Panorama viewer
|
||||||
@@ -1219,6 +1238,8 @@ function closeDashboard() {
|
|||||||
async function loadMyScenes() {
|
async function loadMyScenes() {
|
||||||
const token = localStorage.getItem('jwt');
|
const token = localStorage.getItem('jwt');
|
||||||
const listContainer = document.getElementById('my-scenes-list');
|
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>';
|
listContainer.innerHTML = '<p>Đang tải danh sách...</p>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1235,23 +1256,48 @@ async function loadMyScenes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scenes.forEach(scene => {
|
scenes.forEach(scene => {
|
||||||
const item = document.createElement('div');
|
const assetId = scene.assetId?._id || scene.assetId;
|
||||||
item.className = 'dashboard-item';
|
let thumbUrl = `${API_BASE_URL}/assets/view/${assetId}`;
|
||||||
item.innerHTML = `
|
if (token) thumbUrl += `?token=${token}`;
|
||||||
<div class="item-info">
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'scene-card';
|
||||||
|
card.style.backgroundImage = `url('${thumbUrl}')`;
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="scene-card-overlay">
|
||||||
|
<div class="scene-card-info">
|
||||||
<strong>${scene.name || scene.title}</strong>
|
<strong>${scene.name || scene.title}</strong>
|
||||||
<span>Quyền: ${scene.privacy} - Ngày tạo: ${formatSystemDate(scene.createdAt)}</span>
|
<p class="scene-desc">${scene.description || 'Không có mô tả'}</p>
|
||||||
|
<div class="scene-card-meta">
|
||||||
|
<span>🔒 ${scene.privacy}</span>
|
||||||
|
<span>👤 ${scene.createdBy?.username || 'Bạn'}</span>
|
||||||
|
<span>📅 ${formatSystemDate(scene.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="media-actions" style="border: none; padding: 0;">
|
||||||
|
<button class="edit-btn-small" id="edit-scene-${scene._id}">Sửa</button>
|
||||||
|
<button class="delete-btn-small" id="delete-scene-${scene._id}">Xóa</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-actions">
|
|
||||||
<button class="edit-btn" id="edit-${scene._id}">Sửa</button>
|
|
||||||
<button class="delete-btn" onclick="deleteScene('${scene._id}')">Xóa</button>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
listContainer.appendChild(item);
|
listContainer.appendChild(card);
|
||||||
// Gán sự kiện sửa bằng code để truyền object scene an toàn
|
|
||||||
document.getElementById(`edit-${scene._id}`).onclick = () => {
|
// Xử lý nút Sửa: Logic đóng dashboard -> mở modal -> quay lại dashboard
|
||||||
returnToDashboardAfterEdit = false;
|
document.getElementById(`edit-scene-${scene._id}`).onclick = () => {
|
||||||
openEditSceneModal(scene);
|
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) {
|
} catch (e) {
|
||||||
@@ -1313,6 +1359,7 @@ async function loadMyAssets() {
|
|||||||
const editButton = document.createElement('button');
|
const editButton = document.createElement('button');
|
||||||
editButton.className = 'edit-btn-small';
|
editButton.className = 'edit-btn-small';
|
||||||
editButton.innerText = 'Sửa Scene';
|
editButton.innerText = 'Sửa Scene';
|
||||||
|
dashboardReturnTab = 'media-library';
|
||||||
const isChild = asset.parentScenes && asset.parentScenes.length > 0;
|
const isChild = asset.parentScenes && asset.parentScenes.length > 0;
|
||||||
editButton.addEventListener('click', () => openEditFromMedia(scene, isChild));
|
editButton.addEventListener('click', () => openEditFromMedia(scene, isChild));
|
||||||
card.querySelector('.media-actions').appendChild(editButton);
|
card.querySelector('.media-actions').appendChild(editButton);
|
||||||
@@ -1406,6 +1453,7 @@ window.openEditFromMedia = function(scene, isChild = false) {
|
|||||||
alert("Không thể chỉnh sửa: Ảnh này không được gắn với một Scene hợp lệ.");
|
alert("Không thể chỉnh sửa: Ảnh này không được gắn với một Scene hợp lệ.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
dashboardReturnTab = 'media-library';
|
||||||
returnToDashboardAfterEdit = true;
|
returnToDashboardAfterEdit = true;
|
||||||
closeDashboard();
|
closeDashboard();
|
||||||
openEditMetadataModal(scene, isChild);
|
openEditMetadataModal(scene, isChild);
|
||||||
@@ -1454,9 +1502,10 @@ window.openEditMetadataModal = function(scene, isChild = false) {
|
|||||||
function closeEditMetadataModal() {
|
function closeEditMetadataModal() {
|
||||||
document.getElementById('edit-scene-metadata-modal').style.display = 'none';
|
document.getElementById('edit-scene-metadata-modal').style.display = 'none';
|
||||||
if (returnToDashboardAfterEdit) {
|
if (returnToDashboardAfterEdit) {
|
||||||
|
const targetTab = dashboardReturnTab;
|
||||||
returnToDashboardAfterEdit = false;
|
returnToDashboardAfterEdit = false;
|
||||||
openDashboard();
|
openDashboard();
|
||||||
openDashboardTab('media-library');
|
openDashboardTab(targetTab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user