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:
@@ -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")}
|
||||
|
||||
Reference in New Issue
Block a user