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
+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