Files
lms-sb/src/app/admin/users/page.tsx
T
admins d0ba4bf909 Polish: homework filters, users search/popup, admin comments
Homework (/curator/homework):
- Search by student name/email
- Filter by status (pending/reviewed) and course
- Server-side pagination (20 per page) with URL params

Users (/admin/users):
- Search by name/email, filter by role
- Hover popup on each row: enrolled courses + expiry dates + email
- Pagination (20 per page) with URL params

Comments (/admin/comments):
- New admin page with all active comments
- Search by author or text content
- One-click delete (soft-delete) from the table
- Pagination (30 per page)
- Added "Комментарии" link to admin nav

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 13:00:57 +05:00

128 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { prisma } from "@/lib/prisma";
import Link from "next/link";
import { UserPlus } from "lucide-react";
import { UsersTable } from "@/components/admin/users-table";
import { Suspense } from "react";
import { UsersSearch } from "@/components/admin/users-search";
const PAGE_SIZE = 20;
interface Props {
searchParams: Promise<{ search?: string; role?: string; page?: string }>;
}
export default async function UsersPage({ searchParams }: Props) {
const { search = "", role = "", page = "1" } = await searchParams;
const currentPage = Math.max(1, parseInt(page) || 1);
const skip = (currentPage - 1) * PAGE_SIZE;
const where = {
...(search
? {
OR: [
{ name: { contains: search, mode: "insensitive" as const } },
{ email: { contains: search, mode: "insensitive" as const } },
],
}
: {}),
...(role ? { role } : {}),
};
const [users, total] = await Promise.all([
prisma.user.findMany({
where,
orderBy: { createdAt: "desc" },
skip,
take: PAGE_SIZE,
include: {
_count: { select: { enrollments: true } },
enrollments: {
include: { course: { select: { title: true } } },
orderBy: { enrolledAt: "desc" },
},
},
}),
prisma.user.count({ where }),
]);
const totalPages = Math.ceil(total / PAGE_SIZE);
const tableUsers = users.map((u) => ({
id: u.id,
name: u.name,
email: u.email,
role: u.role,
emailVerified: u.emailVerified,
createdAt: u.createdAt,
enrollmentCount: u._count.enrollments,
enrollments: u.enrollments.map((e) => ({
courseId: e.courseId,
courseTitle: e.course.title,
expiresAt: e.expiresAt,
})),
}));
function pageUrl(p: number) {
const params = new URLSearchParams();
if (search) params.set("search", search);
if (role) params.set("role", role);
params.set("page", String(p));
return `/admin/users?${params.toString()}`;
}
return (
<div className="p-8">
<div className="mb-5 flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold text-slate-800">Пользователи</h1>
<p className="text-slate-500 text-sm mt-0.5">{total} пользователей</p>
</div>
<Link
href="/admin/users/new"
className="btn-aubade btn-aubade-accent flex items-center gap-1.5 px-4 py-2 text-sm"
>
<UserPlus size={14} />
Добавить пользователя
</Link>
</div>
{/* Filters */}
<Suspense>
<UsersSearch initialSearch={search} initialRole={role} />
</Suspense>
<UsersTable users={tableUsers} />
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center gap-1 mt-4">
{currentPage > 1 && (
<Link href={pageUrl(currentPage - 1)} className="btn-aubade px-3 py-1 text-xs"></Link>
)}
{Array.from({ length: totalPages }, (_, i) => i + 1)
.filter((p) => p === 1 || p === totalPages || Math.abs(p - currentPage) <= 2)
.reduce<(number | "…")[]>((acc, p, i, arr) => {
if (i > 0 && (p as number) - (arr[i - 1] as number) > 1) acc.push("…");
acc.push(p);
return acc;
}, [])
.map((p, i) =>
p === "…" ? (
<span key={`e${i}`} className="px-2 text-xs" style={{ color: "var(--muted-foreground)" }}></span>
) : (
<Link key={p} href={pageUrl(p as number)} className="px-3 py-1 text-xs"
style={{ border: "2px solid var(--border)", background: p === currentPage ? "var(--foreground)" : "transparent", color: p === currentPage ? "var(--background)" : "var(--foreground)" }}>
{p}
</Link>
)
)}
{currentPage < totalPages && (
<Link href={pageUrl(currentPage + 1)} className="btn-aubade px-3 py-1 text-xs"></Link>
)}
<span className="ml-2 text-xs" style={{ color: "var(--muted-foreground)" }}>стр. {currentPage} из {totalPages}</span>
</div>
)}
</div>
);
}