Files
lms-sb/src/app/admin/users/page.tsx
T

141 lines
4.7 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; balance?: string }>;
}
export default async function UsersPage({ searchParams }: Props) {
const { search = "", role = "", page = "1", balance = "" } = await searchParams;
const currentPage = Math.max(1, parseInt(page) || 1);
const skip = (currentPage - 1) * PAGE_SIZE;
// Collect userIds with non-zero balance if filter is active
let balanceUserIds: string[] | null = null;
if (balance === "nonzero") {
const groups = await prisma.balanceTransaction.groupBy({
by: ["userId"],
_sum: { amount: true },
having: { amount: { _sum: { not: { equals: 0 } } } },
});
balanceUserIds = groups.map((g) => g.userId);
}
const where = {
...(search
? {
OR: [
{ name: { contains: search, mode: "insensitive" as const } },
{ email: { contains: search, mode: "insensitive" as const } },
],
}
: {}),
...(role ? { role } : {}),
...(balanceUserIds !== null ? { id: { in: balanceUserIds } } : {}),
};
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);
if (balance) params.set("balance", balance);
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} initialBalance={balance} />
</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>
);
}