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>
This commit is contained in:
@@ -1,13 +1,12 @@
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { HomeworkFilters } from "@/components/admin/homework-filters";
|
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
|
|
||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
search?: string;
|
q?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
courseId?: string;
|
courseId?: string;
|
||||||
page?: string;
|
page?: string;
|
||||||
@@ -15,20 +14,22 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default async function HomeworkListPage({ searchParams }: Props) {
|
export default async function HomeworkListPage({ searchParams }: Props) {
|
||||||
const { search = "", status = "", courseId = "", page = "1" } = await searchParams;
|
const sp = await searchParams;
|
||||||
const currentPage = Math.max(1, parseInt(page) || 1);
|
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;
|
const skip = (currentPage - 1) * PAGE_SIZE;
|
||||||
|
|
||||||
// Build where clause
|
// Build where clause
|
||||||
const where = {
|
const where = {
|
||||||
...(search
|
...(q
|
||||||
? {
|
? {
|
||||||
user: {
|
|
||||||
OR: [
|
OR: [
|
||||||
{ name: { contains: search, mode: "insensitive" as const } },
|
{ user: { name: { contains: q, mode: "insensitive" as const } } },
|
||||||
{ email: { contains: search, mode: "insensitive" as const } },
|
{ user: { email: { contains: q, mode: "insensitive" as const } } },
|
||||||
|
{ homework: { lesson: { title: { contains: q, mode: "insensitive" as const } } } },
|
||||||
],
|
],
|
||||||
},
|
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(courseId
|
...(courseId
|
||||||
@@ -38,10 +39,8 @@ export default async function HomeworkListPage({ searchParams }: Props) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(status === "pending" ? { status: "PENDING" } : {}),
|
...(status === "pending" ? { feedbacks: { none: {} } } : {}),
|
||||||
...(status === "reviewing" ? { status: "REVIEWING" } : {}),
|
...(status === "reviewed" ? { feedbacks: { some: {} } } : {}),
|
||||||
...(status === "approved" ? { status: "APPROVED" } : {}),
|
|
||||||
...(status === "rejected" ? { status: "REJECTED" } : {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const [submissions, total, courses] = await Promise.all([
|
const [submissions, total, courses] = await Promise.all([
|
||||||
@@ -71,17 +70,25 @@ export default async function HomeworkListPage({ searchParams }: Props) {
|
|||||||
|
|
||||||
const totalPages = Math.ceil(total / PAGE_SIZE);
|
const totalPages = Math.ceil(total / PAGE_SIZE);
|
||||||
|
|
||||||
const pendingCount = submissions.filter((s) => s.status === "PENDING").length;
|
|
||||||
|
|
||||||
function pageUrl(p: number) {
|
function pageUrl(p: number) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (search) params.set("search", search);
|
if (q) params.set("q", q);
|
||||||
if (status) params.set("status", status);
|
if (status) params.set("status", status);
|
||||||
if (courseId) params.set("courseId", courseId);
|
if (courseId) params.set("courseId", courseId);
|
||||||
params.set("page", String(p));
|
params.set("page", String(p));
|
||||||
return `/curator/homework?${params.toString()}`;
|
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 (
|
return (
|
||||||
<div className="p-8 max-w-3xl">
|
<div className="p-8 max-w-3xl">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@@ -92,12 +99,59 @@ export default async function HomeworkListPage({ searchParams }: Props) {
|
|||||||
Домашние задания
|
Домашние задания
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm mt-1" style={{ color: "var(--muted-foreground)" }}>
|
<p className="text-sm mt-1" style={{ color: "var(--muted-foreground)" }}>
|
||||||
{total} {total === 1 ? "работа" : "работ"} · {pendingCount} ожидают проверки
|
{total} {total === 1 ? "работа" : "работ"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<HomeworkFilters courses={courses} />
|
<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>
|
</Suspense>
|
||||||
|
|
||||||
{submissions.length === 0 ? (
|
{submissions.length === 0 ? (
|
||||||
@@ -111,19 +165,16 @@ export default async function HomeworkListPage({ searchParams }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
{submissions.map((s) => {
|
{submissions.map((s) => {
|
||||||
const statusMap: Record<string, { label: string; bg: string; color: string; border: string }> = {
|
const hasReview = s.feedbacks.length > 0;
|
||||||
PENDING: { label: "Новое", bg: "var(--foreground)", color: "var(--background)", border: "var(--foreground)" },
|
const reviewBadge = hasReview
|
||||||
REVIEWING: { label: "На рассмотрении", bg: "oklch(0.9 0.08 80)", color: "oklch(0.4 0.1 80)", border: "oklch(0.75 0.1 80)" },
|
? { label: "С отзывом", bg: "oklch(0.88 0.1 145)", color: "oklch(0.35 0.15 145)" }
|
||||||
APPROVED: { label: "Одобрено", bg: "oklch(0.88 0.1 145)", color: "oklch(0.35 0.15 145)", border: "oklch(0.7 0.15 145)" },
|
: { label: "Без ответа", bg: "var(--foreground)", color: "var(--background)" };
|
||||||
REJECTED: { label: "Отклонено", bg: "oklch(0.9 0.06 27)", color: "oklch(0.45 0.2 27)", border: "oklch(0.75 0.1 27)" },
|
|
||||||
};
|
|
||||||
const st = statusMap[s.status] ?? statusMap.PENDING;
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={s.id}
|
key={s.id}
|
||||||
href={`/curator/homework/${s.id}`}
|
href={`/curator/homework/${s.id}`}
|
||||||
className="flex items-center gap-4 px-4 py-3 text-sm transition-opacity hover:opacity-80"
|
className="flex items-center gap-4 px-4 py-3 text-sm transition-opacity hover:opacity-80"
|
||||||
style={{ border: `2px solid ${st.border}`, display: "flex" }}
|
style={{ border: "2px solid var(--border)", display: "flex" }}
|
||||||
>
|
>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="font-medium truncate">{s.user.name}</p>
|
<p className="font-medium truncate">{s.user.name}</p>
|
||||||
@@ -137,9 +188,9 @@ export default async function HomeworkListPage({ searchParams }: Props) {
|
|||||||
<div className="text-right shrink-0">
|
<div className="text-right shrink-0">
|
||||||
<span
|
<span
|
||||||
className="text-xs px-2 py-0.5 font-medium"
|
className="text-xs px-2 py-0.5 font-medium"
|
||||||
style={{ background: st.bg, color: st.color }}
|
style={{ background: reviewBadge.bg, color: reviewBadge.color }}
|
||||||
>
|
>
|
||||||
{st.label}
|
{reviewBadge.label}
|
||||||
</span>
|
</span>
|
||||||
<p className="text-xs mt-1" style={{ color: "var(--muted-foreground)" }}>
|
<p className="text-xs mt-1" style={{ color: "var(--muted-foreground)" }}>
|
||||||
{new Date(s.submittedAt).toLocaleDateString("ru-RU")}
|
{new Date(s.submittedAt).toLocaleDateString("ru-RU")}
|
||||||
@@ -186,7 +237,7 @@ export default async function HomeworkListPage({ searchParams }: Props) {
|
|||||||
<Link href={pageUrl(currentPage + 1)} className="btn-aubade px-3 py-1 text-xs">→</Link>
|
<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)" }}>
|
<span className="ml-2 text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||||||
стр. {currentPage} из {totalPages}
|
Страница {currentPage} из {totalPages} · Всего: {total}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user