diff --git a/apps/web/app/[locale]/(admin)/admin/moderation/flagged/[id]/page.tsx b/apps/web/app/[locale]/(admin)/admin/moderation/flagged/[id]/page.tsx new file mode 100644 index 0000000..4e26b0e --- /dev/null +++ b/apps/web/app/[locale]/(admin)/admin/moderation/flagged/[id]/page.tsx @@ -0,0 +1,269 @@ +'use client'; + +import { AlertTriangle, ArrowLeft, Flag, RefreshCw, ShieldAlert, UserX } from 'lucide-react'; +import { use, useCallback, useEffect, useState } from 'react'; +import { StatusChip } from '@/components/design-system/status-chip'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Textarea } from '@/components/ui/textarea'; +import { Link } from '@/i18n/navigation'; +import { + adminApi, + type FlaggedAction, + type FlaggedListingDetail, + type FlagReason, +} from '@/lib/admin-api'; + +const REASON_LABELS: Record = { + SCAM: 'Lừa đảo', + DUPLICATE: 'Trùng lặp', + WRONG_INFO: 'Sai thông tin', + ALREADY_SOLD: 'Đã bán', + INAPPROPRIATE: 'Không phù hợp', +}; + +const ACTION_LABELS: Record = { + dismiss_flags: { title: 'Bỏ qua báo cáo', verb: 'Bỏ qua', danger: false }, + suspend_listing: { title: 'Tạm ngưng tin đăng', verb: 'Tạm ngưng', danger: true }, + warn_seller: { title: 'Cảnh báo người bán', verb: 'Cảnh báo', danger: false }, +}; + +export default function FlaggedListingDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [dialogAction, setDialogAction] = useState(null); + const [note, setNote] = useState(''); + const [actionLoading, setActionLoading] = useState(false); + const [actionError, setActionError] = useState(null); + + const fetchDetail = useCallback(async () => { + setLoading(true); + setError(null); + try { + const res = await adminApi.getFlaggedListingDetail(id); + setData(res); + } catch (e) { + setError(e instanceof Error ? e.message : 'Không thể tải chi tiết tin đăng'); + } finally { + setLoading(false); + } + }, [id]); + + useEffect(() => { + void fetchDetail(); + }, [fetchDetail]); + + const runAction = async () => { + if (!dialogAction) return; + setActionLoading(true); + setActionError(null); + try { + await adminApi.moderateFlaggedListings({ + listingIds: [id], + action: dialogAction, + note: note || undefined, + }); + setDialogAction(null); + setNote(''); + await fetchDetail(); + } catch (e) { + setActionError(e instanceof Error ? e.message : 'Thao tác thất bại'); + } finally { + setActionLoading(false); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (error || !data) { + return ( +
+ + + Quay lại danh sách + +
+ {error ?? 'Không tìm thấy tin đăng'} +
+ +
+ ); + } + + const { listing, flags, totalReports, distinctReasons } = data; + + return ( +
+ + + Quay lại danh sách + + + {actionError && ( +
+ {actionError} +
+ )} + +
+
+

{listing.title}

+
+ + · + {listing.propertyType} + · + {listing.transactionType === 'SALE' ? 'Bán' : 'Cho thuê'} + · + + {new Intl.NumberFormat('vi-VN').format(listing.priceVND)} ₫ + +
+
+
+ + + +
+
+ +
+ + +

Ảnh tin đăng

+ {listing.photos.length === 0 ? ( +

Không có ảnh.

+ ) : ( +
+ {listing.photos.map((src, i) => ( + {`Ảnh + ))} +
+ )} +
+
+ + + +

Người bán

+
+
{listing.seller.fullName}
+
{listing.seller.email ?? '—'}
+
{listing.seller.phone}
+
+
+ Số báo cáo + {totalReports} +
+
+ {distinctReasons.map((r) => ( + + {REASON_LABELS[r] ?? r} + + ))} +
+
+
+
+ + + +

+ Danh sách báo cáo ({flags.length}) +

+ {flags.length === 0 ? ( +

Chưa có báo cáo nào.

+ ) : ( +
    + {flags.map((f) => ( +
  • +
    + {f.reporter.fullName} +
    + + {REASON_LABELS[f.reason] ?? f.reason} + + + + {new Date(f.createdAt).toLocaleString('vi-VN')} + +
    +
    + {f.description && ( +

    {f.description}

    + )} +
  • + ))} +
+ )} +
+
+ + !o && setDialogAction(null)}> + + + {dialogAction ? ACTION_LABELS[dialogAction].title : ''} + + + + Thao tác sẽ áp dụng cho tin đăng này. + + + +