fix: admin empty thùng rác

This commit is contained in:
2026-06-22 12:28:21 +07:00
parent 860395cb14
commit 01d6d7439f
54 changed files with 992 additions and 361 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 224 KiB

+3 -2
View File
@@ -4,9 +4,10 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<script src="https://accounts.google.com/gsi/client" async defer></script>
<title>Travel Planner</title>
<script type="module" crossorigin src="/assets/index-C3QeRTiI.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-_0oFafPI.css">
<script type="module" crossorigin src="/assets/index-DtsqetSG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-sqDjZKsa.css">
</head>
<body>
<div id="root"></div>
+1
View File
File diff suppressed because one or more lines are too long
+155 -28
View File
@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { X, User, Trash2, Shield, ShieldAlert, Lock, Unlock, Loader2, Image as ImageIcon, Map, FileText, CheckCircle, Star, Settings } from 'lucide-react';
import { useConfirm } from '../hooks/useConfirm';
import { useNotification } from '../hooks/useNotification';
interface UserManagementModalProps {
isOpen: boolean;
@@ -9,6 +10,7 @@ interface UserManagementModalProps {
export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen, onClose }) => {
const confirm = useConfirm();
const notify = useNotification();
const [activeTab, setActiveTab] = useState<'users' | 'tours' | 'photos' | 'notes' | 'recommendations' | 'trash' | 'filters' | 'reports'>('users');
const [users, setUsers] = useState<any[]>([]);
const [photos, setPhotos] = useState<any[]>([]);
@@ -116,7 +118,10 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
};
const handleDelete = async (id: string) => {
if (!confirm('Bạn có chắc chắn muốn xóa người dùng này?')) return;
if (!await confirm({
title: 'Xóa người dùng',
message: 'Bạn có chắc chắn muốn xóa người dùng này?'
})) return;
try {
const res = await fetch(`/api/v1/users/${id}`, {
method: 'DELETE',
@@ -469,9 +474,18 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
const handleRestoreTrash = async () => {
if (selectedTrashIds.length === 0) return;
if (!confirm(`Bạn có chắc muốn khôi phục ${selectedTrashIds.length} mục đã chọn?`)) return;
if (!await confirm({
title: 'Khôi phục các mục',
message: `Bạn có chắc muốn khôi phục ${selectedTrashIds.length} mục đã chọn?`
})) return;
setTrashLoading(true);
const idsToRestore = [...selectedTrashIds];
// Convert plural to singular: 'photos' -> 'photo', 'tours' -> 'tour', 'notes' -> 'note'
const itemType = trashSubTab === 'photos' ? 'photo' : trashSubTab === 'tours' ? 'tour' : 'note';
try {
console.log(`[Trash] Starting restore for ${idsToRestore.length} ${itemType} items`);
const res = await fetch('/api/v1/admin/trash/restore', {
method: 'POST',
headers: {
@@ -479,31 +493,49 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
type: trashSubTab,
ids: selectedTrashIds
type: itemType,
ids: idsToRestore
})
});
if (res.ok) {
console.log('[Trash] Restore successful');
setSelectedTrashIds([]);
fetchTrashData();
notify({ title: '✓ Thành công', message: `Đã khôi phục ${idsToRestore.length} mục.`, type: 'success' });
setTimeout(() => {
fetchTrashData();
setTrashLoading(false);
}, 500);
} else {
const errorData = await res.json().catch(() => ({}));
const errorMsg = errorData?.message || `Lỗi HTTP ${res.status}`;
console.error('[Trash] Restore failed:', errorMsg);
notify({ title: '✗ Khôi phục thất bại', message: errorMsg, type: 'error' });
setTrashLoading(false);
}
} catch (e) {
console.error(e);
} finally {
} catch (e: any) {
console.error('[Trash] Restore error:', e);
notify({ title: '✗ Lỗi', message: e?.message || 'Lỗi khi khôi phục các mục', type: 'error' });
setTrashLoading(false);
}
};
const handleDeletePermanentTrash = async () => {
if (selectedTrashIds.length === 0) return;
if (!confirm(`CẢNH BÁO: Bạn có chắc muốn xóa VĨNH VIỄN ${selectedTrashIds.length} mục đã chọn? Thao tác này không thể hoàn tác.`)) return;
if (!await confirm({
title: '⚠️ XÓA VĨNH VIỄN',
message: `Cảnh báo: Bạn có chắc muốn XÓA VĨNH VIỄN ${selectedTrashIds.length} mục đã chọn? Thao tác này KHÔNG THỂ hoàn tác!`
})) return;
setTrashLoading(true);
const idsToDelete = [...selectedTrashIds];
// Convert plural to singular: 'photos' -> 'photo', 'tours' -> 'tour', 'notes' -> 'note'
const itemType = trashSubTab === 'photos' ? 'photo' : trashSubTab === 'tours' ? 'tour' : 'note';
setSelectedTrashIds([]);
try {
console.log('[Trash] Starting permanent delete for', idsToDelete.length, 'items');
console.log(`[Trash] Starting permanent delete for ${idsToDelete.length} ${itemType} items:`, idsToDelete);
const res = await fetch('/api/v1/admin/trash/delete-permanent', {
method: 'POST',
headers: {
@@ -511,7 +543,7 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
type: trashSubTab,
type: itemType,
ids: idsToDelete
})
});
@@ -519,36 +551,117 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
console.log('[Trash] Delete response status:', res.status);
if (res.ok) {
console.log('[Trash] Delete successful, clearing state immediately');
const responseData = await res.json();
console.log('[Trash] Delete successful, response:', responseData);
setError('');
alert('Đã xóa vĩnh viễn thành công!');
// Small delay to let alert close, then refresh data
// Build success message
const { deleted = 0, failed = 0, errors = [] } = responseData;
if (failed === 0) {
notify({ title: '✓ Xóa thành công', message: `Đã xóa vĩnh viễn ${deleted} mục.`, type: 'success' });
} else {
let errorMsg = `Xóa thành công ${deleted} mục, thất bại ${failed} mục.`;
if (errors && errors.length > 0) {
errorMsg += ` Lỗi: ${errors[0]}`;
}
notify({ title: '⚠️ Xóa một phần', message: errorMsg, type: 'error' });
}
// Refresh trash data after delete
setTimeout(() => {
console.log('[Trash] Refreshing trash data after delete');
fetchTrashData();
setTrashLoading(false);
}, 500);
return; // Important: exit early so finally doesn't run again
}, 800);
} else {
const errorData = await res.json().catch(() => ({}));
const errorMsg = errorData.message || `HTTP ${res.status}`;
console.error('[Trash] Delete failed:', errorMsg);
const errorMsg = errorData?.message || `Lỗi HTTP ${res.status}`;
console.error('[Trash] Delete failed with status', res.status, ':', errorMsg);
setError(errorMsg);
alert(`Xóa thất bại: ${errorMsg}`);
notify({ title: '✗ Xóa thất bại', message: errorMsg, type: 'error' });
// Refresh on error
fetchTrashData();
setTrashLoading(false);
// Refresh to see current state
setTimeout(() => {
fetchTrashData();
setTrashLoading(false);
}, 500);
}
} catch (e: any) {
console.error('[Trash] Delete error:', e);
setError(e.message || 'Lỗi khi xóa các mục');
alert(`Lỗi: ${e.message}`);
const errorMsg = e?.message || 'Lỗi mạng khi xóa các mục';
setError(errorMsg);
notify({ title: '✗ Lỗi', message: errorMsg, type: 'error' });
// Refresh on error
fetchTrashData();
setTrashLoading(false);
setTimeout(() => {
fetchTrashData();
setTrashLoading(false);
}, 500);
}
};
const handleEmptyAllTrash = async () => {
if (!await confirm({
title: '⚠️ LÀMTRỐNG THÙNG RÁC',
message: `Cảnh báo: Thao tác này sẽ XÓA VĨNH VIỄN TẤT CẢ mục trong thùng rác! KHÔNG THỂ hoàn tác!`
})) return;
setTrashLoading(true);
try {
console.log('[Trash] Starting to empty ALL trash');
const res = await fetch('/api/v1/admin/trash/empty-all', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
console.log('[Trash] Empty all response status:', res.status);
if (res.ok) {
const responseData = await res.json();
console.log('[Trash] Empty all successful, response:', responseData);
setError('');
setSelectedTrashIds([]);
const { totalDeleted = 0 } = responseData;
notify({
title: '✓ Thùng rác đã được làm trống',
message: `Đã xóa vĩnh viễn ${totalDeleted} mục.`,
type: 'success'
});
// Refresh trash data
setTimeout(() => {
console.log('[Trash] Refreshing trash data after empty all');
fetchTrashData();
setTrashLoading(false);
}, 800);
} else {
const errorData = await res.json().catch(() => ({}));
const errorMsg = errorData?.message || `Lỗi HTTP ${res.status}`;
console.error('[Trash] Empty all failed with status', res.status, ':', errorMsg);
setError(errorMsg);
notify({ title: '✗ Làm trống thất bại', message: errorMsg, type: 'error' });
setTimeout(() => {
fetchTrashData();
setTrashLoading(false);
}, 500);
}
} catch (e: any) {
console.error('[Trash] Empty all error:', e);
const errorMsg = e?.message || 'Lỗi mạng khi làm trống thùng rác';
setError(errorMsg);
notify({ title: '✗ Lỗi', message: errorMsg, type: 'error' });
setTimeout(() => {
fetchTrashData();
setTrashLoading(false);
}, 500);
}
};
@@ -563,11 +676,11 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
body: JSON.stringify({ days: retentionDaysInput })
});
if (res.ok) {
alert('Đã cập nhật số ngày lưu trữ trong thùng rác.');
notify({ title: '✓ Cập nhật thành công', message: 'Số ngày lưu trữ đã được cập nhật.', type: 'success' });
fetchTrashData();
} else {
const data = await res.json();
alert(data.message || 'Lỗi khi cập nhật số ngày lưu trữ.');
notify({ title: '✗ Lỗi', message: data.message || 'Lỗi khi cập nhật số ngày lưu trữ.', type: 'error' });
}
} catch (e) {
console.error(e);
@@ -1263,6 +1376,20 @@ export const UserManagementModal: React.FC<UserManagementModalProps> = ({ isOpen
</div>
</div>
{/* Empty All Trash button */}
{(trashData.tours?.length > 0 || trashData.photos?.length > 0 || trashData.notes?.length > 0) && (
<div className="bg-red-50 p-4 rounded-2xl border border-red-100">
<button
onClick={handleEmptyAllTrash}
disabled={trashLoading}
className="w-full px-4 py-3 bg-red-600 hover:bg-red-700 disabled:bg-gray-400 text-white rounded-xl font-bold text-sm uppercase tracking-wide flex items-center justify-center gap-2 shadow-md transition-all active:scale-95 cursor-pointer"
>
{trashLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash2 className="w-4 h-4" />}
Làm trống thùng rác ({(trashData.tours?.length || 0) + (trashData.photos?.length || 0) + (trashData.notes?.length || 0)} mục)
</button>
</div>
)}
{/* Sub-tab selection */}
<div className="flex gap-2 border-b border-gray-100 pb-2">
<button