diff --git a/backend/routes/apiRoutes.js b/backend/routes/apiRoutes.js index fdb29c8..7c29927 100644 --- a/backend/routes/apiRoutes.js +++ b/backend/routes/apiRoutes.js @@ -568,21 +568,67 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => { */ router.delete('/scenes/:id', protect, async (req, res) => { try { - const scene = await Scene.findById(req.params.id); - if (!scene || scene.createdBy.toString() !== req.user._id.toString()) { - return res.status(403).json({ message: 'Not authorized' }); + const rootSceneId = req.params.id; + const rootScene = await Scene.findById(rootSceneId); + + if (!rootScene) { + return res.status(404).json({ message: 'Scene không tồn tại' }); } - // Delete physical file if exists - const asset = await Asset.findById(scene.assetId); - if (asset && fs.existsSync(asset.filePath)) { - fs.unlinkSync(asset.filePath); + // Kiểm tra quyền: Người tạo hoặc Admin + const isAdmin = req.user.role === 'Chủ sở hữu' || req.user.role === 'admin'; + const isOwner = rootScene.createdBy.toString() === req.user._id.toString(); + + 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); - await Scene.findByIdAndDelete(req.params.id); + // 1. Tìm tất cả scene con dây chuyền (BFS) + 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) { 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) => { try { const scenes = await Scene.find({ createdBy: req.user._id }) + .populate('createdBy', 'username') .populate('assetId') .sort({ createdAt: -1 }); res.json(scenes); diff --git a/backend/uploads/processed_1780896356863_9cae060e.JPG.jpg b/backend/uploads/processed_1780896356863_9cae060e.JPG.jpg deleted file mode 100644 index d9ecd60..0000000 Binary files a/backend/uploads/processed_1780896356863_9cae060e.JPG.jpg and /dev/null differ diff --git a/backend/uploads/processed_1780912760399_87af6fb5.JPG.jpg b/backend/uploads/processed_1780912760399_87af6fb5.JPG.jpg deleted file mode 100644 index debf274..0000000 Binary files a/backend/uploads/processed_1780912760399_87af6fb5.JPG.jpg and /dev/null differ diff --git a/frontend/css/style.css b/frontend/css/style.css index f9c4e64..5259a8c 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -755,6 +755,59 @@ html, body { .delete-btn-small { background: #dc3545; color: white; } .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-scene-metadata-modal .modal-content { background: rgba(30, 30, 30, 0.95); diff --git a/frontend/index.html b/frontend/index.html index 4c0418a..9f32bcb 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -287,6 +287,18 @@ + +
+