الملفات
back_end_oudelaa/oudelaa_dashboard/app/(dashboard)/reports/page.tsx
boutmoun123 8863f61d00
فشلت بعض الفحوصات
Deploy To Ghaymah / deploy (push) Has been cancelled
Add Oudelaa dashboard API integration
2026-05-25 20:36:52 +03:00

349 أسطر
13 KiB
TypeScript

"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { CheckCircle2, RefreshCcw, ShieldAlert } from "lucide-react";
import { NoPermissionState } from "@/components/auth/no-permission-state";
import { useSuperAdminSession } from "@/components/auth/session-context";
import { PageHeader } from "@/components/dashboard/page-header";
import { PaginationControls } from "@/components/dashboard/pagination-controls";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { EmptyState } from "@/components/ui/empty-state";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/components/ui/toast";
import { getItems, getPagination } from "@/lib/api/core";
import { listPlatformReports, updatePlatformReportStatus } from "@/lib/api/reports";
import { formatDateTime } from "@/lib/format";
import { SUPERADMIN_PERMISSIONS, hasPermission } from "@/lib/permissions";
import type { ApiUser, PlatformReport, ReportsResponse, ReportStatus, ReportTargetType } from "@/types/api";
const reportStatuses: Array<{ value: ReportStatus; label: string }> = [
{ value: "open", label: "Open" },
{ value: "in_review", label: "In review" },
{ value: "resolved", label: "Resolved" },
{ value: "rejected", label: "Rejected" },
];
const targetTypes: Array<{ value: ReportTargetType; label: string }> = [
{ value: "user", label: "User" },
{ value: "post", label: "Post" },
{ value: "comment", label: "Comment" },
{ value: "listing", label: "Listing" },
{ value: "repair_shop", label: "Repair shop" },
];
const reasonLabels: Record<string, string> = {
spam: "Spam",
harassment: "Harassment",
hate_speech: "Hate speech",
nudity: "Nudity",
violence: "Violence",
scam: "Scam",
intellectual_property: "Intellectual property",
self_harm: "Self harm",
other: "Other",
};
function reporterLabel(reporter: PlatformReport["reporterId"]) {
if (!reporter || typeof reporter === "string") {
return reporter || "-";
}
const user = reporter as ApiUser;
return user.stageName || user.name || user.username || user.email || user._id || "-";
}
function statusVariant(status: ReportStatus): "success" | "muted" | "warning" | "danger" {
if (status === "resolved") return "success";
if (status === "rejected") return "muted";
if (status === "in_review") return "warning";
return "danger";
}
export default function ReportsPage() {
const { permissions } = useSuperAdminSession();
const [statusFilter, setStatusFilter] = useState("open");
const [targetFilter, setTargetFilter] = useState("all");
const [page, setPage] = useState(1);
const [response, setResponse] = useState<ReportsResponse | null>(null);
const [selectedReport, setSelectedReport] = useState<PlatformReport | null>(null);
const [resolutionNote, setResolutionNote] = useState("");
const [updating, setUpdating] = useState(false);
const [loading, setLoading] = useState(true);
const { toast } = useToast();
const filtersRef = useRef({ statusFilter, targetFilter });
const canModerateContent = hasPermission(permissions, SUPERADMIN_PERMISSIONS.CONTENT_MODERATE);
filtersRef.current = { statusFilter, targetFilter };
const loadReports = useCallback(async () => {
if (!canModerateContent) {
setLoading(false);
return;
}
setLoading(true);
try {
const { statusFilter: currentStatus, targetFilter: currentTarget } = filtersRef.current;
const reports = await listPlatformReports({
page,
limit: 15,
status: currentStatus === "all" ? undefined : currentStatus,
targetType: currentTarget === "all" ? undefined : currentTarget,
sortOrder: "desc",
});
setResponse(reports);
const items = getItems(reports) as PlatformReport[];
setSelectedReport((current) =>
current ? items.find((item) => item._id === current._id) ?? items[0] ?? null : items[0] ?? null,
);
} catch (error) {
toast({ title: "Failed to load reports", description: String(error), variant: "danger" });
} finally {
setLoading(false);
}
}, [canModerateContent, page, toast]);
useEffect(() => {
void loadReports();
}, [loadReports]);
const reports = getItems(response) as PlatformReport[];
const summary = useMemo(
() => ({
open: reports.filter((item) => item.status === "open").length,
inReview: reports.filter((item) => item.status === "in_review").length,
resolved: reports.filter((item) => item.status === "resolved").length,
rejected: reports.filter((item) => item.status === "rejected").length,
}),
[reports],
);
const applyFilters = () => {
if (page === 1) {
void loadReports();
return;
}
setPage(1);
};
const updateStatus = async (status: ReportStatus) => {
if (!selectedReport) return;
setUpdating(true);
try {
const updated = await updatePlatformReportStatus(selectedReport._id, status, resolutionNote);
setSelectedReport(updated);
setResolutionNote("");
await loadReports();
toast({ title: "Report updated", description: `${selectedReport._id} -> ${status}`, variant: "success" });
} catch (error) {
toast({ title: "Report update failed", description: String(error), variant: "danger" });
} finally {
setUpdating(false);
}
};
if (!canModerateContent) {
return (
<div className="space-y-5 pb-8">
<PageHeader
title="Reports"
subtitle="Review user-submitted reports and update moderation workflow status."
/>
<NoPermissionState description="This page needs the content.moderate permission." />
</div>
);
}
return (
<div className="space-y-5 pb-8">
<PageHeader
title="Reports"
subtitle="Review reports with fixed reasons, status filters, and resolution notes."
actions={
<Button variant="outline" onClick={() => void loadReports()} disabled={loading}>
<RefreshCcw className="h-4 w-4" />
Refresh
</Button>
}
/>
<section className="grid gap-4 md:grid-cols-4">
<Card>
<CardHeader>
<CardTitle>Open</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold text-foreground">{summary.open}</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>In review</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold text-foreground">{summary.inReview}</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Resolved</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold text-foreground">{summary.resolved}</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Rejected</CardTitle>
</CardHeader>
<CardContent className="text-3xl font-bold text-foreground">{summary.rejected}</CardContent>
</Card>
</section>
<Card>
<CardHeader>
<CardTitle>Filters</CardTitle>
</CardHeader>
<CardContent className="grid gap-3 md:grid-cols-3">
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger>
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All statuses</SelectItem>
{reportStatuses.map((status) => (
<SelectItem key={status.value} value={status.value}>
{status.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={targetFilter} onValueChange={setTargetFilter}>
<SelectTrigger>
<SelectValue placeholder="Target" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All targets</SelectItem>
{targetTypes.map((target) => (
<SelectItem key={target.value} value={target.value}>
{target.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" onClick={applyFilters} disabled={loading}>
Apply
</Button>
</CardContent>
</Card>
<section className="grid gap-4 xl:grid-cols-12">
<Card className="xl:col-span-8">
<CardHeader>
<CardTitle>Report queue</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{!reports.length && !loading ? (
<EmptyState title="No reports" description="No reports match the selected filters." />
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Reporter</TableHead>
<TableHead>Target</TableHead>
<TableHead>Reason</TableHead>
<TableHead>Status</TableHead>
<TableHead>Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{reports.map((report) => (
<TableRow
key={report._id}
className={selectedReport?._id === report._id ? "bg-primary/10" : undefined}
onClick={() => setSelectedReport(report)}
>
<TableCell>{reporterLabel(report.reporterId)}</TableCell>
<TableCell>
<div className="space-y-1">
<Badge variant="muted">{report.targetType}</Badge>
<div className="font-mono text-xs text-muted-foreground">{report.targetId}</div>
</div>
</TableCell>
<TableCell>{reasonLabels[report.reason] ?? report.reason}</TableCell>
<TableCell>
<Badge variant={statusVariant(report.status)}>{report.status}</Badge>
</TableCell>
<TableCell>{formatDateTime(report.createdAt)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
<PaginationControls
pagination={getPagination(response)}
loading={loading}
onPageChange={setPage}
/>
</CardContent>
</Card>
<Card className="xl:col-span-4">
<CardHeader>
<CardTitle>Report details</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{!selectedReport ? (
<EmptyState title="Select a report" description="Choose a report row to review details." />
) : (
<>
<div className="rounded-xl border border-border/70 bg-secondary/20 p-4">
<div className="flex items-center justify-between gap-3">
<Badge variant={statusVariant(selectedReport.status)}>{selectedReport.status}</Badge>
<ShieldAlert className="h-4 w-4 text-muted-foreground" />
</div>
<div className="mt-3 text-sm text-foreground">
{selectedReport.details || "No additional details."}
</div>
<div className="mt-3 break-all font-mono text-xs text-muted-foreground">
{selectedReport._id}
</div>
</div>
<Textarea
placeholder="Resolution note"
value={resolutionNote}
onChange={(event) => setResolutionNote(event.target.value)}
/>
<div className="grid gap-2">
<Button
variant="outline"
disabled={updating}
onClick={() => void updateStatus("in_review")}
>
<ShieldAlert className="h-4 w-4" />
Mark in review
</Button>
<Button disabled={updating} onClick={() => void updateStatus("resolved")}>
<CheckCircle2 className="h-4 w-4" />
Resolve report
</Button>
<Button
variant="danger"
disabled={updating}
onClick={() => void updateStatus("rejected")}
>
Reject report
</Button>
</div>
</>
)}
</CardContent>
</Card>
</section>
</div>
);
}