Add homework review workflow: statuses, audio, file attachments, tabs

- HomeworkSubmission: add status (PENDING/REVIEWING/APPROVED/REJECTED) + statusAt
- HomeworkFeedback: add files (Json) + audioUrl fields
- Curator detail page: meta table, content tabs, feedback history with audio/files
- FeedbackForm: file upload, audio recorder (Web Audio API + S3), action buttons
- AudioRecorder component: record → preview → upload to S3
- ContentTabs: toggle between homework description and lesson content (TipTap read-only)
- Homework list: 4-color status badges with proper filtering
- API routes: /api/curator/upload and /api/curator/audio-upload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-08 14:01:55 +05:00
parent 768a38b9d3
commit 3855bbd4be
15 changed files with 743 additions and 98 deletions
+17 -16
View File
@@ -38,8 +38,10 @@ export default async function HomeworkListPage({ searchParams }: Props) {
},
}
: {}),
...(status === "pending" ? { feedbacks: { none: {} } } : {}),
...(status === "reviewed" ? { feedbacks: { some: {} } } : {}),
...(status === "pending" ? { status: "PENDING" } : {}),
...(status === "reviewing" ? { status: "REVIEWING" } : {}),
...(status === "approved" ? { status: "APPROVED" } : {}),
...(status === "rejected" ? { status: "REJECTED" } : {}),
};
const [submissions, total, courses] = await Promise.all([
@@ -50,7 +52,7 @@ export default async function HomeworkListPage({ searchParams }: Props) {
take: PAGE_SIZE,
include: {
user: { select: { name: true, email: true } },
feedbacks: { select: { id: true } },
feedbacks: { select: { id: true }, take: 1 },
homework: {
include: {
lesson: {
@@ -69,7 +71,7 @@ export default async function HomeworkListPage({ searchParams }: Props) {
const totalPages = Math.ceil(total / PAGE_SIZE);
const pendingCount = submissions.filter((s) => s.feedbacks.length === 0).length;
const pendingCount = submissions.filter((s) => s.status === "PENDING").length;
function pageUrl(p: number) {
const params = new URLSearchParams();
@@ -109,16 +111,19 @@ export default async function HomeworkListPage({ searchParams }: Props) {
) : (
<div className="space-y-1.5">
{submissions.map((s) => {
const isPending = s.feedbacks.length === 0;
const statusMap: Record<string, { label: string; bg: string; color: string; border: string }> = {
PENDING: { label: "Новое", bg: "var(--foreground)", color: "var(--background)", border: "var(--foreground)" },
REVIEWING: { label: "На рассмотрении", bg: "oklch(0.9 0.08 80)", color: "oklch(0.4 0.1 80)", border: "oklch(0.75 0.1 80)" },
APPROVED: { label: "Одобрено", bg: "oklch(0.88 0.1 145)", color: "oklch(0.35 0.15 145)", border: "oklch(0.7 0.15 145)" },
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 (
<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 ${isPending ? "var(--foreground)" : "var(--border)"}`,
display: "flex",
}}
style={{ border: `2px solid ${st.border}`, display: "flex" }}
>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{s.user.name}</p>
@@ -131,14 +136,10 @@ export default async function HomeworkListPage({ searchParams }: Props) {
</div>
<div className="text-right shrink-0">
<span
className="text-xs px-2 py-0.5"
style={{
border: "1px solid var(--border)",
background: isPending ? "var(--foreground)" : "transparent",
color: isPending ? "var(--background)" : "var(--muted-foreground)",
}}
className="text-xs px-2 py-0.5 font-medium"
style={{ background: st.bg, color: st.color }}
>
{isPending ? "Новое" : "Проверено"}
{st.label}
</span>
<p className="text-xs mt-1" style={{ color: "var(--muted-foreground)" }}>
{new Date(s.submittedAt).toLocaleDateString("ru-RU")}