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; onClose: () => void; } export const UserManagementModal: React.FC = ({ 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([]); const [photos, setPhotos] = useState([]); const [reports, setReports] = useState([]); const [reportsLoading, setReportsLoading] = useState(false); const [moderationSetting, setModerationSetting] = useState({ blockNsfw: false, blurFaces: false }); const [wordFilters, setWordFilters] = useState([]); const [newWord, setNewWord] = useState(''); const [newReplacement, setNewReplacement] = useState(''); const [loading, setLoading] = useState(false); const [photosLoading, setPhotosLoading] = useState(false); const [trashLoading, setTrashLoading] = useState(false); const [error, setError] = useState(''); // Active Tours const [tours, setTours] = useState([]); const [toursLoading, setToursLoading] = useState(false); const [selectedTourIds, setSelectedTourIds] = useState([]); // Active Notes const [notes, setNotes] = useState([]); const [notesLoading, setNotesLoading] = useState(false); const [selectedNoteIds, setSelectedNoteIds] = useState([]); // Active Photos multiselect const [selectedPhotoIds, setSelectedPhotoIds] = useState([]); // Recommended Locations const [adminRecommendations, setAdminRecommendations] = useState([]); const [recommendationsLoading, setRecommendationsLoading] = useState(false); const [selectedRecIds, setSelectedRecIds] = useState([]); // Trash Bin State const [trashData, setTrashData] = useState<{ retentionDays: number; tours: any[]; photos: any[]; notes: any[]; }>({ retentionDays: 30, tours: [], photos: [], notes: [] }); const [trashSubTab, setTrashSubTab] = useState<'tours' | 'photos' | 'notes'>('tours'); const [selectedTrashIds, setSelectedTrashIds] = useState([]); const [retentionDaysInput, setRetentionDaysInput] = useState(30); const handleToggleBlacklist = async (id: string, currentStatus: boolean) => { try { const response = await fetch(`/api/v1/admin/reports/${id}/blacklist`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: JSON.stringify({ isBlacklisted: !currentStatus }) }); if (!response.ok) throw new Error('Thao tác thất bại'); fetchReports(); } catch (err: any) { alert(err.message); } }; const handleDeleteReport = async (id: string) => { if (!await confirm({ title: 'Xóa báo cáo', message: 'Bạn có chắc muốn xóa báo cáo này?' })) return; try { const response = await fetch(`/api/v1/admin/reports/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (!response.ok) throw new Error('Xóa báo cáo thất bại'); fetchReports(); } catch (err: any) { alert(err.message); } }; const handleUpdateModeration = async (field: 'blockNsfw' | 'blurFaces', value: boolean) => { const updated = { ...moderationSetting, [field]: value }; setModerationSetting(updated); try { await fetch('/api/v1/admin/moderation', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: JSON.stringify(updated) }); } catch (e) { console.error(e); } }; const handleToggleBlock = async (id: string) => { try { await fetch(`/api/v1/users/block/${id}`, { method: 'POST', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); fetchUsers(); } catch (err) { alert('Lỗi khi thay đổi trạng thái block'); } }; const handleDelete = async (id: string) => { 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', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (!res.ok) { const data = await res.json(); throw new Error(data.message); } fetchUsers(); } catch (err: any) { alert(err.message); } }; const handleAddWordFilter = async () => { if (!newWord.trim()) return; try { const res = await fetch('/api/v1/admin/moderation/word-filters', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: JSON.stringify({ word: newWord, replacement: newReplacement }) }); if (res.ok) { setNewWord(''); setNewReplacement(''); fetchWordFilters(); } } catch (e) { console.error(e); } }; const handleDeleteWordFilter = async (id: string) => { try { const res = await fetch(`/api/v1/admin/moderation/word-filters/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (res.ok) { fetchWordFilters(); } } catch (e) { console.error(e); } }; const fetchReports = async () => { setReportsLoading(true); try { const response = await fetch(`/api/v1/admin/reports`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (!response.ok) throw new Error('Không thể tải danh sách báo cáo'); const data = await response.json(); setReports(data); } catch (err: any) { setError(err.message); } finally { setReportsLoading(false); } }; const fetchUsers = async () => { setLoading(true); try { const response = await fetch(`/api/v1/users`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (!response.ok) throw new Error('Không thể tải danh sách người dùng'); const data = await response.json(); setUsers(data); } catch (err: any) { setError(err.message); } finally { setLoading(false); } }; const fetchPhotos = async () => { setPhotosLoading(true); try { const response = await fetch(`/api/v1/public-photos`); if (!response.ok) throw new Error('Không thể tải danh sách ảnh công cộng'); const data = await response.json(); setPhotos(data); } catch (err: any) { setError(err.message); } finally { setPhotosLoading(false); } }; const fetchTours = async () => { setToursLoading(true); try { const res = await fetch('/api/v1/admin/tours', { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (res.ok) { const data = await res.json(); setTours(data); } } catch (e) { console.error(e); } finally { setToursLoading(false); } }; const fetchNotes = async () => { setNotesLoading(true); try { const res = await fetch('/api/v1/admin/notes', { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (res.ok) { const data = await res.json(); setNotes(data); } } catch (e) { console.error(e); } finally { setNotesLoading(false); } }; const fetchRecommendations = async () => { setRecommendationsLoading(true); try { const res = await fetch('/api/v1/admin/recommendations', { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (res.ok) { const data = await res.json(); setAdminRecommendations(data); } } catch (e) { console.error(e); } finally { setRecommendationsLoading(false); } }; const fetchTrashData = async () => { setTrashLoading(true); try { const res = await fetch('/api/v1/admin/trash', { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (res.ok) { const data = await res.json(); setTrashData(data); setRetentionDaysInput(data.retentionDays || 30); } } catch (e) { console.error(e); } finally { setTrashLoading(false); } }; const handleDeleteTour = async (id: string) => { if (!confirm('Bạn có chắc chắn muốn chuyển tour này vào thùng rác? Tất cả ghi chú và hình ảnh thuộc tour này cũng sẽ bị ẩn đi.')) return; try { const res = await fetch(`/api/v1/admin/tours/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (res.ok) { fetchTours(); } } catch (e) { console.error(e); } }; const handleDeleteNote = async (id: string) => { if (!confirm('Bạn có chắc chắn muốn chuyển ghi chú này vào thùng rác?')) return; try { const res = await fetch(`/api/v1/admin/notes/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (res.ok) { fetchNotes(); } } catch (e) { console.error(e); } }; const handleDeletePhoto = async (id: string) => { if (!confirm('Bạn có chắc chắn muốn xóa bức ảnh công khai này?')) return; try { const res = await fetch(`/api/v1/photos/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (!res.ok) { const data = await res.json(); throw new Error(data.message); } fetchPhotos(); } catch (err: any) { alert(err.message); } }; const handleBulkDeleteTours = async () => { if (selectedTourIds.length === 0) return; if (!confirm(`Bạn có chắc muốn chuyển ${selectedTourIds.length} Tour đã chọn vào thùng rác?`)) return; setToursLoading(true); try { for (const id of selectedTourIds) { await fetch(`/api/v1/admin/tours/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); } setSelectedTourIds([]); fetchTours(); } catch (e) { console.error(e); } finally { setToursLoading(false); } }; const handleBulkDeleteNotes = async () => { if (selectedNoteIds.length === 0) return; if (!confirm(`Bạn có chắc muốn chuyển ${selectedNoteIds.length} Ghi chú đã chọn vào thùng rác?`)) return; setNotesLoading(true); try { for (const id of selectedNoteIds) { await fetch(`/api/v1/admin/notes/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); } setSelectedNoteIds([]); fetchNotes(); } catch (e) { console.error(e); } finally { setNotesLoading(false); } }; const handleBulkDeletePhotos = async () => { if (selectedPhotoIds.length === 0) return; if (!confirm(`Bạn có chắc muốn chuyển ${selectedPhotoIds.length} Ảnh đã chọn vào thùng rác?`)) return; setPhotosLoading(true); try { for (const id of selectedPhotoIds) { await fetch(`/api/v1/photos/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); } setSelectedPhotoIds([]); fetchPhotos(); } catch (e) { console.error(e); } finally { setPhotosLoading(false); } }; const handleApproveRecommendation = async (id: string, isApproved: boolean) => { try { const res = await fetch(`/api/v1/admin/recommendations/${id}/approve`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: JSON.stringify({ isApproved }) }); if (res.ok) { fetchRecommendations(); } } catch (e) { console.error(e); } }; const handleDeleteRecommendation = async (id: string) => { if (!confirm('Bạn có chắc chắn muốn xóa địa điểm đề xuất này?')) return; try { const res = await fetch(`/api/v1/admin/recommendations/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (res.ok) { fetchRecommendations(); } } catch (e) { console.error(e); } }; const handleBulkApproveRecs = async (isApproved: boolean) => { if (selectedRecIds.length === 0) return; const actionText = isApproved ? 'phê duyệt' : 'bỏ phê duyệt'; if (!confirm(`Bạn có chắc muốn ${actionText} ${selectedRecIds.length} địa điểm đã chọn?`)) return; setRecommendationsLoading(true); try { for (const id of selectedRecIds) { await fetch(`/api/v1/admin/recommendations/${id}/approve`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: JSON.stringify({ isApproved }) }); } setSelectedRecIds([]); fetchRecommendations(); } catch (e) { console.error(e); } finally { setRecommendationsLoading(false); } }; const handleBulkDeleteRecs = async () => { if (selectedRecIds.length === 0) return; if (!confirm(`Bạn có chắc muốn xóa vĩnh viễn ${selectedRecIds.length} địa điểm đề xuất đã chọn?`)) return; setRecommendationsLoading(true); try { for (const id of selectedRecIds) { await fetch(`/api/v1/admin/recommendations/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); } setSelectedRecIds([]); fetchRecommendations(); } catch (e) { console.error(e); } finally { setRecommendationsLoading(false); } }; const handleRestoreTrash = async () => { if (selectedTrashIds.length === 0) 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: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: JSON.stringify({ type: itemType, ids: idsToRestore }) }); if (res.ok) { console.log('[Trash] Restore successful'); setSelectedTrashIds([]); 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: 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 (!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} ${itemType} items:`, idsToDelete); const res = await fetch('/api/v1/admin/trash/delete-permanent', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: JSON.stringify({ type: itemType, ids: idsToDelete }) }); console.log('[Trash] Delete response status:', res.status); if (res.ok) { const responseData = await res.json(); console.log('[Trash] Delete successful, response:', responseData); setError(''); // 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); }, 800); } else { const errorData = await res.json().catch(() => ({})); const errorMsg = errorData?.message || `Lỗi HTTP ${res.status}`; console.error('[Trash] Delete failed with status', res.status, ':', errorMsg); setError(errorMsg); notify({ title: '✗ Xóa thất bại', message: errorMsg, type: 'error' }); // Refresh to see current state setTimeout(() => { fetchTrashData(); setTrashLoading(false); }, 500); } } catch (e: any) { console.error('[Trash] Delete error:', e); 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 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); } }; const handleSaveRetentionDays = async () => { try { const res = await fetch('/api/v1/admin/trash/retention-days', { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: JSON.stringify({ days: retentionDaysInput }) }); if (res.ok) { 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(); 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); } }; const fetchModerationSettings = async () => { try { const res = await fetch('/api/v1/moderation/settings'); if (res.ok) { const data = await res.json(); setModerationSetting({ blockNsfw: data.blockNsfw, blurFaces: data.blurFaces }); } } catch (e) { console.error(e); } }; const fetchWordFilters = async () => { try { const res = await fetch('/api/v1/admin/moderation/word-filters', { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (res.ok) { const data = await res.json(); setWordFilters(data); } } catch (e) { console.error(e); } }; useEffect(() => { if (isOpen) { if (activeTab === 'users') { fetchUsers(); } else if (activeTab === 'tours') { fetchTours(); } else if (activeTab === 'photos') { fetchPhotos(); } else if (activeTab === 'notes') { fetchNotes(); } else if (activeTab === 'recommendations') { fetchRecommendations(); } else if (activeTab === 'trash') { fetchTrashData(); } else if (activeTab === 'filters') { fetchModerationSettings(); fetchWordFilters(); } else if (activeTab === 'reports') { fetchReports(); } } }, [isOpen, activeTab]); useEffect(() => { setSelectedTrashIds([]); }, [trashSubTab]); const getCountdownDays = (deletedAtStr: string, retentionDays: number) => { const deletedAt = new Date(deletedAtStr); const expiryDate = new Date(deletedAt.getTime() + retentionDays * 24 * 60 * 60 * 1000); const now = new Date(); const diffTime = expiryDate.getTime() - now.getTime(); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return diffDays > 0 ? diffDays : 0; }; if (!isOpen) return null; const currentTrashList = trashSubTab === 'tours' ? trashData.tours : trashSubTab === 'photos' ? trashData.photos : trashData.notes; const isAllTrashSelected = currentTrashList?.length > 0 && selectedTrashIds.length === currentTrashList?.length; const toggleSelectAllTrash = () => { if (isAllTrashSelected) { setSelectedTrashIds([]); } else { setSelectedTrashIds(currentTrashList.map((t: any) => t.id)); } }; const isAllToursSelected = tours.length > 0 && selectedTourIds.length === tours.length; const toggleSelectAllTours = () => { if (isAllToursSelected) { setSelectedTourIds([]); } else { setSelectedTourIds(tours.map(t => t.id)); } }; const isAllNotesSelected = notes.length > 0 && selectedNoteIds.length === notes.length; const toggleSelectAllNotes = () => { if (isAllNotesSelected) { setSelectedNoteIds([]); } else { setSelectedNoteIds(notes.map(n => n.id)); } }; const isAllPhotosSelected = photos.length > 0 && selectedPhotoIds.length === photos.length; const toggleSelectAllPhotos = () => { if (isAllPhotosSelected) { setSelectedPhotoIds([]); } else { setSelectedPhotoIds(photos.map(p => p.id)); } }; const isAllRecsSelected = adminRecommendations.length > 0 && selectedRecIds.length === adminRecommendations.length; const toggleSelectAllRecs = () => { if (isAllRecsSelected) { setSelectedRecIds([]); } else { setSelectedRecIds(adminRecommendations.map(r => r.id)); } }; return (
{/* Header */}

Hệ thống quản trị

Quản lý thành viên, kiểm duyệt tour, ghi chú, địa điểm đề xuất và dọn dẹp hệ thống.

{/* Tab Selection */}
{/* Content Body */}
{error && (
{error}
)} {activeTab === 'users' ? ( loading ? (
) : ( {users.map(u => ( ))}
Người dùng Vai trò Trạng thái Thao tác
{u.name?.charAt(0) || }
{u.name || 'N/A'}
{u.email}
{u.isAdmin ? ( ADMIN ) : ( USER )} {u.isBlocked ? ( Đã khóa ) : ( Đang hoạt động )}
) ) : activeTab === 'tours' ? ( toursLoading ? (
) : (
Chọn {selectedTourIds.length} trên {tours.length} Tour {selectedTourIds.length > 0 && ( )}
{tours.map(t => ( ))}
Tên Tour Người tạo Thành viên Ngày tạo Thao tác
{ if (e.target.checked) { setSelectedTourIds([...selectedTourIds, t.id]); } else { setSelectedTourIds(selectedTourIds.filter(id => id !== t.id)); } }} className="rounded text-blue-600 focus:ring-blue-500 cursor-pointer" />
{t.title}
{t.creator?.name || 'Không rõ'}
{t.creator?.email}
{t._count?.participants || 0} người {new Date(t.createdAt).toLocaleDateString('vi-VN')}
) ) : activeTab === 'photos' ? ( photosLoading ? (
) : photos.length === 0 ? (
Chưa có ảnh công cộng nào được tải lên.
) : (
{selectedPhotoIds.length > 0 && ( )}
{photos.map(p => (
Public content
{ if (e.target.checked) { setSelectedPhotoIds([...selectedPhotoIds, p.id]); } else { setSelectedPhotoIds(selectedPhotoIds.filter(id => id !== p.id)); } }} className="w-4 h-4 rounded text-blue-600 focus:ring-blue-500 cursor-pointer shadow" />
Đăng bởi: {p.uploader?.name || 'Ẩn danh'}
{new Date(p.capturedAt).toLocaleDateString('vi-VN', { day: '2-digit', month: '2-digit', year: 'numeric' })}
))}
) ) : activeTab === 'notes' ? ( notesLoading ? (
) : notes.length === 0 ? (
Chưa có ghi chú nào hoạt động.
) : (
Chọn {selectedNoteIds.length} trên {notes.length} ghi chú {selectedNoteIds.length > 0 && ( )}
{notes.map(n => ( ))}
Tiêu đề Thuộc Tour Người tạo Nội dung Ngày tạo Thao tác
{ if (e.target.checked) { setSelectedNoteIds([...selectedNoteIds, n.id]); } else { setSelectedNoteIds(selectedNoteIds.filter(id => id !== n.id)); } }} className="rounded text-blue-600 focus:ring-blue-500 cursor-pointer" />
{n.title}
{n.tour?.title || 'Không rõ'}
{n.user?.name || 'Ẩn danh'}
{n.user?.email}
]*>/g, '')}> {n.content?.replace(/<[^>]*>/g, '')} {new Date(n.createdAt).toLocaleDateString('vi-VN')}
) ) : activeTab === 'recommendations' ? ( recommendationsLoading ? (
) : adminRecommendations.length === 0 ? (
Chưa có đề xuất địa điểm nào.
) : (
Chọn {selectedRecIds.length} trên {adminRecommendations.length} địa điểm {selectedRecIds.length > 0 && (
)}
{adminRecommendations.map(r => ( ))}
Địa điểm Loại Đánh giá Liên hệ & Địa chỉ Mô tả Trạng thái Thao tác
{ if (e.target.checked) { setSelectedRecIds([...selectedRecIds, r.id]); } else { setSelectedRecIds(selectedRecIds.filter(id => id !== r.id)); } }} className="rounded text-blue-600 focus:ring-blue-500 cursor-pointer" />
{r.name}
{r.latitude && r.longitude && ( GPS: {r.latitude}, {r.longitude} )}
{r.type === 'RESTAURANT' ? 'Nhà hàng' : r.type === 'HOTEL' ? 'Khách sạn' : 'Homestay'} {Array.from({ length: r.stars || 5 }).map((_, i) => ( ))} {r.phone &&
SĐT: {r.phone}
} {r.email &&
{r.email}
} {r.address &&
{r.address}
}
{r.description} {r.isApproved ? ( Đã duyệt ) : ( Chờ duyệt )}
) ) : activeTab === 'trash' ? ( trashLoading ? (
) : (
{/* Retention days setting block */}
Cấu hình thời gian giữ rác Số ngày lưu trữ các mục trong thùng rác trước khi tự động xóa vĩnh viễn trên máy chủ.
setRetentionDaysInput(parseInt(e.target.value) || 30)} className="w-20 px-3 py-1.5 border border-gray-200 rounded-xl text-center text-sm font-bold bg-white" /> ngày
{/* Empty All Trash button */} {(trashData.tours?.length > 0 || trashData.photos?.length > 0 || trashData.notes?.length > 0) && (
)} {/* Sub-tab selection */}
{/* Bulk Actions inside Trash */}
Chọn {selectedTrashIds.length} mục trong danh mục rác hiện tại {selectedTrashIds.length > 0 && (
)}
{/* Table details */} {(!currentTrashList || currentTrashList.length === 0) ? (
Thư mục rác trống!
) : (
{currentTrashList.map((item: any) => { const countdown = getCountdownDays(item.deletedAt, trashData.retentionDays || 30); return ( ); })}
Đối tượng Thông tin chi tiết Ngày xóa Tự động xóa
{ if (e.target.checked) { setSelectedTrashIds([...selectedTrashIds, item.id]); } else { setSelectedTrashIds(selectedTrashIds.filter(id => id !== item.id)); } }} className="rounded text-blue-600 focus:ring-blue-500 cursor-pointer" /> {trashSubTab === 'photos' ? (
Trash content
) : trashSubTab === 'tours' ? (
{item.title}
) : (
{item.title}
)}
{trashSubTab === 'tours' ? ( Tạo bởi: {item.creator?.name || 'Không rõ'} ) : trashSubTab === 'photos' ? ( {item.imageUrl} (Tải lên bởi: {item.uploader?.name || 'Ẩn danh'}) ) : (
Ghi chú của: {item.user?.name || 'Ẩn danh'} Thuộc Tour: {item.tour?.title}
)}
{new Date(item.deletedAt).toLocaleDateString('vi-VN')} {new Date(item.deletedAt).toLocaleTimeString('vi-VN', { hour: '2-digit', minute: '2-digit' })} {countdown > 0 ? `Còn ${countdown} ngày` : 'Sắp bị xóa'}
)}
) ) : null} {activeTab === 'reports' && ( reportsLoading ? (
) : reports.length === 0 ? (
Chưa có báo cáo sai phạm nào.
) : (
{reports.map((item) => ( ))}
Đối tượng Loại Liên hệ & Địa chỉ Lý do báo cáo Trạng thái Thao tác
{item.name}
{item.latitude && item.longitude && ( GPS: {item.latitude}, {item.longitude} )}
{item.type} {item.phone &&
SĐT: {item.phone}
} {item.email &&
{item.email}
} {item.address &&
{item.address}
}
{item.reason} {item.isBlacklisted ? ( Blacklisted ) : ( Chờ duyệt )}
) )} {activeTab === 'filters' && (

⚙️ Cấu hình kiểm duyệt hình ảnh

Lọc hình ảnh khiêu dâm (NSFW) Tự động phát hiện và chặn tải lên các hình ảnh có nội dung người lớn nhạy cảm.
Tự động làm mờ khuôn mặt Tự động nhận diện khuôn mặt người trong ảnh để làm mờ bảo mật trước khi lưu trữ.

📝 Cấu hình bộ lọc văn bản

{/* Add Word Filter Form */}
setNewWord(e.target.value)} className="w-full px-4 py-2 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 bg-gray-50/20" />
setNewReplacement(e.target.value)} className="w-full px-4 py-2 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 bg-gray-50/20" />
{/* Word Filter List */} {wordFilters.length === 0 ? (
Chưa cấu hình bộ lọc từ khóa nào.
) : (
{wordFilters.map((wf) => ( ))}
Từ cấm Từ thay thế Thao tác
{wf.word} {wf.replacement}
)}
)}
); };