fix(admin): replace silent error handling with visible error banners

Admin action handlers (ban/unban, approve/reject listings, KYC actions)
previously swallowed errors silently. Admins now see inline error messages
when API calls fail, with dismiss buttons.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 02:32:14 +07:00
parent 6123fc427d
commit 4ef54027d6
4 changed files with 47 additions and 15 deletions

View File

@@ -9,6 +9,7 @@ import {
ChevronRight, ChevronRight,
FileText, FileText,
ShieldCheck, ShieldCheck,
X,
} from 'lucide-react'; } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
@@ -168,6 +169,7 @@ export default function AdminKycPage() {
const [rejectReason, setRejectReason] = useState(''); const [rejectReason, setRejectReason] = useState('');
const [actionLoading, setActionLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false);
const [actionError, setActionError] = useState<string | null>(null);
const fetchQueue = useCallback(async () => { const fetchQueue = useCallback(async () => {
if (!tokens?.accessToken) return; if (!tokens?.accessToken) return;
@@ -196,8 +198,8 @@ export default function AdminKycPage() {
setApproveNotes(''); setApproveNotes('');
setSelectedItem(null); setSelectedItem(null);
fetchQueue(); fetchQueue();
} catch { } catch (e) {
// silently fail setActionError(e instanceof Error ? e.message : 'Thao tác thất bại. Vui lòng thử lại.');
} finally { } finally {
setActionLoading(false); setActionLoading(false);
} }
@@ -212,8 +214,8 @@ export default function AdminKycPage() {
setRejectReason(''); setRejectReason('');
setSelectedItem(null); setSelectedItem(null);
fetchQueue(); fetchQueue();
} catch { } catch (e) {
// silently fail setActionError(e instanceof Error ? e.message : 'Thao tác thất bại. Vui lòng thử lại.');
} finally { } finally {
setActionLoading(false); setActionLoading(false);
} }
@@ -221,6 +223,15 @@ export default function AdminKycPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{actionError && (
<div className="flex items-center justify-between rounded-md border border-destructive/50 bg-destructive/10 px-4 py-2 text-sm text-destructive">
<span>{actionError}</span>
<button onClick={() => setActionError(null)} className="ml-2">
<X className="h-4 w-4" />
</button>
</div>
)}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold tracking-tight">Duyệt KYC</h1> <h1 className="text-2xl font-bold tracking-tight">Duyệt KYC</h1>

View File

@@ -8,6 +8,7 @@ import {
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
AlertTriangle, AlertTriangle,
X,
} from 'lucide-react'; } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
@@ -58,6 +59,7 @@ export default function AdminModerationPage() {
const [approveNotes, setApproveNotes] = useState(''); const [approveNotes, setApproveNotes] = useState('');
const [rejectReason, setRejectReason] = useState(''); const [rejectReason, setRejectReason] = useState('');
const [actionLoading, setActionLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false);
const [actionError, setActionError] = useState<string | null>(null);
// Bulk action // Bulk action
const [bulkAction, setBulkAction] = useState<'approve' | 'reject' | null>(null); const [bulkAction, setBulkAction] = useState<'approve' | 'reject' | null>(null);
@@ -89,8 +91,8 @@ export default function AdminModerationPage() {
setApproveDialog(null); setApproveDialog(null);
setApproveNotes(''); setApproveNotes('');
fetchQueue(); fetchQueue();
} catch { } catch (e) {
// silently fail setActionError(e instanceof Error ? e.message : 'Thao tác thất bại. Vui lòng thử lại.');
} finally { } finally {
setActionLoading(false); setActionLoading(false);
} }
@@ -104,8 +106,8 @@ export default function AdminModerationPage() {
setRejectDialog(null); setRejectDialog(null);
setRejectReason(''); setRejectReason('');
fetchQueue(); fetchQueue();
} catch { } catch (e) {
// silently fail setActionError(e instanceof Error ? e.message : 'Thao tác thất bại. Vui lòng thử lại.');
} finally { } finally {
setActionLoading(false); setActionLoading(false);
} }
@@ -125,8 +127,8 @@ export default function AdminModerationPage() {
setBulkAction(null); setBulkAction(null);
setBulkReason(''); setBulkReason('');
fetchQueue(); fetchQueue();
} catch { } catch (e) {
// silently fail setActionError(e instanceof Error ? e.message : 'Thao tác thất bại. Vui lòng thử lại.');
} finally { } finally {
setActionLoading(false); setActionLoading(false);
} }
@@ -152,6 +154,15 @@ export default function AdminModerationPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{actionError && (
<div className="flex items-center justify-between rounded-md border border-destructive/50 bg-destructive/10 px-4 py-2 text-sm text-destructive">
<span>{actionError}</span>
<button onClick={() => setActionError(null)} className="ml-2">
<X className="h-4 w-4" />
</button>
</div>
)}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold tracking-tight">Kiểm duyệt tin đăng</h1> <h1 className="text-2xl font-bold tracking-tight">Kiểm duyệt tin đăng</h1>

View File

@@ -186,6 +186,7 @@ export default function AdminUsersPage() {
const [banDialog, setBanDialog] = useState<{ userId: string; isActive: boolean } | null>(null); const [banDialog, setBanDialog] = useState<{ userId: string; isActive: boolean } | null>(null);
const [banReason, setBanReason] = useState(''); const [banReason, setBanReason] = useState('');
const [actionLoading, setActionLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false);
const [actionError, setActionError] = useState<string | null>(null);
const fetchUsers = useCallback(async () => { const fetchUsers = useCallback(async () => {
if (!tokens?.accessToken) return; if (!tokens?.accessToken) return;
@@ -217,8 +218,8 @@ export default function AdminUsersPage() {
try { try {
const detail = await adminApi.getUserDetail(tokens.accessToken, userId); const detail = await adminApi.getUserDetail(tokens.accessToken, userId);
setSelectedUser(detail); setSelectedUser(detail);
} catch { } catch (e) {
// silently fail setActionError(e instanceof Error ? e.message : 'Không thể tải chi tiết người dùng');
} finally { } finally {
setDetailLoading(false); setDetailLoading(false);
} }
@@ -242,8 +243,8 @@ export default function AdminUsersPage() {
setBanDialog(null); setBanDialog(null);
setSelectedUser(null); setSelectedUser(null);
fetchUsers(); fetchUsers();
} catch { } catch (e) {
// silently fail setActionError(e instanceof Error ? e.message : 'Thao tác thất bại. Vui lòng thử lại.');
} finally { } finally {
setActionLoading(false); setActionLoading(false);
} }
@@ -264,6 +265,15 @@ export default function AdminUsersPage() {
</p> </p>
</div> </div>
{actionError && (
<div className="flex items-center justify-between rounded-md border border-destructive/50 bg-destructive/10 px-4 py-2 text-sm text-destructive">
<span>{actionError}</span>
<button onClick={() => setActionError(null)} className="ml-2">
<X className="h-4 w-4" />
</button>
</div>
)}
{/* Filters */} {/* Filters */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-end"> <div className="flex flex-col gap-3 sm:flex-row sm:items-end">
<form onSubmit={handleSearch} className="flex flex-1 gap-2"> <form onSubmit={handleSearch} className="flex flex-1 gap-2">

File diff suppressed because one or more lines are too long