7084806aac
- 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>
247 lines
8.5 KiB
TypeScript
247 lines
8.5 KiB
TypeScript
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>
|
||
);
|
||
}
|