新增重复课程小结审核
This commit is contained in:
+294
-1
@@ -1021,6 +1021,88 @@ def list_admin_tasks(tasks_path: Path, status_filter: str = "", task_type: str =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def duplicate_review_task_from_group(task_id: int, group: dict, now: str) -> dict:
|
||||||
|
key = group.get("key") or {}
|
||||||
|
return {
|
||||||
|
"id": task_id,
|
||||||
|
"type": "course_summary_duplicate_review",
|
||||||
|
"status": "pending",
|
||||||
|
"created_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
"duplicate_group_id": str(group.get("duplicate_group_id") or ""),
|
||||||
|
"student": str(key.get("student") or ""),
|
||||||
|
"teacher": str(key.get("teacher") or ""),
|
||||||
|
"subject": str(key.get("subject") or ""),
|
||||||
|
"date_iso": str(key.get("date_iso") or ""),
|
||||||
|
"time_range": str(key.get("time_range") or ""),
|
||||||
|
"summary": {
|
||||||
|
"student": str(key.get("student") or ""),
|
||||||
|
"teacher": str(key.get("teacher") or ""),
|
||||||
|
"subject": str(key.get("subject") or ""),
|
||||||
|
"date_iso": str(key.get("date_iso") or ""),
|
||||||
|
"time_range": str(key.get("time_range") or ""),
|
||||||
|
"group": "",
|
||||||
|
"body": "",
|
||||||
|
},
|
||||||
|
"duplicate_candidates": group.get("candidates") or [],
|
||||||
|
"reasons": ["同一节课存在多条课程小结,请选择删除其中一条"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_course_summary_duplicate_review_tasks(
|
||||||
|
tasks_path: Path,
|
||||||
|
summaries_root: Path,
|
||||||
|
target_key: tuple[str, str, str, str, str] | None = None,
|
||||||
|
) -> dict:
|
||||||
|
tasks = read_admin_tasks(tasks_path)
|
||||||
|
active_tasks = {
|
||||||
|
str(task.get("duplicate_group_id") or ""): task
|
||||||
|
for task in tasks.get("items", [])
|
||||||
|
if task.get("type") == "course_summary_duplicate_review" and task.get("status") in {"pending", "conflict"}
|
||||||
|
}
|
||||||
|
now = datetime.now().isoformat(timespec="seconds")
|
||||||
|
created: list[dict] = []
|
||||||
|
groups = find_course_summary_duplicate_groups(summaries_root)
|
||||||
|
if target_key is not None:
|
||||||
|
groups = [
|
||||||
|
group
|
||||||
|
for group in groups
|
||||||
|
if course_summary_record_key(
|
||||||
|
str((group.get("key") or {}).get("student") or ""),
|
||||||
|
str((group.get("key") or {}).get("teacher") or ""),
|
||||||
|
str((group.get("key") or {}).get("subject") or ""),
|
||||||
|
str((group.get("key") or {}).get("date_iso") or ""),
|
||||||
|
str((group.get("key") or {}).get("time_range") or ""),
|
||||||
|
)
|
||||||
|
== target_key
|
||||||
|
]
|
||||||
|
changed = False
|
||||||
|
refreshed = 0
|
||||||
|
for group in groups:
|
||||||
|
group_id = str(group.get("duplicate_group_id") or "")
|
||||||
|
if not group_id:
|
||||||
|
continue
|
||||||
|
if group_id in active_tasks:
|
||||||
|
active_tasks[group_id]["duplicate_candidates"] = group.get("candidates") or []
|
||||||
|
active_tasks[group_id]["updated_at"] = now
|
||||||
|
changed = True
|
||||||
|
refreshed += 1
|
||||||
|
continue
|
||||||
|
task = duplicate_review_task_from_group(int(tasks["next_id"]), group, now)
|
||||||
|
tasks["next_id"] = int(tasks["next_id"]) + 1
|
||||||
|
tasks["items"].append(task)
|
||||||
|
active_tasks[group_id] = task
|
||||||
|
created.append(task)
|
||||||
|
if created or changed:
|
||||||
|
write_admin_tasks(tasks_path, tasks)
|
||||||
|
return {
|
||||||
|
"scanned": len(groups),
|
||||||
|
"created": len(created),
|
||||||
|
"refreshed": refreshed,
|
||||||
|
"items": [task_to_dict(task) for task in created],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def replace_class_record_line(original_text: str, original_line: str, corrected_line: str) -> str:
|
def replace_class_record_line(original_text: str, original_line: str, corrected_line: str) -> str:
|
||||||
lines = original_text.splitlines()
|
lines = original_text.splitlines()
|
||||||
matched = [index for index, line in enumerate(lines) if line.strip() == original_line]
|
matched = [index for index, line in enumerate(lines) if line.strip() == original_line]
|
||||||
@@ -1501,6 +1583,8 @@ OPERATION_LABELS = {
|
|||||||
"admin_task_reject": "审核驳回",
|
"admin_task_reject": "审核驳回",
|
||||||
"admin_course_summary_update_time": "课程小结补齐时间",
|
"admin_course_summary_update_time": "课程小结补齐时间",
|
||||||
"admin_course_summary_delete": "课程小结删除",
|
"admin_course_summary_delete": "课程小结删除",
|
||||||
|
"admin_course_summary_duplicate_scan": "重复小结扫描",
|
||||||
|
"admin_course_summary_duplicate_delete": "重复小结删除",
|
||||||
"register-class-records": "登记上课记录",
|
"register-class-records": "登记上课记录",
|
||||||
"register-payments": "登记缴费记录",
|
"register-payments": "登记缴费记录",
|
||||||
"admin-create-account": "新增课时账户",
|
"admin-create-account": "新增课时账户",
|
||||||
@@ -1532,6 +1616,7 @@ TYPE_LABELS = {
|
|||||||
"class_record_correction": "上课记录纠错",
|
"class_record_correction": "上课记录纠错",
|
||||||
"class_record_deletion": "上课记录删除",
|
"class_record_deletion": "上课记录删除",
|
||||||
"course_summary_review": "课程小结审核",
|
"course_summary_review": "课程小结审核",
|
||||||
|
"course_summary_duplicate_review": "重复小结审核",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1891,6 +1976,9 @@ def iter_course_summary_markdown(root: Path) -> Iterable[dict]:
|
|||||||
body = text[start:end].strip()
|
body = text[start:end].strip()
|
||||||
body_without_meta = re.sub(r"^(?:>\s+.*\n)+\s*", "", body).strip()
|
body_without_meta = re.sub(r"^(?:>\s+.*\n)+\s*", "", body).strip()
|
||||||
body_text = body_without_meta or body
|
body_text = body_without_meta or body
|
||||||
|
source_id_match = re.search(r"来源ID:`([^`]+)`", body)
|
||||||
|
sent_at_match = re.search(r"发送时间:`([^`]+)`", body)
|
||||||
|
sender_match = re.search(r"发送者:`([^`]+)`", body)
|
||||||
item_id = sha1_text(f"{identity['relative_path']}|{title}|{index}|{body_text[:200]}", 20)
|
item_id = sha1_text(f"{identity['relative_path']}|{title}|{index}|{body_text[:200]}", 20)
|
||||||
yield {
|
yield {
|
||||||
**identity,
|
**identity,
|
||||||
@@ -1900,6 +1988,10 @@ def iter_course_summary_markdown(root: Path) -> Iterable[dict]:
|
|||||||
"time_range": parse_course_summary_time_text(f"{title}\n{body_text}"),
|
"time_range": parse_course_summary_time_text(f"{title}\n{body_text}"),
|
||||||
"group": group,
|
"group": group,
|
||||||
"body": body_text,
|
"body": body_text,
|
||||||
|
"raw_body": body,
|
||||||
|
"source_id": source_id_match.group(1) if source_id_match else "",
|
||||||
|
"message_time": sent_at_match.group(1) if sent_at_match else "",
|
||||||
|
"sender": sender_match.group(1) if sender_match else "",
|
||||||
"body_preview": body_text[:260] + ("..." if len(body_text) > 260 else ""),
|
"body_preview": body_text[:260] + ("..." if len(body_text) > 260 else ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1950,6 +2042,108 @@ def course_summary_record_key(student: str, teacher: str, subject: str, date_iso
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def course_summary_duplicate_key(item: dict) -> tuple[str, str, str, str, str]:
|
||||||
|
return course_summary_record_key(
|
||||||
|
str(item.get("student") or ""),
|
||||||
|
str(item.get("teacher") or ""),
|
||||||
|
str(item.get("subject") or ""),
|
||||||
|
str(item.get("date_iso") or ""),
|
||||||
|
str(item.get("time_range") or ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def course_summary_source_meta(item: dict) -> dict:
|
||||||
|
body = str(item.get("raw_body") or item.get("body") or "")
|
||||||
|
source_id_match = re.search(r"来源ID:`([^`]+)`", body)
|
||||||
|
sent_at_match = re.search(r"发送时间:`([^`]+)`", body)
|
||||||
|
sender_match = re.search(r"发送者:`([^`]+)`", body)
|
||||||
|
return {
|
||||||
|
"source_id": source_id_match.group(1) if source_id_match else "",
|
||||||
|
"message_time": sent_at_match.group(1) if sent_at_match else "",
|
||||||
|
"sender": sender_match.group(1) if sender_match else "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def course_summary_duplicate_candidate(item: dict) -> dict:
|
||||||
|
meta = course_summary_source_meta(item)
|
||||||
|
body = str(item.get("body") or "")
|
||||||
|
return {
|
||||||
|
"id": str(item.get("id") or ""),
|
||||||
|
"title": str(item.get("title") or ""),
|
||||||
|
"student": str(item.get("student") or ""),
|
||||||
|
"teacher": str(item.get("teacher") or ""),
|
||||||
|
"subject": str(item.get("subject") or ""),
|
||||||
|
"date_iso": str(item.get("date_iso") or ""),
|
||||||
|
"time_range": str(item.get("time_range") or ""),
|
||||||
|
"group": str(item.get("group") or ""),
|
||||||
|
"source_path": str(item.get("source_path") or ""),
|
||||||
|
"relative_path": str(item.get("relative_path") or ""),
|
||||||
|
"source_id": str(item.get("source_id") or meta["source_id"]),
|
||||||
|
"message_time": str(item.get("message_time") or meta["message_time"]),
|
||||||
|
"sender": str(item.get("sender") or meta["sender"]),
|
||||||
|
"body": body,
|
||||||
|
"body_preview": body[:260] + ("..." if len(body) > 260 else ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def course_summary_quality_score(item: dict) -> tuple[int, int, int, str]:
|
||||||
|
title = str(item.get("title") or "")
|
||||||
|
body = str(item.get("body") or "")
|
||||||
|
raw_body = str(item.get("raw_body") or body)
|
||||||
|
has_standard_time = 1 if re.search(r"\d{1,2}:\d{2}-\d{1,2}:\d{2}", title) else 0
|
||||||
|
has_source_meta = 1 if "来源ID:`" in raw_body or "发送时间:`" in raw_body or "发送者:`" in raw_body else 0
|
||||||
|
return (has_standard_time, has_source_meta, len(body), str(item.get("id") or ""))
|
||||||
|
|
||||||
|
|
||||||
|
def course_summary_duplicate_group_id(key: tuple[str, str, str, str, str], candidate_ids: list[str]) -> str:
|
||||||
|
return sha1_text("|".join([*key, *sorted(candidate_ids)]), 20)
|
||||||
|
|
||||||
|
|
||||||
|
def find_course_summary_duplicate_groups(root: Path) -> list[dict]:
|
||||||
|
grouped: dict[tuple[str, str, str, str, str], list[dict]] = defaultdict(list)
|
||||||
|
for item in iter_course_summary_markdown(root):
|
||||||
|
if not item.get("date_iso") or not item.get("time_range"):
|
||||||
|
continue
|
||||||
|
key = course_summary_duplicate_key(item)
|
||||||
|
if not all(key):
|
||||||
|
continue
|
||||||
|
grouped[key].append(item)
|
||||||
|
|
||||||
|
groups: list[dict] = []
|
||||||
|
for key, items in grouped.items():
|
||||||
|
if len(items) <= 1:
|
||||||
|
continue
|
||||||
|
candidates = [course_summary_duplicate_candidate(item) for item in sorted(items, key=lambda item: str(item.get("id") or ""))]
|
||||||
|
groups.append(
|
||||||
|
{
|
||||||
|
"duplicate_group_id": course_summary_duplicate_group_id(key, [candidate["id"] for candidate in candidates]),
|
||||||
|
"key": {
|
||||||
|
"student": key[0],
|
||||||
|
"teacher": key[1],
|
||||||
|
"subject": key[2],
|
||||||
|
"date_iso": key[3],
|
||||||
|
"time_range": key[4],
|
||||||
|
},
|
||||||
|
"candidates": candidates,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
groups.sort(key=lambda group: (
|
||||||
|
str(group["key"].get("date_iso") or ""),
|
||||||
|
str(group["key"].get("student") or ""),
|
||||||
|
str(group["key"].get("teacher") or ""),
|
||||||
|
str(group["key"].get("subject") or ""),
|
||||||
|
str(group["key"].get("time_range") or ""),
|
||||||
|
))
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_course_summary_duplicate_group(root: Path, duplicate_group_id: str) -> dict | None:
|
||||||
|
for group in find_course_summary_duplicate_groups(root):
|
||||||
|
if str(group.get("duplicate_group_id") or "") == duplicate_group_id:
|
||||||
|
return group
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def course_summary_to_public(item: dict, teachers: list[Teacher]) -> dict:
|
def course_summary_to_public(item: dict, teachers: list[Teacher]) -> dict:
|
||||||
display_names = teacher_alias_map(teachers)
|
display_names = teacher_alias_map(teachers)
|
||||||
teacher = str(item.get("teacher") or "")
|
teacher = str(item.get("teacher") or "")
|
||||||
@@ -1984,8 +2178,10 @@ def course_summary_index_for_records(root: Path) -> dict[tuple[str, str, str, st
|
|||||||
time_range,
|
time_range,
|
||||||
)
|
)
|
||||||
index[key].append(item)
|
index[key].append(item)
|
||||||
for values in index.values():
|
for key, values in list(index.items()):
|
||||||
values.sort(key=lambda item: (str(item.get("title") or ""), str(item.get("id") or "")))
|
values.sort(key=lambda item: (str(item.get("title") or ""), str(item.get("id") or "")))
|
||||||
|
if len(values) > 1:
|
||||||
|
index[key] = [max(values, key=course_summary_quality_score)]
|
||||||
return index
|
return index
|
||||||
|
|
||||||
|
|
||||||
@@ -2114,6 +2310,72 @@ def delete_course_summary(root: Path, summary_id: str) -> dict:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_duplicate_course_summary_task(
|
||||||
|
tasks_path: Path,
|
||||||
|
summaries_root: Path,
|
||||||
|
task_id: int,
|
||||||
|
delete_summary_id: str,
|
||||||
|
) -> dict:
|
||||||
|
target_id = delete_summary_id.strip()
|
||||||
|
if not target_id:
|
||||||
|
raise ValueError("请选择要删除的课程小结")
|
||||||
|
tasks = read_admin_tasks(tasks_path)
|
||||||
|
task = find_admin_task(tasks, task_id)
|
||||||
|
if task.get("type") != "course_summary_duplicate_review":
|
||||||
|
raise ValueError("该任务不是重复小结审核")
|
||||||
|
if task.get("status") not in {"pending", "conflict"}:
|
||||||
|
raise ValueError("该任务已处理,不能重复批准")
|
||||||
|
|
||||||
|
group_id = str(task.get("duplicate_group_id") or "")
|
||||||
|
current_group = refresh_course_summary_duplicate_group(summaries_root, group_id)
|
||||||
|
if current_group is None:
|
||||||
|
task["status"] = "conflict"
|
||||||
|
task["updated_at"] = datetime.now().isoformat(timespec="seconds")
|
||||||
|
task["message"] = "重复小结已不存在或已被处理"
|
||||||
|
write_admin_tasks(tasks_path, tasks)
|
||||||
|
raise ValueError(task["message"])
|
||||||
|
|
||||||
|
candidates = current_group.get("candidates") or []
|
||||||
|
candidate_ids = {str(candidate.get("id") or "") for candidate in candidates}
|
||||||
|
if target_id not in candidate_ids:
|
||||||
|
task["status"] = "conflict"
|
||||||
|
task["updated_at"] = datetime.now().isoformat(timespec="seconds")
|
||||||
|
task["duplicate_candidates"] = candidates
|
||||||
|
task["message"] = "选择的小结已不存在,请重新选择"
|
||||||
|
write_admin_tasks(tasks_path, tasks)
|
||||||
|
raise ValueError(task["message"])
|
||||||
|
if len(candidates) <= 1:
|
||||||
|
task["status"] = "conflict"
|
||||||
|
task["updated_at"] = datetime.now().isoformat(timespec="seconds")
|
||||||
|
task["duplicate_candidates"] = candidates
|
||||||
|
task["message"] = "当前重复组已不足两条,无需删除"
|
||||||
|
write_admin_tasks(tasks_path, tasks)
|
||||||
|
raise ValueError(task["message"])
|
||||||
|
|
||||||
|
original_tasks = json.dumps(tasks, ensure_ascii=False, indent=2) + "\n"
|
||||||
|
delete_result = delete_course_summary(summaries_root, target_id)
|
||||||
|
now = datetime.now().isoformat(timespec="seconds")
|
||||||
|
task["status"] = "approved"
|
||||||
|
task["updated_at"] = now
|
||||||
|
task["reviewed_at"] = now
|
||||||
|
task["deleted_summary_id"] = target_id
|
||||||
|
task["deleted_summary_title"] = str(delete_result.get("title") or "")
|
||||||
|
task["backup_id"] = str(delete_result.get("backup_id") or "")
|
||||||
|
task["duplicate_candidates"] = candidates
|
||||||
|
new_tasks = json.dumps(tasks, ensure_ascii=False, indent=2) + "\n"
|
||||||
|
try:
|
||||||
|
atomic_write_text(tasks_path, new_tasks)
|
||||||
|
except Exception:
|
||||||
|
atomic_write_text(tasks_path, original_tasks)
|
||||||
|
raise
|
||||||
|
return {
|
||||||
|
"task": task_to_dict(task),
|
||||||
|
"deleted_summary_id": target_id,
|
||||||
|
"backup_id": str(delete_result.get("backup_id") or ""),
|
||||||
|
"deleted": delete_result,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def course_summary_path(root: Path, summary: dict) -> Path:
|
def course_summary_path(root: Path, summary: dict) -> Path:
|
||||||
student = safe_filename_part(summary["student"])
|
student = safe_filename_part(summary["student"])
|
||||||
teacher = safe_filename_part(summary["teacher"] or "待核对老师")
|
teacher = safe_filename_part(summary["teacher"] or "待核对老师")
|
||||||
@@ -2397,6 +2659,11 @@ def register_course_summary_texts(
|
|||||||
raise ValueError(";".join(reasons))
|
raise ValueError(";".join(reasons))
|
||||||
|
|
||||||
saved = save_course_summary_markdown(summaries_root, normalized)
|
saved = save_course_summary_markdown(summaries_root, normalized)
|
||||||
|
duplicate_tasks = create_course_summary_duplicate_review_tasks(
|
||||||
|
tasks_path,
|
||||||
|
summaries_root,
|
||||||
|
target_key=course_summary_duplicate_key(normalized),
|
||||||
|
)
|
||||||
register_result = register_class_record_lines(classnotes_path, accounts_path, line=proposed_line)
|
register_result = register_class_record_lines(classnotes_path, accounts_path, line=proposed_line)
|
||||||
seen_source_ids.add(source_id)
|
seen_source_ids.add(source_id)
|
||||||
seen_semantic_keys.add(semantic_key)
|
seen_semantic_keys.add(semantic_key)
|
||||||
@@ -2435,6 +2702,16 @@ def register_course_summary_texts(
|
|||||||
result["saved"] += 1 if saved.get("added") else 0
|
result["saved"] += 1 if saved.get("added") else 0
|
||||||
result["auto_registered"] += 1
|
result["auto_registered"] += 1
|
||||||
result["operation_log_ids"].append(log_id)
|
result["operation_log_ids"].append(log_id)
|
||||||
|
if duplicate_tasks["created"]:
|
||||||
|
scan_log_id = append_operation_log(
|
||||||
|
operation_logs_path,
|
||||||
|
"重复小结扫描",
|
||||||
|
"待审核",
|
||||||
|
source_id=source_id,
|
||||||
|
student=normalized["student"],
|
||||||
|
created_tasks=duplicate_tasks["created"],
|
||||||
|
)
|
||||||
|
result["operation_log_ids"].append(scan_log_id)
|
||||||
result["items"].append(
|
result["items"].append(
|
||||||
{
|
{
|
||||||
"source_id": source_id,
|
"source_id": source_id,
|
||||||
@@ -2519,6 +2796,11 @@ def ingest_course_summaries(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
saved = save_course_summary_markdown(summaries_root, normalized)
|
saved = save_course_summary_markdown(summaries_root, normalized)
|
||||||
|
duplicate_tasks = create_course_summary_duplicate_review_tasks(
|
||||||
|
tasks_path,
|
||||||
|
summaries_root,
|
||||||
|
target_key=course_summary_duplicate_key(normalized),
|
||||||
|
)
|
||||||
result["saved"] += 1 if saved.get("added") else 0
|
result["saved"] += 1 if saved.get("added") else 0
|
||||||
|
|
||||||
reasons, proposed_line = auto_register_reasons(normalized, classnotes_path, accounts_path)
|
reasons, proposed_line = auto_register_reasons(normalized, classnotes_path, accounts_path)
|
||||||
@@ -2564,6 +2846,17 @@ def ingest_course_summaries(
|
|||||||
saved_path=str(saved.get("path") or ""),
|
saved_path=str(saved.get("path") or ""),
|
||||||
)
|
)
|
||||||
result["operation_log_ids"].append(log_id)
|
result["operation_log_ids"].append(log_id)
|
||||||
|
if duplicate_tasks["created"]:
|
||||||
|
scan_log_id = append_operation_log(
|
||||||
|
operation_logs_path,
|
||||||
|
"重复小结扫描",
|
||||||
|
"待审核",
|
||||||
|
batch_id=batch_id,
|
||||||
|
source_id=source_id,
|
||||||
|
student=normalized["student"],
|
||||||
|
created_tasks=duplicate_tasks["created"],
|
||||||
|
)
|
||||||
|
result["operation_log_ids"].append(scan_log_id)
|
||||||
result["items"].append(
|
result["items"].append(
|
||||||
{
|
{
|
||||||
"source_id": source_id,
|
"source_id": source_id,
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ from ..config import (
|
|||||||
from ..data import (
|
from ..data import (
|
||||||
append_operation_log,
|
append_operation_log,
|
||||||
approve_admin_task,
|
approve_admin_task,
|
||||||
|
create_course_summary_duplicate_review_tasks,
|
||||||
delete_course_summary,
|
delete_course_summary,
|
||||||
list_admin_tasks,
|
list_admin_tasks,
|
||||||
list_operation_logs,
|
list_operation_logs,
|
||||||
migrate_operation_log_labels,
|
migrate_operation_log_labels,
|
||||||
query_course_summaries,
|
query_course_summaries,
|
||||||
reject_admin_task,
|
reject_admin_task,
|
||||||
|
resolve_duplicate_course_summary_task,
|
||||||
rollback_operation_log,
|
rollback_operation_log,
|
||||||
update_course_summary_time,
|
update_course_summary_time,
|
||||||
)
|
)
|
||||||
@@ -139,6 +141,33 @@ def admin_approve_task(task_id: int, _user: str = Depends(verify_admin_auth)):
|
|||||||
return {"ok": True, **result}
|
return {"ok": True, **result}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/admin/tasks/{task_id}/resolve-duplicate-summary")
|
||||||
|
def admin_resolve_duplicate_summary(task_id: int, payload: dict, _user: str = Depends(verify_admin_auth)):
|
||||||
|
delete_summary_id = str(payload.get("delete_summary_id") or "")
|
||||||
|
try:
|
||||||
|
with write_lock:
|
||||||
|
result = resolve_duplicate_course_summary_task(
|
||||||
|
ADMIN_TASKS_PATH,
|
||||||
|
COURSE_SUMMARIES_ROOT,
|
||||||
|
task_id,
|
||||||
|
delete_summary_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 ""),
|
||||||
|
summary_id=str(result.get("deleted_summary_id") or ""),
|
||||||
|
backup_id=str(result.get("backup_id") 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")
|
@router.post("/api/admin/tasks/{task_id}/reject")
|
||||||
def admin_reject_task(task_id: int, _user: str = Depends(verify_admin_auth)):
|
def admin_reject_task(task_id: int, _user: str = Depends(verify_admin_auth)):
|
||||||
try:
|
try:
|
||||||
@@ -158,6 +187,23 @@ def admin_reject_task(task_id: int, _user: str = Depends(verify_admin_auth)):
|
|||||||
return {"ok": True, "task": task}
|
return {"ok": True, "task": task}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/admin/course-summaries/duplicate-scan")
|
||||||
|
def admin_scan_duplicate_course_summaries(_user: str = Depends(verify_admin_auth)):
|
||||||
|
try:
|
||||||
|
with write_lock:
|
||||||
|
result = create_course_summary_duplicate_review_tasks(ADMIN_TASKS_PATH, COURSE_SUMMARIES_ROOT)
|
||||||
|
append_operation_log(
|
||||||
|
OPERATION_LOGS_PATH,
|
||||||
|
"重复小结扫描",
|
||||||
|
"待审核" if result.get("created") else "完成",
|
||||||
|
scanned_groups=int(result.get("scanned") or 0),
|
||||||
|
created_tasks=int(result.get("created") or 0),
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
return {"ok": True, **result}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/admin/course-summaries/{summary_id}/time")
|
@router.post("/api/admin/course-summaries/{summary_id}/time")
|
||||||
def admin_update_course_summary_time(summary_id: str, payload: dict, _user: str = Depends(verify_admin_auth)):
|
def admin_update_course_summary_time(summary_id: str, payload: dict, _user: str = Depends(verify_admin_auth)):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -219,6 +219,7 @@
|
|||||||
<section id="summariesPanel" class="panel admin-panel" hidden>
|
<section id="summariesPanel" class="panel admin-panel" hidden>
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h2>课程小结审核</h2>
|
<h2>课程小结审核</h2>
|
||||||
|
<div class="quick-actions">
|
||||||
<select id="summaryReviewStatus" aria-label="课程小结审核状态筛选">
|
<select id="summaryReviewStatus" aria-label="课程小结审核状态筛选">
|
||||||
<option value="pending">待审核</option>
|
<option value="pending">待审核</option>
|
||||||
<option value="conflict">冲突</option>
|
<option value="conflict">冲突</option>
|
||||||
@@ -226,6 +227,8 @@
|
|||||||
<option value="rejected">已驳回</option>
|
<option value="rejected">已驳回</option>
|
||||||
<option value="">全部状态</option>
|
<option value="">全部状态</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button id="duplicateSummaryScanBtn" class="chip" type="button">扫描重复小结</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="summaryReviewMeta" class="summary-grid"></div>
|
<div id="summaryReviewMeta" class="summary-grid"></div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
@@ -338,6 +341,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>
|
||||||
<option value="审核批准">审核批准</option>
|
<option value="审核批准">审核批准</option>
|
||||||
<option value="审核驳回">审核驳回</option>
|
<option value="审核驳回">审核驳回</option>
|
||||||
<option value="课程小结补齐时间">课程小结补齐时间</option>
|
<option value="课程小结补齐时间">课程小结补齐时间</option>
|
||||||
|
|||||||
+93
-8
@@ -49,6 +49,7 @@ const reviewRows = document.querySelector("#reviewRows");
|
|||||||
const summaryReviewStatus = document.querySelector("#summaryReviewStatus");
|
const summaryReviewStatus = document.querySelector("#summaryReviewStatus");
|
||||||
const summaryReviewMeta = document.querySelector("#summaryReviewMeta");
|
const summaryReviewMeta = document.querySelector("#summaryReviewMeta");
|
||||||
const summaryReviewRows = document.querySelector("#summaryReviewRows");
|
const summaryReviewRows = document.querySelector("#summaryReviewRows");
|
||||||
|
const duplicateSummaryScanBtn = document.querySelector("#duplicateSummaryScanBtn");
|
||||||
const summaryReviewDrawerBackdrop = document.querySelector("#summaryReviewDrawerBackdrop");
|
const summaryReviewDrawerBackdrop = document.querySelector("#summaryReviewDrawerBackdrop");
|
||||||
const summaryReviewDrawerClose = document.querySelector("#summaryReviewDrawerClose");
|
const summaryReviewDrawerClose = document.querySelector("#summaryReviewDrawerClose");
|
||||||
const summaryReviewDrawerTitle = document.querySelector("#summaryReviewDrawerTitle");
|
const summaryReviewDrawerTitle = document.querySelector("#summaryReviewDrawerTitle");
|
||||||
@@ -509,6 +510,7 @@ async function loadReviews() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function summaryReviewKind(item) {
|
function summaryReviewKind(item) {
|
||||||
|
if (item.type === "course_summary_duplicate_review") return "重复小结";
|
||||||
const sourceId = String(item.source_id || "");
|
const sourceId = String(item.source_id || "");
|
||||||
const reasons = Array.isArray(item.reasons) ? item.reasons.join(" ") : "";
|
const reasons = Array.isArray(item.reasons) ? item.reasons.join(" ") : "";
|
||||||
if (sourceId.startsWith("history-missing:") || reasons.includes("历史 classnotes缺失")) {
|
if (sourceId.startsWith("history-missing:") || reasons.includes("历史 classnotes缺失")) {
|
||||||
@@ -539,6 +541,31 @@ function renderSummaryPreview(summary) {
|
|||||||
return `<div class="summary-body summary-preview">${escapeHtml(preview || "暂无原文")}</div>`;
|
return `<div class="summary-body summary-preview">${escapeHtml(preview || "暂无原文")}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDuplicateSummaryReview(item) {
|
||||||
|
return item && item.type === "course_summary_duplicate_review";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDuplicateCandidate(candidate, canReview) {
|
||||||
|
const source = [
|
||||||
|
candidate.source_id ? `来源ID:${candidate.source_id}` : "",
|
||||||
|
candidate.message_time ? `发送时间:${candidate.message_time}` : "",
|
||||||
|
candidate.sender ? `发送者:${candidate.sender}` : "",
|
||||||
|
candidate.relative_path ? `文件:${candidate.relative_path}` : "",
|
||||||
|
].filter(Boolean);
|
||||||
|
return `<div class="record-summary-item duplicate-summary-candidate">
|
||||||
|
<div class="record-summary-title">${escapeHtml(candidate.title || "课程小结")}</div>
|
||||||
|
<div class="drawer-text">${source.map(escapeHtml).join("<br>") || "暂无来源信息"}</div>
|
||||||
|
<div class="summary-body">${escapeHtml(candidate.body || candidate.body_preview || "暂无正文")}</div>
|
||||||
|
<button class="small-button duplicate-summary-delete" type="button" data-summary-id="${escapeHtml(candidate.id)}" ${canReview ? "" : "disabled"}>删除这一条</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDuplicateCandidates(item, canReview) {
|
||||||
|
const candidates = Array.isArray(item.duplicate_candidates) ? item.duplicate_candidates : [];
|
||||||
|
if (!candidates.length) return "<span class=\"muted\">暂无重复候选</span>";
|
||||||
|
return candidates.map((candidate) => renderDuplicateCandidate(candidate, canReview)).join("");
|
||||||
|
}
|
||||||
|
|
||||||
function findSummaryReview(taskId) {
|
function findSummaryReview(taskId) {
|
||||||
return currentSummaryReviews.find((item) => String(item.id) === String(taskId));
|
return currentSummaryReviews.find((item) => String(item.id) === String(taskId));
|
||||||
}
|
}
|
||||||
@@ -564,6 +591,7 @@ function openSummaryReviewDrawer(taskId) {
|
|||||||
const reasons = Array.isArray(item.reasons) ? item.reasons : [];
|
const reasons = Array.isArray(item.reasons) ? item.reasons : [];
|
||||||
const kind = summaryReviewKind(item);
|
const kind = summaryReviewKind(item);
|
||||||
const canReview = canReviewSummary(item);
|
const canReview = canReviewSummary(item);
|
||||||
|
const duplicateReview = isDuplicateSummaryReview(item);
|
||||||
activeSummaryReview = item;
|
activeSummaryReview = item;
|
||||||
|
|
||||||
summaryReviewDrawerTitle.textContent = `课程小结详情 #${item.id}`;
|
summaryReviewDrawerTitle.textContent = `课程小结详情 #${item.id}`;
|
||||||
@@ -573,35 +601,51 @@ function openSummaryReviewDrawer(taskId) {
|
|||||||
].filter(Boolean).join(" ");
|
].filter(Boolean).join(" ");
|
||||||
summaryReviewDrawerStatus.innerHTML = `<span class="status ${taskStatusClass(item.status)}">${escapeHtml(item.status || "")}</span>${item.message ? ` <small>${escapeHtml(item.message)}</small>` : ""}`;
|
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>") || "暂无课程信息";
|
summaryReviewDrawerCourse.innerHTML = courseInfoLines(summary).map(escapeHtml).join("<br>") || "暂无课程信息";
|
||||||
summaryReviewDrawerLine.innerHTML = item.proposed_line ? renderReviewLine(item.proposed_line) : "<span class=\"muted\">暂无</span>";
|
summaryReviewDrawerLine.innerHTML = duplicateReview
|
||||||
|
? "<span class=\"muted\">请选择下方重复小结删除</span>"
|
||||||
|
: item.proposed_line ? renderReviewLine(item.proposed_line) : "<span class=\"muted\">暂无</span>";
|
||||||
summaryReviewDrawerReasons.innerHTML = reasons.length ? reasons.map(escapeHtml).join("<br>") : "待人工复核";
|
summaryReviewDrawerReasons.innerHTML = reasons.length ? reasons.map(escapeHtml).join("<br>") : "待人工复核";
|
||||||
|
if (duplicateReview) {
|
||||||
|
summaryReviewDrawerBody.innerHTML = renderDuplicateCandidates(item, canReview);
|
||||||
|
} else {
|
||||||
summaryReviewDrawerBody.textContent = summary.body || "暂无原文";
|
summaryReviewDrawerBody.textContent = summary.body || "暂无原文";
|
||||||
|
}
|
||||||
summaryReviewDrawerSource.innerHTML = [
|
summaryReviewDrawerSource.innerHTML = [
|
||||||
item.source_id ? `来源ID:${escapeHtml(item.source_id)}` : "",
|
item.source_id ? `来源ID:${escapeHtml(item.source_id)}` : "",
|
||||||
item.saved_path ? `保存文件:${escapeHtml(item.saved_path)}` : "",
|
item.saved_path ? `保存文件:${escapeHtml(item.saved_path)}` : "",
|
||||||
item.batch_id ? `批次:${escapeHtml(item.batch_id)}` : "",
|
item.batch_id ? `批次:${escapeHtml(item.batch_id)}` : "",
|
||||||
].filter(Boolean).join("<br>") || "暂无来源信息";
|
].filter(Boolean).join("<br>") || "暂无来源信息";
|
||||||
summaryReviewDrawerApprove.disabled = !canReview;
|
summaryReviewDrawerApprove.hidden = duplicateReview;
|
||||||
|
summaryReviewDrawerApprove.disabled = !canReview || duplicateReview;
|
||||||
summaryReviewDrawerReject.disabled = !canReview;
|
summaryReviewDrawerReject.disabled = !canReview;
|
||||||
summaryReviewDrawerBackdrop.hidden = false;
|
summaryReviewDrawerBackdrop.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSummaryReviewDrawer() {
|
function closeSummaryReviewDrawer() {
|
||||||
activeSummaryReview = null;
|
activeSummaryReview = null;
|
||||||
|
summaryReviewDrawerApprove.hidden = false;
|
||||||
summaryReviewDrawerBackdrop.hidden = true;
|
summaryReviewDrawerBackdrop.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSummaryReviews() {
|
async function loadSummaryReviews() {
|
||||||
summaryReviewRows.innerHTML = `<tr><td colspan="7" class="empty">正在读取</td></tr>`;
|
summaryReviewRows.innerHTML = `<tr><td colspan="7" class="empty">正在读取</td></tr>`;
|
||||||
const params = new URLSearchParams({ type: "course_summary_review" });
|
const reviewParams = new URLSearchParams({ type: "course_summary_review" });
|
||||||
if (summaryReviewStatus.value) params.set("status", summaryReviewStatus.value);
|
const duplicateParams = new URLSearchParams({ type: "course_summary_duplicate_review" });
|
||||||
|
if (summaryReviewStatus.value) {
|
||||||
|
reviewParams.set("status", summaryReviewStatus.value);
|
||||||
|
duplicateParams.set("status", summaryReviewStatus.value);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const data = await fetchJson(`/api/admin/tasks?${params.toString()}`);
|
const [reviewData, duplicateData] = await Promise.all([
|
||||||
currentSummaryReviews = data.items || [];
|
fetchJson(`/api/admin/tasks?${reviewParams.toString()}`),
|
||||||
summaryReviewMeta.innerHTML = [metric("当前结果", `${data.count} 条`)].join("");
|
fetchJson(`/api/admin/tasks?${duplicateParams.toString()}`),
|
||||||
|
]);
|
||||||
|
currentSummaryReviews = [...(reviewData.items || []), ...(duplicateData.items || [])].sort((a, b) => Number(b.id || 0) - Number(a.id || 0));
|
||||||
|
summaryReviewMeta.innerHTML = [metric("当前结果", `${currentSummaryReviews.length} 条`)].join("");
|
||||||
summaryReviewRows.innerHTML = currentSummaryReviews
|
summaryReviewRows.innerHTML = currentSummaryReviews
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
const canReview = canReviewSummary(item);
|
const canReview = canReviewSummary(item);
|
||||||
|
const duplicateReview = isDuplicateSummaryReview(item);
|
||||||
const summary = item.summary || {};
|
const summary = item.summary || {};
|
||||||
const reasons = Array.isArray(item.reasons) ? item.reasons : [];
|
const reasons = Array.isArray(item.reasons) ? item.reasons : [];
|
||||||
return `<tr>
|
return `<tr>
|
||||||
@@ -614,7 +658,7 @@ async function loadSummaryReviews() {
|
|||||||
<td class="record-action-cell">
|
<td class="record-action-cell">
|
||||||
<div class="record-actions">
|
<div class="record-actions">
|
||||||
<button class="small-button summary-detail" type="button" data-task-id="${escapeHtml(item.id)}">查看详情</button>
|
<button class="small-button summary-detail" type="button" data-task-id="${escapeHtml(item.id)}">查看详情</button>
|
||||||
<button class="small-button summary-approve" type="button" data-task-id="${escapeHtml(item.id)}" ${canReview ? "" : "disabled"}>批准</button>
|
<button class="small-button summary-approve" type="button" data-task-id="${escapeHtml(item.id)}" ${canReview && !duplicateReview ? "" : "disabled"}>${duplicateReview ? "详情选择" : "批准"}</button>
|
||||||
<button class="small-button summary-reject" type="button" data-task-id="${escapeHtml(item.id)}" ${canReview ? "" : "disabled"}>驳回</button>
|
<button class="small-button summary-reject" type="button" data-task-id="${escapeHtml(item.id)}" ${canReview ? "" : "disabled"}>驳回</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -710,6 +754,32 @@ async function deleteCourseSummary(summaryId) {
|
|||||||
await loadSummarySearch();
|
await loadSummarySearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveDuplicateSummary(taskId, summaryId) {
|
||||||
|
if (!confirm("确认删除这条重复课程小结?系统会先备份来源文件。")) return;
|
||||||
|
await fetchJson(`/api/admin/tasks/${encodeURIComponent(taskId)}/resolve-duplicate-summary`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ delete_summary_id: summaryId }),
|
||||||
|
});
|
||||||
|
closeSummaryReviewDrawer();
|
||||||
|
await loadSummaryReviews();
|
||||||
|
await loadOperationLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scanDuplicateSummaries() {
|
||||||
|
duplicateSummaryScanBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
const result = await fetchJson("/api/admin/course-summaries/duplicate-scan", { method: "POST" });
|
||||||
|
alert(`扫描完成:发现 ${result.scanned || 0} 组重复,新增 ${result.created || 0} 条审核任务`);
|
||||||
|
await loadSummaryReviews();
|
||||||
|
await loadOperationLogs();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.message);
|
||||||
|
} finally {
|
||||||
|
duplicateSummaryScanBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderLogDetail(item) {
|
function renderLogDetail(item) {
|
||||||
const details = [];
|
const details = [];
|
||||||
if (item.task_type) details.push(`类型:${item.task_type}`);
|
if (item.task_type) details.push(`类型:${item.task_type}`);
|
||||||
@@ -719,6 +789,9 @@ function renderLogDetail(item) {
|
|||||||
if (item.teacher || item.subject) details.push(`老师/科目:${item.teacher || ""} ${item.subject || ""}`.trim());
|
if (item.teacher || item.subject) details.push(`老师/科目:${item.teacher || ""} ${item.subject || ""}`.trim());
|
||||||
if (item.time_range) details.push(`时间:${item.time_range}`);
|
if (item.time_range) details.push(`时间:${item.time_range}`);
|
||||||
if (item.proposed_line) details.push(`记录:${item.proposed_line}`);
|
if (item.proposed_line) details.push(`记录:${item.proposed_line}`);
|
||||||
|
if (item.summary_id) details.push(`小结ID:${item.summary_id}`);
|
||||||
|
if (item.scanned_groups !== undefined) details.push(`扫描重复组:${item.scanned_groups}`);
|
||||||
|
if (item.created_tasks !== undefined) details.push(`新增任务:${item.created_tasks}`);
|
||||||
if (Array.isArray(item.reasons) && item.reasons.length) details.push(`原因:${item.reasons.join(";")}`);
|
if (Array.isArray(item.reasons) && item.reasons.length) details.push(`原因:${item.reasons.join(";")}`);
|
||||||
if (item.error) details.push(`错误:${item.error}`);
|
if (item.error) details.push(`错误:${item.error}`);
|
||||||
if (item.backup_id) details.push(`备份:${item.backup_id}`);
|
if (item.backup_id) details.push(`备份:${item.backup_id}`);
|
||||||
@@ -970,6 +1043,7 @@ reviewRows.addEventListener("click", (event) => {
|
|||||||
if (reject) reviewTask(reject.dataset.taskId, "reject");
|
if (reject) reviewTask(reject.dataset.taskId, "reject");
|
||||||
});
|
});
|
||||||
summaryReviewStatus.addEventListener("change", loadSummaryReviews);
|
summaryReviewStatus.addEventListener("change", loadSummaryReviews);
|
||||||
|
duplicateSummaryScanBtn.addEventListener("click", scanDuplicateSummaries);
|
||||||
summaryReviewRows.addEventListener("click", (event) => {
|
summaryReviewRows.addEventListener("click", (event) => {
|
||||||
const detail = event.target.closest(".summary-detail");
|
const detail = event.target.closest(".summary-detail");
|
||||||
const approve = event.target.closest(".summary-approve");
|
const approve = event.target.closest(".summary-approve");
|
||||||
@@ -979,6 +1053,11 @@ summaryReviewRows.addEventListener("click", (event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (approve) {
|
if (approve) {
|
||||||
|
const item = findSummaryReview(approve.dataset.taskId);
|
||||||
|
if (isDuplicateSummaryReview(item)) {
|
||||||
|
openSummaryReviewDrawer(approve.dataset.taskId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
summaryReviewTask(approve.dataset.taskId, "approve");
|
summaryReviewTask(approve.dataset.taskId, "approve");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -994,6 +1073,12 @@ summaryReviewDrawerApprove.addEventListener("click", () => {
|
|||||||
summaryReviewDrawerReject.addEventListener("click", () => {
|
summaryReviewDrawerReject.addEventListener("click", () => {
|
||||||
if (activeSummaryReview) summaryReviewTask(activeSummaryReview.id, "reject");
|
if (activeSummaryReview) summaryReviewTask(activeSummaryReview.id, "reject");
|
||||||
});
|
});
|
||||||
|
summaryReviewDrawerBody.addEventListener("click", (event) => {
|
||||||
|
const button = event.target.closest(".duplicate-summary-delete");
|
||||||
|
if (button && activeSummaryReview) {
|
||||||
|
resolveDuplicateSummary(activeSummaryReview.id, button.dataset.summaryId).catch((error) => alert(error.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
if (event.key === "Escape" && !summaryReviewDrawerBackdrop.hidden) closeSummaryReviewDrawer();
|
if (event.key === "Escape" && !summaryReviewDrawerBackdrop.hidden) closeSummaryReviewDrawer();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user