Sửa chế độ chia sẻ privacy trực tiếp khi nhấn chuột phải lên scene
This commit is contained in:
@@ -9,6 +9,10 @@ const sceneSchema = new mongoose.Schema({
|
||||
scene_url: {
|
||||
type: String
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
assetId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Asset',
|
||||
@@ -33,10 +37,17 @@ const sceneSchema = new mongoose.Schema({
|
||||
unique: true,
|
||||
sparse: true
|
||||
},
|
||||
shareTokenExpires: {
|
||||
type: Date
|
||||
},
|
||||
sharedWith: [{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
}],
|
||||
sharedEmails: [{
|
||||
type: String,
|
||||
trim: true
|
||||
}],
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
@@ -165,6 +165,33 @@ router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/users/search
|
||||
* @desc Tìm kiếm người dùng theo username hoặc email để chia sẻ
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/users/search', protect, async (req, res) => {
|
||||
const query = req.query.q;
|
||||
if (!query || query.length < 2) return res.json([]);
|
||||
|
||||
try {
|
||||
const users = await User.find({
|
||||
$and: [
|
||||
{ _id: { $ne: req.user._id } }, // Không tìm chính mình
|
||||
{
|
||||
$or: [
|
||||
{ username: { $regex: query, $options: 'i' } },
|
||||
{ email: { $regex: query, $options: 'i' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}).select('username email').limit(10);
|
||||
res.json(users);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route GET /api/scenes
|
||||
* @desc Get all accessible scenes for the map (respecting privacy rules)
|
||||
@@ -183,7 +210,8 @@ router.get('/scenes', optionalAuth, async (req, res) => {
|
||||
{ privacy: 'member' },
|
||||
{ privacy: 'shared' }, // shareToken will be required to fetch panorama, but coordinates show on map
|
||||
{ createdBy: req.user._id },
|
||||
{ sharedWith: req.user._id }
|
||||
{ sharedWith: req.user._id },
|
||||
{ sharedEmails: req.user.email }
|
||||
]
|
||||
};
|
||||
} else {
|
||||
@@ -223,12 +251,17 @@ router.get('/scenes/:id', optionalAuth, async (req, res) => {
|
||||
return res.status(404).json({ message: 'Scene not found' });
|
||||
}
|
||||
|
||||
// Kiểm tra Token hết hạn
|
||||
const isTokenValid = scene.shareToken &&
|
||||
(!scene.shareTokenExpires || new Date() < scene.shareTokenExpires);
|
||||
|
||||
const userEmail = req.user ? req.user.email : null;
|
||||
const hasAccess =
|
||||
scene.privacy === 'public' ||
|
||||
(scene.privacy === 'member' && req.user) ||
|
||||
(scene.privacy === 'member' && req.user && (scene.sharedWith.includes(req.user._id) || (userEmail && scene.sharedEmails.includes(userEmail)))) ||
|
||||
(req.user && scene.createdBy._id.toString() === req.user._id.toString()) ||
|
||||
(req.user && scene.sharedWith.includes(req.user._id)) ||
|
||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken);
|
||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isTokenValid);
|
||||
|
||||
if (!hasAccess) {
|
||||
return res.status(403).json({ message: 'Access denied to this scene' });
|
||||
@@ -382,12 +415,17 @@ router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res
|
||||
return res.status(403).json({ message: 'Access denied' });
|
||||
}
|
||||
} else {
|
||||
// Kiểm tra Token hết hạn
|
||||
const isTokenValid = scene.shareToken &&
|
||||
(!scene.shareTokenExpires || new Date() < scene.shareTokenExpires);
|
||||
|
||||
const userEmail = req.user ? req.user.email : null;
|
||||
const hasAccess =
|
||||
scene.privacy === 'public' ||
|
||||
(scene.privacy === 'member' && req.user) ||
|
||||
(scene.privacy === 'member' && req.user && (scene.sharedWith.includes(req.user._id) || (userEmail && scene.sharedEmails.includes(userEmail)))) ||
|
||||
(req.user && scene.createdBy.toString() === req.user._id.toString()) ||
|
||||
(req.user && scene.sharedWith.includes(req.user._id)) ||
|
||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken);
|
||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken && isTokenValid);
|
||||
|
||||
if (!hasAccess) {
|
||||
return res.status(403).json({ message: 'Access denied: You do not have permission to view this asset' });
|
||||
@@ -423,7 +461,7 @@ router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res
|
||||
*/
|
||||
router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
|
||||
try {
|
||||
const { title, privacy, sharedWithUsers, lat, lng } = req.body;
|
||||
const { title, description, privacy, sharedWithUsers, sharedEmails, shareExpireDays, lat, lng } = req.body;
|
||||
const scene = await Scene.findById(req.params.id);
|
||||
|
||||
if (!scene || scene.createdBy.toString() !== req.user._id.toString()) {
|
||||
@@ -434,10 +472,27 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
|
||||
|
||||
// Update basic info
|
||||
scene.name = title || scene.name;
|
||||
scene.description = description !== undefined ? description : scene.description;
|
||||
scene.privacy = privacy || scene.privacy;
|
||||
if (lat) scene.gps.lat = parseFloat(lat);
|
||||
if (lng) scene.gps.lng = parseFloat(lng);
|
||||
|
||||
// Cập nhật danh sách chia sẻ
|
||||
if (sharedWithUsers) {
|
||||
try {
|
||||
scene.sharedWith = JSON.parse(sharedWithUsers);
|
||||
} catch (e) {
|
||||
console.error("Lỗi parse sharedWithUsers:", e);
|
||||
}
|
||||
}
|
||||
if (sharedEmails) {
|
||||
try {
|
||||
scene.sharedEmails = JSON.parse(sharedEmails);
|
||||
} catch (e) {
|
||||
console.error("Lỗi parse sharedEmails:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// LOGIC ĐỒNG BỘ QUYỀN RIÊNG TƯ (CASCADING PRIVACY)
|
||||
// Nếu quyền chia sẻ thay đổi, cập nhật toàn bộ các Scene con liên kết trực tiếp
|
||||
if (privacy && privacy !== oldPrivacy) {
|
||||
@@ -467,8 +522,18 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (privacy === 'shared' && !scene.shareToken) {
|
||||
scene.shareToken = crypto.randomBytes(24).toString('hex');
|
||||
if (privacy === 'shared') {
|
||||
if (!scene.shareToken) {
|
||||
scene.shareToken = crypto.randomBytes(24).toString('hex');
|
||||
}
|
||||
// Thiết lập ngày hết hạn nếu có truyền lên
|
||||
if (shareExpireDays && shareExpireDays !== 'never') {
|
||||
const expires = new Date();
|
||||
expires.setDate(expires.getDate() + parseInt(shareExpireDays));
|
||||
scene.shareTokenExpires = expires;
|
||||
} else if (shareExpireDays === 'never') {
|
||||
scene.shareTokenExpires = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Update image if new one is uploaded
|
||||
|
||||
+99
-1
@@ -597,9 +597,11 @@ html, body {
|
||||
#logout-confirm-modal,
|
||||
#delete-asset-confirm-modal,
|
||||
#success-modal {
|
||||
z-index: 5500; /* Cao hơn Dashboard (4500) và Close Button (5000) */
|
||||
z-index: 5500;
|
||||
}
|
||||
|
||||
#share-member-modal, #share-link-modal { z-index: 6000; }
|
||||
|
||||
/* --- Pannellum Custom Hotspot (Callout Bubble) --- */
|
||||
|
||||
/* Container chính của hotspot do Pannellum quản lý */
|
||||
@@ -752,3 +754,99 @@ html, body {
|
||||
.edit-btn-small { background: #28a745; color: white; }
|
||||
.delete-btn-small { background: #dc3545; color: white; }
|
||||
.media-actions button:hover { opacity: 0.8; }
|
||||
|
||||
/* --- Edit Metadata Modal (Dark Theme) --- */
|
||||
#edit-scene-metadata-modal .modal-content {
|
||||
background: rgba(30, 30, 30, 0.95);
|
||||
color: #fff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
#edit-scene-metadata-modal .form-group label {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
#edit-scene-metadata-modal input,
|
||||
#edit-scene-metadata-modal textarea,
|
||||
#edit-scene-metadata-modal select {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#edit-mini-map {
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* --- Privacy Settings Enhancements --- */
|
||||
.privacy-settings-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.privacy-settings-btn:hover {
|
||||
background: rgba(0, 123, 255, 0.4);
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
/* Search Dropdown */
|
||||
.search-dropdown {
|
||||
position: absolute;
|
||||
top: 100%; left: 0; width: 100%;
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #444;
|
||||
border-top: none;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.search-item {
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-item:hover { background: #3a3a3a; color: #00d4ff; }
|
||||
|
||||
/* Shared Users List */
|
||||
.shared-users-list {
|
||||
margin-top: 10px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.share-list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255,255,255,0.05);
|
||||
margin-bottom: 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.remove-share-btn {
|
||||
color: #ff4d4d;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
+109
-1
@@ -182,6 +182,111 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for Editing Scene Metadata (No Upload) -->
|
||||
<div id="edit-scene-metadata-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-btn" onclick="closeEditMetadataModal()">×</span>
|
||||
<h2 id="edit-metadata-modal-title">Sửa 3D Scene</h2>
|
||||
<form id="edit-scene-metadata-form" onsubmit="submitEditScene(event)">
|
||||
<input type="hidden" id="edit-modal-scene-id">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-modal-title">Tên Scene:</label>
|
||||
<input type="text" id="edit-modal-title" required placeholder="Tên cảnh quay">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-modal-description">Mô tả:</label>
|
||||
<textarea id="edit-modal-description" rows="3" placeholder="Mô tả chi tiết về cảnh này..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Vị trí tọa độ (Click bản đồ để đổi):</label>
|
||||
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
|
||||
<input type="text" id="edit-modal-lat" readonly>
|
||||
<input type="text" id="edit-modal-lng" readonly>
|
||||
</div>
|
||||
<div id="edit-mini-map" style="height: 200px; width: 100%; border-radius: 4px; border: 1px solid #ccc; background: #eee;"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="edit-privacy-container">
|
||||
<label for="edit-modal-privacy">Quyền riêng tư:</label>
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<select id="edit-modal-privacy" onchange="handleEditPrivacyChange()" style="flex: 1;">
|
||||
<option value="public">Public (Everyone)</option>
|
||||
<option value="private">Private (Only Me)</option>
|
||||
<option value="member">Members (Specific People)</option>
|
||||
<option value="shared">Shared via Link</option>
|
||||
</select>
|
||||
<button type="button" id="btn-edit-privacy-settings" class="privacy-settings-btn" title="Cài đặt chia sẻ" onclick="openPrivacySettingsModal()" style="display: none;">⚙️</button>
|
||||
</div>
|
||||
<div id="edit-child-privacy-info" style="display: none; color: #888; font-size: 12px; margin-top: 5px;">
|
||||
ℹ️ Cảnh con kế thừa quyền riêng tư từ cảnh cha.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer" style="padding: 0; border: none; background: transparent;">
|
||||
<button type="button" class="cancel-btn" onclick="closeEditMetadataModal()">Hủy bỏ</button>
|
||||
<button type="submit" class="save-btn">Lưu thay đổi</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Manage Shared Members -->
|
||||
<div id="share-member-modal" class="modal-overlay">
|
||||
<div class="modal-content logout-modal-dark" style="max-width: 500px;">
|
||||
<h2 style="color: #fff; margin-bottom: 15px;">Chia sẻ với thành viên</h2>
|
||||
<div class="form-group">
|
||||
<label>Thêm thành viên (Username hoặc Email):</label>
|
||||
<div style="position: relative;">
|
||||
<input type="text" id="share-user-search" placeholder="Nhập tên hoặc email..." oninput="searchUsersToShare(this.value)">
|
||||
<div id="search-results-dropdown" class="search-dropdown" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shared-list-container">
|
||||
<label>Danh sách đã chia sẻ:</label>
|
||||
<div id="current-shared-list" class="shared-users-list">
|
||||
<!-- Sẽ được fill bằng JS -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" style="padding: 20px 0 0 0; background: transparent; border: none;">
|
||||
<button onclick="closeShareMemberModal()" class="save-btn">Xong</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Shared Link -->
|
||||
<div id="share-link-modal" class="modal-overlay">
|
||||
<div class="modal-content logout-modal-dark" style="max-width: 500px; text-align: center;">
|
||||
<h2 style="color: #fff; margin-bottom: 15px;">Liên kết chia sẻ</h2>
|
||||
<p style="color: #ccc; font-size: 14px; margin-bottom: 20px;">Bất kỳ ai có liên kết này đều có thể xem cảnh quay mà không cần đăng nhập.</p>
|
||||
|
||||
<div class="form-group" style="text-align: left; margin-bottom: 20px;">
|
||||
<label style="color: #fff;">Thời hạn liên kết:</label>
|
||||
<select id="share-link-expire" style="background: #222; color: #fff; border: 1px solid #444;">
|
||||
<option value="7">7 ngày (Mặc định)</option>
|
||||
<option value="1">1 ngày</option>
|
||||
<option value="30">30 ngày</option>
|
||||
<option value="never">Vĩnh viễn</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="link-display-area">
|
||||
<input type="text" id="shared-link-input" readonly style="background: rgba(0,0,0,0.3); color: #00d4ff; border: 1px solid #444; padding: 12px; width: 100%; border-radius: 4px; font-family: monospace; font-size: 13px;">
|
||||
</div>
|
||||
|
||||
<div class="action-buttons" style="margin-top: 25px;">
|
||||
<button onclick="copySharedLink()" class="edit-btn-large" style="background: #007bff; width: 100%;">
|
||||
📋 Sao chép liên kết & Đóng
|
||||
</button>
|
||||
<button onclick="closeShareLinkModal()" class="edit-btn-large" style="background: #444; width: 100%; margin-top: 10px;">
|
||||
Đóng
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Asset Confirmation Modal -->
|
||||
<div id="delete-asset-confirm-modal" class="modal-overlay">
|
||||
<div class="modal-content action-modal-content logout-modal-dark">
|
||||
@@ -210,8 +315,11 @@
|
||||
<h2 id="action-modal-title">Tùy chọn Scene</h2>
|
||||
<p id="action-modal-desc">Bạn muốn thực hiện thao tác gì với scene này?</p>
|
||||
<div class="action-buttons">
|
||||
<button id="btn-edit-privacy-action" class="edit-btn-large" style="background: #6f42c1;">
|
||||
<span class="icon">🔒</span> Chỉnh sửa privacy
|
||||
</button>
|
||||
<button id="btn-edit-action" class="edit-btn-large">
|
||||
<span class="icon">✏️</span> Chỉnh sửa thông tin
|
||||
<span class="icon">✏️</span> Chế độ sửa scene
|
||||
</button>
|
||||
<button id="btn-delete-action" class="delete-btn-large">
|
||||
<span class="icon">🗑️</span> Xóa vĩnh viễn
|
||||
|
||||
+272
-6
@@ -10,11 +10,22 @@ let miniMapMarker = null;
|
||||
let systemSettings = { timezone: 'Asia/Ho_Chi_Minh', language: 'vi' };
|
||||
let returnToDashboardAfterEdit = false;
|
||||
let assetIdToDelete = null;
|
||||
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();
|
||||
|
||||
@@ -26,8 +37,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Chạy tuần tự để tránh xung đột luồng xử lý
|
||||
checkAuthStatus(); // 2. Kiểm tra đăng nhập
|
||||
|
||||
// 3. Khôi phục cảnh đang xem nếu có (sau khi người dùng reload trang)
|
||||
restoreActiveScene();
|
||||
// 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) {
|
||||
@@ -662,16 +678,25 @@ async function handleEditDeleteScene(scene) {
|
||||
const modal = document.getElementById('action-choice-modal');
|
||||
const title = document.getElementById('action-modal-title');
|
||||
const editBtn = document.getElementById('btn-edit-action');
|
||||
const editPrivacyBtn = document.getElementById('btn-edit-privacy-action');
|
||||
const deleteBtn = document.getElementById('btn-delete-action');
|
||||
|
||||
title.innerText = `Scene: ${scene.title}`;
|
||||
modal.style.display = 'flex';
|
||||
|
||||
// Hành động Chỉnh sửa privacy
|
||||
editPrivacyBtn.onclick = () => {
|
||||
returnToDashboardAfterEdit = false;
|
||||
closeActionModal();
|
||||
// Mở modal metadata, false vì ảnh trên map luôn là ảnh mẹ (không phải child)
|
||||
openEditMetadataModal(scene, false);
|
||||
};
|
||||
|
||||
// Gán sự kiện cho nút Sửa
|
||||
editBtn.onclick = () => {
|
||||
returnToDashboardAfterEdit = false;
|
||||
closeActionModal();
|
||||
openEditSceneModal(scene);
|
||||
openEditMetadataModal(scene, false);
|
||||
};
|
||||
|
||||
// Gán sự kiện cho nút Xóa
|
||||
@@ -805,9 +830,22 @@ async function openScene(sceneId, privacy, shareToken, force = false, initialPit
|
||||
initPanoramaViewer(secureImageUrl, hotspots || [], sceneOwnerId, initialPitch, initialYaw);
|
||||
|
||||
} catch (error) {
|
||||
if (typeof closeViewer === 'function') closeViewer();
|
||||
|
||||
localStorage.removeItem('activeSceneId');
|
||||
localStorage.removeItem('activeScenePrivacy');
|
||||
localStorage.removeItem('activeSceneToken');
|
||||
|
||||
// Kiểm tra nếu đang truy cập qua link trực tiếp (URL có sceneId) mà gặp lỗi (do xóa token hoặc token không hợp lệ)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('sceneId')) {
|
||||
alert("Bạn không có quyền truy cập hoặc liên kết chia sẻ đã hết hạn. Quay về bản đồ công cộng.");
|
||||
// Xóa toàn bộ tham số URL và tải lại trang để làm mới trạng thái (về trang chủ dành cho khách)
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
@@ -1275,7 +1313,8 @@ async function loadMyAssets() {
|
||||
const editButton = document.createElement('button');
|
||||
editButton.className = 'edit-btn-small';
|
||||
editButton.innerText = 'Sửa Scene';
|
||||
editButton.addEventListener('click', () => openEditFromMedia(scene)); // Pass scene object directly
|
||||
const isChild = asset.parentScenes && asset.parentScenes.length > 0;
|
||||
editButton.addEventListener('click', () => openEditFromMedia(scene, isChild));
|
||||
card.querySelector('.media-actions').appendChild(editButton);
|
||||
}
|
||||
|
||||
@@ -1362,16 +1401,243 @@ window.closeSuccessModal = function(e) {
|
||||
modal.style.display = 'none';
|
||||
};
|
||||
|
||||
window.openEditFromMedia = function(scene) {
|
||||
window.openEditFromMedia = function(scene, isChild = false) {
|
||||
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;
|
||||
}
|
||||
returnToDashboardAfterEdit = true;
|
||||
closeDashboard();
|
||||
openEditSceneModal(scene);
|
||||
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) {
|
||||
returnToDashboardAfterEdit = false;
|
||||
openDashboard();
|
||||
openDashboardTab('media-library');
|
||||
}
|
||||
}
|
||||
|
||||
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') {
|
||||
const baseUrl = window.location.origin + window.location.pathname;
|
||||
const token = currentEditingScene.shareToken || 'đang_tạo_mới...';
|
||||
document.getElementById('shared-link-input').value = `${baseUrl}?sceneId=${currentEditingScene._id}&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 = `<div class="search-item" onclick="addEmailToShare('${query}')">Thêm email: ${query}</div>`;
|
||||
dropdown.style.display = 'block';
|
||||
} else {
|
||||
dropdown.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
|
||||
function addMemberToShare(user) {
|
||||
if (!sharedUsersData.some(u => (u._id || u) === user._id)) {
|
||||
sharedUsersData.push(user);
|
||||
renderSharedList();
|
||||
}
|
||||
document.getElementById('share-user-search').value = '';
|
||||
document.getElementById('search-results-dropdown').style.display = 'none';
|
||||
}
|
||||
|
||||
window.addEmailToShare = function(email) {
|
||||
if (!sharedEmailsData.includes(email)) {
|
||||
sharedEmailsData.push(email);
|
||||
renderSharedList();
|
||||
}
|
||||
document.getElementById('share-user-search').value = '';
|
||||
document.getElementById('search-results-dropdown').style.display = 'none';
|
||||
};
|
||||
|
||||
function renderSharedList() {
|
||||
const list = document.getElementById('current-shared-list');
|
||||
list.innerHTML = '';
|
||||
|
||||
sharedUsersData.forEach(user => {
|
||||
const name = user.username || 'User';
|
||||
list.innerHTML += `<div class="share-list-item">👤 ${name} <span class="remove-share-btn" onclick="removeShared('user', '${user._id || user}')">×</span></div>`;
|
||||
});
|
||||
|
||||
sharedEmailsData.forEach(email => {
|
||||
list.innerHTML += `<div class="share-list-item">📧 ${email} <span class="remove-share-btn" onclick="removeShared('email', '${email}')">×</span></div>`;
|
||||
});
|
||||
}
|
||||
|
||||
window.removeShared = function(type, id) {
|
||||
if (type === 'user') sharedUsersData = sharedUsersData.filter(u => (u._id || u) !== id);
|
||||
else sharedEmailsData = sharedEmailsData.filter(e => e !== id);
|
||||
renderSharedList();
|
||||
};
|
||||
|
||||
window.closeShareMemberModal = () => document.getElementById('share-member-modal').style.display = 'none';
|
||||
window.closeShareLinkModal = () => document.getElementById('share-link-modal').style.display = 'none';
|
||||
|
||||
/**
|
||||
* Copy link chia sẻ và đóng modal
|
||||
*/
|
||||
window.copySharedLink = function() {
|
||||
const linkInput = document.getElementById('shared-link-input');
|
||||
linkInput.select();
|
||||
navigator.clipboard.writeText(linkInput.value).then(() => {
|
||||
showSuccessModal("Đã sao chép liên kết vào bộ nhớ!");
|
||||
closeShareLinkModal();
|
||||
});
|
||||
};
|
||||
|
||||
async function submitEditScene(e) {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('edit-modal-scene-id').value;
|
||||
const token = localStorage.getItem('jwt');
|
||||
|
||||
// Sử dụng FormData vì API Backend hiện tại đang dùng Multer
|
||||
const formData = new FormData();
|
||||
formData.append('title', document.getElementById('edit-modal-title').value);
|
||||
formData.append('description', document.getElementById('edit-modal-description').value);
|
||||
formData.append('lat', document.getElementById('edit-modal-lat').value);
|
||||
formData.append('lng', document.getElementById('edit-modal-lng').value);
|
||||
formData.append('privacy', document.getElementById('edit-modal-privacy').value);
|
||||
formData.append('shareExpireDays', document.getElementById('share-link-expire').value);
|
||||
// Đính kèm dữ liệu chia sẻ nâng cao
|
||||
formData.append('sharedWithUsers', JSON.stringify(sharedUsersData.map(u => u._id || u)));
|
||||
formData.append('sharedEmails', JSON.stringify(sharedEmailsData));
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/scenes/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
body: formData
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.message);
|
||||
|
||||
showSuccessModal("Đã cập nhật thông tin cảnh thành công!");
|
||||
closeEditMetadataModal();
|
||||
loadScenes();
|
||||
} catch (err) {
|
||||
alert("Lỗi cập nhật: " + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a specific tab within the dashboard.
|
||||
* @param {string} tabName - The ID of the tab pane to open (e.g., 'profile', 'my-scenes').
|
||||
|
||||
Reference in New Issue
Block a user