diff --git a/backend/routes/apiRoutes.js b/backend/routes/apiRoutes.js index 6461ae1..aab4a8a 100644 --- a/backend/routes/apiRoutes.js +++ b/backend/routes/apiRoutes.js @@ -583,13 +583,95 @@ router.get('/me/scenes', protect, async (req, res) => { */ router.get('/me/assets', protect, async (req, res) => { try { - const assets = await Asset.find({ uploadedBy: req.user._id }).sort({ createdAt: -1 }); + // Sử dụng Aggregation để lấy Asset kèm thông tin Scene và Parent Scene + const query = req.user.role === 'Chủ sở hữu' ? {} : { uploadedBy: req.user._id }; + + const assets = await Asset.aggregate([ + { $match: query }, + { + $lookup: { + from: 'scenes', // Tên collection trong DB (thường là số nhiều) + localField: '_id', + foreignField: 'assetId', + as: 'linkedScene' + } + }, + { $unwind: { path: '$linkedScene', preserveNullAndEmptyArrays: true } }, + { + $lookup: { + from: 'hotspots', + localField: 'linkedScene._id', + foreignField: 'target_scene_id', + as: 'incomingHotspots' + } + }, + { + $lookup: { + from: 'scenes', + localField: 'incomingHotspots.parent_scene_id', + foreignField: '_id', + as: 'parentScenes' + } + }, + { + $project: { + filePath: 0 // Bảo mật: Không trả về đường dẫn vật lý đầy đủ + } + }, + { $sort: { createdAt: -1 } } + ]); + res.json(assets); } catch (error) { res.status(500).json({ message: error.message }); } }); +/** + * @route DELETE /api/assets/:id + * @desc Xóa Asset + Xóa Scene liên quan (nếu có) + Xóa file vật lý + * @access Private (Chỉ người upload hoặc Admin) + */ +router.delete('/assets/:id', protect, async (req, res) => { + try { + const asset = await Asset.findById(req.params.id); + if (!asset) return res.status(404).json({ message: 'Ảnh không tồn tại' }); + + // Kiểm tra quyền: Người upload hoặc Admin (Chủ sở hữu) + const isOwner = asset.uploadedBy.toString() === req.user._id.toString(); + const isAdmin = req.user.role === 'Chủ sở hữu' || req.user.role === 'admin'; + + if (!isOwner && !isAdmin) { + return res.status(403).json({ message: 'Bạn không có quyền xóa tập tin này' }); + } + + // 1. Tìm và xóa Scene liên quan nếu có + const linkedScene = await Scene.findOne({ assetId: asset._id }); + if (linkedScene) { + // Xóa toàn bộ hotspot trỏ đến hoặc xuất phát từ scene này + await Hotspot.deleteMany({ + $or: [ + { parent_scene_id: linkedScene._id }, + { target_scene_id: linkedScene._id } + ] + }); + await Scene.findByIdAndDelete(linkedScene._id); + } + + // 2. Xóa file vật lý trên disk + if (fs.existsSync(asset.filePath)) { + fs.unlinkSync(asset.filePath); + } + + // 3. Xóa Asset trong DB + await Asset.findByIdAndDelete(req.params.id); + + res.json({ message: 'Đã xóa ảnh và các dữ liệu liên quan thành công' }); + } 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) diff --git a/frontend/css/style.css b/frontend/css/style.css index 37d48f1..cb70bb3 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -689,3 +689,64 @@ html, body { visibility: visible; top: -55px; /* Nhích lên một chút khi xuất hiện */ } + +/* --- Media Library Dashboard --- */ +.dashboard-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 20px; + margin-top: 15px; +} + +.media-card { + background: rgba(255, 255, 255, 0.05); + border-radius: 12px; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + flex-direction: column; +} + +.media-card.trash-item { border-color: rgba(220, 53, 69, 0.4); } + +.media-thumb { + position: relative; + width: 100%; + height: 120px; + background: #000; +} + +.media-thumb img { width: 100%; height: 100%; object-fit: cover; } + +.badge-trash { + position: absolute; top: 8px; right: 8px; + background: #dc3545; color: white; font-size: 10px; + padding: 2px 6px; border-radius: 4px; font-weight: bold; +} + +.media-info { padding: 12px; flex: 1; } +.media-info strong { display: block; font-size: 15px; margin-bottom: 5px; } +.media-info .desc { font-size: 12px; color: #aaa; margin-bottom: 8px; } +.media-info .parent-link { font-size: 11px; color: #007bff; margin-bottom: 5px; } +.media-info .date { font-size: 10px; color: #666; } + +.media-actions { + padding: 10px; + display: flex; + gap: 8px; + border-top: 1px solid rgba(255, 255, 255, 0.05); +} + +.edit-btn-small, .delete-btn-small { + flex: 1; + padding: 6px; + font-size: 11px; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: 600; +} + +.edit-btn-small { background: #28a745; color: white; } +.delete-btn-small { background: #dc3545; color: white; } +.media-actions button:hover { opacity: 0.8; } diff --git a/frontend/js/main_map.js b/frontend/js/main_map.js index 884da08..a9db2cc 100644 --- a/frontend/js/main_map.js +++ b/frontend/js/main_map.js @@ -1200,6 +1200,114 @@ async function loadMyScenes() { } } +/** + * 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 = ` +
+ Thumbnail + ${isTrash ? 'Ảnh rác' : ''} +
+
+ ${scene ? (scene.name || scene.title) : 'Chưa gắn Scene'} +

${scene?.description || 'Không có mô tả'}

+ ${parentNames ? `` : ''} + Tải lên: ${formatSystemDate(asset.createdAt)} +
+
+
+ `; + card.innerHTML = innerHtml; + + // Add edit button and its event listener separately + if (scene && asset.uploadedBy === currentUserId) { + const editButton = document.createElement('button'); + editButton.className = 'edit-btn-small'; + editButton.innerText = 'Sửa Scene'; + editButton.addEventListener('click', () => openEditFromMedia(scene)); // Pass scene object directly + card.querySelector('.media-actions').appendChild(editButton); + } + + // Add delete button and its event listener + if (asset.uploadedBy === currentUserId || (isTrash && userRole === 'Chủ sở hữu')) { + const deleteButton = document.createElement('button'); + deleteButton.className = 'delete-btn-small'; + deleteButton.innerText = 'Xóa'; + deleteButton.addEventListener('click', () => deleteAsset(asset._id)); + card.querySelector('.media-actions').appendChild(deleteButton); + } + + gridContainer.appendChild(card); + }); + } catch (e) { + gridContainer.innerHTML = `

Lỗi nạp media: ${e.message}

`; + } +} + +/** + * Xóa ảnh khỏi kho media + */ +window.deleteAsset = async function(assetId) { + if (!confirm('Bạn có chắc chắn muốn xóa ảnh này? Nếu ảnh đang được gắn vào một cảnh, cảnh đó cũng sẽ bị xóa vĩnh viễn.')) { + return; + } + + const token = localStorage.getItem('jwt'); + try { + const res = await fetch(`${API_BASE_URL}/assets/${assetId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } + }); + const data = await res.json(); + + if (!res.ok) throw new Error(data.message); + + alert(data.message); + 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) { + alert("Lỗi khi xóa: " + e.message); + } +}; + +window.openEditFromMedia = function(scene) { + if (!scene || !scene._id) { + 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; + } + openEditSceneModal(scene); +}; + /** * Opens a specific tab within the dashboard. * @param {string} tabName - The ID of the tab pane to open (e.g., 'profile', 'my-scenes'). @@ -1225,6 +1333,9 @@ function openDashboardTab(tabName) { if (tabName === 'my-scenes') { loadMyScenes(); } + if (tabName === 'media-library') { + loadMyAssets(); + } } // Đánh dấu nút tab được chọn là active