ba0a630fd9
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
117 lines
3.9 KiB
TypeScript
117 lines
3.9 KiB
TypeScript
import { prisma } from "@/lib/prisma";
|
||
import { notFound } from "next/navigation";
|
||
import Link from "next/link";
|
||
|
||
interface Props {
|
||
params: Promise<{ quizId: string }>;
|
||
}
|
||
|
||
export default async function AdminQuizAttemptsPage({ params }: Props) {
|
||
const { quizId } = await params;
|
||
|
||
const quiz = await prisma.quiz.findUnique({
|
||
where: { id: quizId },
|
||
include: {
|
||
questions: { orderBy: { order: "asc" } },
|
||
attempts: { orderBy: { completedAt: "desc" } },
|
||
lesson: {
|
||
select: {
|
||
title: true,
|
||
module: {
|
||
select: {
|
||
course: { select: { title: true } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
if (!quiz) notFound();
|
||
|
||
const userIds = [...new Set(quiz.attempts.map((a) => a.userId))];
|
||
const users = await prisma.user.findMany({
|
||
where: { id: { in: userIds } },
|
||
select: { id: true, name: true, email: true },
|
||
});
|
||
const userMap = Object.fromEntries(users.map((u) => [u.id, u]));
|
||
|
||
return (
|
||
<div className="p-8 max-w-4xl">
|
||
<nav
|
||
className="text-xs mb-6 uppercase tracking-widest"
|
||
style={{ color: "var(--muted-foreground)" }}
|
||
>
|
||
<Link href="/admin/quizzes" className="hover:underline">
|
||
Тесты
|
||
</Link>
|
||
<span className="mx-2">/</span>
|
||
<span style={{ color: "var(--foreground)" }}>{quiz.lesson.title}</span>
|
||
</nav>
|
||
|
||
<div className="mb-6">
|
||
<h1 className="text-lg font-bold">{quiz.lesson.title}</h1>
|
||
<p className="text-xs mt-1 uppercase tracking-widest" style={{ color: "var(--muted-foreground)" }}>
|
||
{quiz.lesson.module.course.title} · {quiz.questions.length} вопросов · {quiz.attempts.length} ответов
|
||
</p>
|
||
</div>
|
||
|
||
{quiz.attempts.length === 0 ? (
|
||
<p className="text-sm" style={{ color: "var(--muted-foreground)" }}>
|
||
Ответов пока нет
|
||
</p>
|
||
) : (
|
||
<div className="space-y-4">
|
||
{quiz.attempts.map((attempt) => {
|
||
const answers = attempt.answers as Record<string, string>;
|
||
const user = userMap[attempt.userId];
|
||
const date = new Date(attempt.completedAt).toLocaleString("ru-RU", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
});
|
||
|
||
return (
|
||
<div
|
||
key={attempt.id}
|
||
className="px-4 py-4 space-y-3"
|
||
style={{ border: "2px solid var(--border)" }}
|
||
>
|
||
<div className="flex items-center justify-between gap-2">
|
||
<div>
|
||
<p className="text-sm font-medium">{user?.name ?? "—"}</p>
|
||
<p className="text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||
{user?.email ?? attempt.userId}
|
||
</p>
|
||
</div>
|
||
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||
{date}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="space-y-2 pt-2" style={{ borderTop: "1px solid var(--border)" }}>
|
||
{quiz.questions.map((q, idx) => (
|
||
<div key={q.id}>
|
||
<p
|
||
className="text-xs font-medium mb-0.5"
|
||
style={{ color: "var(--muted-foreground)" }}
|
||
>
|
||
{idx + 1}. {q.text}
|
||
</p>
|
||
<p className="text-sm whitespace-pre-wrap">
|
||
{answers[q.id]?.trim() || <span style={{ color: "var(--muted-foreground)" }}>—</span>}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|