Sửa chữa giao diện, lỗi privacy

This commit is contained in:
2026-06-11 09:02:54 +07:00
parent edd91d4d64
commit be149f26ca
9 changed files with 236 additions and 64 deletions
+3 -3
View File
@@ -34,7 +34,7 @@ router.post('/create', protect, async (req, res) => {
const { parent_scene_id, target_scene_id, title, description, coordinates } = req.body; const { parent_scene_id, target_scene_id, title, description, coordinates } = req.body;
const parentScene = await Scene.findById(parent_scene_id); const parentScene = await Scene.findById(parent_scene_id);
if (!parentScene || parentScene.createdBy.toString() !== req.user._id.toString()) { if (!parentScene || (parentScene.createdBy.toString() !== req.user._id.toString() && req.user.role !== 'admin')) {
return res.status(403).json({ message: 'Không có quyền tạo hotspot cho scene này' }); return res.status(403).json({ message: 'Không có quyền tạo hotspot cho scene này' });
} }
@@ -102,7 +102,7 @@ router.put('/update/:id', protect, async (req, res) => {
if (!hotspot) return res.status(404).json({ message: 'Hotspot không tồn tại' }); if (!hotspot) return res.status(404).json({ message: 'Hotspot không tồn tại' });
const parentScene = await Scene.findById(hotspot.parent_scene_id); const parentScene = await Scene.findById(hotspot.parent_scene_id);
if (parentScene.createdBy.toString() !== req.user._id.toString()) { if (parentScene.createdBy.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
return res.status(403).json({ message: 'Không có quyền cập nhật' }); return res.status(403).json({ message: 'Không có quyền cập nhật' });
} }
@@ -141,7 +141,7 @@ router.delete('/delete/:id', protect, async (req, res) => {
if (!hotspot) return res.status(404).json({ message: 'Hotspot không tồn tại' }); if (!hotspot) return res.status(404).json({ message: 'Hotspot không tồn tại' });
const parentScene = await Scene.findById(hotspot.parent_scene_id); const parentScene = await Scene.findById(hotspot.parent_scene_id);
if (parentScene.createdBy.toString() !== req.user._id.toString()) { if (parentScene.createdBy.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
return res.status(403).json({ message: 'Không có quyền xóa' }); return res.status(403).json({ message: 'Không có quyền xóa' });
} }
+1 -1
View File
@@ -216,7 +216,7 @@ router.put('/:id', protect, uploadSinglePanorama, async (req, res) => {
const { title, description, privacy, sharedWithUsers, sharedEmails, shareExpireDays, lat, lng } = req.body; const { title, description, privacy, sharedWithUsers, sharedEmails, shareExpireDays, lat, lng } = req.body;
const scene = await Scene.findById(req.params.id); const scene = await Scene.findById(req.params.id);
if (!scene || scene.createdBy.toString() !== req.user._id.toString()) { if (!scene || (scene.createdBy.toString() !== req.user._id.toString() && req.user.role !== 'admin')) {
return res.status(403).json({ message: 'Not authorized' }); return res.status(403).json({ message: 'Not authorized' });
} }
+46 -17
View File
@@ -1076,8 +1076,12 @@ html, body {
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
} }
/* --- Edit Metadata Modal (Dark Theme) --- */ /* --- Unified Dark Theme for Modals (Tour, Scene, Metadata, Hotspot, Actions) --- */
#edit-scene-metadata-modal .modal-content { #edit-scene-metadata-modal .modal-content,
#create-tour-modal .modal-content,
#create-scene-modal .modal-content,
#hotspot-modal .modal-content,
#action-choice-modal .modal-content {
background: rgba(30, 30, 30, 0.95); background: rgba(30, 30, 30, 0.95);
color: #fff; color: #fff;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);
@@ -1085,36 +1089,54 @@ html, body {
max-width: 500px; max-width: 500px;
} }
#edit-scene-metadata-modal .form-group label { #edit-scene-metadata-modal .form-group label,
#create-tour-modal .form-group label,
#create-scene-modal .form-group label,
#hotspot-modal .form-group label,
#action-choice-modal .form-group label {
color: #ccc; color: #ccc;
} }
#edit-scene-metadata-modal input, #edit-scene-metadata-modal input,
#edit-scene-metadata-modal textarea, #edit-scene-metadata-modal textarea,
#edit-scene-metadata-modal select { #edit-scene-metadata-modal select {
#create-tour-modal input,
#create-tour-modal select,
#create-tour-modal textarea,
#create-scene-modal input,
#create-scene-modal select,
#create-scene-modal textarea,
#hotspot-modal input,
#hotspot-modal textarea,
#hotspot-modal select {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff; color: #fff;
} }
/* Trạng thái vô hiệu hóa (Disabled) cho các trường nhập liệu trong Modal sửa */ /* Select option colors for Dark Theme */
#edit-scene-metadata-modal input:disabled, #edit-scene-metadata-modal select option,
#edit-scene-metadata-modal textarea:disabled, #create-tour-modal select option,
#edit-scene-metadata-modal select:disabled { #create-scene-modal select option,
background: rgba(255, 255, 255, 0.02) !important; #hotspot-modal select option {
color: #777 !important; background-color: #1e1e1e;
cursor: not-allowed; color: #fff;
border-color: rgba(255, 255, 255, 0.1) !important;
} }
/* Tùy chỉnh màu sắc cho danh sách lựa chọn (dropdown options) trong modal tối */ #hs-mini-map {
#edit-scene-metadata-modal select option { height: 200px;
background-color: #000; /* Nền đen cho các item */ width: 100%;
color: #fff; /* Chữ trắng */ border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
margin-top: 10px;
} }
#edit-scene-metadata-modal select option:hover { #edit-scene-metadata-modal .modal-header,
background-color: #555; /* Nền xám khi di chuột qua (tùy trình duyệt hỗ trợ) */ #create-tour-modal .modal-header,
#create-scene-modal .modal-header,
#hotspot-modal .modal-header,
#action-choice-modal .modal-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
} }
#edit-mini-map { #edit-mini-map {
@@ -1372,3 +1394,10 @@ html, body {
color: #dc3545; color: #dc3545;
border: 1px solid rgba(220, 53, 69, 0.4); border: 1px solid rgba(220, 53, 69, 0.4);
} }
/* Temporary hiding rules for notification overlays */
body.notification-active #dashboard-overlay,
body.notification-active .modal,
body.notification-active .modal-overlay:not(#success-modal):not(#error-modal) {
display: none !important;
}
+1 -1
View File
@@ -526,7 +526,7 @@
</div> </div>
<!-- Hotspot Editor Modal --> <!-- Hotspot Editor Modal -->
<div id="hotspot-modal" class="modal-overlay" style="display: none; z-index: 3000;"> <div id="hotspot-modal" class="modal-overlay" style="display: none; z-index: 3000;">
<div class="modal-content"> <div class="modal-content action-modal-content logout-modal-dark" style="max-width: 500px; border-top: 4px solid #ffc107;">
<div class="modal-header"> <div class="modal-header">
<h3 id="hotspot-modal-title">Biên tập điểm điều hướng</h3> <h3 id="hotspot-modal-title">Biên tập điểm điều hướng</h3>
<span class="close-btn" onclick="closeHotspotModal()">&times;</span> <span class="close-btn" onclick="closeHotspotModal()">&times;</span>
+185 -42
View File
@@ -711,7 +711,11 @@ function openCreateTourModal(lat, lng) {
* Đóng Modal tạo Tour * Đóng Modal tạo Tour
*/ */
function closeTourModal() { function closeTourModal() {
document.getElementById('create-tour-modal').style.display = 'none'; const modal = document.getElementById('create-tour-modal');
if (modal) {
modal.style.display = 'none';
modal.removeAttribute('data-original-display');
}
if (tempMarker) { if (tempMarker) {
map.removeLayer(tempMarker); map.removeLayer(tempMarker);
tempMarker = null; tempMarker = null;
@@ -733,9 +737,14 @@ function openEditTourModal(tour) {
const token = localStorage.getItem('jwt'); const token = localStorage.getItem('jwt');
if (!token) return; if (!token) return;
dashboardReturnTab = 'my-scenes'; const isDashboardOpen = document.getElementById('dashboard-overlay').style.display === 'flex';
returnToDashboardAfterEdit = true; if (isDashboardOpen) {
closeDashboard(); dashboardReturnTab = 'my-scenes';
returnToDashboardAfterEdit = true;
closeDashboard();
} else {
returnToDashboardAfterEdit = false;
}
const tourIdInput = document.getElementById('tour-id'); const tourIdInput = document.getElementById('tour-id');
if (tourIdInput) tourIdInput.value = tour._id; if (tourIdInput) tourIdInput.value = tour._id;
@@ -833,8 +842,16 @@ function openCreateSceneModal(lat, lng, tourId = null) {
* Closes the Create Scene Modal and removes temporary marker * Closes the Create Scene Modal and removes temporary marker
*/ */
function closeModal() { function closeModal() {
document.getElementById('create-tour-modal').style.display = 'none'; // Đảm bảo đóng cả modal Tour const tourModal = document.getElementById('create-tour-modal');
document.getElementById('create-scene-modal').style.display = 'none'; if (tourModal) {
tourModal.style.display = 'none';
tourModal.removeAttribute('data-original-display');
}
const sceneModal = document.getElementById('create-scene-modal');
if (sceneModal) {
sceneModal.style.display = 'none';
sceneModal.removeAttribute('data-original-display');
}
if (tempMarker) { if (tempMarker) {
map.removeLayer(tempMarker); map.removeLayer(tempMarker);
tempMarker = null; tempMarker = null;
@@ -1144,12 +1161,12 @@ async function handleEditDeleteScene(scene) {
// Cập nhật nhãn và sự kiện cho nút Xóa // Cập nhật nhãn và sự kiện cho nút Xóa
deleteBtn.innerHTML = tour ? '<span class="icon">🗑️</span> Xóa vĩnh viễn Tour' : '<span class="icon">🗑️</span> Xóa vĩnh viễn'; deleteBtn.innerHTML = tour ? '<span class="icon">🗑️</span> Xóa vĩnh viễn Tour' : '<span class="icon">🗑️</span> Xóa vĩnh viễn';
deleteBtn.onclick = () => { deleteBtn.onclick = async () => {
returnToDashboardAfterEdit = false; // Đảm bảo không mở dashboard nếu xóa từ map returnToDashboardAfterEdit = false; // Đảm bảo không mở dashboard nếu xóa từ map
closeActionModal(); closeActionModal();
if (tour) { if (tour) {
// Tái sử dụng logic xóa tour từ dashboard // Tái sử dụng logic xóa tour từ dashboard
if (confirm(`Bạn có chắc muốn xóa Tour "${tour.name}" và toàn bộ cảnh bên trong?`)) { if (await window.showConfirmModal(`Bạn có chắc muốn xóa Tour "${tour.name}" và toàn bộ cảnh bên trong?`)) {
confirmDeleteTourFromMap(tour._id); confirmDeleteTourFromMap(tour._id);
} }
} else { } else {
@@ -1187,10 +1204,15 @@ async function confirmDeleteTourFromMap(tourId) {
*/ */
window.deleteScene = async function(sceneId, sceneData = null) { // Thêm sceneData để tránh fetch lại window.deleteScene = async function(sceneId, sceneData = null) { // Thêm sceneData để tránh fetch lại
sceneIdToDelete = sceneId; sceneIdToDelete = sceneId;
// Tạm thời đóng dashboard và lưu trạng thái để mở lại sau // Tạm thời đóng dashboard nếu nó đang mở
dashboardReturnTab = 'my-scenes'; const isDashboardOpen = document.getElementById('dashboard-overlay').style.display === 'flex';
returnToDashboardAfterEdit = true; if (isDashboardOpen) {
closeDashboard(); dashboardReturnTab = 'my-scenes';
returnToDashboardAfterEdit = true;
closeDashboard();
} else {
returnToDashboardAfterEdit = false;
}
const confirmModal = document.getElementById('delete-scene-confirm-modal'); const confirmModal = document.getElementById('delete-scene-confirm-modal');
const confirmMessageElem = document.getElementById('delete-scene-confirm-message'); // Giả định có element này trong HTML const confirmMessageElem = document.getElementById('delete-scene-confirm-message'); // Giả định có element này trong HTML
@@ -1546,7 +1568,7 @@ window.handleHotspotCreation = async function(pitch, yaw, existingHotspot = null
return; return;
} }
await saveHotspotToDB(pitch, yaw, formData.get('title'), formData.get('description'), finalTargetId, existingHotspot?._id); await saveHotspotToDB(pitch, yaw, formData.get('title'), formData.get('description'), finalTargetId, existingHotspot?._id);
modal.style.display = 'none'; closeHotspotModal();
}; };
}; };
@@ -1554,7 +1576,11 @@ window.handleHotspotCreation = async function(pitch, yaw, existingHotspot = null
* Đóng Modal biên tập Hotspot * Đóng Modal biên tập Hotspot
*/ */
function closeHotspotModal() { function closeHotspotModal() {
document.getElementById('hotspot-modal').style.display = 'none'; const modal = document.getElementById('hotspot-modal');
if (modal) {
modal.style.display = 'none';
modal.removeAttribute('data-original-display');
}
} }
/** /**
@@ -1680,7 +1706,7 @@ async function saveHotspotToDB(pitch, yaw, title, description, targetSceneId, ho
* Gọi lệnh: systemReset() từ trình duyệt * Gọi lệnh: systemReset() từ trình duyệt
*/ */
window.systemReset = async function() { window.systemReset = async function() {
if (!confirm("CẢNH BÁO: Thao tác này sẽ xóa sạch TOÀN BỘ scene và ảnh trên server. Bạn có chắc chắn?")) return; if (!await window.showConfirmModal("CẢNH BÁO: Thao tác này sẽ xóa sạch TOÀN BỘ scene và ảnh trên server. Bạn có chắc chắn?")) return;
const token = localStorage.getItem('jwt'); const token = localStorage.getItem('jwt');
try { try {
@@ -1720,7 +1746,7 @@ window.openHotspotMenu = function(hotspot) {
// Hành động Xóa: Xác nhận và gọi API xóa // Hành động Xóa: Xác nhận và gọi API xóa
deleteBtn.onclick = async () => { deleteBtn.onclick = async () => {
if (confirm('Bạn có chắc chắn muốn xóa điểm điều hướng này?')) { if (await window.showConfirmModal('Bạn có chắc chắn muốn xóa điểm điều hướng này?')) {
closeHotspotActionModal(); closeHotspotActionModal();
await deleteHotspot(hotspot._id); await deleteHotspot(hotspot._id);
} }
@@ -1831,6 +1857,10 @@ async function loadMyTours() {
card.style.borderLeft = `5px solid ${tour.privacy === 'public' ? '#28a745' : '#ffc107'}`; card.style.borderLeft = `5px solid ${tour.privacy === 'public' ? '#28a745' : '#ffc107'}`;
const currentUserId = localStorage.getItem('userId');
const userRole = localStorage.getItem('role');
const isTourOwner = (tour.createdBy?._id === currentUserId || tour.createdBy === currentUserId || userRole === 'admin' || userRole === 'Chủ sở hữu');
card.innerHTML = ` card.innerHTML = `
<div class="scene-card-overlay"> <div class="scene-card-overlay">
<div class="scene-card-info"> <div class="scene-card-info">
@@ -1844,8 +1874,10 @@ async function loadMyTours() {
</div> </div>
</div> </div>
<div class="media-actions" style="border: none; padding: 0;"> <div class="media-actions" style="border: none; padding: 0;">
<button class="edit-btn-small" id="edit-tour-${tour._id}" style="background:#007bff">Sửa</button> ${isTourOwner ? `
<button class="delete-btn-small" id="delete-tour-${tour._id}">Xóa</button> <button class="edit-btn-small" id="edit-tour-${tour._id}" style="background:#007bff">Sửa</button>
<button class="delete-btn-small" id="delete-tour-${tour._id}">Xóa</button>
` : ''}
<button class="edit-btn-small" id="view-tour-${tour._id}" style="background:#28a745">Xem</button> <button class="edit-btn-small" id="view-tour-${tour._id}" style="background:#28a745">Xem</button>
</div> </div>
</div> </div>
@@ -1853,26 +1885,32 @@ async function loadMyTours() {
listContainer.appendChild(card); listContainer.appendChild(card);
// Nút Sửa Tour // Nút Sửa Tour
document.getElementById(`edit-tour-${tour._id}`).onclick = () => { const editBtn = document.getElementById(`edit-tour-${tour._id}`);
openEditTourModal(tour); if (editBtn) {
}; editBtn.onclick = () => {
openEditTourModal(tour);
};
}
// Nút Xóa Tour: Gọi API xóa Tour (bao gồm xóa cascade các scene bên trong) // Nút Xóa Tour: Gọi API xóa Tour (bao gồm xóa cascade các scene bên trong)
document.getElementById(`delete-tour-${tour._id}`).onclick = async () => { const deleteBtn = document.getElementById(`delete-tour-${tour._id}`);
if (confirm(`Bạn có chắc muốn xóa Tour "${tour.name}" và toàn bộ ${tour.scenes?.length || 0} cảnh bên trong?`)) { if (deleteBtn) {
try { deleteBtn.onclick = async () => {
const res = await fetch(`${API_BASE_URL}/tours/${tour._id}`, { if (await window.showConfirmModal(`Bạn có chắc muốn xóa Tour "${tour.name}" và toàn bộ ${tour.scenes?.length || 0} cảnh bên trong?`)) {
method: 'DELETE', try {
headers: { 'Authorization': `Bearer ${token}` } const res = await fetch(`${API_BASE_URL}/tours/${tour._id}`, {
}); method: 'DELETE',
if (res.ok) { headers: { 'Authorization': `Bearer ${token}` }
showNotification("Đã xóa Tour thành công", "success"); });
loadMyTours(); if (res.ok) {
loadScenes(); showNotification("Đã xóa Tour thành công", "success");
} loadMyTours();
} catch (e) { showNotification("Lỗi xóa tour", "error"); } loadScenes();
} }
}; } catch (e) { showNotification("Lỗi xóa tour", "error"); }
}
};
}
// Nút Xem Tour: Bay tới vị trí và mở cảnh khởi đầu // Nút Xem Tour: Bay tới vị trí và mở cảnh khởi đầu
document.getElementById(`view-tour-${tour._id}`).onclick = () => { document.getElementById(`view-tour-${tour._id}`).onclick = () => {
@@ -1990,7 +2028,7 @@ window.updateUserByAdmin = async function(userId) {
}; };
window.deleteUserByAdmin = async function(userId) { window.deleteUserByAdmin = async function(userId) {
if (!confirm('Xóa vĩnh viễn người dùng này?')) return; if (!await window.showConfirmModal('Xóa vĩnh viễn người dùng này?')) return;
const token = localStorage.getItem('jwt'); const token = localStorage.getItem('jwt');
try { try {
const res = await fetch(`${API_BASE_URL}/admin/users/${userId}`, { const res = await fetch(`${API_BASE_URL}/admin/users/${userId}`, {
@@ -2170,12 +2208,26 @@ async function loadMyAssets() {
*/ */
window.deleteAsset = function(assetId) { window.deleteAsset = function(assetId) {
assetIdToDelete = assetId; assetIdToDelete = assetId;
const isDashboardOpen = document.getElementById('dashboard-overlay').style.display === 'flex';
if (isDashboardOpen) {
dashboardReturnTab = 'media-library';
returnToDashboardAfterEdit = true;
closeDashboard();
} else {
returnToDashboardAfterEdit = false;
}
document.getElementById('delete-asset-confirm-modal').style.display = 'flex'; document.getElementById('delete-asset-confirm-modal').style.display = 'flex';
}; };
window.closeDeleteAssetModal = function() { window.closeDeleteAssetModal = function() {
document.getElementById('delete-asset-confirm-modal').style.display = 'none'; document.getElementById('delete-asset-confirm-modal').style.display = 'none';
assetIdToDelete = null; assetIdToDelete = null;
if (returnToDashboardAfterEdit) {
const targetTab = dashboardReturnTab;
returnToDashboardAfterEdit = false;
openDashboard();
openDashboardTab(targetTab);
}
}; };
window.confirmDeleteAsset = async function() { window.confirmDeleteAsset = async function() {
@@ -2207,6 +2259,40 @@ window.confirmDeleteAsset = async function() {
} }
}; };
window.hiddenModalsStack = window.hiddenModalsStack || [];
window.tempHideActiveModals = function(exceptModalIds = []) {
const modals = document.querySelectorAll('.modal, .modal-overlay');
const hiddenInThisTurn = [];
modals.forEach(modal => {
if (exceptModalIds.includes(modal.id)) return;
const style = window.getComputedStyle(modal);
if (style.display !== 'none' && modal.style.display !== 'none') {
const originalDisplay = modal.style.display || style.display;
modal.setAttribute('data-original-display', originalDisplay);
modal.style.display = 'none';
hiddenInThisTurn.push(modal);
}
});
if (hiddenInThisTurn.length > 0) {
window.hiddenModalsStack.push(hiddenInThisTurn);
}
};
window.restorePreviousModals = function() {
if (window.hiddenModalsStack && window.hiddenModalsStack.length > 0) {
const lastHiddenGroup = window.hiddenModalsStack.pop();
lastHiddenGroup.forEach(modal => {
if (modal.hasAttribute('data-original-display')) {
const originalDisplay = modal.getAttribute('data-original-display') || 'flex';
modal.style.display = originalDisplay;
modal.removeAttribute('data-original-display');
}
});
}
};
/** /**
* Hiển thị Modal thông báo thành công * Hiển thị Modal thông báo thành công
*/ */
@@ -2215,6 +2301,7 @@ window.showSuccessModal = function(message, icon = '✓') {
const msgElem = document.getElementById('success-modal-message'); const msgElem = document.getElementById('success-modal-message');
const iconElem = document.getElementById('success-modal-icon'); const iconElem = document.getElementById('success-modal-icon');
if (modal && msgElem && iconElem) { if (modal && msgElem && iconElem) {
window.tempHideActiveModals(['success-modal']);
msgElem.innerText = message; msgElem.innerText = message;
iconElem.innerText = icon; iconElem.innerText = icon;
modal.style.display = 'flex'; modal.style.display = 'flex';
@@ -2231,7 +2318,10 @@ window.closeSuccessModal = function(e) {
if (!modal) return; if (!modal) return;
// Nếu nhấn từ code (không có e) hoặc click trúng overlay thì đóng // Nếu nhấn từ code (không có e) hoặc click trúng overlay thì đóng
if (e && e.target !== modal) return; if (e && e.target !== modal) return;
modal.style.display = 'none'; if (modal.style.display !== 'none') {
modal.style.display = 'none';
window.restorePreviousModals();
}
}; };
/** /**
@@ -2243,6 +2333,7 @@ window.showErrorModal = function(message, title = "Thông báo", icon = '⚠️'
const titleElem = document.getElementById('error-modal-title'); const titleElem = document.getElementById('error-modal-title');
const iconElem = document.getElementById('error-modal-icon'); const iconElem = document.getElementById('error-modal-icon');
if (modal && msgElem && iconElem) { if (modal && msgElem && iconElem) {
window.tempHideActiveModals(['error-modal']);
msgElem.innerText = message; msgElem.innerText = message;
iconElem.innerText = icon; iconElem.innerText = icon;
if (titleElem) titleElem.innerText = title; if (titleElem) titleElem.innerText = title;
@@ -2258,7 +2349,10 @@ window.closeErrorModal = function(e) {
if (!modal) return; if (!modal) return;
// Nếu nhấn từ code (không có e) hoặc click trúng overlay thì đóng // Nếu nhấn từ code (không có e) hoặc click trúng overlay thì đóng
if (e && e.target !== modal) return; if (e && e.target !== modal) return;
modal.style.display = 'none'; if (modal.style.display !== 'none') {
modal.style.display = 'none';
window.restorePreviousModals();
}
}; };
/** /**
@@ -2274,6 +2368,51 @@ window.showNotification = function(message, type = 'success') {
} }
}; };
// Đè hàm alert mặc định của trình duyệt để gọi modal tương ứng
window.alert = function(message) {
window.showErrorModal(message, 'Thông báo', '⚠️');
};
// Hàm hiển thị confirm dạng modal bất đồng bộ
window.showConfirmModal = function(message) {
return new Promise((resolve) => {
// Tạm ẩn các modal đang mở
window.tempHideActiveModals(['generic-confirm-modal']);
let modal = document.getElementById('generic-confirm-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'generic-confirm-modal';
modal.className = 'modal-overlay';
modal.style.zIndex = '9999';
modal.innerHTML = `
<div class="modal-content action-modal-content logout-modal-dark" style="border-top: 4px solid #ffc107; max-width: 400px; text-align: center;">
<div style="font-size: 40px; color: #ffc107; margin-bottom: 10px;">⚠️</div>
<h2 style="color: #fff; margin-bottom: 10px;">Xác nhận</h2>
<p id="generic-confirm-message" style="color: #ccc; margin-bottom: 25px; line-height: 1.5; font-size: 14px; text-align: center;"></p>
<div class="action-buttons" style="display: flex; gap: 10px; justify-content: center;">
<button id="generic-confirm-ok-btn" class="delete-btn-large" style="background: #dc3545; flex: 1; padding: 10px 20px; font-size: 14px; cursor: pointer; border: none; border-radius: 4px; color: white;">Xác nhận</button>
<button id="generic-confirm-cancel-btn" class="edit-btn-large" style="background: #6c757d; flex: 1; padding: 10px 20px; font-size: 14px; cursor: pointer; border: none; border-radius: 4px; color: white;">Hủy bỏ</button>
</div>
</div>
`;
document.body.appendChild(modal);
}
document.getElementById('generic-confirm-message').innerText = message;
modal.style.display = 'flex';
const handleResolve = (value) => {
modal.style.display = 'none';
window.restorePreviousModals();
resolve(value);
};
document.getElementById('generic-confirm-ok-btn').onclick = () => handleResolve(true);
document.getElementById('generic-confirm-cancel-btn').onclick = () => handleResolve(false);
});
};
window.openEditFromMedia = function(scene, isChild = false) { window.openEditFromMedia = function(scene, isChild = false) {
if (!scene || !scene._id) { if (!scene || !scene._id) {
showNotification("Không thể chỉnh sửa: Ảnh này không được gắn với một Scene hợp lệ.", 'error'); showNotification("Không thể chỉnh sửa: Ảnh này không được gắn với một Scene hợp lệ.", 'error');
@@ -2347,7 +2486,11 @@ window.openEditMetadataModal = function(scene, isChildArg = null) {
}; };
function closeEditMetadataModal() { function closeEditMetadataModal() {
document.getElementById('edit-scene-metadata-modal').style.display = 'none'; const modal = document.getElementById('edit-scene-metadata-modal');
if (modal) {
modal.style.display = 'none';
modal.removeAttribute('data-original-display');
}
if (returnToDashboardAfterEdit) { if (returnToDashboardAfterEdit) {
const targetTab = dashboardReturnTab; const targetTab = dashboardReturnTab;
returnToDashboardAfterEdit = false; returnToDashboardAfterEdit = false;
@@ -2586,7 +2729,7 @@ async function handleBackup() {
*/ */
async function handleRestore(input) { async function handleRestore(input) {
if (!input.files || !input.files[0]) return; if (!input.files || !input.files[0]) return;
if (!confirm("CẢNH BÁO: Khôi phục dữ liệu sẽ xóa sạch dữ liệu hiện tại và thay thế bằng dữ liệu từ bản sao lưu. Bạn có chắc chắn?")) { if (!await window.showConfirmModal("CẢNH BÁO: Khôi phục dữ liệu sẽ xóa sạch dữ liệu hiện tại và thay thế bằng dữ liệu từ bản sao lưu. Bạn có chắc chắn?")) {
input.value = ''; return; input.value = ''; return;
} }
const token = localStorage.getItem('jwt'); const token = localStorage.getItem('jwt');
@@ -2643,7 +2786,7 @@ async function updateSystemSettings(e) {
*/ */
window.recalculateAllTourCenters = async function() { window.recalculateAllTourCenters = async function() {
const token = localStorage.getItem('jwt'); const token = localStorage.getItem('jwt');
if (!confirm("Bạn có chắc chắn muốn tính toán lại tọa độ trung tâm cho TOÀN BỘ Tour trong hệ thống? Việc này có thể mất một chút thời gian nếu dữ liệu lớn.")) return; if (!await window.showConfirmModal("Bạn có chắc chắn muốn tính toán lại tọa độ trung tâm cho TOÀN BỘ Tour trong hệ thống? Việc này có thể mất một chút thời gian nếu dữ liệu lớn.")) return;
try { try {
showNotification("Đang xử lý tính toán lại...", "success"); showNotification("Đang xử lý tính toán lại...", "success");
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 MiB

After

Width:  |  Height:  |  Size: 6.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 MiB

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 MiB

After

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB