Add 'approve without feedback' button for homework review
Curators/admins can mark a submission APPROVED (and complete the lesson) without writing feedback. The student sees 'ДЗ принято ✓' and can no longer resubmit. No email is sent on silent approval. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -108,6 +108,51 @@ export async function setReviewing(submissionId: string) {
|
|||||||
revalidatePath(`/curator/homework/${submissionId}`);
|
revalidatePath(`/curator/homework/${submissionId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Принять ДЗ без письменного ответа: ставим APPROVED и засчитываем урок,
|
||||||
|
* фидбек не создаём и студенту письмо не шлём (тихое принятие).
|
||||||
|
*/
|
||||||
|
export async function approveWithoutFeedback(submissionId: string) {
|
||||||
|
await requireCurator();
|
||||||
|
|
||||||
|
const submission = await prisma.homeworkSubmission.findUnique({
|
||||||
|
where: { id: submissionId },
|
||||||
|
include: {
|
||||||
|
homework: {
|
||||||
|
include: {
|
||||||
|
lesson: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
module: { select: { course: { select: { slug: true } } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!submission) throw new Error("Submission not found");
|
||||||
|
|
||||||
|
const lesson = submission.homework.lesson;
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
await tx.homeworkSubmission.update({
|
||||||
|
where: { id: submissionId },
|
||||||
|
data: { status: "APPROVED", statusAt: new Date() },
|
||||||
|
});
|
||||||
|
await tx.lessonProgress.upsert({
|
||||||
|
where: { userId_lessonId: { userId: submission.userId, lessonId: lesson.id } },
|
||||||
|
create: { userId: submission.userId, lessonId: lesson.id },
|
||||||
|
update: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
revalidatePath("/curator/homework");
|
||||||
|
revalidatePath(`/curator/homework/${submissionId}`);
|
||||||
|
revalidatePath(`/courses/${lesson.module.course.slug}/lessons/${lesson.id}`);
|
||||||
|
revalidatePath(`/courses/${lesson.module.course.slug}`);
|
||||||
|
revalidatePath("/dashboard");
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteSubmission(submissionId: string) {
|
export async function deleteSubmission(submissionId: string) {
|
||||||
await requireCurator();
|
await requireCurator();
|
||||||
await prisma.homeworkSubmission.delete({ where: { id: submissionId } });
|
await prisma.homeworkSubmission.delete({ where: { id: submissionId } });
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useTransition, useRef } from "react";
|
import { useState, useTransition, useRef } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { submitFeedback, setReviewing } from "./actions";
|
import { submitFeedback, setReviewing, approveWithoutFeedback } from "./actions";
|
||||||
import { AudioRecorder } from "@/components/curator/audio-recorder";
|
import { AudioRecorder } from "@/components/curator/audio-recorder";
|
||||||
|
|
||||||
interface FileItem {
|
interface FileItem {
|
||||||
@@ -69,6 +69,13 @@ export function FeedbackForm({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleApproveNoFeedback() {
|
||||||
|
startTransition(async () => {
|
||||||
|
await approveWithoutFeedback(submissionId);
|
||||||
|
router.push("/curator/homework");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isWorking = pending || uploading;
|
const isWorking = pending || uploading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -164,6 +171,17 @@ export function FeedbackForm({
|
|||||||
{pending ? "Отправка..." : "Отправить ответ"}
|
{pending ? "Отправка..." : "Отправить ответ"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleApproveNoFeedback}
|
||||||
|
disabled={isWorking}
|
||||||
|
className="btn-aubade px-4 py-2 text-sm"
|
||||||
|
style={{ opacity: isWorking ? 0.5 : 1 }}
|
||||||
|
title="Засчитать ДЗ без письменного ответа"
|
||||||
|
>
|
||||||
|
Принять и отметить выполненным
|
||||||
|
</button>
|
||||||
|
|
||||||
{currentStatus !== "REVIEWING" && (
|
{currentStatus !== "REVIEWING" && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface Submission {
|
|||||||
files: HWFile[];
|
files: HWFile[];
|
||||||
audioUrl?: string | null;
|
audioUrl?: string | null;
|
||||||
submittedAt: Date;
|
submittedAt: Date;
|
||||||
|
status: string;
|
||||||
feedbacks: Feedback[];
|
feedbacks: Feedback[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +48,9 @@ export function HomeworkSection({ homework, submission, slug, lessonId, allowAud
|
|||||||
const [editing, setEditing] = useState(!submission);
|
const [editing, setEditing] = useState(!submission);
|
||||||
|
|
||||||
const isReviewed = submission && submission.feedbacks.length > 0;
|
const isReviewed = submission && submission.feedbacks.length > 0;
|
||||||
|
const isApproved = submission?.status === "APPROVED";
|
||||||
|
// Финализировано: есть фидбек ИЛИ ДЗ принято кнопкой без ответа. Нельзя пересдавать.
|
||||||
|
const isLocked = Boolean(isReviewed || isApproved);
|
||||||
|
|
||||||
const inputStyle = {
|
const inputStyle = {
|
||||||
border: "2px solid var(--border)",
|
border: "2px solid var(--border)",
|
||||||
@@ -138,8 +142,45 @@ export function HomeworkSection({ homework, submission, slug, lessonId, allowAud
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Принято без письменного ответа */}
|
||||||
|
{isApproved && !isReviewed && submission && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs px-2 py-0.5 font-bold uppercase tracking-widest" style={{ border: "2px solid var(--foreground)", background: "var(--accent)" }}>
|
||||||
|
ДЗ принято ✓
|
||||||
|
</span>
|
||||||
|
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
Сдано {new Date(submission.submittedAt).toLocaleDateString("ru-RU")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{submission.text && (
|
||||||
|
<div className="px-4 py-3 text-sm whitespace-pre-wrap opacity-70" style={{ border: "2px solid var(--border)" }}>
|
||||||
|
{submission.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{submission.audioUrl && (
|
||||||
|
<div className="px-4 py-2" style={{ border: "1px solid var(--border)" }}>
|
||||||
|
<p className="text-xs mb-1" style={{ color: "var(--muted-foreground)" }}>Аудио-ответ:</p>
|
||||||
|
<audio controls src={submission.audioUrl} style={{ height: 32, width: "100%" }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{submission.files.length > 0 && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{submission.files.map((f) => (
|
||||||
|
<a key={f.url} href={f.url} target="_blank" rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 px-3 py-2 text-xs" style={{ border: "2px solid var(--border)" }}>
|
||||||
|
<span>📎</span>
|
||||||
|
<span className="flex-1 underline">{f.name}</span>
|
||||||
|
<span style={{ color: "var(--muted-foreground)" }}>{formatSize(f.size)}</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Submitted, pending review */}
|
{/* Submitted, pending review */}
|
||||||
{submission && !isReviewed && !editing && (
|
{submission && !isLocked && !editing && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -182,7 +223,7 @@ export function HomeworkSection({ homework, submission, slug, lessonId, allowAud
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
{editing && !isReviewed && (
|
{editing && !isLocked && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<textarea
|
<textarea
|
||||||
value={text}
|
value={text}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export async function submitHomework(
|
|||||||
include: { feedbacks: true },
|
include: { feedbacks: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existing?.feedbacks && existing.feedbacks.length > 0) {
|
if ((existing?.feedbacks && existing.feedbacks.length > 0) || existing?.status === "APPROVED") {
|
||||||
throw new Error("Работа уже проверена");
|
throw new Error("Работа уже проверена");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user