完善课程小结审核操作
This commit is contained in:
@@ -1585,6 +1585,8 @@ OPERATION_LABELS = {
|
||||
"admin_course_summary_delete": "课程小结删除",
|
||||
"admin_course_summary_duplicate_scan": "重复小结扫描",
|
||||
"admin_course_summary_duplicate_delete": "重复小结删除",
|
||||
"admin_course_summary_review_update": "课程小结审核修正",
|
||||
"admin_course_summary_link_existing": "课程小结关联已有记录",
|
||||
"register-class-records": "登记上课记录",
|
||||
"register-payments": "登记缴费记录",
|
||||
"admin-create-account": "新增课时账户",
|
||||
@@ -2553,6 +2555,80 @@ def create_incomplete_course_summary_review_task(
|
||||
return create_course_summary_review_task(tasks_path, summary, "", reasons)
|
||||
|
||||
|
||||
def editable_course_summary_payload(summary: dict, updates: dict) -> dict:
|
||||
merged = dict(summary)
|
||||
for key in ("student", "date_iso", "time_range", "teacher", "subject"):
|
||||
if key in updates:
|
||||
merged[key] = str(updates.get(key) or "").strip()
|
||||
normalized = normalize_course_summary(merged)
|
||||
normalized["source_id"] = str(summary.get("source_id") or normalized.get("source_id") or "")
|
||||
normalized["group"] = str(summary.get("group") or "")
|
||||
normalized["sender"] = str(summary.get("sender") or "")
|
||||
normalized["sender_id"] = str(summary.get("sender_id") or "")
|
||||
normalized["message_time"] = str(summary.get("message_time") or "")
|
||||
normalized["message_date"] = str(summary.get("message_date") or normalized.get("message_date") or "")
|
||||
normalized["db"] = str(summary.get("db") or "")
|
||||
normalized["local_id"] = str(summary.get("local_id") or "")
|
||||
normalized["title"] = str(summary.get("title") or "")
|
||||
normalized["body"] = str(summary.get("body") or normalized.get("body") or "")
|
||||
normalized["recognition_source"] = str(summary.get("recognition_source") or "")
|
||||
normalized["confidence"] = str(summary.get("confidence") or "")
|
||||
normalized["teacher_trusted"] = True
|
||||
normalized["remark"] = str(summary.get("remark") or "")
|
||||
return normalized
|
||||
|
||||
|
||||
def update_course_summary_review_task(
|
||||
tasks_path: Path,
|
||||
classnotes_path: Path,
|
||||
accounts_path: Path,
|
||||
task_id: int,
|
||||
updates: dict,
|
||||
) -> dict:
|
||||
tasks = read_admin_tasks(tasks_path)
|
||||
task = find_admin_task(tasks, task_id)
|
||||
if task.get("type") != "course_summary_review":
|
||||
raise ValueError("该任务不是课程小结审核")
|
||||
if task.get("status") not in {"pending", "conflict"}:
|
||||
raise ValueError("该任务已处理,不能修改")
|
||||
summary = task.get("summary") or {}
|
||||
updated_summary = editable_course_summary_payload(summary, updates)
|
||||
reasons, proposed_line = auto_register_reasons(updated_summary, classnotes_path, accounts_path)
|
||||
now = datetime.now().isoformat(timespec="seconds")
|
||||
task["status"] = "pending"
|
||||
task["updated_at"] = now
|
||||
task.pop("message", None)
|
||||
task["student"] = updated_summary["student"]
|
||||
task["summary"] = updated_summary
|
||||
task["proposed_line"] = proposed_line
|
||||
task["reasons"] = reasons
|
||||
write_admin_tasks(tasks_path, tasks)
|
||||
return {"task": task_to_dict(task)}
|
||||
|
||||
|
||||
def link_existing_course_summary_task(tasks_path: Path, classnotes_path: Path, task_id: int) -> dict:
|
||||
tasks = read_admin_tasks(tasks_path)
|
||||
task = find_admin_task(tasks, task_id)
|
||||
if task.get("type") != "course_summary_review":
|
||||
raise ValueError("该任务不是课程小结审核")
|
||||
if task.get("status") not in {"pending", "conflict"}:
|
||||
raise ValueError("该任务已处理,不能重复操作")
|
||||
proposed_line = str(task.get("proposed_line") or "").strip()
|
||||
if not proposed_line:
|
||||
raise ValueError("课程小结缺少候选登记行,不能关联已有记录")
|
||||
existing_lines = {raw.strip() for raw in classnotes_path.read_text(encoding="utf-8").splitlines()}
|
||||
if proposed_line not in existing_lines:
|
||||
raise ValueError("候选登记行在上课记录中不存在,不能关联已有记录")
|
||||
now = datetime.now().isoformat(timespec="seconds")
|
||||
task["status"] = "approved"
|
||||
task["updated_at"] = now
|
||||
task["reviewed_at"] = now
|
||||
task["linked_record_line"] = proposed_line
|
||||
task["message"] = "已关联已有上课记录,未重复扣课时"
|
||||
write_admin_tasks(tasks_path, tasks)
|
||||
return {"task": task_to_dict(task), "linked_record_line": proposed_line}
|
||||
|
||||
|
||||
def approve_course_summary_task(
|
||||
tasks_path: Path,
|
||||
classnotes_path: Path,
|
||||
|
||||
@@ -18,6 +18,7 @@ from ..data import (
|
||||
approve_admin_task,
|
||||
create_course_summary_duplicate_review_tasks,
|
||||
delete_course_summary,
|
||||
link_existing_course_summary_task,
|
||||
list_admin_tasks,
|
||||
list_operation_logs,
|
||||
migrate_operation_log_labels,
|
||||
@@ -25,6 +26,7 @@ from ..data import (
|
||||
reject_admin_task,
|
||||
resolve_duplicate_course_summary_task,
|
||||
rollback_operation_log,
|
||||
update_course_summary_review_task,
|
||||
update_course_summary_time,
|
||||
)
|
||||
|
||||
@@ -168,6 +170,54 @@ def admin_resolve_duplicate_summary(task_id: int, payload: dict, _user: str = De
|
||||
return {"ok": True, **result}
|
||||
|
||||
|
||||
@router.post("/api/admin/tasks/{task_id}/course-summary-review")
|
||||
def admin_update_course_summary_review(task_id: int, payload: dict, _user: str = Depends(verify_admin_auth)):
|
||||
try:
|
||||
with write_lock:
|
||||
result = update_course_summary_review_task(
|
||||
ADMIN_TASKS_PATH,
|
||||
CLASSNOTES_PATH,
|
||||
ACCOUNTS_PATH,
|
||||
task_id,
|
||||
payload,
|
||||
)
|
||||
task = result.get("task", {})
|
||||
append_operation_log(
|
||||
OPERATION_LOGS_PATH,
|
||||
"课程小结审核修正",
|
||||
"已更新",
|
||||
task_id=task_id,
|
||||
task_type=str(task.get("type") or ""),
|
||||
student=str(task.get("student") or ""),
|
||||
source_id=str(task.get("source_id") or ""),
|
||||
proposed_line=str(task.get("proposed_line") or ""),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return {"ok": True, **result}
|
||||
|
||||
|
||||
@router.post("/api/admin/tasks/{task_id}/link-existing-course-summary")
|
||||
def admin_link_existing_course_summary(task_id: int, _user: str = Depends(verify_admin_auth)):
|
||||
try:
|
||||
with write_lock:
|
||||
result = link_existing_course_summary_task(ADMIN_TASKS_PATH, CLASSNOTES_PATH, task_id)
|
||||
task = result.get("task", {})
|
||||
append_operation_log(
|
||||
OPERATION_LOGS_PATH,
|
||||
"课程小结关联已有记录",
|
||||
"已批准",
|
||||
task_id=task_id,
|
||||
task_type=str(task.get("type") or ""),
|
||||
student=str(task.get("student") or ""),
|
||||
source_id=str(task.get("source_id") or ""),
|
||||
proposed_line=str(result.get("linked_record_line") or ""),
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
return {"ok": True, **result}
|
||||
|
||||
|
||||
@router.post("/api/admin/tasks/{task_id}/reject")
|
||||
def admin_reject_task(task_id: int, _user: str = Depends(verify_admin_auth)):
|
||||
try:
|
||||
|
||||
@@ -267,6 +267,31 @@
|
||||
<div class="drawer-label">课程信息</div>
|
||||
<div id="summaryReviewDrawerCourse" class="drawer-text"></div>
|
||||
</div>
|
||||
<div id="summaryReviewEditSection" class="drawer-section" hidden>
|
||||
<div class="drawer-label">修正信息</div>
|
||||
<div class="form-grid compact-grid">
|
||||
<label>
|
||||
学生
|
||||
<input id="summaryReviewEditStudent" autocomplete="off" />
|
||||
</label>
|
||||
<label>
|
||||
日期
|
||||
<input id="summaryReviewEditDate" type="date" />
|
||||
</label>
|
||||
<label>
|
||||
时间
|
||||
<input id="summaryReviewEditTime" autocomplete="off" placeholder="08:00-10:00" />
|
||||
</label>
|
||||
<label>
|
||||
老师
|
||||
<input id="summaryReviewEditTeacher" autocomplete="off" />
|
||||
</label>
|
||||
<label>
|
||||
科目
|
||||
<input id="summaryReviewEditSubject" autocomplete="off" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-section">
|
||||
<div class="drawer-label">建议登记行</div>
|
||||
<div id="summaryReviewDrawerLine"></div>
|
||||
@@ -285,6 +310,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="drawer-actions">
|
||||
<button id="summaryReviewDrawerSave" class="secondary-button" type="button">保存修正</button>
|
||||
<button id="summaryReviewDrawerLink" class="secondary-button" type="button">关联已有记录</button>
|
||||
<button id="summaryReviewDrawerReject" class="secondary-button" type="button">驳回</button>
|
||||
<button id="summaryReviewDrawerApprove" type="button">批准</button>
|
||||
</div>
|
||||
@@ -341,6 +368,8 @@
|
||||
<option value="修改老师档案">修改老师档案</option>
|
||||
<option value="课程小结接收">课程小结接收</option>
|
||||
<option value="课程小结登记">课程小结登记</option>
|
||||
<option value="课程小结审核修正">课程小结审核修正</option>
|
||||
<option value="课程小结关联已有记录">课程小结关联已有记录</option>
|
||||
<option value="重复小结扫描">重复小结扫描</option>
|
||||
<option value="重复小结删除">重复小结删除</option>
|
||||
<option value="审核批准">审核批准</option>
|
||||
|
||||
@@ -56,10 +56,18 @@ const summaryReviewDrawerTitle = document.querySelector("#summaryReviewDrawerTit
|
||||
const summaryReviewDrawerMeta = document.querySelector("#summaryReviewDrawerMeta");
|
||||
const summaryReviewDrawerStatus = document.querySelector("#summaryReviewDrawerStatus");
|
||||
const summaryReviewDrawerCourse = document.querySelector("#summaryReviewDrawerCourse");
|
||||
const summaryReviewEditSection = document.querySelector("#summaryReviewEditSection");
|
||||
const summaryReviewEditStudent = document.querySelector("#summaryReviewEditStudent");
|
||||
const summaryReviewEditDate = document.querySelector("#summaryReviewEditDate");
|
||||
const summaryReviewEditTime = document.querySelector("#summaryReviewEditTime");
|
||||
const summaryReviewEditTeacher = document.querySelector("#summaryReviewEditTeacher");
|
||||
const summaryReviewEditSubject = document.querySelector("#summaryReviewEditSubject");
|
||||
const summaryReviewDrawerLine = document.querySelector("#summaryReviewDrawerLine");
|
||||
const summaryReviewDrawerReasons = document.querySelector("#summaryReviewDrawerReasons");
|
||||
const summaryReviewDrawerBody = document.querySelector("#summaryReviewDrawerBody");
|
||||
const summaryReviewDrawerSource = document.querySelector("#summaryReviewDrawerSource");
|
||||
const summaryReviewDrawerSave = document.querySelector("#summaryReviewDrawerSave");
|
||||
const summaryReviewDrawerLink = document.querySelector("#summaryReviewDrawerLink");
|
||||
const summaryReviewDrawerApprove = document.querySelector("#summaryReviewDrawerApprove");
|
||||
const summaryReviewDrawerReject = document.querySelector("#summaryReviewDrawerReject");
|
||||
const summarySearchForm = document.querySelector("#summarySearchForm");
|
||||
@@ -574,6 +582,11 @@ function canReviewSummary(item) {
|
||||
return item && (item.status === "pending" || item.status === "conflict");
|
||||
}
|
||||
|
||||
function canLinkExistingSummary(item) {
|
||||
const reasons = Array.isArray(item && item.reasons) ? item.reasons : [];
|
||||
return Boolean(item && item.proposed_line && reasons.includes("classnotes 已存在同一条上课记录"));
|
||||
}
|
||||
|
||||
function courseInfoLines(summary) {
|
||||
return [
|
||||
summary.student ? `学生:${summary.student}` : "",
|
||||
@@ -584,6 +597,24 @@ function courseInfoLines(summary) {
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
function fillSummaryReviewEditor(summary) {
|
||||
summaryReviewEditStudent.value = summary.student || "";
|
||||
summaryReviewEditDate.value = summary.date_iso || "";
|
||||
summaryReviewEditTime.value = summary.time_range || "";
|
||||
summaryReviewEditTeacher.value = summary.teacher || "";
|
||||
summaryReviewEditSubject.value = summary.subject || "";
|
||||
}
|
||||
|
||||
function summaryReviewEditorPayload() {
|
||||
return {
|
||||
student: summaryReviewEditStudent.value.trim(),
|
||||
date_iso: summaryReviewEditDate.value,
|
||||
time_range: summaryReviewEditTime.value.trim(),
|
||||
teacher: summaryReviewEditTeacher.value.trim(),
|
||||
subject: summaryReviewEditSubject.value.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function openSummaryReviewDrawer(taskId) {
|
||||
const item = findSummaryReview(taskId);
|
||||
if (!item) return;
|
||||
@@ -601,6 +632,8 @@ function openSummaryReviewDrawer(taskId) {
|
||||
].filter(Boolean).join(" ");
|
||||
summaryReviewDrawerStatus.innerHTML = `<span class="status ${taskStatusClass(item.status)}">${escapeHtml(item.status || "")}</span>${item.message ? ` <small>${escapeHtml(item.message)}</small>` : ""}`;
|
||||
summaryReviewDrawerCourse.innerHTML = courseInfoLines(summary).map(escapeHtml).join("<br>") || "暂无课程信息";
|
||||
summaryReviewEditSection.hidden = duplicateReview;
|
||||
if (!duplicateReview) fillSummaryReviewEditor(summary);
|
||||
summaryReviewDrawerLine.innerHTML = duplicateReview
|
||||
? "<span class=\"muted\">请选择下方重复小结删除</span>"
|
||||
: item.proposed_line ? renderReviewLine(item.proposed_line) : "<span class=\"muted\">暂无</span>";
|
||||
@@ -615,6 +648,10 @@ function openSummaryReviewDrawer(taskId) {
|
||||
item.saved_path ? `保存文件:${escapeHtml(item.saved_path)}` : "",
|
||||
item.batch_id ? `批次:${escapeHtml(item.batch_id)}` : "",
|
||||
].filter(Boolean).join("<br>") || "暂无来源信息";
|
||||
summaryReviewDrawerSave.hidden = duplicateReview;
|
||||
summaryReviewDrawerSave.disabled = !canReview || duplicateReview;
|
||||
summaryReviewDrawerLink.hidden = duplicateReview || !canLinkExistingSummary(item);
|
||||
summaryReviewDrawerLink.disabled = !canReview || duplicateReview || !canLinkExistingSummary(item);
|
||||
summaryReviewDrawerApprove.hidden = duplicateReview;
|
||||
summaryReviewDrawerApprove.disabled = !canReview || duplicateReview;
|
||||
summaryReviewDrawerReject.disabled = !canReview;
|
||||
@@ -623,6 +660,9 @@ function openSummaryReviewDrawer(taskId) {
|
||||
|
||||
function closeSummaryReviewDrawer() {
|
||||
activeSummaryReview = null;
|
||||
summaryReviewEditSection.hidden = true;
|
||||
summaryReviewDrawerSave.hidden = false;
|
||||
summaryReviewDrawerLink.hidden = true;
|
||||
summaryReviewDrawerApprove.hidden = false;
|
||||
summaryReviewDrawerBackdrop.hidden = true;
|
||||
}
|
||||
@@ -754,6 +794,38 @@ async function deleteCourseSummary(summaryId) {
|
||||
await loadSummarySearch();
|
||||
}
|
||||
|
||||
async function saveSummaryReviewEdits() {
|
||||
if (!activeSummaryReview) return;
|
||||
try {
|
||||
const result = await fetchJson(`/api/admin/tasks/${encodeURIComponent(activeSummaryReview.id)}/course-summary-review`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(summaryReviewEditorPayload()),
|
||||
});
|
||||
activeSummaryReview = result.task;
|
||||
await loadSummaryReviews();
|
||||
openSummaryReviewDrawer(activeSummaryReview.id);
|
||||
await loadOperationLogs();
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function linkExistingSummaryReview() {
|
||||
if (!activeSummaryReview) return;
|
||||
if (!confirm("确认关联已有上课记录?系统不会重复扣课时。")) return;
|
||||
try {
|
||||
await fetchJson(`/api/admin/tasks/${encodeURIComponent(activeSummaryReview.id)}/link-existing-course-summary`, {
|
||||
method: "POST",
|
||||
});
|
||||
closeSummaryReviewDrawer();
|
||||
await loadSummaryReviews();
|
||||
await loadOperationLogs();
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveDuplicateSummary(taskId, summaryId) {
|
||||
if (!confirm("确认删除这条重复课程小结?系统会先备份来源文件。")) return;
|
||||
await fetchJson(`/api/admin/tasks/${encodeURIComponent(taskId)}/resolve-duplicate-summary`, {
|
||||
@@ -1067,6 +1139,8 @@ summaryReviewDrawerClose.addEventListener("click", closeSummaryReviewDrawer);
|
||||
summaryReviewDrawerBackdrop.addEventListener("click", (event) => {
|
||||
if (event.target === summaryReviewDrawerBackdrop) closeSummaryReviewDrawer();
|
||||
});
|
||||
summaryReviewDrawerSave.addEventListener("click", saveSummaryReviewEdits);
|
||||
summaryReviewDrawerLink.addEventListener("click", linkExistingSummaryReview);
|
||||
summaryReviewDrawerApprove.addEventListener("click", () => {
|
||||
if (activeSummaryReview) summaryReviewTask(activeSummaryReview.id, "approve");
|
||||
});
|
||||
|
||||
@@ -369,6 +369,10 @@ select:focus {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.compact-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.span-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user