2436f77de5
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>
228 lines
6.9 KiB
TypeScript
228 lines
6.9 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useTransition, useRef } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import Link from "next/link";
|
||
import { submitFeedback, setReviewing, approveWithoutFeedback } from "./actions";
|
||
import { AudioRecorder } from "@/components/curator/audio-recorder";
|
||
|
||
interface FileItem {
|
||
name: string;
|
||
url: string;
|
||
size: number;
|
||
}
|
||
|
||
function formatSize(bytes: number) {
|
||
if (bytes < 1024) return `${bytes} Б`;
|
||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} КБ`;
|
||
return `${(bytes / 1024 / 1024).toFixed(1)} МБ`;
|
||
}
|
||
|
||
export function FeedbackForm({
|
||
submissionId,
|
||
currentStatus,
|
||
}: {
|
||
submissionId: string;
|
||
currentStatus: string;
|
||
}) {
|
||
const [text, setText] = useState("");
|
||
const [files, setFiles] = useState<FileItem[]>([]);
|
||
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
||
const [uploading, setUploading] = useState(false);
|
||
const [pending, startTransition] = useTransition();
|
||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
const router = useRouter();
|
||
|
||
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||
const picked = Array.from(e.target.files ?? []);
|
||
if (!picked.length) return;
|
||
setUploading(true);
|
||
const uploaded: FileItem[] = [];
|
||
for (const f of picked) {
|
||
const form = new FormData();
|
||
form.append("file", f);
|
||
const res = await fetch("/api/curator/upload", { method: "POST", body: form });
|
||
const data = await res.json();
|
||
if (data.url) uploaded.push({ name: data.name, url: data.url, size: data.size });
|
||
}
|
||
setFiles((prev) => [...prev, ...uploaded]);
|
||
setUploading(false);
|
||
if (fileInputRef.current) fileInputRef.current.value = "";
|
||
}
|
||
|
||
function handleAction(action: "approve" | "reject") {
|
||
if (!text.trim()) return;
|
||
startTransition(async () => {
|
||
await submitFeedback(submissionId, {
|
||
text: text.trim(),
|
||
files,
|
||
audioUrl,
|
||
action,
|
||
});
|
||
router.push("/curator/homework");
|
||
});
|
||
}
|
||
|
||
function handleReviewing() {
|
||
startTransition(async () => {
|
||
await setReviewing(submissionId);
|
||
});
|
||
}
|
||
|
||
function handleApproveNoFeedback() {
|
||
startTransition(async () => {
|
||
await approveWithoutFeedback(submissionId);
|
||
router.push("/curator/homework");
|
||
});
|
||
}
|
||
|
||
const isWorking = pending || uploading;
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
<p
|
||
className="text-xs font-bold uppercase tracking-widest"
|
||
style={{ color: "var(--muted-foreground)" }}
|
||
>
|
||
Ваш ответ
|
||
</p>
|
||
|
||
{/* Text */}
|
||
<textarea
|
||
value={text}
|
||
onChange={(e) => setText(e.target.value)}
|
||
placeholder="Напишите обратную связь студенту..."
|
||
disabled={isWorking}
|
||
style={{
|
||
border: "2px solid var(--border)",
|
||
background: "var(--background)",
|
||
outline: "none",
|
||
width: "100%",
|
||
padding: "0.5rem 0.75rem",
|
||
fontSize: "16px",
|
||
fontFamily: "inherit",
|
||
resize: "vertical",
|
||
minHeight: "120px",
|
||
}}
|
||
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
|
||
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
|
||
/>
|
||
|
||
{/* File upload */}
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
<label
|
||
className="btn-aubade text-xs px-3 py-1.5 cursor-pointer"
|
||
style={{ opacity: isWorking ? 0.5 : 1 }}
|
||
>
|
||
📎 Прикрепить файл
|
||
<input
|
||
ref={fileInputRef}
|
||
type="file"
|
||
multiple
|
||
className="hidden"
|
||
onChange={handleFileChange}
|
||
disabled={isWorking}
|
||
/>
|
||
</label>
|
||
{uploading && (
|
||
<span className="text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||
Загрузка...
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{files.length > 0 && (
|
||
<div className="space-y-1">
|
||
{files.map((f, i) => (
|
||
<div
|
||
key={f.url}
|
||
className="flex items-center gap-2 px-3 py-1.5 text-xs"
|
||
style={{ border: "1px solid var(--border)" }}
|
||
>
|
||
<span>📎</span>
|
||
<span className="flex-1 truncate">{f.name}</span>
|
||
<span style={{ color: "var(--muted-foreground)" }}>{formatSize(f.size)}</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => setFiles((prev) => prev.filter((_, j) => j !== i))}
|
||
style={{ color: "oklch(0.577 0.245 27.325)" }}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Audio recorder */}
|
||
<AudioRecorder value={audioUrl} onChange={setAudioUrl} />
|
||
|
||
{/* Action buttons */}
|
||
<div className="flex items-center gap-2 flex-wrap pt-1">
|
||
<button
|
||
type="button"
|
||
onClick={() => handleAction("approve")}
|
||
disabled={isWorking || !text.trim()}
|
||
className="btn-aubade-accent px-4 py-2 text-sm"
|
||
style={{ opacity: isWorking || !text.trim() ? 0.5 : 1 }}
|
||
>
|
||
{pending ? "Отправка..." : "Отправить ответ"}
|
||
</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" && (
|
||
<button
|
||
type="button"
|
||
onClick={handleReviewing}
|
||
disabled={isWorking}
|
||
className="px-4 py-2 text-sm"
|
||
style={{
|
||
border: "2px solid var(--border)",
|
||
background: "oklch(0.9 0.08 80)",
|
||
color: "oklch(0.4 0.1 80)",
|
||
opacity: isWorking ? 0.5 : 1,
|
||
}}
|
||
>
|
||
На рассмотрение
|
||
</button>
|
||
)}
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => handleAction("reject")}
|
||
disabled={isWorking || !text.trim()}
|
||
className="px-4 py-2 text-sm"
|
||
style={{
|
||
border: "2px solid var(--border)",
|
||
background: "oklch(0.9 0.06 27)",
|
||
color: "oklch(0.45 0.2 27)",
|
||
opacity: isWorking || !text.trim() ? 0.5 : 1,
|
||
}}
|
||
>
|
||
Отклонить и отправить
|
||
</button>
|
||
|
||
<Link
|
||
href="/curator/homework"
|
||
className="px-4 py-2 text-sm"
|
||
style={{ border: "2px solid var(--border)", color: "var(--muted-foreground)" }}
|
||
>
|
||
К списку ДЗ
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|