Files
lms-sb/src/app/curator/homework/page.tsx
T
admins a239607360 Show homework badge by review status, not just feedback presence
Curator list labeled APPROVED-without-feedback submissions as 'Без ответа'
(badge keyed on feedbacks only). Key it on status: Принято / С отзывом /
На рассмотрении / Отклонено / Без ответа. Dashboards already count by status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 10:23:35 +05:00

260 lines
9.4 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" ? { status: { in: ["PENDING", "REVIEWING"] } } : {}),
...(status === "reviewed" ? { status: { in: ["APPROVED", "REJECTED"] } } : {}),
};
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;
// Метка по статусу проверки, а не только по наличию письменного отзыва:
// принятое кнопкой ДЗ (APPROVED без фидбека) — это «Принято», не «Без ответа».
let reviewBadge: { label: string; bg: string; color: string };
if (s.status === "APPROVED") {
reviewBadge = {
label: hasReview ? "С отзывом" : "Принято",
bg: "oklch(0.88 0.1 145)",
color: "oklch(0.35 0.15 145)",
};
} else if (s.status === "REJECTED") {
reviewBadge = { label: "Отклонено", bg: "oklch(0.9 0.06 27)", color: "oklch(0.45 0.2 27)" };
} else if (s.status === "REVIEWING") {
reviewBadge = { label: "На рассмотрении", bg: "oklch(0.9 0.08 80)", color: "oklch(0.4 0.1 80)" };
} else {
reviewBadge = { 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>
);
}