Files
lms-sb/src/app/curator/homework/page.tsx
T
admins 7084806aac Add search, filters, pagination to homework list
- Replace client HomeworkFilters component with server-side <form method="GET">
- Switch search param from `search` to `q`, add lesson title to search scope
- Change status filter from DB status field to feedback-count logic (pending=no feedbacks, reviewed=has feedbacks)
- Update pagination label to "Страница X из Y · Всего: N"
- Preserve all existing submission links and layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 14:29:47 +05:00

247 lines
8.5 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 { Suspense } from "react";
const PAGE_SIZE = 20;
interface Props {
searchParams: Promise<{
q?: string;
status?: string;
courseId?: string;
page?: string;
}>;
}
export default async function HomeworkListPage({ searchParams }: Props) {
const sp = await searchParams;
const q = sp.q ?? "";
const status = sp.status ?? "";
const courseId = sp.courseId ?? "";
const currentPage = Math.max(1, parseInt(sp.page ?? "1") || 1);
const skip = (currentPage - 1) * PAGE_SIZE;
// Build where clause
const where = {
...(q
? {
OR: [
{ user: { name: { contains: q, mode: "insensitive" as const } } },
{ user: { email: { contains: q, mode: "insensitive" as const } } },
{ homework: { lesson: { title: { contains: q, mode: "insensitive" as const } } } },
],
}
: {}),
...(courseId
? {
homework: {
lesson: { module: { courseId } },
},
}
: {}),
...(status === "pending" ? { feedbacks: { none: {} } } : {}),
...(status === "reviewed" ? { feedbacks: { some: {} } } : {}),
};
const [submissions, total, courses] = await Promise.all([
prisma.homeworkSubmission.findMany({
where,
orderBy: { submittedAt: "desc" },
skip,
take: PAGE_SIZE,
include: {
user: { select: { name: true, email: true } },
feedbacks: { select: { id: true }, take: 1 },
homework: {
include: {
lesson: {
select: {
title: true,
module: { select: { title: true, course: { select: { id: true, title: true } } } },
},
},
},
},
},
}),
prisma.homeworkSubmission.count({ where }),
prisma.course.findMany({ orderBy: { title: "asc" }, select: { id: true, title: true } }),
]);
const totalPages = Math.ceil(total / PAGE_SIZE);
function pageUrl(p: number) {
const params = new URLSearchParams();
if (q) params.set("q", q);
if (status) params.set("status", status);
if (courseId) params.set("courseId", courseId);
params.set("page", String(p));
return `/curator/homework?${params.toString()}`;
}
const inputStyle: React.CSSProperties = {
border: "2px solid var(--border)",
background: "var(--background)",
outline: "none",
padding: "0.4rem 0.75rem",
fontSize: "0.8rem",
fontFamily: "inherit",
color: "var(--foreground)",
};
return (
<div className="p-8 max-w-3xl">
<div className="mb-6">
<h1
className="text-xs font-bold uppercase tracking-widest"
style={{ color: "var(--muted-foreground)" }}
>
Домашние задания
</h1>
<p className="text-sm mt-1" style={{ color: "var(--muted-foreground)" }}>
{total} {total === 1 ? "работа" : "работ"}
</p>
</div>
{/* Filters */}
<Suspense>
<form method="GET" className="flex flex-wrap gap-2 mb-5">
<input
name="q"
defaultValue={q}
placeholder="Поиск по ученику или уроку"
style={{ ...inputStyle, width: 260 }}
/>
<select
name="courseId"
defaultValue={courseId}
style={{ ...inputStyle, appearance: "none", paddingRight: "1.5rem", cursor: "pointer", maxWidth: 220 }}
>
<option value="">Все курсы</option>
{courses.map((c) => (
<option key={c.id} value={c.id}>{c.title}</option>
))}
</select>
<select
name="status"
defaultValue={status}
style={{ ...inputStyle, appearance: "none", paddingRight: "1.5rem", cursor: "pointer" }}
>
<option value="">Все</option>
<option value="pending">Без ответа</option>
<option value="reviewed">С отзывом</option>
</select>
<button
type="submit"
className="px-3 py-1 text-xs font-medium"
style={{ border: "2px solid var(--foreground)", background: "var(--foreground)", color: "var(--background)", cursor: "pointer" }}
>
Найти
</button>
{(q || status || courseId) && (
<Link
href="/curator/homework"
className="px-3 py-1 text-xs"
style={{ border: "2px solid var(--border)", color: "var(--muted-foreground)" }}
>
Сбросить
</Link>
)}
</form>
</Suspense>
{submissions.length === 0 ? (
<div className="card-aubade p-10 text-center">
<p className="text-3xl mb-2">📭</p>
<p className="font-bold">Ничего не найдено</p>
<p className="text-sm mt-1" style={{ color: "var(--muted-foreground)" }}>
Попробуйте изменить фильтры
</p>
</div>
) : (
<div className="space-y-1.5">
{submissions.map((s) => {
const hasReview = s.feedbacks.length > 0;
const reviewBadge = hasReview
? { label: "С отзывом", bg: "oklch(0.88 0.1 145)", color: "oklch(0.35 0.15 145)" }
: { label: "Без ответа", bg: "var(--foreground)", color: "var(--background)" };
return (
<Link
key={s.id}
href={`/curator/homework/${s.id}`}
className="flex items-center gap-4 px-4 py-3 text-sm transition-opacity hover:opacity-80"
style={{ border: "2px solid var(--border)", display: "flex" }}
>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{s.user.name}</p>
<p className="text-xs truncate" style={{ color: "var(--muted-foreground)" }}>
{s.homework.lesson.module.course.title} · {s.homework.lesson.title}
</p>
<p className="text-xs" style={{ color: "var(--muted-foreground)" }}>
{s.user.email}
</p>
</div>
<div className="text-right shrink-0">
<span
className="text-xs px-2 py-0.5 font-medium"
style={{ background: reviewBadge.bg, color: reviewBadge.color }}
>
{reviewBadge.label}
</span>
<p className="text-xs mt-1" style={{ color: "var(--muted-foreground)" }}>
{new Date(s.submittedAt).toLocaleDateString("ru-RU")}
</p>
</div>
</Link>
);
})}
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center gap-1 mt-5">
{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} · Всего: {total}
</span>
</div>
)}
</div>
);
}